#include "paint/stroke_script.h"

#include <array>
#include <cerrno>
#include <cmath>
#include <cstdlib>

namespace pp::paint {

namespace {

[[nodiscard]] std::string_view trim(std::string_view text) noexcept
{
    while (!text.empty() && (text.front() == ' ' || text.front() == '\t' || text.front() == '\r')) {
        text.remove_prefix(1);
    }

    while (!text.empty() && (text.back() == ' ' || text.back() == '\t' || text.back() == '\r')) {
        text.remove_suffix(1);
    }

    return text;
}

[[nodiscard]] std::string_view strip_comment(std::string_view line) noexcept
{
    const auto comment = line.find('#');
    if (comment == std::string_view::npos) {
        return line;
    }

    return line.substr(0, comment);
}

[[nodiscard]] pp::foundation::Result<float> parse_float_token(std::string_view token) noexcept
{
    token = trim(token);
    if (token.empty() || token.size() >= 64U) {
        return pp::foundation::Result<float>::failure(
            pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
    }

    std::array<char, 64> buffer {};
    for (std::size_t i = 0; i < token.size(); ++i) {
        buffer[i] = token[i];
    }

    char* end = nullptr;
    errno = 0;
    const auto value = std::strtof(buffer.data(), &end);
    if (errno != 0 || end != buffer.data() + static_cast<std::ptrdiff_t>(token.size()) || !std::isfinite(value)) {
        return pp::foundation::Result<float>::failure(
            pp::foundation::Status::invalid_argument("stroke script numeric token is invalid"));
    }

    return pp::foundation::Result<float>::success(value);
}

[[nodiscard]] pp::foundation::Result<std::size_t> split_tokens(
    std::string_view line,
    std::array<std::string_view, 8>& tokens) noexcept
{
    std::size_t count = 0;
    std::size_t offset = 0;
    while (offset < line.size()) {
        while (offset < line.size() && (line[offset] == ' ' || line[offset] == '\t')) {
            ++offset;
        }

        if (offset >= line.size()) {
            break;
        }

        const auto token_start = offset;
        while (offset < line.size() && line[offset] != ' ' && line[offset] != '\t') {
            ++offset;
        }

        if (count >= tokens.size()) {
            return pp::foundation::Result<std::size_t>::failure(
                pp::foundation::Status::invalid_argument("stroke script line has too many tokens"));
        }

        tokens[count] = line.substr(token_start, offset - token_start);
        ++count;
    }

    return pp::foundation::Result<std::size_t>::success(count);
}

[[nodiscard]] pp::foundation::Result<StrokeScriptStroke> parse_stroke_line(std::string_view line) noexcept
{
    std::array<std::string_view, 8> tokens {};
    const auto token_count = split_tokens(line, tokens);
    if (!token_count) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(token_count.status());
    }

    if (token_count.value() != tokens.size() || tokens[0] != "stroke") {
        return pp::foundation::Result<StrokeScriptStroke>::failure(
            pp::foundation::Status::invalid_argument("stroke script line must be 'stroke x1 y1 p1 x2 y2 p2 spacing'"));
    }

    const auto x1 = parse_float_token(tokens[1]);
    const auto y1 = parse_float_token(tokens[2]);
    const auto p1 = parse_float_token(tokens[3]);
    const auto x2 = parse_float_token(tokens[4]);
    const auto y2 = parse_float_token(tokens[5]);
    const auto p2 = parse_float_token(tokens[6]);
    const auto spacing = parse_float_token(tokens[7]);
    if (!x1) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(x1.status());
    }
    if (!y1) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(y1.status());
    }
    if (!p1) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(p1.status());
    }
    if (!x2) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(x2.status());
    }
    if (!y2) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(y2.status());
    }
    if (!p2) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(p2.status());
    }
    if (!spacing) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(spacing.status());
    }

    if (spacing.value() <= 0.0F) {
        return pp::foundation::Result<StrokeScriptStroke>::failure(
            pp::foundation::Status::invalid_argument("stroke script spacing must be greater than zero"));
    }

    return pp::foundation::Result<StrokeScriptStroke>::success(StrokeScriptStroke {
        .start = StrokePoint {
            .x = x1.value(),
            .y = y1.value(),
            .pressure = p1.value(),
        },
        .end = StrokePoint {
            .x = x2.value(),
            .y = y2.value(),
            .pressure = p2.value(),
        },
        .spacing = spacing.value(),
    });
}

}

pp::foundation::Result<StrokeScript> parse_stroke_script(std::string_view text)
{
    if (text.empty()) {
        return pp::foundation::Result<StrokeScript>::failure(
            pp::foundation::Status::invalid_argument("stroke script must not be empty"));
    }

    if (text.size() > max_stroke_script_bytes) {
        return pp::foundation::Result<StrokeScript>::failure(
            pp::foundation::Status::out_of_range("stroke script exceeds the configured size limit"));
    }

    StrokeScript script;
    std::size_t offset = 0;
    while (offset <= text.size()) {
        const auto line_start = offset;
        const auto line_end = text.find('\n', line_start);
        if (line_end == std::string_view::npos) {
            offset = text.size() + 1U;
        } else {
            offset = line_end + 1U;
        }

        auto line = text.substr(line_start, (line_end == std::string_view::npos) ? std::string_view::npos : line_end - line_start);
        if (line.size() > max_stroke_script_line_length) {
            return pp::foundation::Result<StrokeScript>::failure(
                pp::foundation::Status::out_of_range("stroke script line exceeds the configured length limit"));
        }

        line = trim(strip_comment(line));
        if (line.empty()) {
            continue;
        }

        if (script.strokes.size() >= max_stroke_script_strokes) {
            return pp::foundation::Result<StrokeScript>::failure(
                pp::foundation::Status::out_of_range("stroke script stroke count exceeds the configured limit"));
        }

        const auto stroke = parse_stroke_line(line);
        if (!stroke) {
            return pp::foundation::Result<StrokeScript>::failure(stroke.status());
        }

        script.strokes.push_back(stroke.value());
    }

    if (script.strokes.empty()) {
        return pp::foundation::Result<StrokeScript>::failure(
            pp::foundation::Status::invalid_argument("stroke script must contain at least one stroke"));
    }

    return pp::foundation::Result<StrokeScript>::success(script);
}

}
