#pragma once

#include "render_3D/Render_3D.h"

#include <renderive/frame_control/strategy/flow/Flow_Refresh_Strategy.hpp>
#include <renderive/frame_control/strategy/low_latency/Low_Latency_Strategy.hpp>
#include <renderive/frame_control/strategy/manual/Manual_Refresh_Strategy.hpp>
#include <renderive/state/Double_State_Strategy.hpp>

#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <utility>
#include <variant>
#include <vector>

namespace renderive::detail {

struct Object_3D_State {
    Object_3D_Id id;
    Sphere_3D sphere;
    std::uint64_t revision{1};
    bool operator==(const Object_3D_State&) const = default;
};

struct Render_3D_State {
    Extent_2D viewport{800, 600};
    Camera_3D camera;
    Clear_Color clear_color;
    float device_scale_x{1.0F};
    float device_scale_y{1.0F};
    float render_scale{1.0F};
    float user_scale{1.0F};
    std::vector<Object_3D_State> objects;
    bool operator==(const Render_3D_State&) const = default;
};

struct Render_3D_Frame_Data {
    Render_3D_State state;
    std::uint64_t revision{};
    std::uint64_t frame_sequence{};
};

struct Logical_State_Base {};

class Logical_State_Buffer final
    : public ::Double_State_Strategy<Logical_State_Base, Render_3D_State, std::mutex> {
    using Base = ::Double_State_Strategy<Logical_State_Base, Render_3D_State, std::mutex>;

public:
    struct Published_Frame {
        Render_3D_State state;
        std::uint64_t revision{};
    };

    explicit Logical_State_Buffer(const Render_3D_State& initial) : Base(initial) {}

    [[nodiscard]] Published_Frame publish_frame() {
        std::lock_guard publication_lock(publication_mutex_);
        Base::publish();
        return {Base::render_state_value(), Base::state_revision()};
    }

private:
    std::mutex publication_mutex_;
};

class Frame_Scheduler final {
    using Manual = ::Manual_Refresh_Strategy<Render_3D_Frame_Data, std::mutex>;
    using Low_Latency = ::Low_Latency_Strategy<Render_3D_Frame_Data, std::mutex>;
    using Playback = ::Flow_Refresh_Strategy<Render_3D_Frame_Data, std::mutex>;
    using Strategy = std::variant<std::unique_ptr<Manual>, std::unique_ptr<Low_Latency>, std::unique_ptr<Playback>>;

public:
    Frame_Scheduler(Frame_3D_Control_Mode mode, double maximum_frames_per_second)
        : strategy_(make_strategy(mode, maximum_frames_per_second)) {}

    [[nodiscard]] bool prepare(Logical_State_Buffer& states) {
        return std::visit(
            [&](auto& strategy) {
                auto lease = strategy->acquire_painter();
                if (!lease)
                    return false;
                auto published = states.publish_frame();
                lease->state = std::move(published.state);
                lease->revision = published.revision;
                lease->frame_sequence = lease->statistics.sequence;
                return true;
            },
            strategy_);
    }

    [[nodiscard]] bool refresh() {
        auto* manual = std::get_if<std::unique_ptr<Manual>>(&strategy_);
        return manual != nullptr && (*manual)->refresh();
    }

    [[nodiscard]] bool discard_pending() {
        if (auto* manual = std::get_if<std::unique_ptr<Manual>>(&strategy_))
            return (*manual)->discard_pending_frame();
        if (auto* low_latency = std::get_if<std::unique_ptr<Low_Latency>>(&strategy_))
            return (*low_latency)->discard_pending_frame();
        return false;
    }

    template <class Render>
    requires std::invocable<Render, const Render_3D_Frame_Data&>
    [[nodiscard]] bool render(Render&& render) {
        return std::visit(
            [&](auto& strategy) {
                auto lease = strategy->acquire_renderer();
                if (!lease)
                    return false;
                return static_cast<bool>(std::invoke(std::forward<Render>(render), static_cast<const Render_3D_Frame_Data&>(*lease)));
            },
            strategy_);
    }

private:
    static Strategy make_strategy(Frame_3D_Control_Mode mode, double maximum_frames_per_second) {
        switch (mode) {
        case Frame_3D_Control_Mode::Manual:
            return Strategy(std::in_place_type<std::unique_ptr<Manual>>, std::make_unique<Manual>());
        case Frame_3D_Control_Mode::Low_Latency:
            return Strategy(
                std::in_place_type<std::unique_ptr<Low_Latency>>,
                std::make_unique<Low_Latency>(Observer_State<>{}, typename Low_Latency::Configuration{maximum_frames_per_second}));
        case Frame_3D_Control_Mode::Playback:
            return Strategy(std::in_place_type<std::unique_ptr<Playback>>, std::make_unique<Playback>());
        }
        throw std::invalid_argument("unknown Render_3D frame control mode");
    }

    Strategy strategy_;
};

} // namespace renderive::detail
