// deck_groups.cpp — see deck_groups.h. Pure data; no host types.

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

#include <utility>

#include "core/instrument/ui/master_meter.h"  // kMeterColumnW (what the column's interior needs)

namespace reasampler::instrument::ui {

namespace {
int id(DeckParam p) { return static_cast<int>(p); }
double clamp(double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi : v); }

// Segment width of the three Staged|Spline toggles. Sized so each env group's caption row stays
// no wider than its knob row; the binding group is PITCH ENV, which reaches its four-cell knob
// row at 47 (AMP, the next tightest, at 55). Well inside the ceiling — raising it would widen
// the CONTOUR row, which has 152px of slack, not the SOUND row.
constexpr int kEnvModeSegW = 23;
}  // namespace

double deckBipolarFromNorm(double norm) { return clamp(norm, 0.0, 1.0) * 2.0 - 1.0; }
double deckNormFromBipolar(double value) { return clamp(value, -1.0, 1.0) * 0.5 + 0.5; }

std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
    const bool trigger = (playMode == PlayMode::Trigger);
    std::vector<DeckGroupDesc> out;
    {
        // PITCH/RATE. The three cells make the knob row 180, which is what the group measures
        // from; the caption row (caption + gap + two 48px segments) must stay under it, so the
        // caption reserve has a hard ceiling of 80 — past that the caption row overtakes the knob
        // row and the group grows past 192. Widening the group is not the answer if the text ever
        // outgrows 80: narrow the Varisp|Presrv segments to 44 instead.
        DeckGroupDesc pitch;
        pitch.id = kGroupPitch;
        pitch.captionWidth = 70;
        pitch.captionToggle = {id(DeckParam::kPitchEngine), 48};
        pitch.cellIds = {id(DeckParam::kKeyTrack), id(DeckParam::kRate), id(DeckParam::kPitch)};
        out.push_back(std::move(pitch));
    }
    {
        DeckGroupDesc penv;
        penv.id = kGroupPitchEnv;
        penv.captionWidth = 58;
        penv.captionRadio = {id(DeckParam::kPitchEnvSelect)};
        penv.captionToggle = {id(DeckParam::kPitchEnvEnable), 32};
        // The mode toggle rides the caption slack rather than the knob row — costs no group
        // width; see this module's CLAUDE.md bullet (knob_deck) for the headroom this relies on.
        penv.captionToggle2 = {id(DeckParam::kPitchEnvMode), kEnvModeSegW};
        penv.cellIds = {id(DeckParam::kPitchEnvAttack),
                        id(DeckParam::kPitchEnvHold),
                        id(DeckParam::kPitchEnvDecay),
                        id(DeckParam::kPitchEnvDepth)};
        out.push_back(std::move(penv));
    }
    {
        // Tone shaping left-to-right, then the three modulation depths that all target cutoff.
        DeckGroupDesc filter;
        filter.id = kGroupFilter;
        filter.captionWidth = 46;
        filter.captionToggle = {id(DeckParam::kFilterEnable), 32};
        filter.cellIds = {id(DeckParam::kFilterMorph),
                          id(DeckParam::kFilterCutoff),
                          id(DeckParam::kFilterQ),
                          id(DeckParam::kFilterDrive),
                          id(DeckParam::kFilterModAmt),
                          id(DeckParam::kFilterVel),
                          id(DeckParam::kFilterKeyTrack)};
        // The morph law rides the caption slack. Moving it back to the knob row costs the
        // group 92px and the SOUND row stops fitting its block.
        filter.captionToggle2 = {id(DeckParam::kFilterLaw), 44};
        out.push_back(std::move(filter));
    }
    {
        DeckGroupDesc fenv;
        fenv.id = kGroupFilterEnv;
        fenv.captionWidth = 66;
        fenv.captionRadio = {id(DeckParam::kFilterEnvSelect)};
        fenv.captionToggle2 = {id(DeckParam::kFilterEnvMode), kEnvModeSegW};
        if (trigger) {
            fenv.cellIds = {id(DeckParam::kFilterTrigAttack), id(DeckParam::kFilterTrigHold),
                            id(DeckParam::kFilterTrigDecay), -1, -1};
        } else {
            fenv.cellIds = {id(DeckParam::kFilterEnvAttack),
                            id(DeckParam::kFilterEnvHold),
                            id(DeckParam::kFilterEnvDecay),
                            id(DeckParam::kFilterEnvSustain),
                            id(DeckParam::kFilterEnvRelease)};
        }
        out.push_back(std::move(fenv));
    }
    {
        DeckGroupDesc amp;
        amp.id = kGroupAmpEnv;
        amp.captionWidth = 78;
        amp.captionRadio = {id(DeckParam::kAmpEnvSelect)};
        amp.captionToggle = {id(DeckParam::kPlayMode), 44};
        amp.captionToggle2 = {id(DeckParam::kAmpEnvMode), kEnvModeSegW};
        if (trigger) {
            // The play span first, then the AHD that shapes it, time-ordered left-to-right so
            // the row reads like the drawn envelope. One reserve (-1) keeps the group's width —
            // and therefore its neighbours' placement — identical across a mode flip; its
            // pixels go to the four cells that remain (knob_deck.h).
            amp.cellIds = {id(DeckParam::kTrigLength), id(DeckParam::kTrigAttack),
                           id(DeckParam::kTrigHold), id(DeckParam::kTrigDecay), -1};
        } else {
            amp.cellIds = {id(DeckParam::kAttack), id(DeckParam::kHold),
                           id(DeckParam::kDecay), id(DeckParam::kSustain),
                           id(DeckParam::kRelease)};
        }
        out.push_back(std::move(amp));
    }
    {
        // The three velocity transfer curves share one home, immediately left of VOICE: they
        // shape three different destinations but are one gesture, and MASTER is reserved for
        // post-voice-mixer concerns.
        DeckGroupDesc vel;
        vel.id = kGroupVelocity;
        vel.captionWidth = 54;
        vel.cellIds = {id(DeckParam::kAmpVelCurve), id(DeckParam::kPitchVelCurve),
                       id(DeckParam::kFilterVelCurve)};
        out.push_back(std::move(vel));
    }
    {
        DeckGroupDesc voice;
        voice.id = kGroupVoice;
        voice.captionWidth = 38;
        voice.captionToggle = {id(DeckParam::kVoiceMode), 40};
        voice.cellIds = {id(DeckParam::kVoiceCount)};
        voice.rowToggle = {id(DeckParam::kMonoTrigger), 44};
        out.push_back(std::move(voice));
    }
    {
        // The lower slot is reserved and draws NOTHING: blank reads as breathing room where a
        // dashed placeholder would read as unfinished. It is one cell, not two — a second
        // would spend 60 of the layout's whole 82px budget on a control nobody has named.
        DeckGroupDesc master;
        master.id = kGroupMaster;
        master.captionWidth = 46;
        master.captionRadio = {id(DeckParam::kMasterGr), /*passive=*/true};
        master.captionToggle = {id(DeckParam::kLimiterEnable), 32};
        master.cellIds = {id(DeckParam::kMasterGain), -1};
        // The reserve IS what the interior consumes — read from master_meter rather than
        // restated, so the two cannot drift when the column grows into MASTER's banked room.
        master.column = {id(DeckParam::kMasterMeter), kMeterColumnW};
        out.push_back(std::move(master));
    }
    for (DeckGroupDesc& d : out) d.row = deckRowFor(static_cast<DeckGroupId>(d.id));
    return out;
}

