// panel_render.cpp — LICE draw seam of the docked bank panel. Owns WM_PAINT's full
// paint: the vertical split, region headers + tab strip, the two toolbars + More
// button, the footer, the tooltip overlay, and per-card thumbnail/metadata draw —
// everything through the kit by palette role, double-buffered, BitBlt'd once.
// Read-only: reads panel + session state; input/drag seams mutate it. All rect
// derivation comes from panel_layout (the single source both draw and hit-test
// use). No REAPER API functions are called here.

#include <string>
#include <vector>

#include "shell/panel/panel_state.h"

#include "shell/panel/draw_kit.h"
#include "shell/persist/session.h"
#include "core/view/view_mode_model.h"

namespace reasampler::panel {

namespace {

// Bars.beats.subdivisions bottom-left (musical), seconds.milliseconds bottom-right.
// Decorative, non-interactive; a blank musical readout omits the left string.
void drawCardMeta(LICE_IBitmap* bmp, const CellRect& rect, const Sample& s) {
    MusicalLength ml;
    ml.lengthSeconds = s.lengthSeconds;
    ml.tempoBpm      = s.captureTempo;
    ml.timeSigNum    = s.captureTimeSigNum;
    ml.timeSigDenom  = s.captureTimeSigDenom;
    const std::string bars = formatBarsBeats(ml);
    const std::string secs = formatSecondsMs(s.lengthSeconds);

    const int y = rect.y + rect.height - kCardStripHeight;
    if (!bars.empty()) {
        const KitBox left{rect.x + kCardStripPad, y,
                          rect.width / 2 - kCardStripPad, kCardStripHeight};
        text(bmp, left, bars.c_str(), Font::Micro, Role::TextDim, Align::Left);
    }
    const KitBox right{rect.x + rect.width / 2, y,
                       rect.width / 2 - kCardStripPad, kCardStripHeight};
    text(bmp, right, secs.c_str(), Font::ValueMono, Role::TextDim, Align::Right);
}

// The capture's name across the top of the card. The kit clips with an end-ellipsis, so a
// long name shortens ON SCREEN only — the stored label is never truncated. An entry with an
// empty label (old banks predate this track and are not migrated) draws nothing.
void drawCardName(LICE_IBitmap* bmp, const CellRect& rect, const Sample& s) {
    if (s.displayName.empty()) return;
    const CellRect strip = cardNameStrip(rect);
    if (strip.empty()) return;
    // Scrim behind the name: text/primary alone reads ~1:1 against the accent-lime waveform
    // fill a loud capture's peak reaches into this strip. kCardNameScrimAlpha (core/ui/theme.h)
    // is picked so bg/base composited at that alpha over accent/primary clears the 4.5:1 body
    // floor Font::Micro answers to — pinned in test_theme.cpp.
    LICE_FillRect(bmp, strip.x, strip.y, strip.width, strip.height,
                  toLice(roleColor(Role::BgBase)), static_cast<float>(kCardNameScrimAlpha), 0);
    text(bmp, strip, s.displayName.c_str(), Font::Micro, Role::TextPrimary, Align::Left);
}

void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
                   bool selected, bool focused, bool hovered, const Sample* sample) {
    // Selected cards draw the normal cell surface, not an inverted fill — selection
    // is marked purely by the border below, kept orthogonal to hover state.
    const KitBox cell{rect.x, rect.y, rect.width, rect.height};
    const InteractionState state = hovered ? InteractionState::Hover : InteractionState::Rest;
    fillSurface(bmp, cell, Role::BgCell, state);

    // Focus is a distinct inner ring so a focused-AND-selected card reads both.
    const KitColor border = selected ? roleColor(Role::AccentTertiary) : roleColor(Role::LineHairline);
    LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height, toLice(border), 1.0f, 0);
    if (focused) {
        const LICE_pixel ring = toLice(roleColor(Role::TextPrimary));
        LICE_DrawRect(bmp, rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, ring, 1.0f, 0);
    }

    drawWaveform(bmp, cell, env);

    if (sample) {
        drawCardName(bmp, rect, *sample);
        drawCardMeta(bmp, rect, *sample);
    }
}

KitBox toKitBox(const RECT& r) {
    return KitBox{r.left, r.top, r.right - r.left, r.bottom - r.top};
}

