// deck_values.h — the deck's control-id <-> parameter-set BINDING and its snap units: the
// normalized 0..1 a knob shows, the write back into the stored seconds/fractions/positions, and
// the double-click reset. Split from the editor shell so the whole domain map is provable
// without a host; deck_groups owns WHICH controls exist, this owns what each one's value MEANS,
// and param/param_format owns how it READS.

#pragma once

#include "core/instrument/engine/time_stretch.h"   // kStretchRateMin/Max (Rate's own range)
#include "core/instrument/map/play_seconds.h"      // PlaySeconds (the deck's edit target)
#include "core/instrument/ui/deck_groups.h"        // DeckParam
#include "core/instrument/ui/envelope_overlay.h"   // kGateStageMaxSeconds
#include "core/instrument/ui/param_taper.h"        // UnitCategory + the shared tapers
#include "core/util/clamp01.h"

namespace reasampler::instrument::ui {

using map::PlaySeconds;

// Every stage-time knob spans [0, kEnvTimeMaxSeconds] seconds — rate-free, exactly what the
// parameter set stores. An ALIAS of the overlay's schematic domain, which is itself an alias of
// the taper's; param_taper.h owns why the number has one home.
inline constexpr double kEnvTimeMaxSeconds = kGateStageMaxSeconds;

// Pitch depth throw: +/-kVelocityPitchRangeSemitones, centred. The one throw the pitch
// envelope's peak and the velocity->pitch curve's full scale both speak (play_params.h).
inline constexpr double kPitchDepthMaxSemis = kVelocityPitchRangeSemitones;

// Key-track knob ceiling (0..200%), shared by the pitch and filter key-track controls.
inline constexpr double kKeyTrackMax = 2.0;

// The key-track norm <-> stored pair, shared by BOTH key-track controls. It gets its own home
// because the pitch one's value lives beside the play bundle (on InstrumentParams / SampleData)
// and so cannot ride the PlaySeconds binding below — leaving the editor knob, the host's write
// path, the live fold and the snap to each spell the division out. Every one of them calls these.
inline double keyTrackFromNorm(double norm) { return util::clamp01(norm) * kKeyTrackMax; }
inline double keyTrackNormFrom(double keyTrack) { return util::clamp01(keyTrack / kKeyTrackMax); }

// Rate's range: ALIASES of the stretcher's own measured ratio bounds, so the knob's ends are the
// engine's clamp rather than a second opinion of it. The taper takes them as arguments for the
// same reason the depth taper takes its throw — engine/time_stretch.h owns the numbers.
inline constexpr double kRateMinRatio = engine::kStretchRateMin;
inline constexpr double kRateMaxRatio = engine::kStretchRateMax;

// The normalized [0,1] a control shows: stage times through the shared time taper, levels and
// fractions as-is, signed depths through the centre-expanded depth taper, curve exponents over
// their logarithmic travel. Controls backed by per-instance state rather than the parameter set
// (voice count, master gain, the pitch key-track scalar, preview velocity) are not here — the
// shell reads those from the processor.
double deckParamNorm(DeckParam id, const PlaySeconds& play);

// The STORED value a knob's normalized position maps to — the norm -> value half of the binding
// on its own, because the audio thread needs it without a PlaySeconds to write into
// (`param/param_live`). setDeckParam IS this composed with the field lookup below, so the two
// cannot carry different tapers. Answers `norm` unchanged for a control with no stored scalar.
double storedFromNorm(DeckParam id, double norm);

// Applies a committed interaction: a knob's normalized `value`, or a toggle's `segment` (0/1).
// Mutates `play` in place, touching exactly the one field the control names.
void setDeckParam(DeckParam id, PlaySeconds& play, double value, int segment);

// Resets `id` to its default. The default IS what a fresh PlaySeconds carries, so there is no
// second table of defaults to drift from the real one, and the value is COPIED rather than
// round-tripped through norm -> value. That bypass is MANDATORY: a reset must land on the stored
// default bit for bit, and no round trip through a log taper over a non-power-of-two ceiling can
// promise that for every control. Never "simplify" it back into a round trip.
// For knob-valued controls — a toggle has no reset gesture.
void resetDeckParam(DeckParam id, PlaySeconds& play);

// The ADDRESS of the one stored field `id` owns — the mechanism resetDeckParam bypasses the taper
// with. Exposed beyond that one caller so a test can verify a reset (or any other mutation)
// against the actual stored field rather than its normalized read-back, which deckParamNorm does
// not guarantee is injective. Null for a control with no reset gesture (a toggle, radio, or
// curve-popup cell) or one whose value lives outside PlaySeconds (master gain, key-track).
double* deckDoubleField(DeckParam id, PlaySeconds& p);
// The filter's four tone controls store their normalized position as float — see deckFloatField's
// definition for why that is a second resolver rather than a widened first one.
float* deckFloatField(DeckParam id, PlaySeconds& p);

// THE snap-unit table: which whole unit Shift snaps each control to. Includes the deck's
// processor-side ids (voice count, master gain), which have no entry in the two functions above
// because their VALUE lives outside the parameter set — the unit does not.
UnitCategory deckParamUnit(DeckParam id);

// Applies that snap to a control's normalized value. Snapping happens in the DISPLAYED unit, so
// this is where each control's full scale enters: 0..100 %, 0..200 % and +/-100 % all snap to a
// whole displayed percent and therefore take different norm steps.
double snapDeckParamNorm(DeckParam id, double norm);

}  // namespace reasampler::instrument::ui
