// editor_controls.cpp — the ReaSamplerEditor's parameter plumbing: the band-stack layout
// resolve every paint/hit-test path shares, the control-value domain maps (controlValue /
// applyControl — seconds/fraction/frames <-> normalized 0..1), the knob-deck group
// descriptors + control-id<->value binding, and the envelope pack/unpack (the trigger-seam
// converter). Value logic only — no painting, no window plumbing.

#include "shell/instrument/reasampler_editor.h"

#include <algorithm>
#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/map/trigger_seam.h"    // triggerPlayLength / fade fraction converters
#include "core/instrument/ui/knob_deck.h"        // deckHeight / kDeckKnobSize (the band's own height)
#include "core/util/clamp01.h"
#include "shell/instrument/editor_internal.h"    // DeckGroup ids
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using namespace reasampler::instrument::map;  // PlaySeconds vocabulary + trigger_seam converters
using instrument::ui::EnvMode;                // envelope_overlay's mode enum
using instrument::ui::computeSampleBands;
using instrument::ui::chromeRects;
using instrument::ui::deckHeight;
using instrument::ui::kDeckKnobSize;
using instrument::ui::kPad;
using instrument::engine::formatMasterGainLabel;
using instrument::engine::masterGainLinearFromNorm;
using instrument::engine::masterGainNormFromLinear;
using util::clamp01;

namespace {
// Control-surface value domains (the shell owns these — param_slider is engine-free and maps
// only 0..1). Wall-clock time sliders (AHDSR A/H/D/R, pitch env A/D) span [0, kEnvTimeMaxSeconds]
// seconds — rate-free, exactly what the parameter set stores; the build resolves seconds->frames
// at the live rate. Source-timeline fade sliders (Trigger fade-in/out) store source frames
// (never a wall-clock second), but the knob's full-scale throw is a wall-clock intent —
// kFadeMaxSeconds resolved against the live rate at use (fadeMaxFrames()) rather than a baked-in
// rate constant, per the no-hardcoded-rate ruling.
constexpr double kEnvTimeMaxSeconds = 2.0;            // AHDSR A/H/D/R + pitch A/D throw ceiling (seconds)
constexpr double kFadeMaxSeconds = 2.0;              // Trigger fade throw ceiling (wall-clock)
constexpr double kPitchDepthMaxSemis = 24.0;          // AD pitch depth throw: +/-24 st, centered
constexpr double kKeyTrackMax = 2.0;                 // key-track slider ceiling (0..200%)

}  // 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 wrapped height is the only interior measurement the allocator needs.
    FaceLayout fl;
    fl.deckDescs = deckGroupDescs(params_.play);
    fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs, w - 2 * kPad));
    fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize);
    return fl;
}

double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
    // Wall-clock seconds -> normalized over the seconds ceiling; source frames -> normalized over
    // the rate-resolved frames ceiling. Two domains, kept explicit so neither leaks a rate. A
    // stored fade exceeding fadeMaxFrames() at the current host rate reads as norm 1.0 (clamp01
    // pins it) and gets rewritten down on the next knob touch.
    const double fadeMax = fadeMaxFrames();
    const auto secToNorm = [](double s) { return clamp01(s / kEnvTimeMaxSeconds); };
    const auto framesToNorm = [fadeMax](std::int64_t f) {
        // Ceiling unavailable (rate not yet known): inert until fadeMaxFrames() resolves.
        return fadeMax > 0.0 ? clamp01(static_cast<double>(f) / fadeMax) : 0.0;
    };
    switch (static_cast<ParamControl>(id)) {
        case ParamControl::kPlayMode:      return play.playMode == PlayMode::Trigger ? 1.0 : 0.0;
        case ParamControl::kPitchEngine:   return play.pitchEngine == PitchEngine::Preserve ? 1.0 : 0.0;
        case ParamControl::kAttack:        return secToNorm(play.adsr.attackSeconds);
        case ParamControl::kHold:          return secToNorm(play.adsr.holdSeconds);
        case ParamControl::kDecay:         return secToNorm(play.adsr.decaySeconds);
        case ParamControl::kSustain:       return clamp01(play.adsr.sustainLevel);
        case ParamControl::kRelease:       return secToNorm(play.adsr.releaseSeconds);
        case ParamControl::kTrigLength:    return clamp01(play.trigger.lengthFraction);
        case ParamControl::kTrigFadeIn:    return framesToNorm(play.trigger.fadeInFrames);
        case ParamControl::kTrigFadeOut:   return framesToNorm(play.trigger.fadeOutFrames);
        case ParamControl::kPitchEnvEnable:return play.pitchEnv.enabled ? 1.0 : 0.0;
        case ParamControl::kPitchEnvAttack:return secToNorm(play.pitchEnv.attackSeconds);
        case ParamControl::kPitchEnvDecay: return secToNorm(play.pitchEnv.decaySeconds);
        case ParamControl::kPitchEnvDepth:
            // Signed depth centered at 0.5 (0.5 == 0 semitones).
            return clamp01(0.5 + play.pitchEnv.peakSemitones / (2.0 * kPitchDepthMaxSemis));
        default: return 0.0;
    }
}