void kitText(LICE_IBitmap* bmp, const KitBox& box, const char* txt,
             Font font, Role role, Align align) {
    text(bmp, box, txt, font, role, align);
}

// Number of leaves tagged into the currently active mode; 0 when no session. The
// Arrange default (untagged) is not counted — membership tracks tagged leaves only.
int activeModeMemberCount() {
    if (!g_panel.session) return 0;
    const ViewModeModel& view = g_panel.session->view();
    const std::string& active = view.activeModeId();
    if (active.empty()) return 0;
    int n = 0;
    for (const auto& [guid, m] : view.membership().all())
        if (m.modeIds.count(active) != 0) ++n;
    return n;
}

// Draws the footer: band + top divider, LEFT group ([Arrange|Design] toggle, per-mode
// count, Tail BUTTON), the version readout, and the Prune button at the far right.
// READ-ONLY: reads session state; input handlers mutate it.
void drawFooter(LICE_IBitmap* bmp, int w, int h) {
    const RECT f = panelFooter(w, h);
    if (f.top >= f.bottom) return;

    // Footer band + hairline top divider (the base persistent-controls strip).
    fillSurface(bmp, KitBox{f.left, f.top, w, kFooterHeight}, Role::BgPanel,
                InteractionState::Rest);
    LICE_Line(bmp, f.left, f.top, f.right, f.top,
              toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);

    const FooterBarLayout fb = footerBarLayoutFor(w, h);

    // [Arrange|Design] toggle — N mode_switch segments inside footer_bar's toggle box. The
    // active mode's segment carries the accent; others hover-or-rest bg/cell.
    if (!fb.toggle.empty() && g_panel.session) {
        const ViewModeModel& view = g_panel.session->view();
        const std::vector<Mode>& modes = view.modes().all();
        const int n = static_cast<int>(modes.size());
        const HeaderRect th{fb.toggle.x, fb.toggle.y, fb.toggle.width, fb.toggle.height};
        const std::vector<SegmentRect> segs = computeSegmentRects(th, n);
        const std::string& activeId = view.activeModeId();
        for (int i = 0; i < static_cast<int>(segs.size()); ++i) {
            const SegmentRect& s = segs[static_cast<std::size_t>(i)];
            const Mode& mode = modes[static_cast<std::size_t>(i)];
            const bool active = mode.id == activeId;
            // Disabled/dead rationale: core/ui/footer_bar.h.
            const bool live = modeSegmentEnabled(active, g_panel.modeSwitchBlocked,
                                                 modeActivateCommandId(mode.id) != 0);
            const InteractionState state =
                active ? InteractionState::Active
                       : (live ? hoverState(g_panel.hovered, HoverKind::ModeSegment, i)
                               : InteractionState::Disabled);
            fillSurface(bmp, KitBox{s.x, s.y, s.width, s.height}, Role::BgCell, state);
            LICE_DrawRect(bmp, s.x, s.y, s.width, s.height,
                          toLice(roleColor(Role::LineHairline)), 1.0f, 0);
            const Role tr = active ? Role::BgBase : (live ? Role::TextPrimary : Role::TextDim);
            kitText(bmp, KitBox{s.x, s.y, s.width, s.height}, mode.displayName.c_str(),
                    Font::Label, tr, Align::Center);
        }
    }

    // Per-mode member count, a compact dim readout beside the toggle. Passive text, not a control.
    if (!fb.count.empty()) {
        const int members = activeModeMemberCount();
        const std::string countLabel =
            std::to_string(members) + (members == 1 ? " track" : " tracks");
        kitText(bmp, KitBox{fb.count.x, fb.count.y, fb.count.width, fb.count.height},
                countLabel.c_str(), Font::Micro, Role::TextDim, Align::Center);
    }

    // Tail BUTTON — a real kit button with rest/hover states; its click cycles the tail
    // mode. Label is the pure tailToggleLabel.
    if (!fb.tail.empty()) {
        const InteractionState state = hoverState(g_panel.hovered, HoverKind::TailButton, -1);
        const std::string label = tailToggleLabel(currentTail());
        const KitButtonBox box{KitBox{fb.tail.x, fb.tail.y, fb.tail.width, fb.tail.height}};
        drawButton(bmp, box, label.c_str(), state, /*warn=*/false);
    }

    // Version/channel readout. appVersion() renders the configured version string on
    // stable and that string plus "-beta" on beta, so a beta panel self-identifies.
    kitText(bmp, KitBox{f.left, f.top, (f.right - f.left) - 8, f.bottom - f.top},
            appVersion().c_str(), Font::Micro, Role::TextDim, Align::Right);

    // Prune button — the ONLY warn-colored, byte-deleting control. No-op when suppressed
    // (footer too narrow).
    const ButtonRect pb = pruneButtonRectFor(w, h);
    if (!pb.empty()) {
        const InteractionState state = hoverState(g_panel.hovered, HoverKind::PruneButton, -1);
        const KitButtonBox box{KitBox{pb.x, pb.y, pb.width, pb.height}};
        drawButton(bmp, box, "Prune", state, /*warn=*/true);
    }
}

