// editor_input.cpp — the ReaSamplerEditor's input dispatch and drag-state machine: the
// mouse-down routing (curve popup first, then Browse or the three bands in order), the
// onMouseMove drag router, the release commit, and the hover resolver. The per-band
// branches live in the editor_input_<band> TUs; this TU only sequences them.
// Windows-only. All hit-test math is pure; this family routes and mutates editor state.

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32

#include "shell/instrument/editor_internal.h"   // curveBoxFromRect + kCurveDragOffMargin
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

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

void ReaSamplerEditor::onMouseDown(int x, int y) {
    if (!processor_) return;
    RECT cr{};
    GetClientRect(childHwnd_, &cr);
    const int w = cr.right - cr.left;
    const int h = cr.bottom - cr.top;

    if (view_ == View::kBrowse) {
        mouseDownBrowse(w, h, x, y);
        return;
    }

    // The curve popup is modal over the face — while open it owns every left-click.
    if (handlePopupMouseDown(w, h, x, y)) return;

    // Band order matters only where bands can overlap on a degenerate window; each branch
    // reports whether it consumed the click so the next band gets a clean shot.
    const FaceLayout fl = faceLayout(w, h);
    if (mouseDownChrome(fl, x, y)) return;
    if (selectedId_.empty()) return;  // empty state — chrome nav only
    if (mouseDownDeck(fl, x, y)) return;
    mouseDownWaveform(fl, x, y);
}

bool ReaSamplerEditor::onMouseDoubleClick(int x, int y) {
    // Only the Sample face's radial knobs claim the gesture. Everything else — Browse's
    // load accelerator, the spline surfaces' point grammar — is left to the caller's
    // fall-through to onMouseDown, which is the path those surfaces were built on.
    if (!processor_ || view_ != View::kSample || curvePopup_ != CurveTarget::kNone) return false;
    RECT cr{};
    GetClientRect(childHwnd_, &cr);
    const FaceLayout fl = faceLayout(cr.right - cr.left, cr.bottom - cr.top);
    if (doubleClickChrome(fl, x, y)) return true;
    if (selectedId_.empty()) return false;
    return doubleClickDeck(fl, x, y);
}

void ReaSamplerEditor::onMouseMove(int x, int y) {
    if (drag_ == DragKind::kNone) return;
    dragCurX_ = x;  // keep the live cursor position for drag-state draw cues (e.g. drag-off warn)
    dragCurY_ = y;

    // The three band-free drags resolve without a layout pass at all.
    switch (drag_) {
        case DragKind::kDeckKnob:    dragDeck(x, y); return;
        case DragKind::kScrollThumb: dragBrowse(x, y); return;
        case DragKind::kCurveNode:   dragCurve(x, y); return;
        default: break;
    }

    RECT rc{};
    GetClientRect(childHwnd_, &rc);
    const FaceLayout fl = faceLayout(rc.right - rc.left, rc.bottom - rc.top);
    if (drag_ == DragKind::kRootMarker) {
        dragChrome(fl, x, y);
    } else {
        dragWaveform(fl, x, y);
    }
}