void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
                                    int segment) const {
    const double fadeMax = fadeMaxFrames();  // rate-resolved knob full-scale
    const auto normToSec = [](double v) { return clamp01(v) * kEnvTimeMaxSeconds; };
    const auto normToFrames = [fadeMax](double v) -> std::int64_t {
        // Ceiling unavailable (rate not yet known): inert until fadeMaxFrames() resolves.
        if (fadeMax <= 0.0) return 0;
        return static_cast<std::int64_t>(clamp01(v) * fadeMax + 0.5);
    };
    switch (static_cast<ParamControl>(id)) {
        case ParamControl::kPlayMode:
            play.playMode = (segment == 1) ? PlayMode::Trigger : PlayMode::Gate;
            break;
        case ParamControl::kPitchEngine:
            play.pitchEngine = (segment == 1) ? PitchEngine::Preserve : PitchEngine::Varispeed;
            break;
        case ParamControl::kAttack:   play.adsr.attackSeconds = normToSec(value); break;
        case ParamControl::kHold:     play.adsr.holdSeconds = normToSec(value); break;
        case ParamControl::kDecay:    play.adsr.decaySeconds = normToSec(value); break;
        case ParamControl::kSustain:  play.adsr.sustainLevel = clamp01(value); break;
        case ParamControl::kRelease:  play.adsr.releaseSeconds = normToSec(value); break;
        case ParamControl::kTrigLength:
            // lengthFraction is (0,1]; keep a small floor so a zero-length trigger never plays nothing.
            play.trigger.lengthFraction = (std::max)(0.01, clamp01(value));
            break;
        case ParamControl::kTrigFadeIn:  play.trigger.fadeInFrames = normToFrames(value); break;
        case ParamControl::kTrigFadeOut: play.trigger.fadeOutFrames = normToFrames(value); break;
        case ParamControl::kPitchEnvEnable:
            play.pitchEnv.enabled = (segment == 1);
            break;
        case ParamControl::kPitchEnvAttack: play.pitchEnv.attackSeconds = normToSec(value); break;
        case ParamControl::kPitchEnvDecay:  play.pitchEnv.decaySeconds = normToSec(value); break;
        case ParamControl::kPitchEnvDepth:
            play.pitchEnv.peakSemitones = (clamp01(value) - 0.5) * 2.0 * kPitchDepthMaxSemis;
            break;
        default: break;
    }
}

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

double ReaSamplerEditor::fadeMaxFrames() const {
    // The Trigger-fade knob's full-scale throw is kFadeMaxSeconds (2 s wall-clock) resolved
    // against the live rate — the same time base the envelope overlay already uses to place
    // these source-frame fades on screen. Pre-setupProcessing the rate is still 0: rather than
    // substitute a literal rate, callers treat a <= 0 return as "ceiling unavailable yet" and
    // degrade the knob to inert rather than guess a rate. Storage stays source frames — this
    // resolves the UI ceiling only.
    const double rate = liveSampleRate();
    if (rate <= 0.0) return 0.0;
    return kFadeMaxSeconds * rate;
}

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

