// velocity_curve.h — velocity->amp transfer curve. eval(velocity) is called once per note-on
// in Voice::start(), never per frame. Editor hit-test/inverse-map take an explicit pixel Box
// rather than a Rect: this module sits below sampler_core in the link graph and must not gain
// a transitive dependency on editor-layout types.

#pragma once

#include <cstdint>
#include <vector>

namespace reasampler::instrument::engine {

// The MIDI velocity domain [0,127] and the amp range [0,1] — the box every point clamps into.
inline constexpr double kVelMin = 0.0;
inline constexpr double kVelMax = 127.0;
inline constexpr double kAmpMin = 0.0;
inline constexpr double kAmpMax = 1.0;

// A raw-constructed point is NOT auto-clamped (the mutators own that invariant) — build curves
// through the named constructors / addPoint rather than pushing raw points.
struct VelocityPoint {
    double velocity = 0.0;   // X, [0,127]
    double amp      = 0.0;   // Y, [0,1]
};

// Pick radius (px) around a node's drawn point for the editor hit-test.
inline constexpr int kCurveNodeGrabRadius = 6;

// An X-ordered list of control points spanning [0,127], evaluated by a monotone cubic Hermite
// spline (Fritsch-Carlson slope limiting) — a genuine curve, not a polyline, that provably never
// overshoots a segment's amp range. For collinear knots the tangents reduce to the secant slope,
// so the spline reproduces linear()'s straight line to within ~1e-15. The two endpoints (velocity
// 0 and 127) are load-bearing: they keep eval total over the domain and are never deletable.
class VelocityCurve {
public:
    // flat() (endpoints (0,1)/(127,1), every velocity -> unity) is the default — see
    // velocity_curve in the directory CLAUDE.md for why this isn't bit-identical to the
    // pre-existing linear() response.
    static VelocityCurve flat();
    static VelocityCurve linear();

    // Rebuilds from a deserialized point list, repairing the invariant defensively: box-clamps
    // each point, stable-sorts by velocity, forces both endpoints present (synthesized if
    // missing), falls back to flat() if fewer than 2 usable points remain. A corrupt/truncated
    // blob yields a well-formed curve, never an invariant-violating one.
    static VelocityCurve fromPoints(std::vector<VelocityPoint> pts);

    const std::vector<VelocityPoint>& points() const { return points_; }
    std::size_t size() const { return points_.size(); }

    // Degenerate cases (shouldn't occur post-construction): empty curve returns kAmpMax; a
    // one-point curve returns that point's amp.
    double eval(double velocity) const;

    // Inserted at a velocity duplicating an existing point lands immediately after it, so a
    // subsequent move can separate them. Returns the inserted index.
    std::size_t addPoint(double velocity, double amp);

    // Box-clamped and X-clamped between immediate neighbours (monotonic-X grammar). The two
    // endpoints are pinned in X (only their amp moves); out-of-range index is a no-op.
    VelocityPoint movePoint(std::size_t index, double velocity, double amp);

    // Endpoints (index 0 and last) are not deletable; that or an out-of-range index is a no-op
    // returning false.
    bool deletePoint(std::size_t index);

    // The drawn box, in pixels: X = velocity across the width, Y = amp UP the height (amp 1 at
    // top). Passed explicitly rather than a Rect — see header preamble.
    struct Box {
        int left = 0;
        int top = 0;
        int width = 0;
        int height = 0;
    };

    // Index of the first point within the pick radius on both axes, or -1 for a miss. First-match
    // in point order for determinism.
    int pointAtPixel(const Box& box, int x, int y) const;

    // The one point->pixel mapping, exposed so drawing and hit-testing can never drift apart.
    struct CurvePixel {
        int x = 0;
        int y = 0;
    };
    static CurvePixel pixelFromPoint(const Box& box, const VelocityPoint& p);

    // Exact inverse of pixelFromPoint (within the one-pixel quantum) — where an empty-space click
    // lands as a new point. Degenerate box: zero-width reads velocity 0; height <= 1 reads amp 1.
    static VelocityPoint pointFromPixel(const Box& box, int x, int y);

    // `grabCurve` is the curve as of mouse-down (shell snapshots it so the delta is absolute).
    // Maps the pixel delta to velocity/amp over the box, then applies movePoint's clamp. Zero
    // width/height box or out-of-range index returns grabCurve unchanged.
    static VelocityCurve resolvePointDrag(const VelocityCurve& grabCurve, std::size_t index,
                                          const Box& box, int dxPixels, int dyPixels);

    bool equals(const VelocityCurve& other, double eps = 1e-9) const;

private:
    // Always X-ordered with an endpoint at 0 and 127; constructors + deserialize establish the
    // invariant, mutators preserve it.
    std::vector<VelocityPoint> points_;
};

}  // namespace reasampler::instrument::engine
