#include "core/bezier_editor.h"
#include "core/tokens.h"

#include <imgui.h>
#include <imgui_internal.h>

#include <cmath>
#include <cstdio>

namespace fn {

// ---------------------------------------------------------------------------
// Pure evaluation
// ---------------------------------------------------------------------------

ImVec2 bezier_point(const BezierCurve& c, float u) {
    // De Casteljau cubica: 3 niveles de lerp.
    auto lerp2 = [](ImVec2 a, ImVec2 b, float t) {
        return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
    };
    ImVec2 q0 = lerp2(c.p0, c.p1, u);
    ImVec2 q1 = lerp2(c.p1, c.p2, u);
    ImVec2 q2 = lerp2(c.p2, c.p3, u);
    ImVec2 r0 = lerp2(q0, q1, u);
    ImVec2 r1 = lerp2(q1, q2, u);
    return lerp2(r0, r1, u);
}

float bezier_eval(const BezierCurve& c, float t) {
    // Approxima y(x=t) muestreando la curva en N puntos uniformes en u y
    // buscando el segmento donde la X cae cerca de t. Suficiente para
    // easing curves (monotonas en X por construccion).
    if (t <= 0.0f) return c.p0.y;
    if (t >= 1.0f) return c.p3.y;

    constexpr int N = 64;
    ImVec2 prev = c.p0;
    for (int i = 1; i <= N; i++) {
        float u = (float)i / (float)N;
        ImVec2 cur = bezier_point(c, u);
        if (cur.x >= t) {
            // Interp lineal en X dentro del segmento [prev, cur].
            float dx = cur.x - prev.x;
            float k  = (dx > 1e-6f) ? (t - prev.x) / dx : 0.0f;
            return prev.y + (cur.y - prev.y) * k;
        }
        prev = cur;
    }
    return c.p3.y;
}

// ---------------------------------------------------------------------------
// Editor widget
// ---------------------------------------------------------------------------

namespace {

// Convierte coords espacio-curva [0..1] a pixel-space del canvas.
ImVec2 to_canvas(const ImVec2& p, const ImVec2& canvas_min, const ImVec2& canvas_size) {
    // Y se invierte porque ImGui crece hacia abajo.
    return ImVec2(canvas_min.x + p.x * canvas_size.x,
                  canvas_min.y + (1.0f - p.y) * canvas_size.y);
}

// Inverso: pixel-space -> espacio-curva.
ImVec2 to_curve(const ImVec2& p, const ImVec2& canvas_min, const ImVec2& canvas_size) {
    float fx = (p.x - canvas_min.x) / canvas_size.x;
    float fy = 1.0f - (p.y - canvas_min.y) / canvas_size.y;
    return ImVec2(fx, fy);
}

// Drag handle invisible centrado en `pos` con radio `r`. Devuelve true si se
// arrastro este frame; en ese caso `out` contiene la nueva pos en espacio
// curva.
bool drag_handle(const char* id,
                 const ImVec2& pos,
                 const ImVec2& canvas_min,
                 const ImVec2& canvas_size,
                 float r,
                 ImVec2& out)
{
    ImVec2 px = to_canvas(pos, canvas_min, canvas_size);
    ImGui::SetCursorScreenPos(ImVec2(px.x - r, px.y - r));
    ImGui::InvisibleButton(id, ImVec2(r * 2.0f, r * 2.0f));
    bool changed = false;
    if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
        ImVec2 mouse = ImGui::GetIO().MousePos;
        out = to_curve(mouse, canvas_min, canvas_size);
        changed = true;
    }
    return changed;
}

} // namespace