std::vector<DeckGroupDesc> ReaSamplerEditor::deckGroupDescs(const PlaySeconds& play) const {
    // The deck band's groups, left to right. Group widths are mode-independent: AMP ENVELOPE
    // reserves its 5-cell Gate width (Trigger leaves two blank cells), so a Gate<->Trigger
    // flip repopulates in place and never reflows the neighbouring groups.
    std::vector<DeckGroupDesc> out;
    {
        DeckGroupDesc amp;
        amp.id = kGroupAmpEnv;
        amp.captionWidth = 78;
        amp.captionToggle = {static_cast<int>(ParamControl::kPlayMode), 44};
        if (play.playMode == PlayMode::Gate) {
            amp.cellIds = {static_cast<int>(ParamControl::kAttack),
                           static_cast<int>(ParamControl::kHold),
                           static_cast<int>(ParamControl::kDecay),
                           static_cast<int>(ParamControl::kSustain),
                           static_cast<int>(ParamControl::kRelease)};
        } else {
            // Trigger, time-ordered left-to-right (Fade In / Length % / Fade Out — matches
            // the drawn envelope), plus the two reserved blanks.
            amp.cellIds = {static_cast<int>(ParamControl::kTrigFadeIn),
                           static_cast<int>(ParamControl::kTrigLength),
                           static_cast<int>(ParamControl::kTrigFadeOut), -1, -1};
        }
        out.push_back(std::move(amp));
    }
    {
        DeckGroupDesc pitch;
        pitch.id = kGroupPitch;
        pitch.captionWidth = 38;
        pitch.captionToggle = {static_cast<int>(ParamControl::kPitchEngine), 48};
        pitch.cellIds = {static_cast<int>(ParamControl::kKeyTrack)};
        out.push_back(std::move(pitch));
    }
    {
        DeckGroupDesc penv;
        penv.id = kGroupPitchEnv;
        penv.captionWidth = 58;
        penv.captionToggle = {static_cast<int>(ParamControl::kPitchEnvEnable), 32};
        penv.cellIds = {static_cast<int>(ParamControl::kPitchEnvAttack),
                        static_cast<int>(ParamControl::kPitchEnvDecay),
                        static_cast<int>(ParamControl::kPitchEnvDepth)};
        out.push_back(std::move(penv));
    }
    {
        DeckGroupDesc voice;
        voice.id = kGroupVoice;
        voice.captionWidth = 38;
        voice.captionToggle = {static_cast<int>(ParamControl::kVoiceMode), 40};
        voice.cellIds = {static_cast<int>(ParamControl::kVoiceCount)};
        voice.rowToggle = {static_cast<int>(ParamControl::kMonoTrigger), 44};
        out.push_back(std::move(voice));
    }
    {
        DeckGroupDesc master;
        master.id = kGroupMaster;
        master.captionWidth = 46;
        master.cellIds = {static_cast<int>(ParamControl::kMasterGain)};
        out.push_back(std::move(master));
    }
    return out;
}

double ReaSamplerEditor::deckControlNorm(int id) const {
    if (id == -2) return previewVelocity01();  // the chrome preview-velocity knob
    switch (static_cast<ParamControl>(id)) {
        case ParamControl::kKeyTrack:
            return clamp01(params_.keyTrack / kKeyTrackMax);
        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);
    }
}

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;
    }
    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;
    }
}

std::string ReaSamplerEditor::deckValueLabel(int id) const {
    char buf[24];
    buf[0] = '\0';
    const PlaySeconds& play = params_.play;
    switch (id == -2 ? ParamControl::kCount : static_cast<ParamControl>(id)) {
        case ParamControl::kAttack:
            snprintf(buf, sizeof(buf), "%.3fs", play.adsr.attackSeconds); break;
        case ParamControl::kHold:
            snprintf(buf, sizeof(buf), "%.3fs", play.adsr.holdSeconds); break;
        case ParamControl::kDecay:
            snprintf(buf, sizeof(buf), "%.3fs", play.adsr.decaySeconds); break;
        case ParamControl::kSustain:
            snprintf(buf, sizeof(buf), "%.0f%%", play.adsr.sustainLevel * 100.0); break;
        case ParamControl::kRelease:
            snprintf(buf, sizeof(buf), "%.3fs", play.adsr.releaseSeconds); break;
        case ParamControl::kTrigLength:
            snprintf(buf, sizeof(buf), "%.0f%%", play.trigger.lengthFraction * 100.0); break;
        case ParamControl::kTrigFadeIn:
            snprintf(buf, sizeof(buf), "%lldf",
                     static_cast<long long>(play.trigger.fadeInFrames)); break;
        case ParamControl::kTrigFadeOut:
            snprintf(buf, sizeof(buf), "%lldf",
                     static_cast<long long>(play.trigger.fadeOutFrames)); break;
        case ParamControl::kPitchEnvAttack:
            snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.attackSeconds); break;
        case ParamControl::kPitchEnvDecay:
            snprintf(buf, sizeof(buf), "%.3fs", play.pitchEnv.decaySeconds); break;
        case ParamControl::kPitchEnvDepth:
            snprintf(buf, sizeof(buf), "%+.1fst", play.pitchEnv.peakSemitones); break;
        case ParamControl::kKeyTrack:
            snprintf(buf, sizeof(buf), "%.0f%%", params_.keyTrack * 100.0); break;
        case ParamControl::kVoiceCount:
            snprintf(buf, sizeof(buf), "%d", voiceCount_); break;
        case ParamControl::kMasterGain:
            formatMasterGainLabel(deckControlNorm(id), buf, sizeof(buf)); break;
        default:
            // -2 (preview velocity) is labeled at its chrome call site; nothing else here.
            break;
    }
    return std::string(buf);
}