DeckRow deckRowFor(DeckGroupId group) {
    // Every enumerator listed and no default, on the same gate deckParamCommit below relies on.
    switch (group) {
        case kGroupPitch:
        case kGroupFilter:
        case kGroupVelocity:
        case kGroupVoice:
            return DeckRow::Sound;
        case kGroupPitchEnv:
        case kGroupFilterEnv:
        case kGroupAmpEnv:
            return DeckRow::Contour;
        case kGroupMaster:
            return DeckRow::Spanning;
    }
    // Unreachable for a valid enumerator, and Spanning rather than Sound ON PURPOSE: the
    // -Wswitch gate is compiler-dependent, so on a toolchain that does not raise it a dropped
    // case arm falls here instead. Sound is what a new group most plausibly IS, which would
    // make the fall-through invisible; Spanning is the one row nothing may silently join, so
    // the tests' partition count catches it.
    return DeckRow::Spanning;
}

CurveTarget curveTargetFor(int controlId) {
    switch (static_cast<DeckParam>(controlId)) {
        case DeckParam::kAmpVelCurve:    return CurveTarget::kAmp;
        case DeckParam::kPitchVelCurve:  return CurveTarget::kPitch;
        case DeckParam::kFilterVelCurve: return CurveTarget::kFilter;
        default:                         return CurveTarget::kNone;
    }
}

DeckParam curveParamFor(DeckParam knob) {
    switch (knob) {
        case DeckParam::kAttack:            return DeckParam::kAttackCurve;
        case DeckParam::kDecay:             return DeckParam::kDecayCurve;
        case DeckParam::kRelease:           return DeckParam::kReleaseCurve;
        case DeckParam::kTrigAttack:        return DeckParam::kTrigAttackCurve;
        case DeckParam::kTrigDecay:         return DeckParam::kTrigDecayCurve;
        case DeckParam::kPitchEnvAttack:    return DeckParam::kPitchEnvAttackCurve;
        case DeckParam::kPitchEnvDecay:     return DeckParam::kPitchEnvDecayCurve;
        case DeckParam::kFilterEnvAttack:   return DeckParam::kFilterEnvAttackCurve;
        case DeckParam::kFilterEnvDecay:    return DeckParam::kFilterEnvDecayCurve;
        case DeckParam::kFilterEnvRelease:  return DeckParam::kFilterEnvReleaseCurve;
        case DeckParam::kFilterTrigAttack:  return DeckParam::kFilterTrigAttackCurve;
        case DeckParam::kFilterTrigDecay:   return DeckParam::kFilterTrigDecayCurve;
        default:                            return DeckParam::kCount;
    }
}

