// deck_groups.h — WHICH groups the Sample face's knob deck carries and in what order, plus
// the control-id space they are built from. Pure data: knob_deck lays out whatever descriptors
// it is handed, and this module decides what those descriptors are, so the deck's signal-flow
// ordering is provable without a host.

#pragma once

#include <vector>

#include "core/instrument/engine/play_params.h"  // PlayMode (the mode-dependent group faces)
#include "core/instrument/ui/knob_deck.h"        // DeckGroupDesc

namespace reasampler::instrument::ui {

// Deck control ids. Opaque to knob_deck, resolved by the shell's hit-test and value binding.
// Runtime-only — nothing persists them, so the ordering here is free to change.
enum class DeckParam {
    kPlayMode = 0,   // Gate | Trigger toggle
    kPitchEngine,    // Varispeed | Preserve toggle
    kAttack,         // amp AHDSR attack  (Gate)
    kHold,           // amp AHDSR hold    (Gate)
    kDecay,          // amp AHDSR decay   (Gate)
    kSustain,        // amp AHDSR sustain (Gate)
    kRelease,        // amp AHDSR release (Gate)
    kTrigLength,     // Trigger play span, % of the post-start length
    kTrigAttack,     // amp AHD attack    (Trigger)
    kTrigHold,       // amp AHD hold, % of the span left after attack + decay
    kTrigDecay,      // amp AHD decay     (Trigger)
    kPitchEnvEnable, // AHD pitch envelope on|off
    kPitchEnvAttack,
    kPitchEnvHold,   // % of the span left after attack + decay
    kPitchEnvDecay,
    kPitchEnvDepth,  // AHD pitch depth in +/- semitones
    kKeyTrack,       // key-tracking 0..200% (lives on InstrumentParams, not PlaySeconds)
    kRate,           // playback rate 50..200%, linear in semitones over +/-12
    kPitch,          // baseline pitch offset, +/-kPitchDepthMaxSemis, centre-expanded
    // Filter. The four control positions map through filter_params' own laws; the three
    // depths are bipolar and centred at zero.
    kFilterEnable,   // filter on|off caption toggle
    kFilterMorph,    // morph position: high-pass .. low-pass
    kFilterCutoff,   // cutoff, log across the audio band
    kFilterQ,        // resonance
    kFilterDrive,    // in-loop drive depth
    kFilterModAmt,   // filter envelope -> cutoff, +/-100%
    kFilterVel,      // velocity curve -> cutoff depth, +/-100%
    kFilterKeyTrack, // note -> cutoff, 0..200%
    kFilterLaw,      // morph law row toggle: HP-BP-LP | HP-notch-LP
    kFilterEnvAttack,   // filter AHDSR (Gate)
    kFilterEnvHold,
    kFilterEnvDecay,
    kFilterEnvSustain,
    kFilterEnvRelease,
    kFilterTrigAttack,  // filter AHD (Trigger)
    kFilterTrigHold,
    kFilterTrigDecay,
    // Curve exponents. These never get a cell of their own — each is the INNER DIAL of the
    // stage knob it shapes (see curveParamFor), which is why only sloped stages have one.
    kAttackCurve,
    kDecayCurve,
    kReleaseCurve,
    kTrigAttackCurve,
    kTrigDecayCurve,
    kPitchEnvAttackCurve,
    kPitchEnvDecayCurve,
    kFilterEnvAttackCurve,
    kFilterEnvDecayCurve,
    kFilterEnvReleaseCurve,
    kFilterTrigAttackCurve,
    kFilterTrigDecayCurve,
    // VELOCITY: the three transfer-curve cells. Each opens the curve popup rather than
    // dragging a value — see curveTargetFor, which is also what tells a cell apart from a knob.
    kAmpVelCurve,
    kPitchVelCurve,
    kFilterVelCurve,
    // Overlay selection radios — transient view state, not parameters.
    kAmpEnvSelect,
    kPitchEnvSelect,
    kFilterEnvSelect,
    // Staged | Spline mode per envelope. Both states persist either way (play_params.h's
    // SplineEnv); this only picks which one plays and which one the overlay edits.
    kAmpEnvMode,
    kPitchEnvMode,
    kFilterEnvMode,
    // Deck-only controls: processor-side per-instance params — routed to the processor
    // setters, never through the parameter set.
    kVoiceCount,     // polyphony bound (1..32) — a stepped knob in the VOICE group
    kVoiceMode,      // Poly | Mono caption toggle (VOICE group)
    kMonoTrigger,    // Retrig | Legato row toggle (VOICE group; live only in Mono)
    kMasterGain,     // post-mixer master gain knob (-inf..+24 dB taper, MASTER group)
    kLimiterEnable,  // master-bus limiter Off | On caption toggle (MASTER group)
    // MASTER's two readouts. Neither reaches a parameter: the meter's only gesture is the
    // click that clears its latched clip cap, and the bubble is a passive lamp.
    kMasterMeter,
    kMasterGr,
    kCount
};

// Deck group ids. Unscoped so the shell's caption switch reads against the plain `id` int
// knob_deck carries.
enum DeckGroupId {
    kGroupPitch = 0,
    kGroupPitchEnv,
    kGroupFilter,
    kGroupFilterEnv,
    kGroupAmpEnv,
    kGroupVelocity,
    kGroupVoice,
    kGroupMaster,
};

// Which row a group belongs to (the row vocabulary itself is knob_deck's — the layout is what
// reads it). Membership is a property of the GROUP; width is a property of its descriptor.
// Total over DeckGroupId by an exhaustive switch with no default, so a group added without a
// row cannot silently become Sound.
DeckRow deckRowFor(DeckGroupId group);

// Which velocity curve a deck cell edits, or kNone when the control is an ordinary knob. THE
// one place a control id resolves to a curve target — paint (draw a curve thumbnail, not a
// dial) and hit-test (open a popup, not start a drag) both read this predicate rather than
// re-deriving which ids are curve cells. What each resolved target then shows (which stored
// curve, which title) is a separate switch — see the shell's curveFor/curveTitle.
enum class CurveTarget { kNone, kAmp, kPitch, kFilter };
CurveTarget curveTargetFor(int controlId);

// The deck's groups, left to right, in SIGNAL-FLOW order: pitch -> filter -> amp, then the
// two instance-wide groups. `playMode` picks the AMP and FILTER ENV groups' faces — AHDSR in
// Gate, AHD in Trigger — via knob_deck's cell-width reservation (knob_deck.h) so a mode flip
// never reflows the neighbouring groups and never leaves a hole in the narrower face.
std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode);

