// velocity_curve.cpp — see velocity_curve.h. Pure eval + editing/clamp/inverse map; no host types.

#include "core/instrument/engine/velocity_curve.h"

#include <algorithm>
#include <cmath>
#include <utility>

namespace reasampler::instrument::engine {

namespace {

double clampVelocity(double v) { return std::clamp(v, kVelMin, kVelMax); }
double clampValue(double a, CurveDomain d) { return std::clamp(a, curveYMin(d), kCurveYMax); }

// X spans the width for [0,127]; Y spans (height-1) rows for the domain's range with its max at
// the TOP (pixel y increases downward, so this axis is inverted relative to the value).
double velPerPixel(const VelocityCurve::Box& box) {
    const int w = std::max(0, box.width);
    if (w <= 0) return 0.0;
    return (kVelMax - kVelMin) / static_cast<double>(w);
}
double valuePerPixel(const VelocityCurve::Box& box, CurveDomain d) {
    const int h = std::max(0, box.height);
    if (h <= 1) return 0.0;
    return (kCurveYMax - curveYMin(d)) / static_cast<double>(h - 1);
}
// The integer maps are these rounded — ONE mapping, so a sub-pixel trace and an integer hit-test
// cannot drift. Rounding the OFFSET (not the absolute coordinate) keeps the int results identical
// to what they were before the sub-pixel form existed: the offset is non-negative, so truncation
// is floor regardless of where the box sits.
double velToXf(const VelocityCurve::Box& box, double velocity) {
    const int w = std::max(0, box.width);
    if (w <= 0) return static_cast<double>(box.left);
    const double frac = (clampVelocity(velocity) - kVelMin) / (kVelMax - kVelMin);
    return static_cast<double>(box.left) + frac * static_cast<double>(w);
}
double valueToYf(const VelocityCurve::Box& box, double value, CurveDomain d) {
    const int h = std::max(0, box.height);
    if (h <= 1) return static_cast<double>(box.top);
    const double lo = curveYMin(d);
    const double frac = (clampValue(value, d) - lo) / (kCurveYMax - lo);
    return static_cast<double>(box.top) + (1.0 - frac) * static_cast<double>(h - 1);
}
int velToX(const VelocityCurve::Box& box, double velocity) {
    return box.left +
           static_cast<int>(velToXf(box, velocity) - static_cast<double>(box.left) + 0.5);
}
int valueToY(const VelocityCurve::Box& box, double value, CurveDomain d) {
    return box.top +
           static_cast<int>(valueToYf(box, value, d) - static_cast<double>(box.top) + 0.5);
}

}  // namespace

VelocityCurve VelocityCurve::flat() {
    VelocityCurve c;
    const double n = curveNeutral(CurveDomain::Unipolar);
    c.points_ = {{kVelMin, n}, {kVelMax, n}};
    return c;
}

VelocityCurve VelocityCurve::linear() {
    VelocityCurve c;
    c.points_ = {{kVelMin, 0.0}, {kVelMax, kCurveYMax}};
    return c;
}

VelocityCurve VelocityCurve::zero() {
    VelocityCurve c;
    c.domain_ = CurveDomain::Bipolar;
    const double n = curveNeutral(CurveDomain::Bipolar);
    c.points_ = {{kVelMin, n}, {kVelMax, n}};
    return c;
}

VelocityCurve VelocityCurve::rampDown() {
    VelocityCurve c;
    c.points_ = {{kVelMin, kCurveYMax, false}, {kVelMax, 0.0, false}};
    return c;
}

VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts, CurveDomain domain) {
    // Stable sort so coincident-X points keep their wire order (eval stays well-defined for
    // duplicate-X knots).
    // Trim before the endpoint synthesis below can add up to two more, then again after, so a
    // corrupt over-long blob lands at exactly the ceiling with its two endpoints intact.
    if (pts.size() > kMaxCurvePoints) pts.resize(kMaxCurvePoints);
    for (VelocityPoint& p : pts) {
        p.velocity = clampVelocity(p.velocity);
        p.value = clampValue(p.value, domain);
    }
    std::stable_sort(pts.begin(), pts.end(),
                     [](const VelocityPoint& a, const VelocityPoint& b) {
                         return a.velocity < b.velocity;
                     });
    if (pts.size() < 2) {
        return domain == CurveDomain::Bipolar ? zero() : flat();
    }
    if (pts.front().velocity > kVelMin) {
        pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().value, pts.front().hard});
    } else {
        pts.front().velocity = kVelMin;
    }
    if (pts.back().velocity < kVelMax) {
        pts.push_back(VelocityPoint{kVelMax, pts.back().value, pts.back().hard});
    } else {
        pts.back().velocity = kVelMax;
    }
    if (pts.size() > kMaxCurvePoints) {
        // Drop the interior points nearest the end, never an endpoint.
        pts.erase(pts.begin() + static_cast<std::ptrdiff_t>(kMaxCurvePoints) - 1,
                  pts.end() - 1);
    }
    VelocityCurve c;
    c.domain_ = domain;
    c.points_ = std::move(pts);
    return c;
}