// Draws one task-grouped toolbar through the kit. Overflow drops WHOLE trailing buttons
// (the pure layout returns only the buttons that fit), so nothing is drawn clipped.
// `hoverKind` selects which HoverKind this bar's buttons use so the two toolbars' hover
// states never cross. `topDivider` draws the hairline at the band's top edge (bottom
// toolbar) vs. bottom edge (top toolbar). Key binding help is in the hover tooltip, not
// on the button face.
void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
                 const std::vector<ActionBarRow>& rows, HoverKind hoverKind, bool topDivider) {
    if (bar.height <= 0 || bar.width <= 0) return;

    const KitBox band{bar.x, bar.y, bar.width, bar.height};
    fillSurface(bmp, band, Role::BgPanel, InteractionState::Rest);
    const int dividerY = topDivider ? bar.y : bar.y + bar.height - 1;
    LICE_Line(bmp, bar.x, dividerY, bar.x + bar.width, dividerY,
              toLice(roleColor(Role::LineHairline)), 0.5f, 0, false);

    const std::vector<ClusterSpec> clusters = actionBarClusters(rows);
    const std::vector<ActionBarSlot> slots = computeBarSlots(bar, clusters, kBarSpec);

    for (const ActionBarSlot& s : slots) {
        if (s.index < 0 || s.index >= static_cast<int>(rows.size())) continue;
        const ActionBarRow& row = rows[static_cast<std::size_t>(s.index)];
        const int cmd = resolveBarCommandId(row);

        // Disabled when unregistered on this channel or gated off (opposite-mode
        // enablement); else Hover when hovered, else Rest — these are stateless triggers.
        InteractionState state = InteractionState::Rest;
        if (cmd == 0 || !row.enabled) state = InteractionState::Disabled;
        else if (g_panel.hovered.kind == hoverKind && g_panel.hovered.index == s.index)
            state = InteractionState::Hover;

        // Label drawn separately (not passed to drawButton) so its text role tracks state.
        const KitButtonBox box{KitBox{s.x, s.y, s.width, s.height}};
        drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false);

        const Role textRole =
            (state == InteractionState::Disabled) ? Role::TextDim : Role::TextPrimary;
        const KitBox labelBox{s.labelX, s.labelY, s.labelW, s.labelH};
        kitText(bmp, labelBox, row.shortLabel.c_str(), Font::Label, textRole, Align::Center);
    }
}

// Draws the far-right More button (rest/hover) — the entry to the top-toolbar overflow
// popup listing the rare capture variants. No-op when suppressed (band too narrow).
void drawMoreButton(LICE_IBitmap* bmp, int w) {
    const MenuButtonRect mb = topMenuButtonRect(w);
    if (mb.empty()) return;
    const InteractionState state = hoverState(g_panel.hovered, HoverKind::MoreButton, -1);
    const KitButtonBox box{KitBox{mb.x, mb.y, mb.width, mb.height}};
    drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false);
    // Three ASCII dots (portable — no UTF-8/codepage dependency in the LICE text path).
    kitText(bmp, KitBox{mb.x, mb.y, mb.width, mb.height}, "...",
            Font::Label, Role::TextPrimary, Align::Center);
}