bool bezier_editor(const char* id, BezierCurve& curve, ImVec2 size, bool lock_endpoints) {
    using namespace fn_tokens;

    ImGui::PushID(id);

    // Reservar canvas
    ImVec2 canvas_min  = ImGui::GetCursorScreenPos();
    ImVec2 canvas_size = size;
    if (canvas_size.x <= 0.0f) canvas_size.x = ImGui::GetContentRegionAvail().x;
    if (canvas_size.y <= 0.0f) canvas_size.y = 200.0f;
    ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x,
                               canvas_min.y + canvas_size.y);

    ImDrawList* dl = ImGui::GetWindowDrawList();

    // Background + border
    dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm);
    dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm);

    // Grid sutil (4x4)
    ImU32 grid_col = ImGui::GetColorU32(colors::border);
    for (int i = 1; i < 4; i++) {
        float fx = canvas_min.x + canvas_size.x * (float)i / 4.0f;
        float fy = canvas_min.y + canvas_size.y * (float)i / 4.0f;
        dl->AddLine(ImVec2(fx, canvas_min.y), ImVec2(fx, canvas_max.y), grid_col);
        dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid_col);
    }

    // Diagonal de referencia (linear)
    ImU32 diag_col = ImGui::GetColorU32(colors::text_dim);
    dl->AddLine(to_canvas({0,0}, canvas_min, canvas_size),
                to_canvas({1,1}, canvas_min, canvas_size), diag_col, 1.0f);

    // Lineas tangentes p0->p1 y p3->p2
    ImU32 tang_col = ImGui::GetColorU32(colors::text_muted);
    dl->AddLine(to_canvas(curve.p0, canvas_min, canvas_size),
                to_canvas(curve.p1, canvas_min, canvas_size), tang_col, 1.0f);
    dl->AddLine(to_canvas(curve.p3, canvas_min, canvas_size),
                to_canvas(curve.p2, canvas_min, canvas_size), tang_col, 1.0f);

    // La curva Bezier
    ImU32 curve_col = ImGui::GetColorU32(colors::primary);
    dl->AddBezierCubic(
        to_canvas(curve.p0, canvas_min, canvas_size),
        to_canvas(curve.p1, canvas_min, canvas_size),
        to_canvas(curve.p2, canvas_min, canvas_size),
        to_canvas(curve.p3, canvas_min, canvas_size),
        curve_col, 2.0f, 0);

    // Handles
    ImU32 handle_col      = ImGui::GetColorU32(colors::primary);
    ImU32 handle_locked   = ImGui::GetColorU32(colors::text_dim);
    constexpr float r = 6.0f;

    bool changed = false;
    ImVec2 np;

    // p0
    if (lock_endpoints) {
        ImVec2 px = to_canvas(curve.p0, canvas_min, canvas_size);
        dl->AddCircleFilled(px, r, handle_locked);
    } else {
        if (drag_handle("##p0", curve.p0, canvas_min, canvas_size, r, np)) {
            curve.p0 = np; changed = true;
        }
        ImVec2 px = to_canvas(curve.p0, canvas_min, canvas_size);
        dl->AddCircleFilled(px, r, handle_col);
    }
    // p3
    if (lock_endpoints) {
        ImVec2 px = to_canvas(curve.p3, canvas_min, canvas_size);
        dl->AddCircleFilled(px, r, handle_locked);
    } else {
        if (drag_handle("##p3", curve.p3, canvas_min, canvas_size, r, np)) {
            curve.p3 = np; changed = true;
        }
        ImVec2 px = to_canvas(curve.p3, canvas_min, canvas_size);
        dl->AddCircleFilled(px, r, handle_col);
    }
    // p1 (draggable, sin clamp para permitir overshoot)
    if (drag_handle("##p1", curve.p1, canvas_min, canvas_size, r, np)) {
        // Clamp solo X a [0,1] para mantener monotonia razonable; Y libre.
        np.x = (np.x < 0.0f) ? 0.0f : (np.x > 1.0f ? 1.0f : np.x);
        curve.p1 = np; changed = true;
    }
    {
        ImVec2 px = to_canvas(curve.p1, canvas_min, canvas_size);
        dl->AddCircleFilled(px, r, handle_col);
    }
    // p2
    if (drag_handle("##p2", curve.p2, canvas_min, canvas_size, r, np)) {
        np.x = (np.x < 0.0f) ? 0.0f : (np.x > 1.0f ? 1.0f : np.x);
        curve.p2 = np; changed = true;
    }
    {
        ImVec2 px = to_canvas(curve.p2, canvas_min, canvas_size);
        dl->AddCircleFilled(px, r, handle_col);
    }

    // Avanzar el cursor por debajo del canvas para que el siguiente widget no
    // se solape (los InvisibleButton anteriores movieron el cursor).
    ImGui::SetCursorScreenPos(ImVec2(canvas_min.x, canvas_max.y + spacing::xs));
    ImGui::Dummy(ImVec2(canvas_size.x, 1.0f));

    ImGui::PopID();
    return changed;
}

} // namespace fn
