#pragma once
#include "core/ui/rect.h"
// bank_grid — layout math, hit-test, selection, and keyboard nav for the docked bank_panel grid,
// plus its thumbnail cache-key. The panel shell owns SWELL/LICE/PCM; this is the DAW-free half.

#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

namespace reasampler::ui {

// One cell's pixel rect, top-left origin. Draw bounds for one sample's thumbnail.
using CellRect = Rect;

// cellWidth/cellHeight are the target cell size; layout fits as many whole columns as the panel
// width allows (>= 1) and wraps rows as needed. gap is the spacing between cells and the margin.
struct GridSpec {
    int cellWidth = 120;
    int cellHeight = 72;
    int gap = 8;
};

// Columns that fit a panel of the given width. Always >= 1 (a too-narrow panel still shows one
// clipped column).
int columnsForWidth(int panelWidth, const GridSpec& spec);

// Tiles itemCount cells left-to-right, top-to-bottom. Returns exactly itemCount rects in item
// order. A partial last row is left-aligned, not centered or stretched. itemCount == 0 -> empty.
std::vector<CellRect> computeCellRects(int itemCount,
                                       int panelWidth,
                                       const GridSpec& spec);

// Total pixel height the grid occupies (top margin + rows*cellHeight + inter-row gaps + bottom
// margin); 0 when itemCount == 0.
int contentHeight(int itemCount, int panelWidth, const GridSpec& spec);

// Identifies one cached thumbnail. Valid only while sample identity, the draw width it was
// computed at (the envelope has exactly `width` bins per channel), and bank generation all match;
// generation bump invalidates every cached entry without diffing.
struct ThumbnailKey {
    std::string sampleId;
    int width = 0;
    std::uint64_t generation = 0;

    bool operator==(const ThumbnailKey& o) const {
        return sampleId == o.sampleId && width == o.width &&
               generation == o.generation;
    }
};

// Stable string form of the key for use as a map key. sampleId is length-prefixed so a delimiter
// byte inside an id can't forge a collision.
std::string thumbnailKeyString(const ThumbnailKey& key);

// --- Interaction: hit-test, selection, keyboard nav --------------------------

// Index of the first rect containing (px, py), or -1 for a miss (gap, margin, below last row).
// Half-open bounds so adjacent rects never both claim a pixel.
int hitTestCell(int px, int py, const std::vector<CellRect>& rects);

// Panel selection state. `indices` is sorted unique ascending (deterministic for tests and
// highlight order). `focus` is the caret cell (audition/extend target), -1 when none. `anchor` is
// the fixed end a shift-range extends from, -1 when none. Empty selection: focus == anchor == -1.
//
// Invariants upheld by the mutators below: indices sorted/unique; every index (and focus/anchor
// when >= 0) is in [0, itemCount); focus, when >= 0, is a member of indices.
struct Selection {
    std::vector<int> indices;
    int focus = -1;
    int anchor = -1;

    bool operator==(const Selection& o) const {
        return indices == o.indices && focus == o.focus && anchor == o.anchor;
    }
    bool contains(int index) const;
    bool empty() const { return indices.empty(); }
};

// Applies a click on cell `index` to `current`. Modifier semantics (file-manager convention):
//   * plain:  select only `index`; focus = anchor = index.
//   * ctrl:   toggle `index` in/out; focus = index; anchor reseeds to index either way.
//   * shift:  select the inclusive range [anchor, index]; focus = index, anchor unchanged.
//             No prior anchor behaves like a plain click.
// ctrl+shift together: shift wins (range select). index out of range or itemCount <= 0: no-op.
Selection applyClick(const Selection& current, int index, bool ctrl, bool shift,
                     int itemCount);

// Directional key for nav; Enter/Space/Esc drive audition and are a shell concern, not modelled
// here.
enum class NavKey { Left, Right, Up, Down, Home, End };

// Moves focus by one step for `key` in a `cols`-column grid of `itemCount` cells.
//   * Left/Right move linearly; Up/Down move by `cols`. Movement CLAMPS at the grid edges (no
//     wrap) — deliberate: wrap on a partial last row is surprising.
//   * Down from the row above a missing partial-last-row cell clamps to the last cell rather than
//     overshooting past itemCount.
//   * Without shift: moved-to cell becomes the sole selection (focus = anchor = newIndex).
//   * With shift: focus moves to newIndex, selection becomes the inclusive range from anchor
//     (seeded at the origin cell on first extend).
//   * Empty selection: first arrow focuses cell 0 without moving.
// itemCount <= 0 returns `current` unchanged.
Selection navigate(const Selection& current, NavKey key, int cols, int itemCount,
                   bool shift);

// --- Waveform display compression --------------------------------------------
// Maps raw linear amplitude to a perceptual display fraction so quiet content stays visible.

// Below this, amplitude is treated as silence (display fraction 0). Only knob for the curve.
constexpr float kDisplayFloorDb = -60.0f;

// Maps a signed linear amplitude in [-1, 1] (a raw envelope extreme, e.g. PeakBin::max/min) to a
// signed display fraction in [-1, 1]: magnitude -> dB, clamped to [kDisplayFloorDb, 0] and
// normalized so the floor -> 0 and 0 dB -> 1, then the original sign is re-applied. Exact zero
// stays 0; full-scale (|linear| == 1.0f) returns exactly +-1.0f.
float compressAmplitudeForDisplay(float linear);

} // namespace reasampler::ui