// Draws the hover-delay tooltip over the given anchor button, if due. Drawn LAST so it
// overlays the toolbars. Box placement (below anchor, flip above near the bottom edge,
// clamp to client) is the pure tooltip module's.
void drawTooltip(LICE_IBitmap* bmp, int w, int h) {
    if (!g_panel.tooltipShown) return;
    std::string txt;
    int ax = 0, ay = 0, aw = 0, ah = 0;
    if (!currentTooltip(w, h, txt, ax, ay, aw, ah) || txt.empty()) return;

    const int textW = static_cast<int>(txt.size()) * kTooltipCharPx;
    const TooltipBox tb =
        computeTooltip(ax, ay, aw, ah, textW, kTooltipTextH, w, h, TooltipSpec{});
    if (tb.empty()) return;

    // The tooltip surface: a raised bg/cell chip with a hairline border, then the AA text.
    const KitBox box{tb.x, tb.y, tb.width, tb.height};
    fillSurface(bmp, box, Role::BgCell, InteractionState::Hover);
    LICE_DrawRect(bmp, tb.x, tb.y, tb.width, tb.height,
                  toLice(roleColor(Role::LineHairline)), 1.0f, 0);
    kitText(bmp, box, txt.c_str(), Font::Label, Role::TextPrimary, Align::Center);
}

// Draws one region's grid of thumbnails (or an empty-state line) clipped to its
// viewport. `selectionOwner` is true when this region holds the live selection, so
// its cells show selection/focus chrome; the other region draws plain.
void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
                    const BankModel* index, const std::string& emptyMsg,
                    bool selectionOwner, const std::string& projectDir, Region reg) {
    const RECT grid = regionGridRect(region, isBanks);
    if (grid.bottom <= grid.top) return;

    if (!index || index->empty()) {
        kitText(bmp, toKitBox(grid), emptyMsg.c_str(), Font::Label, Role::TextDim, Align::Center);
        return;
    }

    // Iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense
    // BankModel insertion order. Selection/focus are keyed by the occupied-ordinal (selection
    // space); a slot maps back to its ordinal via selectionForSlot.
    const RegionDisplay disp = regionDisplay(region, isBanks, reg);
    // Request one bin per drawn pixel column; drawWaveform's peaks::columnMinMax exact
    // partition makes every column gap-free regardless. computeThumbnail clamps to frame count.
    const int binWidth = kWaveformOversample *
                         waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight});
    for (const SlotCellRect& r : disp.slotRects) {
        if (r.y >= grid.bottom) continue;   // below the viewport: skip (no scroll)
        const CellRect rect{r.x, r.y, r.width, r.height};
        const std::string id = disp.idAtSlot(r.slot);
        if (id.empty()) {
            // Interior gap slot: a subtle empty-slot treatment through the kit — a hairline
            // outline on bg/cell, clearly NOT a card. No selection/focus/waveform, and not a
            // hover or hit target (a click on an empty slot clears selection like any grid miss).
            fillSurface(bmp, KitBox{rect.x, rect.y, rect.width, rect.height},
                        Role::BgCell, InteractionState::Rest);
            LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height,
                          toLice(roleColor(Role::LineHairline)), 1.0f, 0);
            continue;
        }
        const Sample* s = index->query(id);
        if (!s) continue;  // reconciled order should never name a stale id; defensive
        const int sel = disp.selectionForSlot(r.slot);
        const bool selected = selectionOwner && sel >= 0 && g_panel.selection.contains(sel);
        const bool focused  = selectionOwner && sel >= 0 && g_panel.selection.focus == sel;
        const Envelope& env = thumbnailFor(*s, binWidth, projectDir);
        // Grid-cell hover is intentionally not tracked: the cell already carries selection +
        // focus chrome (the centerpiece's "bones"); a third transient hover state on every
        // cell would add repaint churn + visual noise. Hover lights the chrome/buttons/tabs.
        drawThumbnail(bmp, rect, env, selected, focused, /*hovered=*/false, s);
    }
}

