// editor_input_waveform.cpp — the WAVEFORM band's input: grabbing an envelope node or a
// start/loop marker, and resolving both drags live against the pure inverse maps
// (envelope_edit, waveform_view). Windows-only.
//
// Overlay contract: see waveform_view.h's WaveformSurface.

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32

#include <algorithm>
#include <cstdint>
#include <vector>

#include "core/instrument/engine/loop/loop_span.h"     // maxCrossfade (the shared drag-clamp bound)
#include "core/instrument/engine/velocity_curve.h"      // kCurveNodeGrabRadius (target-size arbitration)
#include "core/instrument/ui/envelope_edit.h"   // nodeAtPoint / resolveNodeDrag
#include "core/instrument/ui/spline_edit.h"     // the shared point-editing grammar
#include "core/instrument/ui/waveform_view.h"   // waveformOverlayArea / markerAtPoint / snap
#include "shell/instrument/editor_internal.h"
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using namespace reasampler::ui;
using namespace reasampler::instrument::ui;
using instrument::engine::loop::maxCrossfade;
using instrument::engine::kCurveNodeGrabRadius;

bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
    const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
    const std::int64_t frames = static_cast<std::int64_t>(pcm.size());
    if (frames <= 0) return false;
    const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);

    const DeckEnableState gates = deckEnableState();
    const bool splineLive = overlayIsSpline() && overlayEnvEnabled(overlayEnv_, gates);
    const SplineGesture gesture =
        dragModifiers().ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft;

    // The staged envelope's draggable node and the drawn contour's node are mutually exclusive
    // (overlayEnvInert flips the staged one inert exactly when its envelope is in Spline mode),
    // so at most one of the two hit-tests below is ever live for the same click — both feed the
    // SAME arbitration slot below rather than either one getting its own check-order return.
    const double rate = liveSampleRate();
    const bool nodesLive =
        overlayEnv_ != OverlayEnv::kNone && !overlayEnvInert(overlayEnv_, gates);
    NodeHit envNodeHit;
    StageEnvelope env;
    if (rate > 0.0 && nodesLive) {
        const std::int64_t startFrame = params_.startPoint.value_or(0);
        env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
        const double totalSeconds = static_cast<double>(frames) / rate;
        envNodeHit = nodeAtPoint(env, overlay, totalSeconds, x, y);
    }

    const SetupMarkers m = pickedMarkers(frames);
    const WaveMarks grabbable = grabbableMarks(m);
    const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd};

    // Three affordances can claim the same pixel: a node (the staged envelope's or the drawn
    // contour's — a small fixed pick box either way), a mark's CAP (a small clipped top-strip
    // tab), and a mark's full-height grab column (waveform_view.h's cap-vs-column split already
    // keeps a cap apart from ITS OWN column; this is the cross-affordance case on top of that).
    // resolveWaveformClaim (spline_edit.h) is the ONE arbitration: it measures each claimant's
    // own NOMINAL target area and lets the smallest hit win, since a fixed check order shadows
    // whichever one loses the tie — this seam regressed twice from exactly that fix. Giving every
    // mark a cap changed WHICH mark the cap slot resolves to AND the slot's own nominal area
    // (every cap is one markerHandleRect, so it moved from the old clipped-actual measure — 60 at
    // frame 0 — to the nominal 110); the `cap < node < column` ordering held anyway, because 110
    // is still under the node's fixed pick-box area. Never add here (kAdd is only tried once
    // nothing else has claimed the click, below).
    WaveformClaim node;
    if (envNodeHit.hit) {
        constexpr std::int64_t side = 2 * kNodeGrabRadius + 1;
        node = {true, side * side};
    } else if (splineLive) {
        const VelocityCurve::Box box = splineOverlayBox(overlay);
        // Also require strict in-box, matching splineOverlayClick's own narrowing (spline_edit.h's
        // grammar note) — otherwise this candidate could "win" the arbitration below for a click
        // splineOverlayClick would then refuse, silently swallowing it instead of falling through
        // to the tab/marker checks.
        if (contains(overlay.rect, x, y) && splineFor(overlayEnv_).pointAtPixel(box, x, y) >= 0) {
            constexpr std::int64_t side = 2 * kCurveNodeGrabRadius + 1;
            node = {true, side * side};
        }
    }

    const int capHit = capAtPoint(overlay, frames, grabbable, x, y);
    const WaveformClaim tab =
        (capHit >= 0)
            ? WaveformClaim{true, static_cast<std::int64_t>(2 * kMarkerHandleHalfWidth + 1) *
                                      kMarkerHandleHeight}
            : WaveformClaim{};

    // Nominal, not actual: markerAtPoint clips the column at the overlay edges (a marker at
    // frame 0 has 6 usable columns, not 11) and the node's fixed side clips too at a pick-box
    // corner. Both overestimate in the direction that already produces the intended winner, so
    // the arbitration runs on NOMINAL area, not the measured hit-testable pixel count.
    const int markerHit =
        (grabbable.present[static_cast<int>(WaveMark::kLoopStart)]
             ? markerAtPoint(overlay, frames, markerFrames, 3, x, y)
             // In Trigger only START answers a column, and it is index 0 of the same array.
             : markerAtPoint(overlay, frames, markerFrames, 1, x, y));
    const WaveformClaim marker =
        (markerHit >= 0)
            ? WaveformClaim{true, static_cast<std::int64_t>(2 * kMarkerGrabWidth + 1) *
                                       overlay.rect.height}
            : WaveformClaim{};

    switch (resolveWaveformClaim(node, tab, marker, gesture)) {
        case WaveformClaimant::kNode:
            if (envNodeHit.hit) {
                drag_ = DragKind::kEnvNode;
                envNode_ = envNodeHit.node;
                dragStartX_ = x;
                dragStartY_ = y;
                dragMods_ = dragModifiers();
                dragStartEnv_ = env;
                dragSampleFrames_ = frames;
                dragStartParams_ = params_;
                // Peer of mouseDownDeck's bracket. LATCHING with no id named, because a node or
                // knot drag can move more than one exposed parameter and the grab cannot know
                // which; the release and capture-lost paths close it generically.
                if (processor_) processor_->beginParamGestureLatch();
                return true;  // node moves once the cursor drags
            }
            return splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/false);
        case WaveformClaimant::kTab:
            beginMarkerDrag(static_cast<WaveMarker>(capHit), m, frames, x);
            return true;
        case WaveformClaimant::kMarker:
            beginMarkerDrag(static_cast<WaveMarker>(markerHit), m, frames, x);
            return true;
        case WaveformClaimant::kNone:
            break;
    }
    // Nothing else wanted the click: now the drawn contour may take the empty space.
    if (splineLive) return splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/true);
    return false;
}

bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, int y,
                                          SplineGesture gesture, bool addOnEmptySpace) {
    const VelocityCurve::Box box = splineOverlayBox(waveArea);
    VelocityCurve& contour = splineFor(overlayEnv_);
    SplineEdit edit = resolveSplineEdit(contour, box, gesture, x, y);
    // The overlay's outside-box narrowing — see spline_edit.h's grammar note for why.
    if (edit.kind != SplineEditKind::kNone && edit.kind != SplineEditKind::kAdd &&
        !(x >= box.left && x < box.left + box.width && y >= box.top && y < box.top + box.height)) {
        edit = SplineEdit{};
    }
    if (edit.kind == SplineEditKind::kAdd && !addOnEmptySpace) return false;
    switch (edit.kind) {
        case SplineEditKind::kNone:
            return false;
        case SplineEditKind::kDelete:
            // deletePoint refuses the two endpoints, so a right-click on one is a safe no-op.
            if (!contour.deletePoint(static_cast<std::size_t>(edit.index))) return true;
            hover_ = HoverTarget{};  // a stale index would light a shifted node
            commitAndReload();
            return true;
        case SplineEditKind::kToggleHard:
            if (!contour.toggleHard(static_cast<std::size_t>(edit.index))) return true;
            commitAndReload();
            return true;
        case SplineEditKind::kAdd:
        case SplineEditKind::kGrab:
            break;
    }
    // Snapshot BEFORE the add so a capture-loss rollback cancels the in-flight point too
    // (the same contract the other parameter-editing drags keep).
    dragStartParams_ = params_;
    int index = edit.index;
    if (edit.kind == SplineEditKind::kAdd) {
        const VelocityPoint p = contour.pointFromPixel(box, x, y);
        index = contour.addPoint(p.velocity, p.value);
        // At the ceiling: refused, contour untouched, nothing to roll back. Swallow the click
        // rather than letting it fall through to a marker grab under the cursor.
        if (index < 0) return true;
    }
    drag_ = DragKind::kSplineNode;
    curvePointIndex_ = index;
    dragStartCurve_ = contour;  // AFTER the add — resolvePointDrag's delta base
    // The release-time drag-off-delete bound (onMouseUp), matching the popup's kCurveNode use
    // of the same field — the two spline surfaces share one drag-off grammar, not just the
    // click grammar.
    dragCurveRect_ = waveArea.rect;
    dragStartX_ = x;
    dragStartY_ = y;
    invalidate();  // live feedback; the commit lands on WM_LBUTTONUP
    return true;
}

