// editor_input_deck.cpp — the DECKS band's input: toggles (committed at once, a discrete
// final edit), knob grabs (grab-anchored vertical drag, committed on release), the live
// knob-drag resolution, and the band's hover. Windows-only.

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32

#include "core/instrument/ui/deck_values.h"     // snapDeckParamNorm (Shift's whole-unit table)
#include "core/instrument/ui/knob_deck.h"       // hitTestDeck / layoutDeck
#include "core/instrument/ui/param_slider.h"    // knobDragValue (grab-anchored drag)
#include "shell/instrument/editor_internal.h"
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using namespace reasampler::ui;
using namespace reasampler::instrument::ui;

bool ReaSamplerEditor::deckKnobDisabled(int id) const {
    // Thin int-id wrapper over the pure, CTest-covered predicate — see deckKnobInert
    // (deck_groups.h) for which cells go inert and why.
    return deckKnobInert(static_cast<DeckParam>(id), deckEnableState());
}

bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
    const Rect& band = fl.bands.decks;
    if (!contains(band, x, y)) return false;

    const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
    const DeckHit hit = hitTestDeck(dl, x, y);
    if (hit.kind == DeckHitKind::CaptionRadio) {
        overlayEnv_ = nextOverlaySelection(overlayEnv_, hit.id);
        invalidate();  // view state only: no parameter write, no reload
        return true;
    }
    if (hit.kind == DeckHitKind::CaptionToggle || hit.kind == DeckHitKind::RowToggle) {
        switch (static_cast<ParamControl>(hit.id)) {
            case ParamControl::kVoiceMode: {
                // Processor-side per-instance param: live setter (engine rebuild via the
                // drain-slot swap — tails survive), local snapshot in step.
                const VoiceMode m = (hit.segment == 1) ? VoiceMode::Mono : VoiceMode::Poly;
                if (m != voiceMode_) {
                    voiceMode_ = m;
                    processor_->setVoiceMode(m);
                }
                invalidate();
                break;
            }
            case ParamControl::kMonoTrigger: {
                if (voiceMode_ != VoiceMode::Mono) break;  // Disabled (inert) in Poly
                const MonoTrigger t =
                    (hit.segment == 1) ? MonoTrigger::Legato : MonoTrigger::Retrigger;
                if (t != monoTrigger_) {
                    monoTrigger_ = t;
                    processor_->setMonoTrigger(t);
                }
                invalidate();
                break;
            }
            case ParamControl::kFilterLaw:
                // Inert while the filter is off, matching its Disabled paint.
                if (!params_.play.filter.enabled) break;
                applyParamControl(hit.id, 0.0, hit.segment);
                commitAndReload();
                break;
            case ParamControl::kLimiterEnable: {
                const bool on = (hit.segment == 1);
                if (on != params_.limiterEnabled) {
                    params_.limiterEnabled = on;
                    // Commits the audible state and the persisted state together, here, because
                    // this is a control the user A/Bs. The funnel only ARMS the host's latency
                    // restart — the sync tick delivers it — so nothing on this path calls into
                    // the host from inside a mouse handler.
                    processor_->setLimiterEnabled(on);
                }
                invalidate();
                break;
            }
            default: {
                // Parameter-set toggles (play mode / pitch engine / pitch-env + filter enable,
                // and the three env-mode toggles).
                applyParamControl(hit.id, 0.0, hit.segment);
                commitAndReload();
                // Flipping an EG's Staged|Spline toggle makes THAT envelope's overlay active,
                // so the contour (or the staged shape you just returned to) is what's drawn.
                // overlayEnvForModeToggle answers kNone for every other toggle this default
                // case handles, which is why the assignment is conditional.
                const OverlayEnv modeEnv = overlayEnvForModeToggle(hit.id);
                if (modeEnv != OverlayEnv::kNone) overlayEnv_ = modeEnv;
                break;
            }
        }
        return true;
    }
    if (hit.kind == DeckHitKind::Column) {
        // The meter's ONLY gesture: clear the latched clip cap. Both latches go — the audio
        // thread's is what the next tick would otherwise re-latch the UI's from.
        masterMeter_ = clearMasterMeterClip(masterMeter_);
        processor_->clearMasterBusClip();
        invalidate();
        return true;
    }
    if (hit.kind == DeckHitKind::Knob) {
        // Knobs of a disabled group are drawn but inert.
        if (deckKnobDisabled(hit.id)) return true;
        // A VELOCITY cell summons the popup editor instead of starting a knob drag.
        const CurveTarget curveCell = curveTargetFor(hit.id);
        if (curveCell != CurveTarget::kNone) {
            curvePopup_ = curveCell;
            invalidate();
            return true;
        }
        // A grab on the inner disc drags the CURVE control instead, but only where the stage
        // is sloped; on a Hold or Sustain cell the inner region is just more of the knob.
        const ParamControl curve = curveParamFor(static_cast<ParamControl>(hit.id));
        const bool inner = hit.inner && curve != ParamControl::kCount;
        drag_ = DragKind::kDeckKnob;
        dragParamId_ = inner ? static_cast<int>(curve) : hit.id;
        dragInnerCellId_ = inner ? hit.id : -1;
        dragKnobStartValue_ = deckControlNorm(dragParamId_);
        dragMods_ = dragModifiers();
        // Processor-side knobs (voice count / master gain) are transient live writes with no
        // parameter-set mutation, so they need no rollback snapshot.
        dragStartParams_ = params_;
        dragStartX_ = x;
        dragStartY_ = y;
        // Opens the host's edit bracket for the whole gesture, so a host in touch or latch mode
        // records one continuous edit rather than a burst of one-point ones. Closed on release
        // and on capture-lost; a control with no parameter row is a no-op inside the processor.
        if (processor_) {
            processor_->beginParamGesture(static_cast<instrument::ui::DeckParam>(dragParamId_));
        }
        invalidate();
    }
    // The deck band swallows its own clicks either way — no fall-through to the waveform.
    return true;
}