// Draws the per-slot reorder/replace drop-target highlight on the target slot's cell, but
// ONLY when a same-bank in-grid drag (Reorder or Replace) is live over THIS region (the drag
// source region). An accent/HOT outline, distinct from the accent/tertiary purple selection
// border so it is never confusable; Replace draws a doubled outline so an Alt-over-occupied
// replace reads as a stronger "swap" cue than a plain reorder.
void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Region reg) {
    if (!g_panel.dragging) return;
    if (g_panel.cardGesture != CardGesture::Reorder &&
        g_panel.cardGesture != CardGesture::Replace)
        return;
    if (g_panel.dragSourceRegion != reg) return;  // highlight only the source bank's grid
    if (g_panel.dragTargetSlot < 0) return;

    const RECT grid = regionGridRect(region, isBanks);
    const RegionDisplay disp = regionDisplay(region, isBanks, reg);
    // The drop rects include a trailing row past maxSlot so a beyond-extent target
    // slot gets a visible highlight cue, not silence.
    const int gridW = grid.right - grid.left;
    const int maxSlot = disp.bank ? disp.bank->slots.maxSlot() : -1;
    std::vector<SlotCellRect> dropRects = computeSlotRectsForDrop(maxSlot, gridW, kGrid);
    for (SlotCellRect& r : dropRects) { r.x += grid.left; r.y += grid.top; }
    for (const SlotCellRect& r : dropRects) {
        if (r.slot != g_panel.dragTargetSlot) continue;
        if (r.y >= grid.bottom) return;  // below the viewport (no scroll)
        const LICE_pixel hot = toLice(roleColor(Role::AccentHot));
        LICE_DrawRect(bmp, r.x, r.y, r.width, r.height, hot, 1.0f, 0);
        if (g_panel.cardGesture == CardGesture::Replace)
            LICE_DrawRect(bmp, r.x + 1, r.y + 1, r.width - 2, r.height - 2, hot, 1.0f, 0);
        return;
    }
}

// Draws a region header: title, the active-bank readout, and the full-height button.
void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title,
                      const std::string& activeName, bool poolBtnIsPool) {
    const RECT hdr = regionHeaderRect(region);
    fillSurface(bmp, KitBox{hdr.left, hdr.top, hdr.right - hdr.left, hdr.bottom - hdr.top},
                Role::BgPanel, InteractionState::Rest);
    LICE_Line(bmp, hdr.left, hdr.bottom - 1, hdr.right, hdr.bottom - 1,
              toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);

    // Title, left. The two regions are distinct KINDS of container, so the title carries a
    // CATEGORICAL accent (secondary/tertiary mark kinds, never intensity) — Pool = secondary
    // teal, Banks = tertiary purple. This is a category mark, NOT the "what's live" signal
    // (that stays the primary-lime "Active:" readout beside it). Font::RegionTitle, not
    // Font::Title: an accent this deep only clears the 3:1 indicator floor, so the type has to
    // be large enough to be ENTITLED to it — see draw_kit.cpp's metrics static_assert.
    RECT titleRc = hdr;
    titleRc.left += 8;
    titleRc.right = titleRc.left + 120;
    const Role titleRole = poolBtnIsPool ? Role::AccentSecondary : Role::AccentTertiary;
    kitText(bmp, toKitBox(titleRc), title, Font::RegionTitle, titleRole, Align::Left);

    // Active-bank readout — the UNMISTAKABLE indicator, in the PRIMARY accent role in BOTH
    // region headers so the active/capture-target bank is legible even when it is not the
    // shown tab and even when it is the pool. Primary = "what's live".
    const std::string readout = "Active: " + activeName;
    RECT actRc = hdr;
    actRc.left = titleRc.right + 6;
    actRc.right = createBtnRect(region).left - 6;
    if (actRc.right > actRc.left)
        kitText(bmp, toKitBox(actRc), readout.c_str(), Font::Label, Role::AccentPrimary, Align::Left);

    // Arrow glyph: in split it means "maximize this region"; when already full it means
    // "restore the split".
    const RECT btn = fullHtBtnRect(region);
    const bool thisFull =
        poolBtnIsPool ? (g_panel.fullHeight == BankPanelFullHeight::PoolOnly)
                      : (g_panel.fullHeight == BankPanelFullHeight::BanksOnly);
    const HoverKind hk = poolBtnIsPool ? HoverKind::FullHtPool : HoverKind::FullHtBanks;
    const InteractionState state =
        thisFull ? InteractionState::Active : hoverState(g_panel.hovered, hk, -1);
    const KitButtonBox box{KitBox{btn.left, btn.top, btn.right - btn.left,
                                  btn.bottom - btn.top}};
    drawButton(bmp, box, thisFull ? "v" : "^", state, /*warn=*/false);
}