LiveCommit deckParamCommit(DeckParam id) {
    switch (id) {
        // The note-on-latched controls; the header owns why each one latches.
        case DeckParam::kRate:
        case DeckParam::kKeyTrack:
        case DeckParam::kTrigLength:
            return LiveCommit::NoteOnLatched;
        // Live by the tier's own definition — one atomic store the audio thread picks up at the
        // next block, no bridge read and no re-decode. It reaches the audio beside the live
        // block rather than through it, which is why it carries no LiveValues field; that is a
        // question of ROUTE, and this predicate answers TIER.
        case DeckParam::kMasterGain:
        case DeckParam::kPitch:
        case DeckParam::kAttack:
        case DeckParam::kHold:
        case DeckParam::kDecay:
        case DeckParam::kSustain:
        case DeckParam::kRelease:
        case DeckParam::kTrigAttack:
        case DeckParam::kTrigHold:
        case DeckParam::kTrigDecay:
        case DeckParam::kPitchEnvAttack:
        case DeckParam::kPitchEnvHold:
        case DeckParam::kPitchEnvDecay:
        case DeckParam::kPitchEnvDepth:
        case DeckParam::kFilterMorph:
        case DeckParam::kFilterCutoff:
        case DeckParam::kFilterQ:
        case DeckParam::kFilterDrive:
        case DeckParam::kFilterModAmt:
        case DeckParam::kFilterVel:
        case DeckParam::kFilterKeyTrack:
        case DeckParam::kFilterEnvAttack:
        case DeckParam::kFilterEnvHold:
        case DeckParam::kFilterEnvDecay:
        case DeckParam::kFilterEnvSustain:
        case DeckParam::kFilterEnvRelease:
        case DeckParam::kFilterTrigAttack:
        case DeckParam::kFilterTrigHold:
        case DeckParam::kFilterTrigDecay:
        case DeckParam::kAttackCurve:
        case DeckParam::kDecayCurve:
        case DeckParam::kReleaseCurve:
        case DeckParam::kTrigAttackCurve:
        case DeckParam::kTrigDecayCurve:
        case DeckParam::kPitchEnvAttackCurve:
        case DeckParam::kPitchEnvDecayCurve:
        case DeckParam::kFilterEnvAttackCurve:
        case DeckParam::kFilterEnvDecayCurve:
        case DeckParam::kFilterEnvReleaseCurve:
        case DeckParam::kFilterTrigAttackCurve:
        case DeckParam::kFilterTrigDecayCurve:
            return LiveCommit::Live;
        // Listed rather than defaulted so a newly added control is a COMPILE error here on
        // every toolchain — /we4062 on MSVC, -Werror=switch on GCC/Clang, both set on this
        // library alone in cmake/reasampler_targets.cmake — instead of silently defaulting
        // to non-live. Reasons live in the header.
        case DeckParam::kPlayMode:
        case DeckParam::kPitchEngine:
        case DeckParam::kPitchEnvEnable:
        case DeckParam::kFilterEnable:
        case DeckParam::kAmpVelCurve:
        case DeckParam::kPitchVelCurve:
        case DeckParam::kFilterVelCurve:
        case DeckParam::kFilterLaw:
        case DeckParam::kAmpEnvSelect:
        case DeckParam::kPitchEnvSelect:
        case DeckParam::kFilterEnvSelect:
        // A mode toggle names a different envelope, not a different setting of one — the same
        // reason every other discrete toggle above is excluded.
        case DeckParam::kAmpEnvMode:
        case DeckParam::kPitchEnvMode:
        case DeckParam::kFilterEnvMode:
        case DeckParam::kVoiceCount:
        case DeckParam::kVoiceMode:
        case DeckParam::kMonoTrigger:
        case DeckParam::kLimiterEnable:
        // MASTER's two readouts reach no parameter at all — the same footing as the overlay
        // radios above.
        case DeckParam::kMasterMeter:
        case DeckParam::kMasterGr:
        case DeckParam::kCount:  // not a control
            return LiveCommit::Reload;
    }
    return LiveCommit::Reload;  // unreachable for a valid enumerator; silences a warning.
}