EnvClampBounds ReaSamplerEditor::envClampBounds() const {
    // Match the control-panel sliders' own domains so a node drag can never produce a param a
    // slider couldn't. AHDSR seconds cap at kEnvTimeMaxSeconds; the Trigger fade/length
    // fractions cap at 1.0 (the natural full-span bound the sliders use).
    EnvClampBounds b;
    b.maxAttackSeconds  = kEnvTimeMaxSeconds;
    b.maxHoldSeconds    = kEnvTimeMaxSeconds;
    b.maxDecaySeconds   = kEnvTimeMaxSeconds;
    b.maxReleaseSeconds = kEnvTimeMaxSeconds;
    b.maxFadeInFraction  = 1.0;
    b.maxFadeOutFraction = 1.0;
    b.maxLengthFraction  = 1.0;
    return b;
}

AmpEnvelope ReaSamplerEditor::packEnvelope(const PlaySeconds& play, std::int64_t frames,
                                           std::int64_t startFrame) const {
    AmpEnvelope env;
    env.mode = (play.playMode == PlayMode::Trigger) ? EnvMode::Trigger : EnvMode::Gate;
    // AHDSR seconds copy 1-to-1 (rate-free, the same domain the overlay draws).
    env.attackSeconds  = play.adsr.attackSeconds;
    env.holdSeconds    = play.adsr.holdSeconds;
    env.decaySeconds   = play.adsr.decaySeconds;
    env.sustainLevel   = play.adsr.sustainLevel;
    env.releaseSeconds = play.adsr.releaseSeconds;
    // Trigger: lengthFraction copies 1-to-1; the fades are derived — source frames over the played
    // span (the trigger-seam converter, pack direction). startFrame is the effective start
    // point so the fraction denominator matches the voice's actual post-start span. A zero play
    // length yields 0 fractions.
    env.lengthFraction = play.trigger.lengthFraction;
    const std::int64_t playLen =
        triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
    env.fadeInFraction  = framesToFadeFraction(play.trigger.fadeInFrames,  playLen);
    env.fadeOutFraction = framesToFadeFraction(play.trigger.fadeOutFrames, playLen);
    return env;
}

void ReaSamplerEditor::unpackEnvelope(const AmpEnvelope& env, std::int64_t frames,
                                      std::int64_t startFrame, PlaySeconds& play) const {
    if (env.mode == EnvMode::Gate) {
        play.adsr.attackSeconds  = env.attackSeconds;
        play.adsr.holdSeconds    = env.holdSeconds;
        play.adsr.decaySeconds   = env.decaySeconds;
        play.adsr.sustainLevel   = env.sustainLevel;
        play.adsr.releaseSeconds = env.releaseSeconds;
    } else {
        // Trigger: lengthFraction copies back; the fades convert fractions -> source frames over
        // the played span (the trigger-seam converter, unpack direction). startFrame is the
        // effective start point so the frame denominator matches the voice's actual
        // post-start span. Keep the same (0,1] floor on lengthFraction the slider path enforces
        // so a zero-length trigger never plays nothing.
        play.trigger.lengthFraction = (std::max)(0.01, env.lengthFraction);
        const std::int64_t playLen =
            triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
        play.trigger.fadeInFrames  = fadeFractionToFrames(env.fadeInFraction,  playLen);
        play.trigger.fadeOutFrames = fadeFractionToFrames(env.fadeOutFraction, playLen);
    }
}

void ReaSamplerEditor::applyParamControl(int id, double value, int segment) {
    if (id == static_cast<int>(ParamControl::kKeyTrack)) {
        // keyTrack sits beside the play bundle (0..200% over kKeyTrackMax); the knob maps 0..1.
        params_.keyTrack = clamp01(value) * kKeyTrackMax;
    } else {
        applyControl(id, params_.play, value, segment);
    }
}

}  // namespace reasampler::vst
