#include "spline.h"
#include "stb_ds.h"
#include "transform.h"
#include "math.h"

/* -------------------------------------------------------------------------
   Cubic Spline Basis Matrices
   ------------------------------------------------------------------------- */
static const HMM_Mat4 cubic_hermite_m = {
    2,  -2,  1,  1,
   -3,   3, -2, -1,
    0,   0,  1,  0,
    1,   0,  0,  0
};

static const HMM_Mat4 cubic_hermite_dm = {
    0,   0,  0,  0,
    6,  -6,  3,  3,
   -6,   6, -4, -2,
    0,   0,  1,  0
};

static const HMM_Mat4 cubic_hermite_ddm = {
    0,   0,  0,  0,
    0,   0,  0,  0,
   12, -12,  6,  6,
   -6,   6, -4, -2
};

static const HMM_Mat4 cubic_hermite_dddm = {
    0,   0,  0,  0,
    0,   0,  0,  0,
    0,   0,  0,  0,
   12, -12,  6,  6
};

static const HMM_Mat4 b_spline_m = {
  -1.0f/6,  3.0f/6, -3.0f/6,  1.0f,
   3.0f/6, -6.0f/6,  3.0f/6,  0.0f,
  -3.0f/6,  0.0f,    3.0f/6,  0.0f,
   1.0f/6,  4.0f/6,  1.0f/6,  0.0f
};

static const HMM_Mat4 b_spline_dm = {
   0,     0,     0,    0,
  -3.0f/6, 9.0f/6, -9.0f/6, 3.0f,
   6.0f/6, -12.0f/6, 6.0f/6, 0.0f,
  -3.0f/6,  0.0f,   3.0f/6, 0.0f
};

static const HMM_Mat4 b_spline_ddm = {
   0,    0,    0,    0,
   0,    0,    0,    0,
  -6.0f/6, 18.0f/6, -18.0f/6, 6.0f,
   6.0f/6, -12.0f/6,  6.0f/6, 0.0f
};

static const HMM_Mat4 b_spline_dddm = {
   0,    0,    0,    0,
   0,    0,    0,    0,
   0,    0,    0,    0,
  -6.0f/6, 18.0f/6, -18.0f/6, 6.0f
};

static const HMM_Mat4 bezier_m = {
   -1,  3, -3,  1,
    3, -6,  3,  0,
   -3,  3,  0,  0,
    1,  0,  0,  0
};

static const HMM_Mat4 bezier_dm = {
    0,  0,  0,  0,
   -3,  9, -9,  3,
    6, -12,  6,  0,
   -3,  3,  0,  0
};

static const HMM_Mat4 bezier_ddm = {
    0,  0,   0,  0,
    0,  0,   0,  0,
   -6, 18, -18,  6,
    6, -12,  6,  0
};

static const HMM_Mat4 bezier_dddm = {
    0,  0,  0,  0,
    0,  0,  0,  0,
    0,  0,  0,  0,
   -6, 18, -18,  6
};

/* Catmull–Rom (with tension = 0.5 by default) */
#define CAT_S 0.5f

static const HMM_Mat4 catmull_rom_m = {
  -CAT_S,       2-CAT_S,      CAT_S-2,  CAT_S,
   2*CAT_S,     CAT_S-3,      3-2*CAT_S, -CAT_S,
  -CAT_S,       0,            CAT_S,     0,
   0,           1,            0,         0
};

static const HMM_Mat4 catmull_rom_dm = {
    0,          0,          0,          0,
  -3*CAT_S,   9*CAT_S,    -9*CAT_S,    3*CAT_S,
   4*CAT_S,  -10*CAT_S,    8*CAT_S,   -2*CAT_S,
  -CAT_S,      0,          CAT_S,      0
};

static const HMM_Mat4 catmull_rom_ddm = {
    0,        0,         0,         0,
    0,        0,         0,         0,
  -9*CAT_S, 18*CAT_S, -18*CAT_S,  6*CAT_S,
   4*CAT_S, -10*CAT_S, 8*CAT_S,  -2*CAT_S
};

static const HMM_Mat4 catmull_rom_dddm = {
    0,         0,         0,         0,
    0,         0,         0,         0,
    0,         0,         0,         0,
  -9*CAT_S, 18*CAT_S, -18*CAT_S,  6*CAT_S
};

/* -------------------------------------------------------------------------
   Core “C·T” multiplication:  [ t^3, t^2, t, 1 ] * C
   ------------------------------------------------------------------------- */