// The curve-exponent control a stage knob's INNER DIAL edits, or kCount when the knob shapes
// no curve. THE one place the "every stage except Hold and Sustain is sloped" rule is written
// down: a knob with no entry here draws no inner dial and its inner region resolves as an
// ordinary knob grab.
DeckParam curveParamFor(DeckParam knob);

// How an edit to a control reaches the audio — THE one decision, and the home for why each
// control sits where it does. Moving a control across a line is a change here and nowhere else,
// and Γ-W4-T1 derives the host-exposed parameter set from this same predicate, so a
// misclassification here is a mis-declared parameter there.
//
//   Live          — straight to the voices already sounding. Continuously-valued playback
//                   controls, and the default for anything that is a SETTING of a note rather
//                   than a fact about it.
//   NoteOnLatched — published into the live block like a live control, but read only at
//                   note-on: a sounding voice keeps the value it started with, the next one
//                   takes the new one. NOT the reload tier — a swept knob must never trigger a
//                   WAV re-decode.
//   Reload        — a bridge read, a re-decode and a fresh engine.
//
// The exclusions from Live, each with its reason:
//   - the discrete toggles (play mode, pitch engine, filter enable/law, pitch-envelope enable,
//     the three Staged|Spline mode toggles) name a different sound rather than a different
//     setting of one;
//   - the three capture-anchored overrides (root, loop span, start frame) name positions in
//     the decoded PCM;
//   - kKeyTrack and the three velocity-curve cells feed values a voice latches at note-on by
//     design (the pitch ratio and the curve results), so live delivery would retune or re-gain
//     a note already struck. kFilterVel is NOT one of them: it is the DEPTH over the filter
//     curve's latched result, the exact shape kFilterKeyTrack already has over the latched note
//     number, and it glides through the same base-cutoff ramp;
//   - kTrigLength resolves playEnd_, a fact about the note, not a setting of it;
//   - the overlay radios select what the editor DRAWS and reach no parameter at all.
// Both amp shapes are live: the Trigger fade pair that used to reload folded into the AHD and
// inherited its routing, so a Trigger-mode instance now tracks its amplitude knobs too.
//
// kMasterGain is Live and is the one live control that does NOT ride the live block: it is a
// lock-free atomic on the processor which the audio thread applies as a post-sum multiply. The
// tier answers "does an edit reach the audio without a reload", not "which mechanism carries
// it" — classifying it Reload would have said a gain move re-decodes the WAV, which it never did.
//
// THREE controls are NoteOnLatched: kRate, kKeyTrack and kTrigLength. The exclusions list above
// already gives the latter two their reason — both were Reload until they were promoted so they
// could be automated at all, since a reload per automation point re-decodes the WAV. kRate's
// reason is its own, and is a real feature rather than a plumbing detail: loop points and
// contours both scale with rate, and both are note-on folds — resolveLoop runs once per note-on
// and a contour resolves against the note's own span. A live rate would mean re-folding an
// already-resolved loop and re-mapping a contour mid-note without a discontinuity. kPitch is not
// implicated and is ordinarily Live.
enum class LiveCommit { Live, NoteOnLatched, Reload };
LiveCommit deckParamCommit(DeckParam id);

