// browser_scroll.cpp — see browser_scroll.h. Pure scroll + search geometry; no host types.

#include "core/instrument/ui/browser_scroll.h"

// The Browse modal is a full-window sheet drawn over the Sample face, so it reuses that
// face's chrome metrics rather than minting its own — a divergent title height or pad would
// make the sheet visibly not line up with what it covers.
#include "core/instrument/ui/sample_bands.h"   // kPad / kTitleHeight
#include "core/instrument/ui/sample_chrome.h"  // kNavButtonWidth (the Back button's slot)

#include <algorithm>
#include <cctype>

namespace reasampler::instrument::ui {

namespace {
// The minimum thumb height so a very long bank still yields a grabbable thumb.
constexpr int kMinThumbHeight = 20;

char asciiLower(char c) {
    return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
}  // namespace

int scrollContentHeight(const BrowserLayout& layout, int cardCount) {
    if (cardCount <= 0) return 0;
    const int columns = (std::max)(1, layout.columns);
    const int rows = (cardCount + columns - 1) / columns;  // ceil
    return rows * kBrowserCardHeight;
}

int scrollMaxOffset(const BrowserLayout& layout, int cardCount) {
    const int content = scrollContentHeight(layout, cardCount);
    const int gridH = (std::max)(0, layout.grid.height);
    return (std::max)(0, content - gridH);
}

int clampScrollOffset(const BrowserLayout& layout, int cardCount, int proposedOffset) {
    const int maxOff = scrollMaxOffset(layout, cardCount);
    if (proposedOffset < 0) return 0;
    if (proposedOffset > maxOff) return maxOff;
    return proposedOffset;
}

VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int offset) {
    VisibleRange vr;
    if (cardCount <= 0) return vr;
    const int columns = (std::max)(1, layout.columns);
    const int gridH = (std::max)(0, layout.grid.height);
    if (gridH <= 0 || kBrowserCardHeight <= 0) {
        vr.first = 0;
        vr.last = 0;
        return vr;
    }
    if (offset < 0) offset = 0;
    // First row: floored so a row partially scrolled off the top still draws. Last row:
    // the row containing pixel (offset + gridH - 1), +1 for the exclusive end, so a row
    // straddling the bottom edge still draws.
    const int firstRow = offset / kBrowserCardHeight;
    const int lastRow = (offset + gridH - 1) / kBrowserCardHeight + 1;
    int first = firstRow * columns;
    int last = lastRow * columns;
    if (first > cardCount) first = cardCount;
    if (last > cardCount) last = cardCount;
    if (last < first) last = first;
    vr.first = first;
    vr.last = last;
    return vr;
}

Rect scrolledCardCellRect(const BrowserLayout& layout, int index, int offset) {
    Rect r = cardCellRect(layout, index);
    if (r.right() <= r.x && r.bottom() <= r.y) return r;  // empty (negative index) stays empty
    return Rect::ltrb(r.x, r.y - offset, r.right(), r.bottom() - offset);
}

Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset) {
    const int content = scrollContentHeight(layout, cardCount);
    const int gridH = (std::max)(0, layout.grid.height);
    if (content <= gridH || gridH <= 0) return Rect{};  // fits -> no scrollbar
    const int maxOff = content - gridH;
    if (offset < 0) offset = 0;
    if (offset > maxOff) offset = maxOff;

    const int trackRight = layout.grid.right();
    const int trackLeft = trackRight - kScrollbarWidth;
    const int trackTop = layout.grid.y;

    // Thumb height proportional to the visible fraction, floored/capped to the track.
    int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
    thumbH = (std::max)(kMinThumbHeight, thumbH);
    thumbH = (std::min)(thumbH, gridH);

    const int trackSpan = gridH - thumbH;  // >= 0
    int thumbTop = trackTop;
    if (maxOff > 0 && trackSpan > 0) {
        thumbTop = trackTop + static_cast<int>(
                                  static_cast<long long>(offset) * trackSpan / maxOff);
    }
    return Rect::ltrb(trackLeft, thumbTop, trackRight, thumbTop + thumbH);
}

int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffset,
                      int dyPixels) {
    const int content = scrollContentHeight(layout, cardCount);
    const int gridH = (std::max)(0, layout.grid.height);
    if (content <= gridH || gridH <= 0) return clampScrollOffset(layout, cardCount, startOffset);

    // Same thumb-height formula as scrollThumbRect.
    int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
    thumbH = (std::max)(kMinThumbHeight, thumbH);
    thumbH = (std::min)(thumbH, gridH);
    const int trackSpan = gridH - thumbH;
    if (trackSpan <= 0) return clampScrollOffset(layout, cardCount, startOffset);

    const int maxOff = content - gridH;
    // A 1px thumb move covers maxOff/trackSpan content px. Round to nearest for symmetry.
    const long long deltaOffset =
        (static_cast<long long>(dyPixels) * maxOff + (dyPixels >= 0 ? trackSpan / 2 : -trackSpan / 2)) /
        trackSpan;
    const long long proposed = static_cast<long long>(startOffset) + deltaOffset;
    if (proposed < 0) return 0;
    if (proposed > maxOff) return maxOff;
    return static_cast<int>(proposed);
}

Rect searchBoxRect(int w) {
    if (w <= 0) return Rect{};
    return Rect::ltrb(0, 0, w, kSearchBoxHeight);
}

bool nameMatchesQuery(const std::string& name, const std::string& query) {
    if (query.empty()) return true;
    if (query.size() > name.size()) return false;
    // Case-insensitive substring scan (ASCII fold). Small strings; a naive scan is fine.
    for (std::size_t i = 0; i + query.size() <= name.size(); ++i) {
        bool match = true;
        for (std::size_t j = 0; j < query.size(); ++j) {
            if (asciiLower(name[i + j]) != asciiLower(query[j])) {
                match = false;
                break;
            }
        }
        if (match) return true;
    }
    return false;
}

std::vector<int> filterNameIndices(const std::vector<std::string>& names,
                                   const std::string& query) {
    std::vector<int> out;
    out.reserve(names.size());
    for (int i = 0; i < static_cast<int>(names.size()); ++i) {
        if (nameMatchesQuery(names[static_cast<std::size_t>(i)], query))
            out.push_back(i);
    }
    return out;
}

BrowseModal computeBrowseModal(int w, int h) {
    constexpr int kBrowseFooterH = 30;
    BrowseModal m;
    const int titleH = (std::min)(kTitleHeight, h);
    m.title = Rect::ltrb(0, 0, w, titleH);
    m.back = Rect::ltrb(w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, titleH - 2));
    // Search box below the title, spanning the width (searchBoxRect lays it out from 0).
    const Rect sb = searchBoxRect(w);
    m.search = Rect::ltrb(kPad, titleH, w - kPad, titleH + sb.height);
    const int footerTop = (std::max)(m.search.bottom(), h - kBrowseFooterH);
    m.content = Rect::ltrb(0, m.search.bottom(), w, footerTop);
    // Footer: Cancel (left) + Load (right).
    const int fTop = footerTop + 3;
    const int fBot = (std::max)(fTop, h - 3);
    m.cancel = Rect::ltrb(kPad, fTop, kPad + 90, fBot);
    m.confirm = Rect::ltrb(w - kPad - 90, fTop, w - kPad, fBot);
    return m;
}

}  // namespace reasampler::instrument::ui
