// knob_deck.cpp — see knob_deck.h. Pure arithmetic; no LICE/VST3/REAPER includes.

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

#include <algorithm>

namespace reasampler::instrument::ui {

namespace {

// The knob-row width of a group: cells side by side (no inter-cell gap — the 60px cell
// already carries its own breathing room around the 40px knob), plus the optional row
// toggle after a kDeckToggleGap. A spanning group's cells stack, so its knob row is one
// cell wide plus whatever readout column sits beside it.
int knobRowWidth(const DeckGroupDesc& g) {
    if (g.row == DeckRow::Spanning) {
        int w = g.cellIds.empty() ? 0 : kDeckCellW;
        if (g.column.id >= 0) {
            if (w > 0) w += kDeckColumnGap;
            w += g.column.width;
        }
        return w;
    }
    int w = static_cast<int>(g.cellIds.size()) * kDeckCellW;
    if (g.rowToggle.id >= 0) {
        if (w > 0) w += kDeckToggleGap;
        w += 2 * g.rowToggle.segWidth;
    }
    return w;
}

// The caption-row width: the caption reserve plus the optional caption toggle and radio.
int captionRowWidth(const DeckGroupDesc& g) {
    int w = g.captionWidth;
    if (g.captionToggle.id >= 0) w += kDeckToggleGap + 2 * g.captionToggle.segWidth;
    if (g.captionToggle2.id >= 0) w += kDeckToggleGap + 2 * g.captionToggle2.segWidth;
    if (g.captionRadio.id >= 0) w += kDeckToggleGap + kDeckRadioSize;
    return w;
}

// One knob cell inside `cell`: the centered dial square, its concentric inner disc, and the
// label band beneath.
DeckCellLayout layoutCell(int id, const Rect& cell) {
    DeckCellLayout c;
    c.id = id;
    c.cell = cell;
    const int knobLeft = cell.x + (cell.width - kDeckKnobSize) / 2;
    const int knobTop = cell.y + 4;
    c.knob = Rect::ltrb(knobLeft, knobTop, knobLeft + kDeckKnobSize, knobTop + kDeckKnobSize);
    const int innerLeftPx = knobLeft + (kDeckKnobSize - kDeckInnerDialSize) / 2;
    const int innerTopPx = knobTop + (kDeckKnobSize - kDeckInnerDialSize) / 2;
    c.inner = Rect::ltrb(innerLeftPx, innerTopPx, innerLeftPx + kDeckInnerDialSize,
                         innerTopPx + kDeckInnerDialSize);
    const int labelTop = knobTop + kDeckKnobSize + 4;
    c.label = Rect::ltrb(cell.x, labelTop, cell.right(), labelTop + kDeckCellLabelH);
    return c;
}

// Place one group's inner geometry given its box.
DeckGroupLayout layoutGroup(const DeckGroupDesc& g, const Rect& box) {
    DeckGroupLayout out;
    out.id = g.id;
    out.box = box;

    const int captionTop = box.y + kDeckGroupPadY;
    const int innerLeft = box.x + kDeckGroupPadX;
    const int innerRight = box.right() - kDeckGroupPadX;

    // Caption row: text left, then the compact toggle, then the corner radio at the far edge.
    out.caption = Rect::ltrb(innerLeft, captionTop, innerRight, captionTop + kDeckCaptionH);
    int captionRight = innerRight;
    if (g.captionRadio.id >= 0) {
        const int radioTop = captionTop + (kDeckCaptionH - kDeckRadioSize) / 2;
        out.captionRadio = DeckRadioLayout{
            g.captionRadio.id, Rect::ltrb(innerRight - kDeckRadioSize, radioTop, innerRight,
                                          radioTop + kDeckRadioSize),
            g.captionRadio.passive};
        captionRight = out.captionRadio.box.x - kDeckToggleGap;
        out.caption.width = captionRight - out.caption.x;
    }
    const int togTop = captionTop + (kDeckCaptionH - kDeckToggleH) / 2;
    const auto placeToggle = [&](const DeckToggleDesc& d, DeckToggleLayout& into) {
        if (d.id < 0) return;
        const int segW = d.segWidth;
        const Rect seg1 = Rect::ltrb(captionRight - segW, togTop, captionRight,
                                     togTop + kDeckToggleH);
        const Rect seg0 = Rect::ltrb(seg1.x - segW, togTop, seg1.x, togTop + kDeckToggleH);
        into = DeckToggleLayout{d.id, seg0, seg1};
        captionRight = seg0.x - kDeckToggleGap;
        // Caption text stops at the leftmost toggle: pull the right edge in (XYWH: width).
        out.caption.width = captionRight - out.caption.x;
    };
    placeToggle(g.captionToggle, out.captionToggle);
    placeToggle(g.captionToggle2, out.captionToggle2);

    const int cellTop = captionTop + kDeckCaptionH + kDeckCaptionGap;

    if (g.row == DeckRow::Spanning) {
        // FIXED slots down the left column, one per declared id (reserves advance the slot
        // without drawing a cell), spaced by a whole row pitch so slot k lands exactly on
        // categorical row k's knob baseline. Deliberately NOT the run-division law below.
        int slotTop = cellTop;
        for (int id : g.cellIds) {
            if (id >= 0) {
                out.cells.push_back(layoutCell(
                    id, Rect::ltrb(innerLeft, slotTop, innerLeft + kDeckCellW,
                                   slotTop + kDeckCellH)));
            }
            slotTop += kDeckGroupH + kDeckRowGap;
        }
        if (g.column.id >= 0) {
            // ONE rect spanning every slot, not a readout per row. Right-anchored off
            // innerRight rather than measured past the cell slot, so a wider caption
            // reserve on this group can never detach the column from the padding.
            const int colX = innerRight - g.column.width;
            out.column = DeckColumnLayout{
                g.column.id, Rect::ltrb(colX, cellTop, colX + g.column.width,
                                        box.bottom() - kDeckGroupPadY)};
        }
        return out;
    }

    // Knob row: the cells present divide the whole reserved run (one kDeckCellW per declared
    // id, reserves included). Integer division puts an indivisible residue in symmetric end
    // margins rather than in one odd-width cell — keyboard_strip's uniformity-wins rule.
    const int runWidth = static_cast<int>(g.cellIds.size()) * kDeckCellW;
    int presentCells = 0;
    for (int id : g.cellIds) {
        if (id >= 0) ++presentCells;
    }
    const int cellW = presentCells > 0 ? runWidth / presentCells : 0;
    int x = innerLeft + (runWidth - presentCells * cellW) / 2;
    for (int id : g.cellIds) {
        if (id < 0) continue;
        out.cells.push_back(layoutCell(id, Rect::ltrb(x, cellTop, x + cellW,
                                                      cellTop + kDeckCellH)));
        x += cellW;
    }
    if (g.rowToggle.id >= 0) {
        // Anchored past the whole reserved run, not past the last cell, so a residue margin
        // cannot shift it.
        int tx = innerLeft + runWidth;
        if (!g.cellIds.empty()) tx += kDeckToggleGap;
        const int segW = g.rowToggle.segWidth;
        const int togTop = cellTop + (kDeckCellH - kDeckToggleH) / 2;
        const Rect seg0 = Rect::ltrb(tx, togTop, tx + segW, togTop + kDeckToggleH);
        const Rect seg1 = Rect::ltrb(seg0.right(), togTop, seg0.right() + segW, togTop + kDeckToggleH);
        out.rowToggle = DeckToggleLayout{g.rowToggle.id, seg0, seg1};
    }
    return out;
}

// The gutters between `count` groups whose widths total `total`, justified space-between
// inside `blockW`. Empty for a single group.
std::vector<int> justifyGutters(int count, int total, int blockW) {
    const int gutters = count - 1;
    if (gutters <= 0) return {};
    const int slack = blockW - total;
    if (slack < gutters * kDeckGroupGap) {
        // The block cannot hold the row: minimum gutters, and the row overruns to the right
        // rather than wrapping — see layoutDeck's header note for when this degrade applies.
        return std::vector<int>(static_cast<std::size_t>(gutters), kDeckGroupGap);
    }
    const int base = slack / gutters;
    const int residue = slack % gutters;  // both non-negative: slack >= gutters * 12 > 0
    std::vector<int> out(static_cast<std::size_t>(gutters), base);
    for (int i = 0; i < residue; ++i) ++out[static_cast<std::size_t>(i)];
    return out;
}

}  // namespace

int deckGroupWidth(const DeckGroupDesc& g) {
    return (std::max)(captionRowWidth(g), knobRowWidth(g)) + 2 * kDeckGroupPadX;
}

int deckRowCount(const std::vector<DeckGroupDesc>& groups) {
    bool sound = false, contour = false;
    for (const DeckGroupDesc& g : groups) {
        if (g.row == DeckRow::Sound) sound = true;
        else if (g.row == DeckRow::Contour) contour = true;
    }
    return (sound ? 1 : 0) + (contour ? 1 : 0);
}

int deckHeight(const std::vector<DeckGroupDesc>& groups) {
    const int rows = deckRowCount(groups);
    int h = rows > 0 ? rows * kDeckGroupH + (rows - 1) * kDeckRowGap : 0;
    for (const DeckGroupDesc& g : groups) {
        if (g.row == DeckRow::Spanning) h = (std::max)(h, kDeckSpanningH);
    }
    return h;
}

DeckLayout layoutDeck(const std::vector<DeckGroupDesc>& groups, int left, int top,
                      int availWidth) {
    DeckLayout out;
    if (groups.empty()) return out;

    // Partition by the group's OWN row (indices, so the OUTPUT keeps deck order — the shell
    // and the tests pair a layout with the descriptor at the same position).
    std::vector<std::size_t> rows[2];
    std::vector<std::size_t> spanning;
    for (std::size_t i = 0; i < groups.size(); ++i) {
        const DeckRow row = groups[i].row;
        if (row == DeckRow::Spanning) spanning.push_back(i);
        else rows[row == DeckRow::Contour ? 1 : 0].push_back(i);
    }

    // The spanning decks take the right edge; the row block is what is left of them.
    int spanTotal = 0;
    for (std::size_t i : spanning) spanTotal += deckGroupWidth(groups[i]);
    if (!spanning.empty()) {
        spanTotal += (static_cast<int>(spanning.size()) - 1) * kDeckGroupGap;
    }
    const int blockW = availWidth - (spanning.empty() ? 0 : spanTotal + kDeckGroupGap);

    std::vector<Rect> boxes(groups.size());
    int y = top;
    for (const std::vector<std::size_t>& row : rows) {
        if (row.empty()) continue;  // an absent category collapses; it leaves no empty band
        ++out.rowCount;
        int total = 0;
        for (std::size_t i : row) total += deckGroupWidth(groups[i]);
        const std::vector<int> gutters =
            justifyGutters(static_cast<int>(row.size()), total, blockW);
        int x = left;
        for (std::size_t k = 0; k < row.size(); ++k) {
            const int w = deckGroupWidth(groups[row[k]]);
            boxes[row[k]] = Rect::ltrb(x, y, x + w, y + kDeckGroupH);
            x += w;
            if (k < gutters.size()) x += gutters[k];
        }
        y += kDeckGroupH + kDeckRowGap;
    }

    int sx = left + availWidth - spanTotal;
    for (std::size_t i : spanning) {
        const int w = deckGroupWidth(groups[i]);
        boxes[i] = Rect::ltrb(sx, top, sx + w, top + kDeckSpanningH);
        sx += w + kDeckGroupGap;
    }

    out.groups.reserve(groups.size());
    for (std::size_t i = 0; i < groups.size(); ++i) {
        out.groups.push_back(layoutGroup(groups[i], boxes[i]));
    }
    out.height = deckHeight(groups);
    return out;
}

DeckHit hitTestDeck(const DeckLayout& layout, int x, int y) {
    for (const DeckGroupLayout& g : layout.groups) {
        if (!contains(g.box, x, y)) continue;
        if (g.captionRadio.id >= 0 && !g.captionRadio.passive &&
            contains(g.captionRadio.box, x, y)) {
            return {DeckHitKind::CaptionRadio, g.captionRadio.id, -1, false};
        }
        for (const DeckToggleLayout* t : {&g.captionToggle, &g.captionToggle2}) {
            if (t->id < 0) continue;
            if (contains(t->seg0, x, y)) return {DeckHitKind::CaptionToggle, t->id, 0};
            if (contains(t->seg1, x, y)) return {DeckHitKind::CaptionToggle, t->id, 1};
        }
        if (g.rowToggle.id >= 0) {
            if (contains(g.rowToggle.seg0, x, y))
                return {DeckHitKind::RowToggle, g.rowToggle.id, 0};
            if (contains(g.rowToggle.seg1, x, y))
                return {DeckHitKind::RowToggle, g.rowToggle.id, 1};
        }
        for (const DeckCellLayout& c : g.cells) {
            // Every entry here already has a real id — a reserve yields no DeckCellLayout at all.
            if (contains(c.cell, x, y)) {
                return {DeckHitKind::Knob, c.id, -1, contains(c.inner, x, y)};
            }
        }
        if (g.column.id >= 0 && contains(g.column.box, x, y)) {
            return {DeckHitKind::Column, g.column.id, -1, false};
        }
        // Inside the box but on fence/padding — a miss. First-match is exact while the boxes
        // are disjoint, which they are at every width the row block fits; under the sub-floor
        // overrun an overrunning row can reach the spanning deck and the row group answers.
        return {};
    }
    return {};
}

bool inKnobFace(const Rect& knob, int x, int y) {
    const double dx = x - (knob.x + knob.width / 2.0);
    const double dy = y - (knob.y + knob.height / 2.0);
    // Matches computeKnob's radius rule (param_slider.cpp) so the hit rule never claims more
    // circle than is actually drawn when a caller's rect is non-square (e.g. a squashed chrome row).
    const double r = (std::min)(knob.width, knob.height) / 2.0;
    return dx * dx + dy * dy < r * r;
}

DeckFaceHit hitTestKnobFace(const DeckLayout& layout, int x, int y) {
    for (const DeckGroupLayout& g : layout.groups) {
        if (!contains(g.box, x, y)) continue;
        for (const DeckCellLayout& c : g.cells) {
            if (!inKnobFace(c.knob, x, y)) continue;
            return {c.id, inKnobFace(c.inner, x, y)};
        }
        return {};  // inside the group but off every dial
    }
    return {};
}

}  // namespace reasampler::instrument::ui
