// param_units.h — the plain-value layer the host reads a parameter through: the unit category,
// the plain range, and the norm <-> plain pair. `toPlain` IS the taper's forward map and
// `toNormalized` its inverse, so the host's normalization, the knob's needle angle and the
// overlay node's position are the SAME function rather than three that agree today.

#pragma once

#include "core/instrument/ui/deck_groups.h"  // DeckParam

namespace reasampler::instrument::param {

using ui::DeckParam;

// The eight DISPLAY categories. A category fixes the units string and the digit precision; the
// norm <-> plain LAW is per control, because three of the dimensionless controls (the curve
// exponents, Q, drive) share a display and share no law.
enum class UnitKind {
    Time,             // ms, 0..10000
    Semitones,        // st, -24..+24, always signed
    PercentUnipolar,  // %, 0..100
    PercentKeyTrack,  // %, 0..200
    PercentBipolar,   // %, -100..+100, always signed
    PercentRate,      // %, 50..200, one decimal
    Decibels,         // dB, -60..+24, always signed; norm 0 reads -inf
    Hertz,            // Hz, 20..20000
    Dimensionless,    // no unit, two decimals
};

struct PlainRange {
    double min = 0.0;
    double max = 1.0;
};

UnitKind unitKindFor(DeckParam deck);

// The units string ParameterInfo carries — "" for the dimensionless category. Carried SEPARATELY
// from the digits, which is the SDK's own convention (RangeParameter::toString prints the number;
// the Parameter constructor takes units as its own argument).
const char* unitStringFor(DeckParam deck);

PlainRange plainRangeFor(DeckParam deck);

// A straight line drawn in a host automation lane is NOT linear in these plain units, and that is
// deliberate: exponential in ms, linear in octaves on cutoff, linear in dB on master gain, a
// linear pitch glide on rate, and slow-near-zero on the two centre-expanded semitone throws. It
// follows from reporting real units over a musically-shaped taper; the remedy for a user who
// wants a literal-units ramp is the host's own curve tools, never a change to the taper.
double toPlain(DeckParam deck, double normalized);
double toNormalized(DeckParam deck, double plain);

// The STORED value a host write of `normalized` lands on — `ui::storedFromNorm` for every
// control except the twelve curve exponents, where the knob law's ±0.01 detent is skipped. That
// detent is a DRAG affordance: a drag grid lands on the identity only by luck, so a band wider
// than one drag step snaps to it. A host lane has no grid and `curveFromKnobNorm` already
// answers exactly 1.0 at norm 0.5, so applying the detent here would not make anything
// reachable — it would flatten a knot-drawn near-neutral exponent to 1.0 on any lane pass.
// BOTH host write paths take this map (the model's `writeHostParam`, the audio thread's
// `applyLiveParam`), which is what keeps them from landing different values in the same block.
double hostStoredFromNorm(DeckParam deck, double normalized);

// WHERE a control's value actually lives. The host's read and write paths branch on this, and
// the exposed set is asserted against it: a control promoted into the list with no home would
// otherwise no-op silently in BOTH directions, with nothing to catch it at compile time.
enum class ValueHome {
    None,             // not a scalar control at all — a toggle, a radio, a curve-popup cell
    ParamSetNorm,     // the filter's four: the stored double IS the normalized position
    ParamSet,         // every other knob the parameter set carries
    InstanceScalar,   // beside the parameter set: master gain, and the pitch key-track scalar
};
ValueHome valueHomeFor(DeckParam deck);

// The filter's four tone controls STORE their normalized position (payload v9), so their default
// normalized value is that stored double verbatim and no taper participates in a host's
// reset-to-default. Reporting Hz / Q / drive depth for them means CALLING their frozen laws, not
// replacing them.
bool storesNormalized(DeckParam deck);

// The default, read off a default-constructed PlaySeconds — there is no second table of defaults,
// and no normalized default is ever written as a literal. defaultNormalized is COMPUTED as
// toNormalized(defaultPlain) for every tapered control, which is what makes a host's
// reset-to-default and the editor's double-click land on the same value.
double defaultPlain(DeckParam deck);
double defaultNormalized(DeckParam deck);

}  // namespace reasampler::instrument::param