HMM_Vec4 spline_CT(HMM_Mat4 *C, float t)
{
    float t2 = t * t;
    float t3 = t2 * t;
    HMM_Vec4 T = { t3, t2, t, 1.0f };
    return HMM_MulM4V4(*C, T);
}

/* Construct the “geometry matrix” G from four 2D points, then multiply by B */
HMM_Mat4 make_C(const HMM_Vec2 *p, const HMM_Mat4 *B)
{
    HMM_Mat4 G = HMM_M4(); // Zeroed out
    // Only fill XY of each column; if you are storing 3D in HMM_Vec4, adapt as needed
    G.Columns[0].XY = p[0];
    G.Columns[1].XY = p[1];
    G.Columns[2].XY = p[2];
    G.Columns[3].XY = p[3];

    return HMM_MulM4(G, *B);
}

/* Evaluate a single-segment cubic spline at parameter d in [0,1].
   p must be 4 control points, m is the cubic basis matrix. */
HMM_Vec2 cubic_spline_d(HMM_Vec2 *p, HMM_Mat4 *m, float d)
{
    HMM_Mat4 C = make_C(p, m);
    HMM_Vec4 v4 = spline_CT(&C, d);
    return v4.XY;
}

/* -------------------------------------------------------------------------
   Convenience single-segment functions for each basis
   (pos / tan / curv / wig all require the appropriate matrix)
   Typically you pass p[0..3] as the 4 relevant control points.
   ------------------------------------------------------------------------- */
HMM_Vec2 cubic_hermite_pos(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&cubic_hermite_m, d); }
HMM_Vec2 cubic_hermite_tan(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&cubic_hermite_dm, d); }
HMM_Vec2 cubic_hermite_curv(HMM_Vec2 *p, float d){ return cubic_spline_d(p, (HMM_Mat4 *)&cubic_hermite_ddm, d); }
HMM_Vec2 cubic_hermite_wig(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&cubic_hermite_dddm, d); }

HMM_Vec2 b_spline_pos(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&b_spline_m, d); }
HMM_Vec2 b_spline_tan(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&b_spline_dm, d); }
HMM_Vec2 b_spline_curv(HMM_Vec2 *p, float d){ return cubic_spline_d(p, (HMM_Mat4 *)&b_spline_ddm, d); }
HMM_Vec2 b_spline_wig(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&b_spline_dddm, d); }

HMM_Vec2 bezier_pos(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&bezier_m, d); }
HMM_Vec2 bezier_tan(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&bezier_dm, d); }
HMM_Vec2 bezier_curv(HMM_Vec2 *p, float d){ return cubic_spline_d(p, (HMM_Mat4 *)&bezier_ddm, d); }
HMM_Vec2 bezier_wig(HMM_Vec2 *p, float d) { return cubic_spline_d(p, (HMM_Mat4 *)&bezier_dddm, d); }

/* -------------------------------------------------------------------------
   Multi-segment sampling (“spline_v2”) for uniform division
   ------------------------------------------------------------------------- */
HMM_Vec2 *spline_v2(HMM_Vec2 *p, HMM_Mat4 *m, int segs)
{
    // For a single 4-point segment, produce 'segs' points along [0..1).
    // If you want the final 1.0 also, you can do <= 1.0 in the loop, etc.
    HMM_Vec2 *ret = NULL;
    if (segs < 2) return NULL;

    HMM_Mat4 C = make_C(p, m);
    float s = 1.0f / (float)segs;
    for (int i = 0; i <= segs; i++)
    {
        float t = s * i;
        arrput(ret, spline_CT(&C, t).XY);
    }
    return ret;
}

/* -------------------------------------------------------------------------
   Adaptive subdivision by min segment length
   (spline2d_min_seg) – for a single 4-point segment
   ------------------------------------------------------------------------- */
HMM_Vec2 *spline2d_min_seg(float u0, float u1, float min_seg, HMM_Mat4 *C, HMM_Vec2 *ret)
{
    HMM_Vec2 a = spline_CT(C, u0).XY;
    HMM_Vec2 b = spline_CT(C, u1).XY;
    if (HMM_DistV2(a, b) > min_seg)
    {
        float umid = 0.5f * (u0 + u1);
        spline2d_min_seg(u0,  umid, min_seg, C, ret);
        spline2d_min_seg(umid, u1,   min_seg, C, ret);
    }
    else
    {
        // We push 'b' so that we don't double-push 'a'
        arrput(ret, b);
    }
    return ret;
}

/* Example: catmull_rom_min_seg -> subdiv for catmull–rom over one segment
   You would decide how to pick the 4 points from a,b,c,d, then run. */