bool ReaSamplerEditor::doubleClickDeck(const FaceLayout& fl, int x, int y) {
    const Rect& band = fl.bands.decks;
    if (!contains(band, x, y)) return false;
    const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
    // Resolved against the drawn CIRCLES (hitTestKnobFace), not the cell a drag grabs anywhere
    // in: the two rings are concentric, so only a radial resolve can tell "reset the exponent"
    // from "reset the value".
    const DeckFaceHit hit = hitTestKnobFace(dl, x, y);
    if (hit.id < 0) return false;
    if (deckKnobDisabled(hit.id)) return true;  // drawn-but-dead: swallow, change nothing
    // A VELOCITY cell is a popup opener with no scalar value to reset.
    if (curveTargetFor(hit.id) != CurveTarget::kNone) return true;

    const ParamControl curve = curveParamFor(static_cast<ParamControl>(hit.id));
    const bool inner = hit.inner && curve != ParamControl::kCount;
    const int target = inner ? static_cast<int>(curve) : hit.id;
    // The processor-side knobs own their own defaults — they never reach the parameter set.
    if (target == static_cast<int>(ParamControl::kVoiceCount)) {
        voiceCount_ = kDefaultVoiceCount;
        processor_->setVoiceCount(voiceCount_);
        invalidate();
        return true;
    }
    if (target == static_cast<int>(ParamControl::kMasterGain)) {
        processor_->setMasterGainLinear(1.0);
        invalidate();
        return true;
    }
    resetParamControl(target);
    // Same commit tiering a drag of this control takes, so a reset reaches the sounding note
    // exactly as a drag to the same value would.
    if (dragCommitsLive(DragKind::kDeckKnob, target)) commitLive();
    else commitAndReload();
    invalidate();
    return true;
}

void ReaSamplerEditor::dragDeck(int x, int y) {
    // Radial knob: grab-anchored vertical drag — knobDragValue maps the y delta from the
    // value at grab (up = increase), so the value tracks relative motion and never jumps on
    // grab. Live feedback; parameter-set commits land on WM_LBUTTONUP.
    (void)x;
    const DragModifiers mods = dragModifiers();
    if (mods != dragMods_) {
        // Re-anchor (see dragMods_). Reading the anchor back off the control also means a Shift
        // RELEASE re-anchors from the SNAPPED value, so the knob does not spring back.
        dragKnobStartValue_ = deckControlNorm(dragParamId_);
        dragStartY_ = y;
        dragMods_ = mods;
    }
    double norm = knobDragValue(dragKnobStartValue_, y - dragStartY_, mods);
    // The preview-velocity sentinel (-2) and the discrete controls have no whole unit to snap to.
    if (mods.shift && dragParamId_ >= 0) {
        norm = snapDeckParamNorm(static_cast<DeckParam>(dragParamId_), norm);
    }
    applyDeckKnob(dragParamId_, norm);
    // A live control is delivered on every move, not only on release — that is the whole
    // point: the note already sounding tracks the hand on the knob. The processor-side knobs
    // are skipped because applyDeckKnob already wrote them straight through; commitLive would
    // only re-push an unchanged parameter set. The release and capture-lost paths ask this same
    // question; the reset path never reaches the commit for them at all.
    if (!deckKnobIsProcessorSide(dragParamId_) &&
        dragCommitsLive(DragKind::kDeckKnob, dragParamId_)) {
        commitLive();
    }
    invalidate();
}

HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl, int x,
                                        int y) const {
    const Rect& band = fl.bands.decks;
    if (!contains(band, x, y)) return {};
    const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
    const DeckHit dh = hitTestDeck(dl, x, y);
    if (dh.kind == DeckHitKind::None) return {};
    // The meter reports its own state continuously; a hover on it would only mean "the clip
    // cap is clearable", which the cap's presence already says.
    if (dh.kind == DeckHitKind::Column) return {};
    if (dh.kind == DeckHitKind::CaptionRadio) return {HoverKind::kEnvRadio, dh.id};
    if (dh.kind == DeckHitKind::Knob && dh.inner &&
        curveParamFor(static_cast<ParamControl>(dh.id)) != ParamControl::kCount) {
        // Indexed by the OUTER cell id so the paint side can find the cell it belongs to.
        return {HoverKind::kInnerDial, dh.id};
    }
    return {HoverKind::kControl, dh.id};
}

}  // namespace reasampler::vst

#endif  // _WIN32
