// editor_controls.cpp — the ReaSamplerEditor's parameter plumbing: the band-stack layout
// resolve every paint/hit-test path shares, the shell's half of the control-value binding (the
// per-instance controls the parameter set does not carry — key-track, voice count, master gain,
// preview velocity — plus each knob's plain value and its label), and the node-drag clamp
// bounds. The parameter-set half is the pure `deck_values` module. The orthogonal half — which
// stored struct each editor selection names — is editor_models. Value logic only.

#include "shell/instrument/reasampler_editor.h"

#include <algorithm>
#include <cmath>    // isfinite (the gain's -inf label)
#include <cstdint>
#include <cstdio>   // snprintf (deck value labels)
#include <string>
#include <vector>

#include "core/instrument/engine/master_gain.h"  // master-gain dB<->linear<->knob taper
#include "core/instrument/param/param_format.h"  // THE formatter every value label reads through
#include "core/instrument/param/param_id.h"      // whether a control has a parameter row at all
#include "core/instrument/ui/bake_hold.h"        // the Hold knob's ladder map
#include "core/instrument/ui/deck_groups.h"      // sampleDeckGroups (the deck's composition)
#include "core/instrument/ui/deck_values.h"      // the parameter-set binding
#include "core/instrument/ui/knob_deck.h"        // deckHeight / kDeckKnobSize (the band's own height)
#include "core/util/clamp01.h"
#include "core/util/curve_law.h"                 // the ONE curve-exponent domain
#include "shell/instrument/editor_internal.h"
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using namespace reasampler::instrument::map;  // PlaySeconds vocabulary
using instrument::ui::computeSampleBands;
using instrument::ui::chromeRects;
using instrument::ui::deckHeight;
using instrument::ui::kDeckKnobSize;
using instrument::ui::kPad;
using instrument::ui::deckParamNorm;
using instrument::ui::kEnvTimeMaxSeconds;
using instrument::ui::resetDeckParam;
using instrument::ui::sampleDeckGroups;
using instrument::ui::setDeckParam;
using instrument::engine::masterGainLinearFromNorm;
using instrument::engine::masterGainNormFromLinear;
using util::clamp01;

namespace {
// The raw stored curve exponent for a curve-dial control id, read DIRECTLY off the field —
// never round-tripped through curveFromKnobNorm(knobNormFromCurve(x)): the knob-norm law's
// centre detent (curve_law.h) snaps anything near-neutral back to exactly 1.0, so a round trip
// can misreport a stored exponent that isn't neutral as "^1.00".
double curveExponentFor(int id, const PlaySeconds& play) {
    using DeckParam = instrument::ui::DeckParam;  // ReaSamplerEditor::ParamControl is an alias
    switch (static_cast<DeckParam>(id)) {
        case DeckParam::kAttackCurve:           return play.adsr.attackCurve;
        case DeckParam::kDecayCurve:            return play.adsr.decayCurve;
        case DeckParam::kReleaseCurve:          return play.adsr.releaseCurve;
        case DeckParam::kTrigAttackCurve:       return play.trigAhd.attackCurve;
        case DeckParam::kTrigDecayCurve:        return play.trigAhd.decayCurve;
        case DeckParam::kPitchEnvAttackCurve:   return play.pitchEnv.shape.attackCurve;
        case DeckParam::kPitchEnvDecayCurve:    return play.pitchEnv.shape.decayCurve;
        case DeckParam::kFilterEnvAttackCurve:  return play.filter.env.attackCurve;
        case DeckParam::kFilterEnvDecayCurve:   return play.filter.env.decayCurve;
        case DeckParam::kFilterEnvReleaseCurve: return play.filter.env.releaseCurve;
        case DeckParam::kFilterTrigAttackCurve: return play.filter.trigEnv.attackCurve;
        case DeckParam::kFilterTrigDecayCurve:  return play.filter.trigEnv.decayCurve;
        default:                                return util::kCurveNeutral;
    }
}
}  // namespace

ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
    // The ONE resolve every paint and hit-test path goes through, so the band stack, the
    // chrome interior, and the deck descriptors can never be derived three different ways.
    // The deck's own height is the only interior measurement the allocator needs.
    FaceLayout fl;
    fl.deckDescs = sampleDeckGroups(params_.play.playMode);
    fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs));
    fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize);
    return fl;
}

// The parameter-set binding is the pure deck_values module's; these three are the shell's thin
// int-id adapters onto it (ParamControl is an alias of DeckParam).
double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
    return deckParamNorm(static_cast<instrument::ui::DeckParam>(id), play);
}

void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
                                    int segment) const {
    setDeckParam(static_cast<instrument::ui::DeckParam>(id), play, value, segment);
}