HoverTarget ReaSamplerEditor::hoverWaveform(const FaceLayout& fl, int x,
                                            int y) {
    // Caps only: the cap is the grip, so it is the one thing on the overlay a resting pointer
    // can be "on". A hovered mark promotes its own label past the suppression rule.
    //
    // Rejected on the band's own rect FIRST, before anything expensive: this runs on every
    // WM_MOUSEMOVE, and with no loop override set pickedMarkers costs a bridge read plus a bank
    // parse. Every cap lives in the top kMarkerHandleHeight of the band, so that strip is the
    // only place the answer can be anything but a miss.
    const Rect& band = fl.bands.waveform;
    if (band.empty() || x < band.x || x >= band.right() || y < band.y ||
        y >= band.y + kMarkerHandleHeight) {
        return {};
    }
    const auto frames = static_cast<std::int64_t>(monoPcmFor(selectedId_).size());
    if (frames <= 0) return {};
    const OverlayArea overlay = waveformOverlayArea(band);
    const int cap = capAtPoint(overlay, frames, grabbableMarks(pickedMarkers(frames)), x, y);
    if (cap < 0) return {};
    return {HoverKind::kWaveMark, cap};
}

void ReaSamplerEditor::beginMarkerDrag(WaveMarker which, const SetupMarkers& m,
                                       std::int64_t frames, int x) {
    drag_ = DragKind::kWaveMarker;
    waveMarker_ = which;
    dragStartX_ = x;
    dragStartMarkers_ = m;
    dragSampleFrames_ = frames;
    dragStartParams_ = params_;
}

