// browser_scroll.cpp — see browser_scroll.h. PURE scroll + search geometry over the S10
// capture_browser. No host types; only the shared Rect + BrowserLayout.

#include "browser_scroll.h"

#include <algorithm>
#include <cctype>

namespace reasampler::vst {

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 visible ROW: the topmost row whose bottom edge is below the offset. Floor so a row
    // partially scrolled off the top still draws (its lower part is visible).
    const int firstRow = offset / kBrowserCardHeight;
    // Last visible ROW: the row containing the pixel (offset + gridH - 1), inclusive; +1 for
    // the exclusive end. A row straddling the bottom edge still draws.
    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.left && r.bottom <= r.top) return r;  // empty (negative index) stays empty
    return Rect{r.left, r.top - 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.top;

    // Thumb height proportional to the visible fraction, floored at a grabbable minimum but
    // never taller than the track.
    int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
    thumbH = (std::max)(kMinThumbHeight, thumbH);
    thumbH = (std::min)(thumbH, gridH);

    // Thumb top proportional to the offset over the movable track span.
    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{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);

    // Thumb height (same formula as scrollThumbRect) -> movable track span in thumb pixels.
    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{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;
}

}  // namespace reasampler::vst