HMM_Vec2 *catmull_rom_min_seg(HMM_Vec2 *a, HMM_Vec2 *b, HMM_Vec2 *c, HMM_Vec2 *d, float min_seg)
{
    HMM_Vec2 *ret = NULL;
    arrsetcap(ret, 1000);

    // Build the matrix for these four points
    HMM_Vec2 p[4] = { *a, *b, *c, *d };
    HMM_Mat4 C = make_C(p, &catmull_rom_m);

    // Always push the starting point (b in your original code was the second ctrl point, etc.)
    // But usually we want the first actual point in the segment:
    arrput(ret, cubic_spline_d(p, (HMM_Mat4*)&catmull_rom_m, 0.0f));

    // Actually subdiv
    spline2d_min_seg(0.0f, 1.0f, min_seg, &C, ret);

    return ret;
}

/* -------------------------------------------------------------------------
   Adaptive subdivision by “max angle” proxy
   (spline2d_min_angle_2) – for single 4-point segment
   ------------------------------------------------------------------------- */
HMM_Vec2 *spline2d_min_angle_2(float u0, float u1, float max_angle, HMM_Mat4 *C, HMM_Vec2 *arr)
{
    // Heuristic approach: sample midpoints, check “chord vs polyline” difference
    float ustep = (u1 - u0) / 4.0f;
    float um0 = u0 + ustep;
    float um1 = u0 + 2.0f * ustep;
    float um2 = u0 + 3.0f * ustep;

    HMM_Vec2 m0 = spline_CT(C, um0).XY;
    HMM_Vec2 m1 = spline_CT(C, um1).XY;
    HMM_Vec2 m2 = spline_CT(C, um2).XY;

    HMM_Vec2 a  = spline_CT(C, u0).XY;
    HMM_Vec2 b  = spline_CT(C, u1).XY;

    // Chord = distance from a to b
    float chord   = HMM_DistV2(a, b);
    // Polyline = a->m0->m1->m2->b
    float cdist   = HMM_DistV2(a,  m0)
                  + HMM_DistV2(m0, m1)
                  + HMM_DistV2(m1, m2)
                  + HMM_DistV2(m2, b);

    // If the difference is bigger than some threshold (max_angle),
    // subdivide. Otherwise, keep it.
    if ((cdist - chord) > max_angle)
    {
        arr = spline2d_min_angle_2(u0,  um1, max_angle, C, arr);
        arr = spline2d_min_angle_2(um1, u1,  max_angle, C, arr);
    }
    else
    {
        // We accept “b” as a new point
        arrput(arr, b);
    }
    return arr;
}

HMM_Vec2 *spline_min_angle(HMM_Vec2 *p, const HMM_Mat4 *B, float min_angle, HMM_Vec2 *arr)
{
    HMM_Mat4 C = make_C(p, B);
    // Subdivide from 0..1
    float u0 = 0.0f, u1 = 1.0f;
    // Usually we want to ensure the start point is in arr:
    HMM_Vec2 startPt = spline_CT(&C, u0).XY;
    if (arrlen(arr) == 0) {
        arrput(arr, startPt);
    }
    // Now subdiv for angle
    arr = spline2d_min_angle_2(u0, u1, min_angle, &C, arr);
    return arr;
}

/* Example: catmull_rom_ma_v2 – uses “min_angle” over multiple segments 
   Each 4 consecutive points is one segment. We do this for all segments. */
HMM_Vec2 *catmull_rom_ma_v2(HMM_Vec2 *cp, float ma)
{
    int n = arrlen(cp);
    if (n < 4) return NULL;

    HMM_Vec2 *ret = NULL;
    // Pre-allocate some capacity
    arrsetcap(ret, (n-3) * 8);

    // For convenience, let's always ensure we push the very first point:
    arrput(ret, cp[0]);

    // For each segment [i, i+1, i+2, i+3], adaptively sample
    // Then move i by 1 each time if you want Catmull–Rom in “overlapped” fashion
    for (int i = 0; i < n - 3; i++)
    {
        // p[i..i+3]
        ret = spline_min_angle(&cp[i], &catmull_rom_m, ma, ret);
    }
    return ret;
}