void ReaSamplerEditor::resetParamControl(int id) {
    // kKeyTrack is the one knob whose value sits beside the play bundle, so it defaults from
    // InstrumentParams rather than from PlaySeconds — the same split applyParamControl draws.
    if (id == static_cast<int>(ParamControl::kKeyTrack)) {
        params_.keyTrack = InstrumentParams{}.keyTrack;
        return;
    }
    resetDeckParam(static_cast<instrument::ui::DeckParam>(id), params_.play);
}

double ReaSamplerEditor::liveSampleRate() const {
    return processor_ ? processor_->sampleRate() : 0.0;
}

double ReaSamplerEditor::previewVelocity01() const {
    if (!processor_) return static_cast<double>(kPreviewVelocityDefault) / 127.0;
    return static_cast<double>(processor_->previewVelocity()) / 127.0;
}

double ReaSamplerEditor::bakeHoldNorm() const {
    return instrument::ui::bakeHoldNorm(params_.bakeHold);
}

double ReaSamplerEditor::deckControlNorm(int id) const {
    if (id == -2) return previewVelocity01();  // the chrome preview-velocity knob
    if (id == kBakeHoldKnobId) return bakeHoldNorm();
    switch (static_cast<ParamControl>(id)) {
        case ParamControl::kKeyTrack:
            return instrument::ui::keyTrackNormFrom(params_.keyTrack);
        case ParamControl::kVoiceCount:
            return clamp01(static_cast<double>(voiceCount_ - kMinVoiceCount) /
                           static_cast<double>(kMaxVoiceCount - kMinVoiceCount));
        case ParamControl::kMasterGain:
            return masterGainNormFromLinear(processor_ ? processor_->masterGainLinear() : 1.0);
        default:
            return controlValue(id, params_.play);
    }
}

instrument::ui::DeckEnableState ReaSamplerEditor::deckEnableState() const {
    const PlaySeconds& play = params_.play;
    return instrument::ui::DeckEnableState{
        play.pitchEnv.enabled, play.filter.enabled,
        play.ampSpline.mode == EnvMode::Spline,
        play.pitchSpline.mode == EnvMode::Spline,
        play.filterSpline.mode == EnvMode::Spline};
}

bool ReaSamplerEditor::loopControlsLive() const {
    return effectivePlayMode(params_.play) == PlayMode::Gate;
}

instrument::ui::WaveMarks ReaSamplerEditor::waveMarksFor(const SetupMarkers& m) const {
    using instrument::ui::WaveMark;
    instrument::ui::WaveMarks w;
    w.frame[static_cast<int>(WaveMark::kStart)] = m.start;
    w.frame[static_cast<int>(WaveMark::kLoopStart)] = m.loopStart;
    w.frame[static_cast<int>(WaveMark::kLoopEnd)] = m.loopEnd;
    // The crossfade grows LEFT from the seam it closes, which is where it is audible.
    w.frame[static_cast<int>(WaveMark::kCrossfade)] = m.loopEnd - m.crossfade;
    // The crossfade mark belongs to an ACTIVE loop: with the enable off there is no seam for it
    // to sit on, and no length to drag.
    w.present[static_cast<int>(WaveMark::kStart)] = true;
    w.present[static_cast<int>(WaveMark::kLoopStart)] = true;
    w.present[static_cast<int>(WaveMark::kLoopEnd)] = true;
    w.present[static_cast<int>(WaveMark::kCrossfade)] = m.hasLoop;
    return w;
}

instrument::ui::WaveMarks ReaSamplerEditor::grabbableMarks(const SetupMarkers& m) const {
    using instrument::ui::WaveMark;
    instrument::ui::WaveMarks w = waveMarksFor(m);
    if (!loopControlsLive()) {
        w.present[static_cast<int>(WaveMark::kLoopStart)] = false;
        w.present[static_cast<int>(WaveMark::kLoopEnd)] = false;
        w.present[static_cast<int>(WaveMark::kCrossfade)] = false;
    }
    return w;
}

void ReaSamplerEditor::setLoopEnabled(bool on) {
    const auto frames = static_cast<std::int64_t>(monoPcmFor(selectedId_).size());
    if (frames <= 0) return;
    SetupMarkers m = pickedMarkers(frames);
    if (m.hasLoop == on) return;  // a no-op commit would buy a re-decode for nothing
    m.hasLoop = on;
    applyMarkers(m);
    commitAndReload();
}

