#pragma once

#include "Abs_Axis.h"
#include "Axis_Format.h"
#include "../render/Blend2D_Cache.h"

#include <renderive/base/Atomic_Mutex.hpp>
#include <renderive/base/observer/Observer.hpp>
#include <renderive/state/Double_State_Strategy.hpp>

#include <algorithm>
#include <cmath>
#include <type_traits>
#include <utility>
#include <vector>

namespace renderive::detail {

template <class State>
requires std::is_base_of_v<Axis_Base_Properties, State>
class Axis_State_Strategy
    : public Double_State_Strategy<Abs_Axis, State, Atomic_Spin_Mutex,
                                   Observer_State<Renderable_State_Observer>> {
    using State_Observer = Observer_State<Renderable_State_Observer>;
    using Base = Double_State_Strategy<Abs_Axis, State, Atomic_Spin_Mutex, State_Observer>;

public:
    explicit Axis_State_Strategy(State state)
        : Base(state,
               With_Observer<State_Observer>{State_Observer(Renderable_State_Observer(this))}) {}

    template <auto Member, Property_Member_Assignable<State, Member> Value>
    Axis_State_Strategy& set(Value&& value) {
        Base::template set<Member>(std::forward<Value>(value));
        using Member_Value = decltype(std::declval<State&>().*Member);
        if constexpr (Paint_Only_Property_Value<Member_Value>)
            this->paint_changed();
        else
            this->changed();
        return *this;
    }

    template <class Update>
    requires std::invocable<Update, State&>
    Axis_State_Strategy& update(Update&& value) {
        Base::update(std::forward<Update>(value));
        this->changed();
        return *this;
    }

    [[nodiscard]] Axis_Transform transform() const final {
        return Base::read([this](const State& state) {
            return make_transform(state, coordinate_range(state));
        });
    }

    [[nodiscard]] Axis_Transform transform(const Render_State_View& view) const noexcept final {
        const auto& state = view.get(static_cast<const Base&>(*this));
        return make_transform(state, coordinate_range(state));
    }

    [[nodiscard]] double tick_step(Range range) const final {
        return Base::read([this, range](const State& state) {
            return calculate_tick_step(range, state);
        });
    }

    [[nodiscard]] std::string tick_label(double tick) const final {
        return Base::read([this, tick](const State& state) {
            return format_tick_label(tick, state);
        });
    }

protected:
    [[nodiscard]] virtual Range coordinate_range(const State& state) const noexcept = 0;

    [[nodiscard]] virtual double calculate_tick_step(Range range, const State&) const {
        const double raw = range.size() / 5.0;
        if (!(raw > 0.0) || !std::isfinite(raw))
            return 1.0;
        const double scale = std::pow(10.0, std::floor(std::log10(raw)));
        const double normalized = raw / scale;
        const double nice = normalized <= 1.0 ? 1.0
                            : normalized <= 2.0 ? 2.0
                            : normalized <= 5.0 ? 5.0
                                                : 10.0;
        return nice * scale;
    }

    [[nodiscard]] virtual std::string format_tick_label(double tick, const State& state) const {
        int precision = 2;
        if constexpr (requires { state.precision; })
            precision = state.precision.get();
        return localized_axis_number(tick, precision, state.locale);
    }

private:
    static Axis_Transform make_transform(const State& state, Range coordinates) noexcept {
        return {
            coordinates,
            state.orientation == Orientation::Horizontal ? static_cast<double>(state.x)
                                                         : static_cast<double>(state.y),
            static_cast<double>(state.pixel_length),
            state.orientation
        };
    }

    struct Prepared_Line {
        PointF first;
        PointF second;
    };
    struct Prepared_Label {
        PointF position;
        std::string text;
    };
    struct Axis_Prepare_Buffer {
        std::vector<Prepared_Line> lines;
        std::vector<Prepared_Label> labels;
        PointF unit_position;
        double unit_width{};
        bool valid{};
    };
    void prepare_frame(const Prepare_Render_Context& context) final {
        const auto& view = context.frame.render_state;
        const auto& state = view.get(static_cast<const Base&>(*this));
        auto& output = prepare_buffer_;
        output = {};
        if (state.pixel_length == 0)
            return;

        const Range coordinates = coordinate_range(state);
        const Axis_Transform axis = make_transform(state, coordinates);
        const double step = calculate_tick_step(coordinates, state);
        if (!(step > 0.0))
            return;

        const PointF first{static_cast<double>(state.x), static_cast<double>(state.y)};
        const PointF last = state.orientation == Orientation::Horizontal
                                ? PointF{first.x + state.pixel_length, first.y}
                                : PointF{first.x, first.y + state.pixel_length};
        output.lines.push_back({first, last});

        const auto [low, high] = std::minmax(coordinates.origin, coordinates.target);
        const double initial = std::ceil(low / step) * step;
        int tick_index{};
        for (double tick = initial; tick <= high + step * 1e-6 && tick_index < 1000;
             tick += step, ++tick_index) {
            const double pixel = axis.coord_to_pixel(tick);
            PointF tick_start;
            PointF tick_end;
            PointF label;
            if (state.orientation == Orientation::Horizontal) {
                tick_start = {pixel, static_cast<double>(state.y)};
                tick_end = {pixel, static_cast<double>(state.y + state.tick_length)};
                label = {pixel + 2.0, static_cast<double>(state.y + state.tick_length + 2)};
            } else {
                tick_start = {static_cast<double>(state.x), pixel};
                tick_end = {static_cast<double>(state.x + state.tick_length), pixel};
                label = {static_cast<double>(state.x + state.tick_length + 2), pixel - 7.0};
            }
            output.lines.push_back({tick_start, tick_end});
            output.labels.push_back({label, format_tick_label(tick, state)});
            const int subdivisions = std::max(0, this->sub_tick_count(step));
            for (int sub_index = 1; sub_index <= subdivisions; ++sub_index) {
                const double sub_tick = tick + step * sub_index / (subdivisions + 1.0);
                if (sub_tick >= high)
                    break;
                const double sub_pixel = axis.coord_to_pixel(sub_tick);
                if (state.orientation == Orientation::Horizontal) {
                    output.lines.push_back({
                        {sub_pixel, static_cast<double>(state.y)},
                        {sub_pixel, static_cast<double>(state.y + state.sub_tick_length)}});
                } else {
                    output.lines.push_back({
                        {static_cast<double>(state.x), sub_pixel},
                        {static_cast<double>(state.x + state.sub_tick_length), sub_pixel}});
                }
            }
        }
        if (!state.unit_text.empty()) {
            output.unit_position = {last.x + 4.0, last.y + 4.0};
            output.unit_width =
                std::max(4.0, state.unit_text.size() * state.unit_text_font.size * 0.65);
        }
        output.valid = true;
    }
    void paint(Painter& painter, const Paint_Render_Context& context) final {
        const auto& output = prepare_buffer_;
        if (!output.valid)
            return;
        const auto& view = context.frame.render_state;
        const auto& state = view.get(static_cast<const Base&>(*this));
        const Pen axis_pen{state.color, 1.0};
        for (const auto& line : output.lines)
            painter.line(line.first, line.second, axis_pen);
        for (const auto& label : output.labels)
            painter.text(label.position, label.text, state.unit_text_font,
                         state.unit_text_pen, state.label_rotation_degrees);
        if (!state.unit_text.empty()) {
            painter.rect({output.unit_position.x - 2.0, output.unit_position.y - 2.0,
                          output.unit_width + 4.0,
                          state.unit_text_font.size * 1.5 + 4.0},
                         Pen{.style = Line_Style::None}, state.unit_text_background_brush);
            painter.text(output.unit_position, state.unit_text, state.unit_text_font,
                         state.unit_text_pen);
        }
    }
    Axis_Prepare_Buffer prepare_buffer_;
};

} // namespace renderive::detail