// The editor drag kinds that can commit live, in this pure module's own vocabulary (the
// shell's DragKind maps onto it) so the WHOLE routing decision — not just the predicate — is
// testable without a host.
enum class LiveDragKind { kOther, kDeckKnob, kEnvNode };

// How a drag of `kind` commits. A deck knob answers per deckParamCommit (negative ids are the
// shell's processor-side sentinels and out-of-range ids are not controls, so neither reaches the
// enum); an envelope-node drag is Live in either mode, since every stage value it can reach —
// AHDSR or AHD, on any of the three envelopes — is itself Live.
//
// Live and NoteOnLatched take the SAME route out of the editor — one publish of the live block,
// no reload, no engine rebuild. They differ only in who reads the published value, which is the
// engine's business (live_params.h), so the shell needs the distinction only to know that
// neither reloads.
LiveCommit liveCommitFor(LiveDragKind kind, int paramId);

// Which envelope the waveform overlay draws and edits. Exclusive across the three envelope
// decks, and kNone is a valid resting state — the editor opens there. Transient view state:
// never persisted, never a parameter.
enum class OverlayEnv { kNone, kAmp, kPitch, kFilter };

// The envelope a deck's overlay-select radio picks; kNone for any other control id.
OverlayEnv overlayEnvForRadio(int radioId);

// The selection a click on `radioId` produces from `current`. Two rules, provable here rather
// than in the shell: picking another deck's radio switches to it (exclusivity), and clicking
// the ACTIVE one clears back to kNone — "no envelope shown" is a state the user can get back
// to, not an error. A non-radio id leaves the selection alone.
OverlayEnv nextOverlaySelection(OverlayEnv current, int radioId);

// The group states the two inert predicates below read. One struct rather than a growing
// parameter list, so adding a gate is a change at the two predicates and nowhere else.
struct DeckEnableState {
    bool pitchEnvEnabled = false;
    bool filterEnabled   = false;
    bool ampSpline       = false;  // the amp EG is drawn rather than staged
    bool pitchSpline     = false;
    bool filterSpline    = false;
};

// Which envelope a Staged|Spline mode toggle belongs to; kNone for any other control id.
OverlayEnv overlayEnvForModeToggle(int toggleId);

// Whether `env`'s deck group is switched on at all. Amp has no enable toggle and is always on.
// The gate BOTH overlay modes share — a disabled group's contour is as dead as its knobs.
bool overlayEnvEnabled(OverlayEnv env, const DeckEnableState& state);

// Whether the STAGED overlay for `env` is INERT: its deck group is off, so its knobs are
// drawn-but-dead and a node drag on the same params must be too — otherwise a drag reaches a
// param a knob couldn't (envelope_edit.h). An envelope in SPLINE mode is inert here too: the
// staged nodes are not what the overlay is editing.
bool overlayEnvInert(OverlayEnv env, const DeckEnableState& state);

// Whether a deck knob cell is drawn-but-dead: the pitch envelope's four knobs while it is
// disabled, the filter group's tone/modulation knobs (plus its VELOCITY cell, a filter
// parameter that just sits in that group) while the filter is disabled, and every STAGED
// SEGMENT knob of an envelope switched to Spline. The segment knobs' inner curve dials go with
// them — the dial is reached through its outer cell, so one predicate covers both. The DEPTH
// knobs (pitch peak, filter mod amount) stay live in either mode: they scale whichever shape is
// active rather than describing a stage. Mirrors overlayEnvInert's shape for the deck's own
// mouse-down/paint (the shell's deckKnobDisabled is a thin int-id wrapper over this).
bool deckKnobInert(DeckParam id, const DeckEnableState& state);

// The deck's BIPOLAR knob law: 0.5 of the knob's travel is zero depth, the ends are -1 and
// +1. Exact inverses, and exact at the centre detent (0.5 -> 0 -> 0.5), so a knob parked at
// centre can never persist a hair of modulation. Out-of-range norm clamps to the endpoints.
double deckBipolarFromNorm(double norm);
double deckNormFromBipolar(double value);

}  // namespace reasampler::instrument::ui