void ReaSamplerEditor::applyDeckKnob(int id, double norm) {
    if (!processor_) return;
    norm = clamp01(norm);
    if (id == -2) {
        // Preview velocity: live processor write (persisted per-instance; the setter clamps
        // to MIDI 1..127 so the knob's bottom still strikes audibly).
        processor_->setPreviewVelocity(static_cast<std::uint8_t>(norm * 127.0 + 0.5));
        return;
    }
    if (id == kBakeHoldKnobId) {
        // A parameter-set edit like the deck's own knobs: the commit lands on release.
        params_.bakeHold = instrument::ui::bakeHoldFromNorm(norm);
        return;
    }
    switch (static_cast<ParamControl>(id)) {
        case ParamControl::kVoiceCount: {
            // Stepped: quantize the continuous drag to the integer count and track it live
            // for the label/needle. The actual engine rebuild (setVoiceCount) fires ONCE on
            // WM_LBUTTONUP — not per step — so a full drag (~31 steps) costs one rebuild,
            // not thirty.
            const int count =
                kMinVoiceCount +
                static_cast<int>(norm * (kMaxVoiceCount - kMinVoiceCount) + 0.5);
            voiceCount_ = count;
            return;
        }
        case ParamControl::kMasterGain:
            // Post-mixer gain: one atomic store; the audio thread picks it up next block.
            processor_->setMasterGainLinear(masterGainLinearFromNorm(norm));
            return;
        default:
            applyParamControl(id, norm, 0);
            return;
    }
}

double ReaSamplerEditor::deckPlainValue(int id) const {
    const auto deck = static_cast<instrument::ui::DeckParam>(id);
    // A curve exponent is read off its stored field, never round-tripped through the knob law:
    // that law's centre detent snaps anything near-neutral back to exactly 1.0, so a round trip
    // would misreport a stored exponent that isn't neutral as 1.00. The host has only the norm
    // and therefore cannot make this distinction — param/CLAUDE.md records the divergence.
    if (instrument::ui::deckParamUnit(deck) == instrument::ui::UnitCategory::Exponent) {
        return curveExponentFor(id, params_.play);
    }
    return instrument::param::toPlain(deck, deckControlNorm(id));
}

std::string ReaSamplerEditor::deckValueLabel(int id) const {
    if (id < 0 || id >= static_cast<int>(ParamControl::kCount)) return {};
    const auto deck = static_cast<instrument::ui::DeckParam>(id);
    // The one deck knob with no plain-value layer at all: an already-integer count.
    if (deck == ParamControl::kVoiceCount) {
        char buf[24];
        snprintf(buf, sizeof(buf), "%d", voiceCount_);
        return std::string(buf);
    }
    if (instrument::param::paramIdFor(deck) == 0) return {};

    // The digits come from the ONE formatter; everything the editor adds around them is static
    // chrome — a constant prefix or suffix cannot diverge from what the host shows.
    const double plain = deckPlainValue(id);
    char digits[24];
    instrument::param::formatPlainFor(deck, plain, digits, sizeof(digits));
    const auto kind = instrument::param::unitKindFor(deck);
    const char* caret =
        instrument::ui::deckParamUnit(deck) == instrument::ui::UnitCategory::Exponent ? "^" : "";
    // The gain at true silence reads "-inf", not "-infdB": there is no decibel value there.
    if (kind == instrument::param::UnitKind::Decibels && !std::isfinite(plain)) {
        return std::string(digits);
    }
    // The stage times are the one category that carries a space before its unit, and always did —
    // this surface's own typography, not the host's (ParameterInfo::units is the bare string).
    const char* gap = kind == instrument::param::UnitKind::Time ? " " : "";
    return caret + std::string(digits) + gap + instrument::param::unitStringFor(deck);
}

EnvClampBounds ReaSamplerEditor::envClampBounds() const {
    // Lives here, beside controlValue/applyControl, because it must MATCH them: a node drag can
    // never produce a param a knob couldn't. Every stage time caps at kEnvTimeMaxSeconds; the
    // Hold fractions and the sustain level are [0,1] by definition and need no bound here.
    EnvClampBounds b;
    b.maxAttackSeconds  = kEnvTimeMaxSeconds;
    b.maxHoldSeconds    = kEnvTimeMaxSeconds;
    b.maxDecaySeconds   = kEnvTimeMaxSeconds;
    b.maxReleaseSeconds = kEnvTimeMaxSeconds;
    return b;
}

void ReaSamplerEditor::applyParamControl(int id, double value, int segment) {
    if (id == static_cast<int>(ParamControl::kKeyTrack)) {
        // keyTrack sits beside the play bundle, so it takes deck_values' own pair rather than
        // the PlaySeconds binding.
        params_.keyTrack = instrument::ui::keyTrackFromNorm(value);
    } else {
        applyControl(id, params_.play, value, segment);
    }
}

}  // namespace reasampler::vst
