#pragma once
// curve_law — the ONE per-segment envelope curve law: the exponent domain, the map from a
// stage's normalized position to its normalized level, and that map's inverse (mid-segment is
// the special case). Header-only and dependency-free so the engine evaluator, the overlay's
// forward map, and its inverse all read the same law rather than three copies.

#include <cmath>

namespace reasampler::util {

// The per-segment curve is exponential: level = phi^exponent over the stage's normalized
// position phi. 1.0 is the LINEAR neutral (phi^1 == phi), which is why a pre-existing
// instance loading at 1.0 plays exactly as it did.
inline constexpr double kCurveNeutral = 1.0;
inline constexpr double kCurveMin = 0.1;
inline constexpr double kCurveMax = 10.0;

// Normalized position -> normalized level. The neutral exponent is compared EXACTLY so the
// at-rest per-sample path pays one predicted branch instead of a transcendental; every
// positive exponent maps 0 -> 0 and 1 -> 1, so a curved stage can never overshoot its own
// endpoint levels.
//
// MEASURED (Release, MSVC 19.44 x64, one dev machine): a non-neutral exponent costs ~4.7 ns per
// evaluation. At the 16-voice default with all three envelopes in a sloped stage — three
// evaluations per voice per frame, the worst case — that is +224 ns per output frame: 3.1% of
// one core at 44.1 kHz becomes 4.1%. Affordable at the shipped voice ceiling; re-measure before
// putting a fourth per-voice curve on the frame.
inline double curveMap(double phi, double exponent) {
    if (exponent == kCurveNeutral) return phi;
    return std::pow(phi, exponent);
}

inline double clampCurve(double exponent) {
    if (!(exponent >= kCurveMin)) return kCurveMin;  // also catches NaN
    return exponent > kCurveMax ? kCurveMax : exponent;
}

// The exponent's KNOB travel: logarithmic, so the two halves of the throw are the reciprocal
// shaping directions. Written as kCurveMax^(2t-1) rather than as an interpolation between
// log(kCurveMin) and log(kCurveMax) so t == 0.5 evaluates exp(0) == 1.0 EXACTLY — an inexact
// centre would put std::pow on the per-sample path for a stage the user believes is parked at
// the identity. Requires kCurveMin == 1/kCurveMax, which the domain above satisfies.
//
// The detent is what makes the identity REACHABLE: a knob drag delivers start - dy/128, a grid
// that lands on 0.5 only by luck, so a band wider than one drag step (1/128) snaps to neutral
// and a dial swept through the centre cannot skip over it.
inline constexpr double kCurveKnobDetent = 0.01;

// The same travel with the detent NOT applied — the map for a writer that has no drag grid. A
// host automation lane delivers a NUMBER, not a gesture, so snapping it would not make the
// identity reachable (t == 0.5 already evaluates exp(0) == 1.0 exactly here); it would only
// destroy a near-neutral exponent the user set some other way.
inline double curveFromKnobNormUndetented(double norm) {
    const double t = norm < 0.0 ? 0.0 : (norm > 1.0 ? 1.0 : norm);
    return clampCurve(std::exp((2.0 * t - 1.0) * std::log(kCurveMax)));
}

inline double curveFromKnobNorm(double norm) {
    const double t = norm < 0.0 ? 0.0 : (norm > 1.0 ? 1.0 : norm);
    const double off = t - 0.5;
    if (off < kCurveKnobDetent && off > -kCurveKnobDetent) return kCurveNeutral;
    return curveFromKnobNormUndetented(t);
}

inline double knobNormFromCurve(double exponent) {
    const double t = 0.5 + std::log(clampCurve(exponent)) / (2.0 * std::log(kCurveMax));
    return t < 0.0 ? 0.0 : (t > 1.0 ? 1.0 : t);
}

// The normalized level at an arbitrary segment position phi in (0,1), and its inverse. A
// knot's DRAWN x truncates to an integer, which lands it off phi = 0.5 whenever its segment's
// pixel span is odd; reading the knot's y through the phi its own x actually implies (rather
// than assuming 0.5) is what keeps the knot on the trace its own vertices draw.
inline double curveLevelAt(double phi, double exponent) { return curveMap(phi, clampCurve(exponent)); }

// Level -> exponent at phi: u = phi^p, so p = ln(u)/ln(phi). Out-of-domain u clamps to the
// exponent endpoints rather than producing a non-finite exponent.
inline double curveFromLevelAt(double phi, double level) {
    const double lo = curveLevelAt(phi, kCurveMax);  // smallest reachable level at this phi
    const double hi = curveLevelAt(phi, kCurveMin);  // largest
    if (!(level > lo)) return kCurveMax;             // also catches NaN
    if (level >= hi) return kCurveMin;
    return clampCurve(std::log(level) / std::log(phi));
}

// The segment-MIDPOINT (phi = 0.5) case — the knot's placement whenever its pixel span is
// even. Kept under its own name for the existing callers/tests that assume that case.
inline double curveMidLevel(double exponent) { return curveLevelAt(0.5, exponent); }
inline double curveFromMidLevel(double midLevel) { return curveFromLevelAt(0.5, midLevel); }

}  // namespace reasampler::util
