// editor_input_browse.cpp — the Browse modal's input: the click branch (tabs, cards,
// select-then-confirm, scroll-thumb grab, search focus), the thumb drag, the wheel scroll,
// the type-to-filter keystrokes, and the modal's hover. Also carries the degraded
// drop affordance (an OS drop is never ingested). Windows-only.

#include "shell/instrument/reasampler_editor.h"

#ifdef _WIN32

#include <string>
#include <vector>

#include "core/instrument/ui/browser_scroll.h"  // BrowseModal + scroll/search geometry
#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 namespace reasampler::instrument::map;

void ReaSamplerEditor::mouseDownBrowse(int w, int h, int x, int y) {
    const BrowseModal bm = computeBrowseModal(w, h);
    if (contains(bm.back, x, y) || contains(bm.cancel, x, y)) {
        // Cancel/Back: discard the pending pick, return to Sample unchanged.
        browsePendingId_.clear();
        searchFocused_ = false;
        view_ = View::kSample;
        invalidate();
        return;
    }
    if (contains(bm.confirm, x, y)) {
        // Load: commit the pending pick (if any) into the loaded selection + reload, then Sample.
        if (!browsePendingId_.empty()) loadSelection(browsePendingId_);
        browsePendingId_.clear();
        searchFocused_ = false;
        view_ = View::kSample;
        invalidate();
        return;
    }
    if (contains(bm.search, x, y)) { searchFocused_ = true; invalidate(); return; }
    searchFocused_ = false;

    const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height);
    const int bx = x - bm.content.x;
    const int by = y - bm.content.y;
    const int tabCount = static_cast<int>(banks_.size()) + 1;
    const int tab = filterTabHitTest(bl, tabCount, bx, by);
    if (tab >= 0) {
        activeFilterBankId_ = (tab == 0) ? std::string()
                                         : banks_[static_cast<std::size_t>(tab - 1)].id;
        rebuildVisible();
        invalidate();
        return;
    }
    const Rect thumb = scrollThumbRect(bl, static_cast<int>(visible_.size()), scrollOffset_);
    if (thumb.height > 0 &&
        contains(Rect::ltrb(thumb.x + bm.content.x, thumb.y + bm.content.y,
                      thumb.right() + bm.content.x, thumb.bottom() + bm.content.y), x, y)) {
        drag_ = DragKind::kScrollThumb;
        dragStartY_ = y;
        dragStartScrollOffset_ = scrollOffset_;
        return;
    }
    const int card = cardHitTest(bl, static_cast<int>(visible_.size()), bx, by + scrollOffset_);
    if (card >= 0) {
        // Select-then-confirm: a click marks the pending pick; a DOUBLE-click on the same card
        // is the load accelerator (commit + dismiss). Browse never loads on a single click.
        const std::string id = visible_[static_cast<std::size_t>(card)].id;
        if (lastBrowseClickCard_ == card && browsePendingId_ == id) {
            loadSelection(id);
            browsePendingId_.clear();
            lastBrowseClickCard_ = -1;
            searchFocused_ = false;
            view_ = View::kSample;
            invalidate();
        } else {
            browsePendingId_ = id;
            lastBrowseClickCard_ = card;
            invalidate();
        }
        return;
    }
    lastBrowseClickCard_ = -1;
}

void ReaSamplerEditor::dragBrowse(int x, int y) {
    // Map the thumb-drag pixel delta to a new (clamped) scroll offset. The visible-card
    // window recomputes at paint from scrollOffset_.
    (void)x;
    RECT rc{};
    GetClientRect(childHwnd_, &rc);
    const BrowseModal bm = computeBrowseModal(rc.right - rc.left, rc.bottom - rc.top);
    const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height);
    scrollOffset_ = thumbDragToOffset(bl, static_cast<int>(visible_.size()),
                                      dragStartScrollOffset_, y - dragStartY_);
    invalidate();
}

HoverTarget ReaSamplerEditor::hoverBrowse(int w, int h, int x,
                                          int y) const {
    const BrowseModal bm = computeBrowseModal(w, h);
    if (contains(bm.back, x, y)) return {HoverKind::kBack, -1};
    if (contains(bm.cancel, x, y)) return {HoverKind::kBrowseCancel, -1};
    if (contains(bm.confirm, x, y)) return {HoverKind::kBrowseConfirm, -1};
    if (contains(bm.search, x, y)) return {HoverKind::kSearchBox, -1};

    const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height);
    const int bx = x - bm.content.x;
    const int by = y - bm.content.y;
    const int tabCount = static_cast<int>(banks_.size()) + 1;
    const int tab = filterTabHitTest(bl, tabCount, bx, by);
    if (tab >= 0) return {HoverKind::kFilterTab, tab};
    const int card = cardHitTest(bl, static_cast<int>(visible_.size()), bx, by + scrollOffset_);
    if (card >= 0) return {HoverKind::kCard, card};
    return {};
}

void ReaSamplerEditor::onMouseWheel(int delta) {
    // Browser scroll (only in the Browse modal — the sole card grid). One wheel notch
    // (WHEEL_DELTA==120) scrolls roughly one card row; the offset is clamped at paint. A
    // positive delta (wheel up) scrolls toward the top (smaller offset).
    if (view_ != View::kBrowse) return;
    const int rows = delta / 120;
    if (rows == 0) return;
    scrollOffset_ -= rows * kBrowserCardHeight;
    if (scrollOffset_ < 0) scrollOffset_ = 0;  // paint clamps the upper bound to the content
    invalidate();
}

void ReaSamplerEditor::onSearchChar(unsigned int ch) {
    // The curve popup: Esc dismisses (checked first — the popup is modal over the face, and
    // the Browse search cannot hold focus under it). Esc reaches here unconditionally
    // (editor_platform's WM_CHAR routing), including mid-drag on a curve node — closeCurvePopup
    // cancels that drag too, or a subsequent WM_MOUSEMOVE would resolve editedCurve() with the
    // popup already closed.
    if (curvePopup_ != CurveTarget::kNone && ch == 27) {
        closeCurvePopup();
        invalidate();
        return;
    }

    // Type-to-filter search. Only when the search box has focus (a click focuses it).
    // Backspace deletes; a printable ASCII char appends; the visible list recomposes (bank
    // filter, then search).
    if (view_ != View::kBrowse || !searchFocused_) return;
    if (ch == 8) {  // backspace
        if (!searchQuery_.empty()) searchQuery_.pop_back();
    } else if (ch == 27) {  // escape clears + defocuses
        searchQuery_.clear();
        searchFocused_ = false;
    } else if (ch >= 32 && ch < 127) {
        searchQuery_.push_back(static_cast<char>(ch));
    } else {
        return;  // ignore other control chars
    }
    scrollOffset_ = 0;   // a new filter resets the scroll to the top of the narrowed list
    rebuildVisible();
    invalidate();
}

void ReaSamplerEditor::onFilesDropped(int droppedCount) {
    // The instrument is a read-only bank consumer and the cross-artifact ingest relay (editor
    // drop -> extension) is not shipped, so we do not ingest the dropped files and — load-
    // bearing — never insert a timeline item. Instead of silently swallowing the drop, flash a
    // clear affordance pointing at the shipped ingest gesture. dropHintTicks_ counts sync ticks
    // (kSyncTimerIntervalMs each); ~6 ticks keeps the banner up a few seconds, then onSyncTimer
    // decays it to 0.
    (void)droppedCount;  // count is informational; the banner text is drop-count-agnostic
    dropHintTicks_ = 6;
    invalidate();
}

}  // namespace reasampler::vst

#endif  // _WIN32