/* Example: do the same with Bezier in “cubic-bezier” style (control points in groups of 3 “handles”) */
HMM_Vec2 *bezier_cb_ma_v2(HMM_Vec2 *cp, float ma)
{
    int n = arrlen(cp);
    // Typically a Bezier “chain” would use control points in multiples of 3 + 1, etc.
    // E.g. p[0] is start, p[1..3] are control handles for first segment, then p[3..6] for second, etc.
    // Adjust logic to your liking.
    if (n < 4) return NULL;

    HMM_Vec2 *ret = NULL;
    arrsetcap(ret, (n/3) * 8);

    // First point
    arrput(ret, cp[0]);

    // For each cubic Bezier segment: i += 3
    for (int i = 0; i < n - 3; i += 3)
    {
        ret = spline_min_angle(&cp[i], &bezier_m, ma, ret);
    }
    return ret;
}

static HMM_Vec2 catmull_rom_query_internal(HMM_Vec2 *cp, float d, const HMM_Mat4 *M)
{
    int n = arrlen(cp);
    if (n < 4) return HMM_V2(0,0);

    // Number of segments:
    int seg_count = n - 3;
    // Scale d in [0..1] -> which segment?
    float segf = d * seg_count;
    int seg_idx = (int) floorf(segf);
    if (seg_idx >= seg_count) seg_idx = seg_count - 1;
    if (seg_idx < 0) seg_idx = 0;

    // Local parameter in [0..1]
    float u = segf - seg_idx;

    // The control points for that segment are cp[ seg_idx .. seg_idx+3 ]
    return cubic_spline_d(cp + seg_idx, (HMM_Mat4 *)M, u);
}

HMM_Vec2 catmull_rom_pos(HMM_Vec2 *cp, float d)
{
    return catmull_rom_query_internal(cp, d, &catmull_rom_m);
}
HMM_Vec2 catmull_rom_tan(HMM_Vec2 *cp, float d)
{
    return catmull_rom_query_internal(cp, d, &catmull_rom_dm);
}
HMM_Vec2 catmull_rom_curv(HMM_Vec2 *cp, float d)
{
    return catmull_rom_query_internal(cp, d, &catmull_rom_ddm);
}
HMM_Vec2 catmull_rom_wig(HMM_Vec2 *cp, float d)
{
    return catmull_rom_query_internal(cp, d, &catmull_rom_dddm);
}

/* -------------------------------------------------------------------------
   Approximate length of a single 4-point cubic spline segment by
   numeric integration (or sampling) of the velocity magnitude.
   “spline_seglen” below does a quick sampling approach. 
   ------------------------------------------------------------------------- */
float spline_seglen(float t0, float t1, int steps, HMM_Mat4 *Cd, HMM_Mat4 *C)
{
    // Simple uniform sampling of the tangent magnitude
    float total = 0.0f;
    float dt = (t1 - t0) / (float) steps;
    for (int i = 0; i < steps; i++)
    {
        float t = t0 + (i + 0.5f) * dt; // midpoint rule
        // derivative at t
        HMM_Vec2 vel = spline_CT(Cd, t).XY;
        float speed = HMM_LenV2(vel);
        total += speed * dt;
    }
    return total;
}

/* Summation of lengths across all Catmull–Rom segments. */
float catmull_rom_len(HMM_Vec2 *cp)
{
    int stepsPerSegment = 64;
    float len = 0.0f;
    int n = arrlen(cp);
    if (n < 4) return 0.0f;

    for (int i = 0; i < n - 3; i++)
    {
        // Build the position matrix & derivative matrix for this segment
        HMM_Mat4 C  = make_C(&cp[i], &catmull_rom_m);
        HMM_Mat4 Cd = make_C(&cp[i], &catmull_rom_dm);
        // integrate from 0..1
        len += spline_seglen(0.0f, 1.0f, stepsPerSegment, &Cd, &C);
    }
    return len;
}

HMM_Vec2 catmull_rom_closest(HMM_Vec2 *cp, HMM_Vec2 p)
{
    int n = arrlen(cp);
    if (n < 4) return p;

    float bestDist = FLT_MAX;
    HMM_Vec2 bestPt = p;

    int steps = 64; // more steps => more accurate
    for (int seg = 0; seg < n - 3; seg++)
    {
        // Build a single-segment matrix
        HMM_Vec2 segCP[4] = { cp[seg], cp[seg+1], cp[seg+2], cp[seg+3] };
        HMM_Mat4 C  = make_C(segCP, &catmull_rom_m);
        for (int i = 0; i <= steps; i++)
        {
            float t = (float)i / steps;
            HMM_Vec2 pt = spline_CT(&C, t).XY;
            float dist  = HMM_DistV2(p, pt);
            if (dist < bestDist)
            {
                bestDist = dist;
                bestPt   = pt;
            }
        }
    }

    return bestPt;
}
