// param_taper.cpp — see param_taper.h. Pure value math; no host types.

#include "core/instrument/ui/param_taper.h"

#include <cmath>

namespace reasampler::instrument::ui {

namespace {

// The output quanta (header: EXACT PREIMAGE). Powers of TEN on purpose: std::round(v*S)/S is the
// correctly-rounded double of k/S, which is the same double a decimal literal of k/S parses to —
// so a default written as 0.003 or 0.060 lands on the grid exactly. A power-of-two quantum would
// not have that property against decimal literals.
constexpr double kSecondsPerQuantum = 1e9;    // 1 ns
constexpr double kSemitonesPerQuantum = 1e6;  // 1 micro-semitone

// std::nearbyint reads the CURRENT FP rounding mode (MXCSR) — not exclusively ours on a DAW's UI
// thread. Under round-toward-zero it can drop a grid value by a whole quantum, which is exactly
// what the quantization scheme exists to prevent. std::round (half-away-from-zero) is the same
// regardless of that mode, which is what makes the EXACT PREIMAGE guarantee (header) structural.
double resolveTo(double value, double perUnit) { return std::round(value * perUnit) / perUnit; }

// The shifted-log offsets. Both are FITTED AGAINST THE CEILING above them, which is why the
// ceiling could not be raised in a later track: doing the two apart means fitting twice.
constexpr double kTimeOffsetSeconds = 0.003;      // -> 10 ms at 0.181, 100 ms at 0.436
constexpr double kDepthOffsetSemitones = 3.0;     // -> +/-7 st at 0.548 of each half-travel

double timeSpan() { return std::log1p(kStageTimeMaxSeconds / kTimeOffsetSeconds); }

double depthSpan(double maxSemitones) {
    return std::log1p(maxSemitones / kDepthOffsetSemitones);
}

double rateSpanOctaves(double minRatio, double maxRatio) {
    return std::log2(maxRatio / minRatio);
}

// The norm the general formula puts unity at, DERIVED from the bounds rather than assumed to be
// centre — it is 0.5 only when minRatio * maxRatio == 1. Both maps below pin their exact-unity
// case to this one expression, so the detent is where the curve already goes and the round trip
// closes bitwise on it. Spelling it 0.5 was correct for the shipped symmetric bounds and would
// have gone non-monotone the moment they were re-measured asymmetric.
double rateUnityNorm(double minRatio, double maxRatio) {
    return -std::log2(minRatio) / rateSpanOctaves(minRatio, maxRatio);
}

}  // namespace

double timeNormFromSeconds(double seconds) {
    if (!(seconds > 0.0)) return 0.0;  // also catches NaN
    if (seconds >= kStageTimeMaxSeconds) return 1.0;
    return std::log1p(seconds / kTimeOffsetSeconds) / timeSpan();
}

double timeSecondsFromNorm(double norm) {
    if (!(norm > 0.0)) return 0.0;
    if (norm >= 1.0) return kStageTimeMaxSeconds;
    return resolveTo(kTimeOffsetSeconds * std::expm1(norm * timeSpan()), kSecondsPerQuantum);
}

double depthNormFromSemitones(double semitones, double maxSemitones) {
    if (!(maxSemitones > 0.0)) return 0.5;
    if (semitones == 0.0) return 0.5;  // the centre is EXACT, so a knob parked there persists
    const double mag = std::fabs(semitones);                       // no depth at all
    if (!(mag < maxSemitones)) return semitones > 0.0 ? 1.0 : 0.0;
    const double u = std::log1p(mag / kDepthOffsetSemitones) / depthSpan(maxSemitones);
    return semitones > 0.0 ? 0.5 + 0.5 * u : 0.5 - 0.5 * u;
}

double depthSemitonesFromNorm(double norm, double maxSemitones) {
    if (!(maxSemitones > 0.0)) return 0.0;
    if (!(norm > 0.0)) return -maxSemitones;  // also catches NaN
    if (norm >= 1.0) return maxSemitones;
    if (norm == 0.5) return 0.0;
    const double u = std::fabs(norm - 0.5) * 2.0;
    const double mag = resolveTo(kDepthOffsetSemitones * std::expm1(u * depthSpan(maxSemitones)),
                                 kSemitonesPerQuantum);
    return norm > 0.5 ? mag : -mag;
}

double rateNormFromRatio(double ratio, double minRatio, double maxRatio) {
    if (!(maxRatio > minRatio && minRatio > 0.0)) return 0.5;  // degenerate bounds: park at unity
    if (!(ratio > minRatio)) return 0.0;                       // also catches NaN
    if (ratio >= maxRatio) return 1.0;
    // The unity detent is EXACT, so unity persists as unity.
    if (ratio == 1.0) return rateUnityNorm(minRatio, maxRatio);
    return std::log2(ratio / minRatio) / rateSpanOctaves(minRatio, maxRatio);
}

double rateRatioFromNorm(double norm, double minRatio, double maxRatio) {
    if (!(maxRatio > minRatio && minRatio > 0.0)) return 1.0;
    if (!(norm > 0.0)) return minRatio;  // also catches NaN
    if (norm >= 1.0) return maxRatio;
    if (norm == rateUnityNorm(minRatio, maxRatio)) return 1.0;
    // NOT resolved onto a decimal quantum, unlike the two maps above, and the difference is
    // principled rather than an omission: this control's only default is unity, which the exact
    // detent case above already delivers bitwise, so a grid would buy no preimage it does not
    // already have — while costing accuracy at every whole semitone, none of which is a decimal
    // ratio. Left as the plain exponential, accurate to an ulp.
    return minRatio * std::exp2(norm * rateSpanOctaves(minRatio, maxRatio));
}

double snapSecondsToWholeMs(double seconds) {
    if (!(seconds > 0.0)) return 0.0;
    return std::round(seconds * 1000.0) / 1000.0;
}

double snapFractionToWholePercent(double fraction) {
    return std::round(fraction * 100.0) / 100.0;
}

double snapSemitonesToWhole(double semitones) { return std::round(semitones); }

// Rounding lands on 1..10; anything under half a unit clamps to the domain floor rather than to
// zero, which is not an exponent. 1.0, the linear neutral, is therefore one snap from centre.
double snapExponentToWhole(double exponent) {
    return util::clampCurve(std::round(util::clampCurve(exponent)));
}

double snapRateRatioToWholeSemitone(double ratio) {
    if (!(ratio > 0.0)) return 1.0;  // also catches NaN: an unusable rate snaps to unity
    // exp2 of a whole number of twelfths: 0 gives exactly 1.0 and +/-12 exactly halving/doubling,
    // so a snap to the detent or to either end lands on the taper's own endpoint doubles. An
    // in-range input stays in range, which is why this takes no bounds.
    return std::exp2(std::round(12.0 * std::log2(ratio)) / 12.0);
}

}  // namespace reasampler::instrument::ui
