// param_taper.h — THE norm <-> value tapers every variable control shares, plus the modifier
// vocabulary its drag surfaces read. Extracted from deck_values because three consumers in two
// dependency layers read it — the knob's needle, the AHDSR overlay's schematic axis and its drag
// inverse, and the VST3 host's normalization — and three functions that agree today is a defect.

#pragma once

#include "core/util/curve_law.h"  // the exponent domain the whole-number snap clamps into

namespace reasampler::instrument::ui {

// --- the interaction law's modifiers -------------------------------------------------------

// Ctrl divides the drag rate by 20. Shift+Ctrl is SHIFT, Ctrl ignored: with the output quantized
// to whole units a finer drag yields the same sequence of values, so that is an identity rather
// than a compromise — do not "fix" it into a compounded scale.
inline constexpr double kFineDragScale = 0.05;

struct DragModifiers {
    bool shift = false;  // snap to whole units of the control's displayed unit
    bool ctrl  = false;  // fine drag

    bool operator==(const DragModifiers& o) const { return shift == o.shift && ctrl == o.ctrl; }
    bool operator!=(const DragModifiers& o) const { return !(*this == o); }
};

inline bool fineDrag(const DragModifiers& m) { return m.ctrl && !m.shift; }

// Which whole unit Shift snaps a control to. Derived from the control's UNIT rather than from a
// per-widget list, so a control added later inherits the law by naming its category.
enum class UnitCategory {
    None,          // discrete, already-integer, or non-scalar controls — Shift changes nothing
    Milliseconds,
    Semitones,
    Percent,
    Exponent,
    Decibels,
};

// --- the two tapers ------------------------------------------------------------------------
//
// EXACT PREIMAGE, and why it is structural rather than lucky. A host's reset-to-default arrives
// as toPlain(defaultNorm) with no editor-side bypass available, so every default must satisfy
// toPlain(toNormalized(d)) == d BITWISE. No transcendental map delivers that at an arbitrary
// interior point — the image of toPlain is sparser there than the doubles around it — so both
// maps below resolve their output onto a fixed decimal quantum, via std::round rather than
// std::nearbyint: round is half-away-from-zero regardless of the caller's FP rounding mode, so
// the quantization is mode-independent, not just decimal-exact. That turns the guarantee into
// "every value on the quantum grid round-trips exactly" instead of a libm/MXCSR coincidence that
// a compiler upgrade or a host's UI thread could take away. Both quanta sit roughly 3.7-4 orders
// below the finest reachable drag step (time ~3.98, depth ~3.71), so nothing observable is
// quantized. The converse, toNormalized(toPlain(n)) == n at arbitrary n, is NOT required and must
// not be demanded: no log map satisfies it in double, and requiring it would rule out the shape
// the range needs.

// The stage-time domain's upper end — the value at norm 1, and the one home of that number:
// envelope_overlay's kGateStageMaxSeconds and deck_values' kEnvTimeMaxSeconds are both aliases
// of it, so the schematic's canvas edge and the knob's ceiling cannot drift apart. Once the
// instrument reports VST3 parameters this endpoint is a frozen host normalization — moving it
// re-interprets every automation point already recorded in projects we do not own.
inline constexpr double kStageTimeMaxSeconds = 10.0;

// Shifted-log: exactly 0 s at norm 0, exactly kStageTimeMaxSeconds at norm 1, monotone
// throughout, low end expanded so 10 ms sits at ~0.18 of the travel and 100 ms at ~0.44. A pure
// log cannot include zero and zero is a required value, which is what the offset buys.
double timeNormFromSeconds(double seconds);
double timeSecondsFromNorm(double norm);

// Signed depth, symmetric about norm 0.5: exactly 0 semitones at centre, exactly +/-maxSemitones
// at the ends, monotone, with the musically useful middle expanded so +/-7 st reaches ~0.55 of
// each half-travel. The throw is a PARAMETER — the +/-24 st depth constant has its own home in
// the engine's value layer and is not restated here.
double depthNormFromSemitones(double semitones, double maxSemitones);
double depthSemitonesFromNorm(double norm, double maxSemitones);

// Playback RATE as a ratio, exponential across the travel — i.e. LINEAR IN SEMITONES, the one
// exception to the centre expansion above. Centre expansion applies to a semitone knob whose
// throw exceeds +/-12; this throw IS +/-12 (half rate to double rate), already 0.19 st per drag
// pixel, so expanding it would buy resolution nothing needs and cost the extremes.
//
// The bounds are PARAMETERS for the same reason the depth throw is: they belong to the engine's
// stretcher, which owns the measurement they came from, and a second copy here could drift from
// it. The map is monotone and hits them exactly at norm 0 and 1, so a norm in [0,1] cannot reach
// a ratio the engine's own clamp would then move — ONE clamp, at the stretcher, not two.
// Exactly 1.0 at the norm the bounds themselves put unity at — `-log2(minRatio) / span`, which
// is 0.5 only when minRatio * maxRatio == 1 — whenever they bracket it. That detent is this
// control's whole preimage obligation; see rateRatioFromNorm for why it carries no output
// quantum. Pinning it to 0.5 regardless of the bounds is the specific mistake to avoid: it makes
// the map non-monotone the moment the stretcher's measured range stops being symmetric.
double rateNormFromRatio(double ratio, double minRatio, double maxRatio);
double rateRatioFromNorm(double norm, double minRatio, double maxRatio);

// --- Shift's whole-unit snaps, in the VALUE domain -------------------------------------------
//
// Stated over values rather than norms because "whole unit" means whole unit of what the control
// DISPLAYS: two controls sharing a category can have different full scales, so the norm step is
// the caller's business and the unit is this module's.

double snapSecondsToWholeMs(double seconds);
double snapFractionToWholePercent(double fraction);  // 1.0 == 100 %
double snapSemitonesToWhole(double semitones);
double snapExponentToWhole(double exponent);         // clamped into curve_law's own domain
// Rate's unit is the SEMITONE even though it displays as a percent, so its whole unit is one of
// the 25 semitone steps between the bounds — which is what puts an octave and a fifth under the
// hand. Stated over the ratio because that is what the control stores.
double snapRateRatioToWholeSemitone(double ratio);

}  // namespace reasampler::instrument::ui