void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
    const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);
    const int dx = x - dragStartX_;

    if (drag_ == DragKind::kSplineNode) {
        // Same absolute-delta contract as the popup's node drag, against the grab-time contour
        // and the overlay's own box.
        if (curvePointIndex_ < 0) return;
        splineFor(overlayEnv_) = VelocityCurve::resolvePointDrag(
            dragStartCurve_, static_cast<std::size_t>(curvePointIndex_),
            splineOverlayBox(overlay), dx, y - dragStartY_);
        invalidate();  // live feedback; commit on release
        return;
    }

    if (drag_ == DragKind::kEnvNode) {
        // Resolve the grabbed envelope node's new params from the pixel delta (through the
        // pure envelope_edit inverse map, clamped), then unpack them back onto the parameter
        // set. The StageEnvelope was snapshotted at grab (dragStartEnv_) so the delta is
        // absolute.
        const std::int64_t frames = dragSampleFrames_;
        const double rate = liveSampleRate();
        if (frames <= 0 || rate <= 0.0) return;
        const double totalSeconds = static_cast<double>(frames) / rate;
        const DragModifiers mods = dragModifiers();
        if (mods != dragMods_) {
            // Re-anchor (see dragMods_): the node's CURRENT params and the cursor's current
            // position become the origin, so the flip changes only the rate. Re-packing the
            // grab envelope is what makes that true for the delta this resolver measures.
            dragStartEnv_ =
                packEnvelope(overlayEnv_, params_.play, frames, params_.startPoint.value_or(0));
            dragStartX_ = x;
            dragStartY_ = y;
            dragMods_ = mods;
        }
        const StageEnvelope edited =
            resolveNodeDrag(dragStartEnv_, envNode_, overlay, totalSeconds, envClampBounds(),
                            x - dragStartX_, y - dragStartY_, mods);
        unpackEnvelope(overlayEnv_, edited, params_.play);
        if (dragCommitsLive(DragKind::kEnvNode)) commitLive();
        invalidate();  // live feedback; commit on WM_LBUTTONUP
        return;
    }

    // kWaveMarker: resolve the grabbed marker's new frame from the pixel delta,
    // zero-crossing-snap it against the decoded PCM, apply the inter-marker clamps, and write
    // the override live.
    const std::int64_t frames = dragSampleFrames_;
    if (frames <= 0) return;

    // Grabbed frame at grab time, from the snapshot (so the delta is measured from grab).
    const int idx = static_cast<int>(waveMarker_);
    const std::int64_t startVals[4] = {dragStartMarkers_.start, dragStartMarkers_.loopStart,
                                       dragStartMarkers_.loopEnd,
                                       dragStartMarkers_.loopEnd -
                                           dragStartMarkers_.crossfade};
    std::int64_t newFrame = resolveDragFrame(overlay, frames, startVals[idx], dx);

    // Snap to the nearest zero crossing in the decoded PCM. Pure over the cached mono
    // frames — no host types, no file I/O. The crossfade handle is exempt: it sets a fade
    // LENGTH, and the whole point of the fade is that its edges need no zero crossing.
    const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
    if (!pcm.empty() && waveMarker_ != WaveMarker::kCrossfade) {
        newFrame = nearestZeroCrossing(pcm.data(), static_cast<std::int64_t>(pcm.size()),
                                       newFrame);
    }

    // Build the edited marker set from the snapshot, moving only the grabbed marker, then
    // clamp: loopStart <= loopEnd, start in [0, frames-1]. Dragging a LOOP marker turns the
    // enable on — a grab implies intent to loop, and it is what teaches the chrome toggle by
    // demonstration. Dragging START does not: it is live in both modes and says nothing about
    // the loop.
    SetupMarkers m = dragStartMarkers_;
    if (waveMarker_ == WaveMarker::kStart) {
        m.start = newFrame;
    } else if (waveMarker_ == WaveMarker::kLoopStart) {
        m.loopStart = (std::min)(newFrame, m.loopEnd);
        m.hasLoop = true;
    } else if (waveMarker_ == WaveMarker::kLoopEnd) {
        m.loopEnd = (std::max)(newFrame, m.loopStart);
        m.hasLoop = true;
    } else {  // kCrossfade — the handle sits at loopEnd - crossfade, so left still lengthens it
        m.crossfade = (std::max)(std::int64_t{0}, m.loopEnd - newFrame);
    }
    if (m.start < 0) m.start = 0;
    if (m.start > frames - 1) m.start = frames - 1;
    // Shares resolveLoop's own bound (loop_span.h's maxCrossfade) so the handle can't be
    // dragged somewhere the engine would silently clamp back.
    m.crossfade = (std::min)(m.crossfade, maxCrossfade(m.loopStart, m.loopEnd - m.loopStart));
    if (m.crossfade < 0) m.crossfade = 0;

    applyMarkers(m);
    invalidate();  // live feedback; the commit lands on release
}

}  // namespace reasampler::vst

#endif  // _WIN32