OverlayEnv overlayEnvForRadio(int radioId) {
    switch (static_cast<DeckParam>(radioId)) {
        case DeckParam::kAmpEnvSelect:    return OverlayEnv::kAmp;
        case DeckParam::kPitchEnvSelect:  return OverlayEnv::kPitch;
        case DeckParam::kFilterEnvSelect: return OverlayEnv::kFilter;
        default:                          return OverlayEnv::kNone;
    }
}

OverlayEnv nextOverlaySelection(OverlayEnv current, int radioId) {
    const OverlayEnv picked = overlayEnvForRadio(radioId);
    if (picked == OverlayEnv::kNone) return current;  // not a radio: nothing selects
    return (current == picked) ? OverlayEnv::kNone : picked;
}

OverlayEnv overlayEnvForModeToggle(int toggleId) {
    switch (static_cast<DeckParam>(toggleId)) {
        case DeckParam::kAmpEnvMode:    return OverlayEnv::kAmp;
        case DeckParam::kPitchEnvMode:  return OverlayEnv::kPitch;
        case DeckParam::kFilterEnvMode: return OverlayEnv::kFilter;
        default:                        return OverlayEnv::kNone;
    }
}

bool overlayEnvEnabled(OverlayEnv env, const DeckEnableState& state) {
    switch (env) {
        case OverlayEnv::kPitch:  return state.pitchEnvEnabled;
        case OverlayEnv::kFilter: return state.filterEnabled;
        case OverlayEnv::kAmp:
        case OverlayEnv::kNone:
            return true;
    }
    return true;  // unreachable for a valid enumerator; silences a warning.
}

bool overlayEnvInert(OverlayEnv env, const DeckEnableState& state) {
    if (env == OverlayEnv::kNone) return false;
    if (!overlayEnvEnabled(env, state)) return true;
    switch (env) {
        case OverlayEnv::kPitch:  return state.pitchSpline;
        case OverlayEnv::kFilter: return state.filterSpline;
        case OverlayEnv::kAmp:    return state.ampSpline;
        case OverlayEnv::kNone:
            return false;
    }
    return false;  // unreachable for a valid enumerator; silences a warning.
}

bool deckKnobInert(DeckParam id, const DeckEnableState& state) {
    switch (id) {
        case DeckParam::kAttack:
        case DeckParam::kHold:
        case DeckParam::kDecay:
        case DeckParam::kSustain:
        case DeckParam::kRelease:
        case DeckParam::kTrigAttack:
        case DeckParam::kTrigHold:
        case DeckParam::kTrigDecay:
            return state.ampSpline;
        // The Trigger %-length knob is inert whenever ANY spline is active (not just the amp's):
        // a drawn contour always covers the full sample length (splineActive, play_params.h), so
        // the engine ignores lengthFraction in that case regardless of which EG is drawn.
        case DeckParam::kTrigLength:
            return state.ampSpline || state.pitchSpline || state.filterSpline;
        case DeckParam::kPitchEnvAttack:
        case DeckParam::kPitchEnvHold:
        case DeckParam::kPitchEnvDecay:
            return !state.pitchEnvEnabled || state.pitchSpline;
        case DeckParam::kPitchEnvDepth:
            return !state.pitchEnvEnabled;
        case DeckParam::kFilterEnvAttack:
        case DeckParam::kFilterEnvHold:
        case DeckParam::kFilterEnvDecay:
        case DeckParam::kFilterEnvSustain:
        case DeckParam::kFilterEnvRelease:
        case DeckParam::kFilterTrigAttack:
        case DeckParam::kFilterTrigHold:
        case DeckParam::kFilterTrigDecay:
            return !state.filterEnabled || state.filterSpline;
        case DeckParam::kFilterMorph:
        case DeckParam::kFilterCutoff:
        case DeckParam::kFilterQ:
        case DeckParam::kFilterDrive:
        case DeckParam::kFilterModAmt:
        case DeckParam::kFilterVel:
        case DeckParam::kFilterKeyTrack:
        // The filter's velocity curve sits in the VELOCITY group but is a filter parameter:
        // it goes inert with every other one, so no surface can reach a param the knobs can't.
        case DeckParam::kFilterVelCurve:
            return !state.filterEnabled;
        default:
            return false;
    }
}

LiveCommit liveCommitFor(LiveDragKind kind, int paramId) {
    switch (kind) {
        case LiveDragKind::kDeckKnob:
            return (paramId >= 0 && paramId < static_cast<int>(DeckParam::kCount))
                       ? deckParamCommit(static_cast<DeckParam>(paramId))
                       : LiveCommit::Reload;
        case LiveDragKind::kEnvNode:
            return LiveCommit::Live;
        case LiveDragKind::kOther:
            return LiveCommit::Reload;
    }
    return LiveCommit::Reload;
}

}  // namespace reasampler::instrument::ui
