// bank_grid — pure implementation. See bank_grid.h. NO REAPER / SWELL / vendor.

#include "bank_grid.h"

#include <algorithm>
#include <cmath>

namespace reasampler {

namespace {

// Builds a sorted, unique ascending index vector for the inclusive range [a, b]
// (order-agnostic in a/b). Both ends assumed already in-range by the caller.
std::vector<int> rangeIndices(int a, int b) {
    if (a > b) std::swap(a, b);
    std::vector<int> out;
    out.reserve(static_cast<std::size_t>(b - a + 1));
    for (int i = a; i <= b; ++i) out.push_back(i);
    return out;
}

// Clamps `index` to a valid cell (single-selection) result: sole member, focus and
// anchor both at index. Used by plain click and plain arrow.
Selection singleSelection(int index) {
    Selection s;
    s.indices = {index};
    s.focus = index;
    s.anchor = index;
    return s;
}

} // namespace

int columnsForWidth(int panelWidth, const GridSpec& spec) {
    // Layout: [gap][cell][gap][cell]...[cell][gap]. n cells occupy
    // gap + n*(cellWidth + gap). Solve for the largest n that fits panelWidth,
    // clamped to at least 1 so a too-narrow panel still shows a (clipped) column.
    const int cell = spec.cellWidth + spec.gap;
    if (cell <= 0) return 1;  // degenerate spec — one column, avoid divide-by-zero
    const int usable = panelWidth - spec.gap;
    if (usable < spec.cellWidth) return 1;
    const int cols = usable / cell;
    return cols < 1 ? 1 : cols;
}

std::vector<CellRect> computeCellRects(int itemCount,
                                       int panelWidth,
                                       const GridSpec& spec) {
    std::vector<CellRect> rects;
    if (itemCount <= 0) return rects;

    const int cols = columnsForWidth(panelWidth, spec);
    rects.reserve(static_cast<std::size_t>(itemCount));

    for (int i = 0; i < itemCount; ++i) {
        const int col = i % cols;
        const int row = i / cols;
        CellRect r;
        r.x = spec.gap + col * (spec.cellWidth + spec.gap);
        r.y = spec.gap + row * (spec.cellHeight + spec.gap);
        r.width = spec.cellWidth;
        r.height = spec.cellHeight;
        rects.push_back(r);
    }
    return rects;
}

int contentHeight(int itemCount, int panelWidth, const GridSpec& spec) {
    if (itemCount <= 0) return 0;
    const int cols = columnsForWidth(panelWidth, spec);
    // Ceil-divide item count by columns to get the row count (partial last row
    // still occupies a full row of height).
    const int rows = (itemCount + cols - 1) / cols;
    return spec.gap + rows * (spec.cellHeight + spec.gap);
}

std::string thumbnailKeyString(const ThumbnailKey& key) {
    // Length-prefix the sampleId so a delimiter byte inside an id cannot forge a
    // collision with a different (id, width, generation) triple.
    std::string s;
    s.reserve(key.sampleId.size() + 32);
    s += std::to_string(key.sampleId.size());
    s += ':';
    s += key.sampleId;
    s += '|';
    s += std::to_string(key.width);
    s += '|';
    s += std::to_string(key.generation);
    return s;
}

// --- Interaction --------------------------------------------------------------

int hitTestCell(int px, int py, const std::vector<CellRect>& rects) {
    for (std::size_t i = 0; i < rects.size(); ++i) {
        const CellRect& r = rects[i];
        // Half-open bounds so adjacent (gapless) rects never both claim a pixel.
        if (px >= r.x && px < r.x + r.width &&
            py >= r.y && py < r.y + r.height)
            return static_cast<int>(i);
    }
    return -1;
}

bool Selection::contains(int index) const {
    return std::binary_search(indices.begin(), indices.end(), index);
}

Selection applyClick(const Selection& current, int index, bool ctrl, bool shift,
                     int itemCount) {
    if (itemCount <= 0 || index < 0 || index >= itemCount) return current;

    // Shift takes precedence over ctrl (documented): range-select from the anchor.
    if (shift) {
        const int anchor = current.anchor >= 0 && current.anchor < itemCount
                               ? current.anchor
                               : index;  // no valid anchor -> seed at the click
        Selection s;
        s.indices = rangeIndices(anchor, index);
        s.focus = index;
        s.anchor = anchor;  // anchor unchanged across a shift-range
        return s;
    }

    if (ctrl) {
        Selection s = current;
        auto it = std::lower_bound(s.indices.begin(), s.indices.end(), index);
        if (it != s.indices.end() && *it == index)
            s.indices.erase(it);          // toggle OUT
        else
            s.indices.insert(it, index);  // toggle IN (keeps sorted order)
        s.focus = index;
        s.anchor = index;                 // ctrl-click reseeds the range origin
        return s;
    }

    // Plain click: sole selection.
    return singleSelection(index);
}

Selection navigate(const Selection& current, NavKey key, int cols, int itemCount,
                   bool shift) {
    if (itemCount <= 0) return current;
    if (cols < 1) cols = 1;

    // A fresh panel (no focus): the first key press focuses cell 0 without moving,
    // so the user sees the caret appear before it steps.
    if (current.focus < 0 || current.focus >= itemCount) {
        if (shift) {
            Selection s;
            s.indices = {0};
            s.focus = 0;
            s.anchor = 0;
            return s;
        }
        return singleSelection(0);
    }

    const int from = current.focus;
    int to = from;
    switch (key) {
        case NavKey::Left:
            // Move one; clamp at cell 0 (stay put on the first cell).
            if (from > 0) to = from - 1;
            break;
        case NavKey::Right:
            // Move one; clamp at the last cell (stay put on the last cell).
            if (from < itemCount - 1) to = from + 1;
            break;
        case NavKey::Up:
            // Move up a row; if that leaves the grid (top row) stay put.
            if (from - cols >= 0) to = from - cols;
            break;
        case NavKey::Down: {
            // Move down a row. If the cell directly below exists, go there. If it
            // does not (we're above a MISSING partial-last-row cell) but there ARE
            // more cells, clamp to the last cell so the partial row is reachable.
            // If we're already in the last populated row, stay put.
            const int below = from + cols;
            if (below < itemCount)
                to = below;
            else if (from + 1 < itemCount)  // partial last row below us
                to = itemCount - 1;
            break;
        }
        case NavKey::Home:  to = 0; break;
        case NavKey::End:   to = itemCount - 1; break;
    }

    if (!shift) return singleSelection(to);

    // Shift-extend: keep the anchor (seed it at the origin cell on first extend).
    const int anchor = current.anchor >= 0 && current.anchor < itemCount
                           ? current.anchor
                           : from;
    Selection s;
    s.indices = rangeIndices(anchor, to);
    s.focus = to;
    s.anchor = anchor;
    return s;
}

float compressAmplitudeForDisplay(float linear) {
    const float mag = linear < 0.0f ? -linear : linear;

    // The linear magnitude at the floor threshold: 10^(kDisplayFloorDb/20).
    // Any magnitude at or below this maps to display fraction 0.
    // Computed once as a constant expression; std::pow is constexpr in C++20 but
    // not C++17, so derive it via the floor definition directly at runtime — it is
    // only called once per bin, and the branch-free math is cheap.
    const float floorMag = std::pow(10.0f, kDisplayFloorDb / 20.0f);

    if (mag <= floorMag) return 0.0f;  // below floor (and guards log10(0))

    // dB in [kDisplayFloorDb, 0] for magnitude in [floorMag, 1].
    const float db = 20.0f * std::log10(mag);

    // Normalize to [0, 1]: 0 at kDisplayFloorDb, 1 at 0 dB.
    const float fraction = (db - kDisplayFloorDb) / (0.0f - kDisplayFloorDb);

    // Clamp to [0, 1] so floating-point overshoot on |linear| > 1.0 stays bounded,
    // then re-apply the original sign.
    const float clamped = fraction < 0.0f ? 0.0f : (fraction > 1.0f ? 1.0f : fraction);
    return linear < 0.0f ? -clamped : clamped;
}

} // namespace reasampler
