#pragma once

#include <imgui.h>
#include <implot.h>

#include <algorithm>
#include <chrono>
#include <cstddef>
#include <vector>

#include "debug/ComponentGui.hpp"
#include "network/EntityInterpolation.hpp"
#include "world/Transform.hpp"


template<>
class tw::dbg::ComponentGui<tw::Transform> {
private:
    using Clock = std::chrono::high_resolution_clock;

    /** Ten seconds of frames at sixty a second. */
    static constexpr size_t CAPACITY = 600;

    /** How much of the trace the plot shows. */
    static constexpr float WINDOW_IN_SECONDS = 10.0f;

    inline static const Transform*   m_subject    = nullptr;
    inline static int                m_last_frame = -1;
    inline static Clock::time_point  m_started_at {};
    inline static size_t             m_head       = 0;
    inline static size_t             m_count      = 0;
    inline static std::vector<float> m_times;
    inline static std::vector<float> m_positions;

    void restart(const Transform* instance) {
        m_subject    = instance;
        m_last_frame = -1;
        m_started_at = Clock::now();
        m_head       = 0;
        m_count      = 0;

        m_times.resize(CAPACITY);
        m_positions.resize(CAPACITY);
    }

    /** Keeps one sample per frame, so drawing twice does not double up. */
    void sample(float time, float position) {
        const int frame = ImGui::GetFrameCount();
        if(m_last_frame == frame) {
            return;
        }
        m_last_frame = frame;

        m_times[m_head]     = time;
        m_positions[m_head] = position;

        m_head  = (m_head + 1) % CAPACITY;
        m_count = std::min(m_count + 1, CAPACITY);
    }

public:
    void draw(Transform* instance) {
        ImGui::SeparatorText("Drawn Position");

        static int axis = 0;
        ImGui::Combo("Axis", &axis, "X\0Y\0Z\0");

        if(m_subject != instance) {
            restart(instance);
        }

        const float time     = std::chrono::duration<float>(Clock::now() - m_started_at).count();
        const float position = instance->position()[axis];

        sample(time, position);

        ImGui::Text("%.3f", position);

        if(ImPlot::BeginPlot("Position", ImVec2(-1.0f, 180.0f))) {
            ImPlot::SetupAxes("seconds", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
            ImPlot::SetupAxisLimits(ImAxis_X1,
                time - WINDOW_IN_SECONDS, time, ImGuiCond_Always);

            // The trace wraps around, so it is read from wherever the oldest
            // sample ended up.
            const int offset = m_count == CAPACITY ? (int)m_head : 0;
            ImPlot::PlotLine("Drawn", m_times.data(), m_positions.data(),
                (int)m_count, ImPlotLineFlags_None, offset);

            ImPlot::EndPlot();
        }
    }
};

/**
 * Says whether the position above stands still because nothing new arrives for
 * the entity, or because what arrives is the same position over and over.
 */
template<>
class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
private:
    using Clock = std::chrono::high_resolution_clock;

    /** How far back the plot reaches. */
    static constexpr float WINDOW_IN_SECONDS = 10.0f;

    /** Ten seconds of frames at sixty a second. */
    static constexpr size_t CAPACITY = 600;

    inline static const net::EntityPositionInterpolation* m_subject = nullptr;

    inline static int    m_last_frame = -1;
    inline static size_t m_head       = 0;
    inline static size_t m_count      = 0;

    inline static std::vector<Clock::time_point> m_read_times;
    inline static std::vector<glm::vec3>         m_read_values;

    static int64_t millis_since(Clock::time_point point) {
        return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - point).count();
    }

    void restart(const net::EntityPositionInterpolation* instance) {
        m_subject    = instance;
        m_last_frame = -1;
        m_head       = 0;
        m_count      = 0;

        m_read_times.resize(CAPACITY);
        m_read_values.resize(CAPACITY);
    }

    /**
     * Keeps one reading per frame, so drawing twice does not double up. The
     * whole value is kept rather than the axis on show, so switching axis
     * still shows the trace that was already gathered.
     */
    void sample(Clock::time_point time, glm::vec3 value) {
        const int frame = ImGui::GetFrameCount();
        if(m_last_frame == frame) {
            return;
        }
        m_last_frame = frame;

        m_read_times[m_head]  = time;
        m_read_values[m_head] = value;

        m_head  = (m_head + 1) % CAPACITY;
        m_count = std::min(m_count + 1, CAPACITY);
    }

public:
    void draw(net::EntityPositionInterpolation* instance) {
        ImGui::SeparatorText("Received Positions");

        const size_t newest = instance->values().size() - 1;

        ImGui::Text("Newest %.3f %.3f %.3f, %ld ms ago",
            instance->values()[newest].x,
            instance->values()[newest].y,
            instance->values()[newest].z,
            millis_since(instance->times()[newest]));

        ImGui::Text("Oldest %.3f %.3f %.3f, %ld ms ago",
            instance->values()[0].x,
            instance->values()[0].y,
            instance->values()[0].z,
            millis_since(instance->times()[0]));

        static int axis = 0;
        ImGui::Combo("Axis##received", &axis, "X\0Y\0Z\0");

        /** How far behind the present the reader looks; matches the caller. */
        static int delay_in_millis = 1000;
        ImGui::SliderInt("Delay (ms)", &delay_in_millis, 0, 2000);

        if(m_subject != instance) {
            restart(instance);
        }

        // The buffer wraps, so the index says nothing about when a sample
        // arrived. Placing each one at its own age puts it where it belongs
        // without having to know where the ring currently starts. Slots
        // nothing was pushed into are left out.
        const auto   now   = Clock::now();
        const size_t count = instance->size();

        std::vector<float> ages(count);
        std::vector<float> positions(count);

        for(size_t i = 0; i < count; i++) {
            ages[i]      = -std::chrono::duration<float>(now - instance->times()[i]).count();
            positions[i] = instance->values()[i][axis];
        }

        if(ImPlot::BeginPlot("Buffer", ImVec2(-1.0f, 180.0f))) {
            ImPlot::SetupAxes("seconds ago", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
            ImPlot::SetupAxisLimits(ImAxis_X1,
                -WINDOW_IN_SECONDS, 0.0f, ImGuiCond_Always);

            ImPlot::PlotScatter("Received", ages.data(), positions.data(), (int)count);

            // Reading the same way the position is read for drawing shows
            // which two samples the delay lands between, and how far the
            // result sits from either of them.
            const auto point = now - std::chrono::milliseconds(delay_in_millis);
            const auto [from, to, alpha] = instance->get_values_around(point);

            const float read_at  = -std::chrono::duration<float>(now - point).count();
            const float read_out = glm::mix(from, to, alpha)[axis];

            ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 5.0f);
            ImPlot::PlotScatter("Interpolated", &read_at, &read_out, 1);

            // Each reading is kept at the moment it was read for, so the trace
            // ends on the marker above and runs back along the same timeline
            // the received samples sit on.
            sample(point, glm::mix(from, to, alpha));

            std::vector<float> trace_ages(m_count);
            std::vector<float> trace_positions(m_count);

            const size_t oldest = m_count == CAPACITY ? m_head : 0;

            for(size_t i = 0; i < m_count; i++) {
                const size_t index = (oldest + i) % CAPACITY;

                trace_ages[i]      = -std::chrono::duration<float>(now - m_read_times[index]).count();
                trace_positions[i] = m_read_values[index][axis];
            }

            ImPlot::PlotLine("Interpolated Trace",
                trace_ages.data(), trace_positions.data(), (int)m_count);

            ImPlot::EndPlot();
        }
    }
};