// Draws the named-banks tab strip: one tab per named bank (ordinal order), the SHOWN
// tab highlighted, the ACTIVE bank's tab lit with the accent border, overflow
// chevrons when present, plus the "+" create button in the header. During a drag,
// the tab under the pointer gets the drop-target highlight.
void drawTabStrip(LICE_IBitmap* bmp, const RECT& region) {
    const TabStripRect strip = banksTabStripRect(region);
    if (strip.height <= 0) return;
    fillSurface(bmp, KitBox{strip.x, strip.y, strip.width, strip.height},
                Role::BgBase, InteractionState::Rest);

    const std::vector<const Bank*> tabs = namedBanks();
    const int n = static_cast<int>(tabs.size());
    if (n == 0) {
        kitText(bmp, KitBox{strip.x + 8, strip.y, strip.width - 8, strip.height},
                "No named banks -- click + to create one.",
                Font::Label, Role::TextDim, Align::Left);
        return;
    }

    const TabStripLayout layout =
        computeTabStripLayout(strip, n, kTabSpec, g_panel.tabScroll);

    // Chevrons (drawn first so tabs sit above their inner edges).
    if (layout.overflow) {
        const KitBox lc{strip.x, strip.y, kTabSpec.chevronWidth, strip.height};
        const KitBox rc{strip.x + strip.width - kTabSpec.chevronWidth, strip.y,
                        kTabSpec.chevronWidth, strip.height};
        fillSurface(bmp, lc, Role::BgCell, InteractionState::Rest);
        fillSurface(bmp, rc, Role::BgCell, InteractionState::Rest);
        kitText(bmp, lc, "<", Font::Label, Role::TextPrimary, Align::Center);
        kitText(bmp, rc, ">", Font::Label, Role::TextPrimary, Align::Center);
    }

    const std::string activeId = book() ? book()->activeBankId() : std::string();
    const std::vector<TabRect> rects =
        computeTabRects(strip, n, kTabSpec, g_panel.tabScroll);
    for (const TabRect& tr : rects) {
        const Bank* bk = tabs[static_cast<std::size_t>(tr.index)];
        const bool shown = bk->id == g_panel.shownBankId;
        const bool active = bk->id == activeId;
        const bool dropHere = g_panel.dragging &&
                              g_panel.dropKind == DropKind::Tab &&
                              g_panel.dropBankId == bk->id;
        const bool hovered = g_panel.hovered.kind == HoverKind::Tab &&
                             g_panel.hovered.index == tr.index;

        // The ACTIVE bank (capture target) carries the accent; a drag drop-target reads
        // Dragging; the SHOWN (browsed) tab reads Pressed; else hover-or-rest bg/cell.
        const KitBox tb{tr.x, tr.y, tr.width, tr.height};
        InteractionState state = InteractionState::Rest;
        if (active)        state = InteractionState::Active;
        else if (dropHere) state = InteractionState::Dragging;
        else if (shown)    state = InteractionState::Pressed;
        else if (hovered)  state = InteractionState::Hover;
        fillSurface(bmp, tb, Role::BgCell, state);

        // The active bank's tab gets a bright accent border (unmistakable), distinct from the
        // shown tab's fill — active != shown, made visible (kit accent role).
        const KitColor border = active ? roleColor(Role::AccentPrimary) : roleColor(Role::LineHairline);
        LICE_DrawRect(bmp, tr.x, tr.y, tr.width, tr.height, toLice(border), 1.0f, 0);
        if (active)
            LICE_DrawRect(bmp, tr.x + 1, tr.y + 1, tr.width - 2, tr.height - 2,
                          toLice(border), 1.0f, 0);

        // Label: bg/base on the accent-active fill for contrast, else text/primary.
        const Role trole = active ? Role::BgBase : Role::TextPrimary;
        kitText(bmp, KitBox{tr.x + 4, tr.y, tr.width - 8, tr.height},
                bk->displayName.c_str(), Font::Label, trole, Align::Center);
    }
}

// The active bank's display name (for the readout). "Pool" when the pool is active.
std::string activeBankName() {
    BankBook* b = book();
    if (!b) return std::string(kPoolBankName);
    const Bank* bk = b->bank(b->activeBankId());
    return bk ? bk->displayName : std::string(kPoolBankName);
}

}  // namespace