double VelocityCurve::eval(double velocity) const {
    if (points_.empty()) return curveNeutral(domain_);
    if (points_.size() == 1) return clampValue(points_[0].value, domain_);
    const double v = clampVelocity(velocity);
    if (v <= points_.front().velocity) return clampValue(points_.front().value, domain_);
    if (v >= points_.back().velocity) return clampValue(points_.back().value, domain_);
    // Linear walk: this overload is the COLD one (a note-on, a paint column). The per-sample
    // reader is SplineCursor, which shares the same tangent + Hermite functions.
    for (std::size_t i = 0; i + 1 < points_.size(); ++i) {
        const VelocityPoint& a = points_[i];
        const VelocityPoint& b = points_[i + 1];
        if (v >= a.velocity && v <= b.velocity) {
            const double span = b.velocity - a.velocity;
            // Coincident-X neighbours (a step): zero-width segment, no interior to blend.
            if (span <= 0.0) return clampValue(b.value, domain_);
            const double d = (b.value - a.value) / span;
            const SegmentTangents m = segmentTangents(points_.data(), points_.size(), i, d, span);
            const double y = hermiteAt(a.value, b.value, span, m.mA, m.mB,
                                       (v - a.velocity) / span);
            return clampValue(y, domain_);
        }
    }
    return clampValue(points_.back().value, domain_);  // unreachable (v is between the endpoints)
}

int VelocityCurve::addPoint(double velocity, double value) {
    // At the ceiling the add is REFUSED outright rather than trading a point away — the existing
    // contour must come through an over-add bit-identical.
    if (points_.size() >= kMaxCurvePoints) return -1;
    const VelocityPoint p{clampVelocity(velocity), clampValue(value, domain_), false};
    // First index strictly greater, so a duplicate-X point lands immediately after the existing one.
    std::size_t i = 0;
    while (i < points_.size() && points_[i].velocity <= p.velocity) ++i;
    points_.insert(points_.begin() + static_cast<std::ptrdiff_t>(i), p);
    return static_cast<int>(i);
}

bool VelocityCurve::toggleHard(std::size_t index) {
    if (index >= points_.size()) return false;
    points_[index].hard = !points_[index].hard;
    return true;
}

bool VelocityCurve::setHard(std::size_t index, bool hard) {
    if (index >= points_.size()) return false;
    points_[index].hard = hard;
    return true;
}

VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, double value) {
    if (index >= points_.size()) return VelocityPoint{};  // no-op (out of range)
    const bool isFirst = (index == 0);
    const bool isLast  = (index + 1 == points_.size());

    double newValue = clampValue(value, domain_);
    double newVel;
    if (isFirst) {
        newVel = kVelMin;
    } else if (isLast) {
        newVel = kVelMax;
    } else {
        const double lo = points_[index - 1].velocity;
        const double hi = points_[index + 1].velocity;
        newVel = std::clamp(clampVelocity(velocity), lo, hi);
    }
    points_[index] = VelocityPoint{newVel, newValue, points_[index].hard};
    return points_[index];
}