void ReaSamplerEditor::onMouseUp(int x, int y) {
    // Release a held preview note first (the preview button is a momentary key: note-off on up).
    // This runs regardless of drag state — the preview press does not start a drag.
    if (previewingNote_ >= 0) {
        if (processor_) processor_->previewNoteOff(previewingNote_);
        previewingNote_ = -1;
        invalidate();
    }
    if (drag_ == DragKind::kNone) return;
    // Stack RAII rather than a call at each exit: this handler leaves through several early
    // returns, and the release commit's final performEdit must land INSIDE the bracket the grab
    // opened while the bracket itself may not outlive the handler on any path.
    struct GestureClose {
        ReaSamplerProcessor* p;
        ~GestureClose() { if (p) p->endParamGesture(); }
    } gestureClose{processor_};
    const DragKind kind = drag_;
    const int paramId = dragParamId_;
    const int curveIdx = curvePointIndex_;
    const Rect curveRect = dragCurveRect_;
    drag_ = DragKind::kNone;
    dragParamId_ = -1;
    dragInnerCellId_ = -1;
    curvePointIndex_ = -1;
    // hover_ is deliberately not re-resolved during a drag (see resolveHover's caller), so it
    // still names wherever the drag started. Re-resolve now against the release position, for
    // every drag kind — otherwise the next paint latches a stale hover (wrong note name/tooltip,
    // wrong control outline) until the next WM_MOUSEMOVE. This resolve runs before the release
    // branches below can delete or relocate the hovered element; a future branch that does so
    // must clear hover_ itself afterwards (as the curve drag-off delete does below) rather than
    // rely on this resolve, which reflects pre-mutation state.
    resolveHover(x, y);
    // A scrollbar drag is transient UI (no parameter change), and the processor-side knobs
    // (the preview-velocity -2 sentinel, voice count, master gain) are per-instance settings
    // that don't reload the instrument. Master gain is an atomic the audio thread reads
    // directly. Voice count: the label/needle tracks live during the drag but the engine
    // rebuild (setVoiceCount) fires ONCE here on release — not per integer step.
    const bool deckTransient =
        kind == DragKind::kDeckKnob && deckKnobIsProcessorSide(paramId);
    if (kind == DragKind::kScrollThumb || deckTransient) {
        // Commit the voice count now that the drag is complete (one rebuild per full drag).
        if (deckTransient && processor_ &&
                paramId == static_cast<int>(ParamControl::kVoiceCount))
            processor_->setVoiceCount(voiceCount_);
        invalidate();
        return;
    }
    // A bare click on the Hold cell — no drag, no value change — would otherwise buy a bridge
    // read, a WAV re-decode and an engine rebuild for a parameter the engine never reads.
    if (kind == DragKind::kDeckKnob && paramId == kBakeHoldKnobId &&
        params_.bakeHold == dragStartParams_.bakeHold) {
        invalidate();
        return;
    }
    // A live control already reached the voices during the drag; its release commits the
    // final value through the same tier.
    if (dragCommitsLive(kind, paramId)) {
        commitLive();
        invalidate();
        return;
    }
    // Drag-off delete: releasing a curve-node drag well outside its box removes the dragged
    // point on EITHER spline surface — the popup's kCurveNode and the overlay's kSplineNode
    // share this grammar, not just their click grammar (deletePoint refuses the two endpoints,
    // so an endpoint drag-off is a plain move — its amp keeps the last clamped drag value).
    if ((kind == DragKind::kCurveNode || kind == DragKind::kSplineNode) && curveIdx >= 0) {
        const bool off = x < curveRect.x - kCurveDragOffMargin ||
                         x > curveRect.right() + kCurveDragOffMargin ||
                         y < curveRect.y - kCurveDragOffMargin ||
                         y > curveRect.bottom() + kCurveDragOffMargin;
        if (off) {
            if (kind == DragKind::kCurveNode) {
                editedCurve().deletePoint(static_cast<std::size_t>(curveIdx));
            } else {
                splineFor(overlayEnv_).deletePoint(static_cast<std::size_t>(curveIdx));
            }
            hover_ = HoverTarget{};  // stale node index would light a shifted node
        }
    }
    commitAndReload();
}

// Resolve the interactive element under (x, y) into hover_ and repaint only on change (an
// idle move is free). Mirrors onMouseDown's routing order, but read-only.
void ReaSamplerEditor::resolveHover(int x, int y) {
    RECT cr{};
    GetClientRect(childHwnd_, &cr);
    const int w = cr.right - cr.left;
    const int hgt = cr.bottom - cr.top;

    HoverTarget h;  // kNone by default
    if (view_ == View::kBrowse) {
        h = hoverBrowse(w, hgt, x, y);
    } else if (curvePopup_ != CurveTarget::kNone) {  // modal over the face
        h = hoverCurvePopup(w, hgt, x, y);
    } else {
        const FaceLayout fl = faceLayout(w, hgt);
        h = hoverChrome(fl, x, y);
        if (h.kind == HoverKind::kNone && !selectedId_.empty()) h = hoverDeck(fl, x, y);
        if (h.kind == HoverKind::kNone && !selectedId_.empty()) h = hoverWaveform(fl, x, y);
    }

    if (h != hover_) {
        hover_ = h;
        invalidate();
    }
}

}  // namespace reasampler::vst

#endif  // _WIN32
