#include "paint/stroke.h"

#include <algorithm>
#include <cmath>

namespace pp::paint {

namespace {

[[nodiscard]] bool is_finite_point(const StrokePoint& point) noexcept
{
    return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.pressure);
}

[[nodiscard]] float clamp_pressure(float pressure) noexcept
{
    return std::clamp(pressure, 0.0F, 1.0F);
}

[[nodiscard]] float distance_between(const StrokePoint& a, const StrokePoint& b) noexcept
{
    const auto dx = b.x - a.x;
    const auto dy = b.y - a.y;
    return std::sqrt(dx * dx + dy * dy);
}

[[nodiscard]] StrokeSample interpolate_sample(
    const StrokePoint& a,
    const StrokePoint& b,
    float t,
    float distance) noexcept
{
    return StrokeSample {
        .x = a.x + ((b.x - a.x) * t),
        .y = a.y + ((b.y - a.y) * t),
        .pressure = clamp_pressure(a.pressure + ((b.pressure - a.pressure) * t)),
        .distance = distance,
    };
}

[[nodiscard]] pp::foundation::Status validate_input(
    std::span<const StrokePoint> points,
    StrokeSamplingConfig config) noexcept
{
    if (points.size() < 2U) {
        return pp::foundation::Status::invalid_argument("stroke sampling requires at least two points");
    }

    if (points.size() > max_stroke_points) {
        return pp::foundation::Status::out_of_range("stroke point count exceeds the configured limit");
    }

    if (!std::isfinite(config.spacing) || config.spacing <= 0.0F) {
        return pp::foundation::Status::invalid_argument("stroke spacing must be finite and greater than zero");
    }

    if (config.max_samples == 0U || config.max_samples > max_stroke_samples) {
        return pp::foundation::Status::out_of_range("stroke sample count limit is outside the configured range");
    }

    for (const auto& point : points) {
        if (!is_finite_point(point)) {
            return pp::foundation::Status::invalid_argument("stroke points must contain finite coordinates and pressure");
        }
    }

    return pp::foundation::Status::success();
}

[[nodiscard]] pp::foundation::Status append_sample(
    std::vector<StrokeSample>& samples,
    StrokeSample sample,
    std::size_t max_samples)
{
    if (samples.size() >= max_samples) {
        return pp::foundation::Status::out_of_range("stroke sampling exceeded the configured sample limit");
    }

    samples.push_back(sample);
    return pp::foundation::Status::success();
}

}

pp::foundation::Result<std::vector<StrokeSample>> sample_stroke(
    std::span<const StrokePoint> points,
    StrokeSamplingConfig config) noexcept
{
    const auto input_status = validate_input(points, config);
    if (!input_status.ok()) {
        return pp::foundation::Result<std::vector<StrokeSample>>::failure(input_status);
    }

    std::vector<StrokeSample> samples;
    samples.reserve(std::min<std::size_t>(points.size(), config.max_samples));

    auto status = append_sample(
        samples,
        StrokeSample {
            .x = points.front().x,
            .y = points.front().y,
            .pressure = clamp_pressure(points.front().pressure),
            .distance = 0.0F,
        },
        config.max_samples);
    if (!status.ok()) {
        return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
    }

    float segment_start_distance = 0.0F;
    float next_sample_distance = config.spacing;
    float total_distance = 0.0F;

    for (std::size_t i = 1; i < points.size(); ++i) {
        const auto& a = points[i - 1U];
        const auto& b = points[i];
        const auto segment_length = distance_between(a, b);
        if (segment_length <= 0.0F) {
            continue;
        }

        const auto segment_end_distance = segment_start_distance + segment_length;
        while (next_sample_distance <= segment_end_distance) {
            const auto t = (next_sample_distance - segment_start_distance) / segment_length;
            status = append_sample(
                samples,
                interpolate_sample(a, b, t, next_sample_distance),
                config.max_samples);
            if (!status.ok()) {
                return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
            }
            next_sample_distance += config.spacing;
        }

        segment_start_distance = segment_end_distance;
        total_distance = segment_end_distance;
    }

    if (total_distance <= 0.0F) {
        return pp::foundation::Result<std::vector<StrokeSample>>::failure(
            pp::foundation::Status::invalid_argument("stroke path must have nonzero length"));
    }

    if (config.include_endpoint && samples.back().distance < total_distance) {
        status = append_sample(
            samples,
            StrokeSample {
                .x = points.back().x,
                .y = points.back().y,
                .pressure = clamp_pressure(points.back().pressure),
                .distance = total_distance,
            },
            config.max_samples);
        if (!status.ok()) {
            return pp::foundation::Result<std::vector<StrokeSample>>::failure(status);
        }
    }

    return pp::foundation::Result<std::vector<StrokeSample>>::success(samples);
}

}