bool VelocityCurve::deletePoint(std::size_t index) {
    if (index >= points_.size()) return false;
    if (index == 0 || index + 1 == points_.size()) return false;  // endpoints are not deletable
    points_.erase(points_.begin() + static_cast<std::ptrdiff_t>(index));
    return true;
}

VelocityCurve::CurvePixel VelocityCurve::pixelFromPoint(const Box& box,
                                                       const VelocityPoint& p) const {
    return CurvePixel{velToX(box, p.velocity), valueToY(box, p.value, domain_)};
}

VelocityCurve::CurvePixelF VelocityCurve::subpixelFromPoint(const Box& box,
                                                           const VelocityPoint& p) const {
    return CurvePixelF{velToXf(box, p.velocity), valueToYf(box, p.value, domain_)};
}

VelocityPoint VelocityCurve::pointFromPixel(const Box& box, int x, int y) const {
    // Exact inverse of velToX/valueToY (within one pixel); degenerate dims collapse the same way.
    VelocityPoint p;
    const int w = std::max(0, box.width);
    const int h = std::max(0, box.height);
    p.velocity = (w <= 0)
        ? kVelMin
        : clampVelocity(kVelMin + static_cast<double>(x - box.left) / static_cast<double>(w) *
                                      (kVelMax - kVelMin));
    const double lo = curveYMin(domain_);
    p.value = (h <= 1)
        ? kCurveYMax
        : clampValue(kCurveYMax - static_cast<double>(y - box.top) / static_cast<double>(h - 1) *
                                      (kCurveYMax - lo),
                     domain_);
    return p;
}

int VelocityCurve::pointAtPixel(const Box& box, int x, int y) const {
    for (std::size_t i = 0; i < points_.size(); ++i) {
        const int px = velToX(box, points_[i].velocity);
        const int py = valueToY(box, points_[i].value, domain_);
        if (std::abs(x - px) <= kCurveNodeGrabRadius && std::abs(y - py) <= kCurveNodeGrabRadius) {
            return static_cast<int>(i);
        }
    }
    return -1;
}

VelocityCurve VelocityCurve::resolvePointDrag(const VelocityCurve& grabCurve, std::size_t index,
                                              const Box& box, int dxPixels, int dyPixels) {
    VelocityCurve out = grabCurve;
    if (index >= out.points_.size()) return out;  // out of range -> no motion
    const double velPerPx = velPerPixel(box);
    const double valPerPx = valuePerPixel(box, grabCurve.domain_);
    if (velPerPx <= 0.0 || valPerPx <= 0.0) return out;  // degenerate box -> no motion

    const VelocityPoint& grab = grabCurve.points_[index];
    const double newVel = grab.velocity + static_cast<double>(dxPixels) * velPerPx;
    // Y increases downward but the value increases upward, so a downward drag (positive dy)
    // LOWERS the value.
    const double newValue = grab.value - static_cast<double>(dyPixels) * valPerPx;
    out.movePoint(index, newVel, newValue);  // applies box + neighbour-X + endpoint-pin clamps
    return out;
}

bool VelocityCurve::equals(const VelocityCurve& other, double eps) const {
    if (domain_ != other.domain_) return false;
    if (points_.size() != other.points_.size()) return false;
    for (std::size_t i = 0; i < points_.size(); ++i) {
        if (std::fabs(points_[i].velocity - other.points_[i].velocity) > eps) return false;
        if (std::fabs(points_[i].value - other.points_[i].value) > eps) return false;
        if (points_[i].hard != other.points_[i].hard) return false;
    }
    return true;
}

}  // namespace reasampler::instrument::engine