void paintPanel(HWND hwnd, HDC hdc) {
    RECT cr{};
    GetClientRect(hwnd, &cr);
    const int w = cr.right - cr.left;
    const int h = cr.bottom - cr.top;
    if (w <= 0 || h <= 0) return;

    LICE_SysBitmap bmp(w, h);
    LICE_Clear(&bmp, toLice(roleColor(Role::BgBase)));

    const std::string projectDir = currentProjectDir();
    const std::string activeName = activeBankName();

    if (poolShown()) {
        const RECT region = poolRegionRect(w, h);
        drawRegionHeader(&bmp, region, "Pool", activeName, /*poolBtnIsPool=*/true);
        drawRegionGrid(&bmp, region, /*isBanks=*/false, indexForRegion(Region::Pool),
                       "No samples in the pool yet. Capture one to see it here.",
                       g_panel.focusedRegion == Region::Pool, projectDir, Region::Pool);
        // Whole-grid drop-target outline for a MOVE/COPY drag; a same-bank reorder shows a
        // per-SLOT highlight instead (drawCardDropTarget below).
        if (g_panel.dragging && g_panel.dropKind == DropKind::PoolRegion &&
            (g_panel.cardGesture == CardGesture::Move ||
             g_panel.cardGesture == CardGesture::Copy)) {
            const RECT grid = regionGridRect(region, false);
            LICE_DrawRect(&bmp, grid.left + 1, grid.top + 1,
                          grid.right - grid.left - 2, grid.bottom - grid.top - 2,
                          toLice(roleColor(Role::AccentHot)), 1.0f, 0);
        }
        drawCardDropTarget(&bmp, region, /*isBanks=*/false, Region::Pool);
    }

    if (poolShown() && banksShown()) {
        const RECT body = splitBody(w, h);
        const int dy = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2;
        LICE_FillRect(&bmp, 0, dy, w, kSplitDividerHeight,
                      toLice(roleColor(Role::BgBase)), 1.0f, 0);
    }

    if (banksShown()) {
        const RECT region = banksRegionRect(w, h);
        drawRegionHeader(&bmp, region, "Banks", activeName, /*poolBtnIsPool=*/false);
        // "+" create button (drawn as part of the banks header) — kit drawButton + hover.
        const RECT cbtn = createBtnRect(region);
        const InteractionState createState =
            hoverState(g_panel.hovered, HoverKind::CreateBank, -1);
        drawButton(&bmp, KitButtonBox{KitBox{cbtn.left, cbtn.top, cbtn.right - cbtn.left,
                                             cbtn.bottom - cbtn.top}},
                   "+", createState, /*warn=*/false);

        drawTabStrip(&bmp, region);
        drawRegionGrid(&bmp, region, /*isBanks=*/true, indexForRegion(Region::Banks),
                       g_panel.shownBankId.empty()
                           ? "Select or create a named bank."
                           : "This bank is empty. Move samples here from the pool.",
                       g_panel.focusedRegion == Region::Banks, projectDir, Region::Banks);
        // BanksRegion fires when the pointer is in the grid but not on a specific tab;
        // Tab draws its own highlight on the individual tab (drawTabStrip above).
        if (g_panel.dragging && g_panel.dropKind == DropKind::BanksRegion &&
            (g_panel.cardGesture == CardGesture::Move ||
             g_panel.cardGesture == CardGesture::Copy)) {
            const RECT grid = regionGridRect(region, true);
            LICE_DrawRect(&bmp, grid.left + 1, grid.top + 1,
                          grid.right - grid.left - 2, grid.bottom - grid.top - 2,
                          toLice(roleColor(Role::AccentHot)), 1.0f, 0);
        }
        drawCardDropTarget(&bmp, region, /*isBanks=*/true, Region::Banks);
    }

    // Toolbars + footer drawn last so they sit over the split body's edges. drawToolbar
    // fills only its passed (action) rect, so fill the WHOLE top band first — otherwise
    // the reserved right strip behind the More button is bare.
    fillSurface(&bmp, KitBox{0, 0, w, kTopToolbarHeight}, Role::BgPanel, InteractionState::Rest);
    drawToolbar(&bmp, topToolbarActionRect(w), topBarRows(), HoverKind::TopBarButton,
                /*topDivider=*/false);
    drawMoreButton(&bmp, w);
    drawToolbar(&bmp, bottomToolbarRect(w, h), bottomBarRows(), HoverKind::BottomBarButton,
                /*topDivider=*/true);
    drawFooter(&bmp, w, h);

    drawTooltip(&bmp, w, h);

    BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
}

}  // namespace reasampler::panel
