// panel_layout.cpp — geometry-glue seam of the docked bank panel: toolbar/footer/menu
// rects, toolbar row/cluster builders, vertical-split geometry + region rects, and
// the slot-order display bridge. Every rect is derived from client size + fullHeight
// state, and both paint and hit-testing call these so they never drift. Also home of
// the public split-state seam (panel_layout.h).
//
// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested
// (the pure tiling/hit-test math lives in action_bar / footer_bar / etc.).

#include <string>
#include <vector>

#include "shell/panel/panel_state.h"
#include "shell/panel/panel_layout.h"

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

// Resolves each button's command id at runtime from the composed named-command
// string, and reads its current key binding for the tooltip. Main-section only.
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_NamedCommandLookup
#define REAPERAPI_WANT_kbd_getTextFromCmd
#define REAPERAPI_WANT_SectionFromUniqueID
#include "reaper_plugin_functions.h"

namespace reasampler::panel {

int modeCount() {
    if (!g_panel.session) return 0;
    return static_cast<int>(g_panel.session->view().modes().size());
}

// topToolbarRect is what the far-right More button anchors into; the action_bar
// buttons tile into the band minus the menu reserve (topToolbarActionRect) so
// they never run under it.

namespace {

ActionBarRect topToolbarRect(int w) {
    ActionBarRect s;
    s.x = 0;
    s.y = 0;
    s.width = w;
    s.height = kTopToolbarHeight;
    return s;
}

MenuBarRect topMenuBarRect(int w) {
    const ActionBarRect bar = topToolbarRect(w);
    return MenuBarRect{bar.x, bar.y, bar.width, bar.height};
}

}  // namespace

// Empty when the band is too narrow to place the button clear of its left
// inset — suppressed gracefully; still reachable via its bindable command.
MenuButtonRect topMenuButtonRect(int w) {
    return computeMenuButton(topMenuBarRect(w), kMenuBtnSpec);
}

// Reserve is still subtracted even when the button is suppressed (0 only for a
// degenerate band), keeping draw and hit-test consistent either way.
ActionBarRect topToolbarActionRect(int w) {
    ActionBarRect bar = topToolbarRect(w);
    const int reserve = menuButtonReserve(topMenuBarRect(w), kMenuBtnSpec);
    bar.width -= reserve;
    if (bar.width < 0) bar.width = 0;
    return bar;
}

RECT panelFooter(int w, int h) {
    RECT rc{};
    rc.left = 0;
    rc.right = w;
    rc.top = h - kFooterHeight;
    rc.bottom = h;
    // Keep the footer below the top toolbar; if the client is too short, collapse it.
    if (rc.top < kTopToolbarHeight) rc.top = rc.bottom;
    return rc;
}

// Left-group layout (mode toggle + count + Tail button). Single source of truth
// for draw and hit-test.
FooterBarLayout footerBarLayoutFor(int w, int h) {
    const RECT f = panelFooter(w, h);
    if (f.top >= f.bottom) return FooterBarLayout{};
    const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
    return computeFooterBar(footer, FooterBarSpec{});
}

// Empty when the footer is degenerate or too narrow to clear the left group /
// version readout — reachable via its bindable command regardless. Set apart at
// the right (footer_bar reserves matching space so the groups never overlap).
ButtonRect pruneButtonRectFor(int w, int h) {
    const RECT f = panelFooter(w, h);
    if (f.top >= f.bottom) return ButtonRect{};
    const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
    return computePruneButton(footer, PruneButtonSpec{});
}

// Used by the scroll-wheel (Manual tail fine-adjust); the Tail-cycle click hits
// the Tail button rect (footer_bar) instead.
bool pointInFooter(int x, int y) {
    if (!g_panel.hwnd) return false;
    RECT cr{};
    GetClientRect(g_panel.hwnd, &cr);
    const RECT f = panelFooter(cr.right - cr.left, cr.bottom - cr.top);
    return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom;
}

// Frequent acts only: Capture (item/track), Re-capture (between capture and
// placement), Placement (insert/insert-conform). The four rare variants live in
// the overflow menu (overflowMenuRows) — same actions, different home.
std::vector<ActionBarRow> topBarRows() {
    std::vector<ActionBarRow> rows;
    for (const CaptureActionDef& def : captureActionTable()) {
        std::string label = def.commandSuffix;
        if (label == "CAPTURE_ITEM")       label = "Capture Item";
        else if (label == "CAPTURE_TRACK") label = "Capture Track";
        rows.push_back({def.commandSuffix, label, def.descriptionPhrase,
                        ActionCluster::Capture, true});
    }
    rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture",
                    "re-capture from source", ActionCluster::Maintenance, true});
    rows.push_back({"INSERT_SELECTED", "Insert",
                    "insert selected sample at edit cursor", ActionCluster::Placement, true});
    rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform",
                    "insert selected sample at edit cursor (conform to tempo)",
                    ActionCluster::Placement, true});
    return rows;
}

// The four rare batch/realtime capture variants pulled off the visible bar, plus
// Cancel RT. fullName is the popup entry text (shortLabel is unused for menu items).
std::vector<ActionBarRow> overflowMenuRows() {
    return {
        {"CAPTURE_BATCH_ITEMS", "Batch Items",
         "batch capture selected items (one per item)", ActionCluster::Capture, true},
        {"CAPTURE_BATCH_RAZOR", "Batch Razor",
         "batch capture razor areas (one per area)", ActionCluster::Capture, true},
        {"CAPTURE_TRACK_REALTIME", "Capture RT",
         "capture selected track (realtime)", ActionCluster::Capture, true},
        {"CANCEL_REALTIME_CAPTURE", "Cancel RT",
         "cancel realtime capture", ActionCluster::Maintenance, true},
    };
}

namespace {

std::string activeModeIdOrEmpty() {
    if (!g_panel.session) return {};
    return g_panel.session->view().activeModeId();
}

}  // namespace

// Four Item/Track x Arrange/Design tag buttons then a set-apart Show Both. A tag
// button is live only for the OPPOSITE of the active mode (tag into the mode
// you're not in) — decided by the pure mode_enable::tagButtonEnabled; disabled
// rows draw Disabled and no-op on click. Show Both is unconditional.
std::vector<ActionBarRow> bottomBarRows() {
    const std::string active = activeModeIdOrEmpty();
    const bool arrangeLive = tagButtonEnabled(active, TagTarget::Arrange);
    const bool designLive = tagButtonEnabled(active, TagTarget::Design);

    std::vector<ActionBarRow> rows;
    rows.push_back({"VIEW_MOVE_ITEMS_ARRANGE", "Item: Arrange",
                    "move selected items -> Arrange", ActionCluster::Tagging, arrangeLive});
    rows.push_back({"VIEW_MOVE_ITEMS_DESIGN", "Item: Design",
                    "move selected items -> Design", ActionCluster::Tagging, designLive});
    rows.push_back({"VIEW_TAG_ARRANGE", "Track: Arrange",
                    "tag selected tracks -> Arrange", ActionCluster::Tagging, arrangeLive});
    rows.push_back({"VIEW_TAG_DESIGN", "Track: Design",
                    "tag selected tracks -> Design", ActionCluster::Tagging, designLive});
    rows.push_back({"VIEW_SHOW_BOTH", "Show Both",
                    "show both for selected tracks", ActionCluster::Switching, true});
    return rows;
}

// Cluster button-count specs in the row list's cluster order, so action_bar's
// flat index lines up with the row list. Empty clusters contribute a 0-count
// spec (action_bar skips them, no gap).
std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows) {
    int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0;
    for (const ActionBarRow& r : rows) {
        switch (r.cluster) {
            case ActionCluster::Capture:      ++nCap;    break;
            case ActionCluster::Placement:    ++nPlace;  break;
            case ActionCluster::Maintenance:  ++nMaint;  break;
            case ActionCluster::Tagging:      ++nTag;    break;
            case ActionCluster::Switching:    ++nSwitch; break;
        }
    }
    return {
        {ActionCluster::Capture, nCap},
        {ActionCluster::Maintenance, nMaint},
        {ActionCluster::Placement, nPlace},
        {ActionCluster::Tagging, nTag},
        {ActionCluster::Switching, nSwitch},
    };
}

// Fixed-height band directly above the footer; degenerate (height 0) when too
// short to host it above the footer.
ActionBarRect bottomToolbarRect(int w, int h) {
    ActionBarRect s;
    const RECT footer = panelFooter(w, h);
    const int footerTop = (footer.top < footer.bottom) ? footer.top : h;
    s.x = 0;
    s.width = w;
    s.height = kBottomToolbarHeight;
    s.y = footerTop - kBottomToolbarHeight;
    if (s.y < kTopToolbarHeight) { s.y = footerTop; s.height = 0; }
    return s;
}

// The named-command lookup string is "_" + the channel-qualified id (REAPER's convention).
int resolveBarCommandId(const ActionBarRow& row) {
    if (!NamedCommandLookup) return 0;
    const std::string named = "_" + channelCommandId(row.suffix);
    return NamedCommandLookup(named.c_str());
}

namespace {

// "" when unbound or not registered.
std::string barBindingText(int cmd) {
    if (cmd != 0 && kbd_getTextFromCmd && SectionFromUniqueID) {
        const char* t = kbd_getTextFromCmd(cmd, SectionFromUniqueID(0));
        if (t) return std::string(t);
    }
    return {};
}

}  // namespace

// The flat action index under (x, y) in `bar` for the given row set, or -1 (miss). Pure hit-test.
int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector<ActionBarRow>& rows) {
    if (bar.height <= 0) return -1;
    return hitTestActionBar(x, y, bar, actionBarClusters(rows), kBarSpec);
}

// Resolves the hovered element to (anchor rect, text); false when the hover has
// no tooltip (grid / chrome / More button — its own popup is its affordance).
// Draw lives in panel_render; timing is applied by the caller.
bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& aw, int& ah) {
    const Hover& hv = g_panel.hovered;
    std::vector<ActionBarRow> rows;
    ActionBarRect bar{};
    if (hv.kind == HoverKind::TopBarButton) {
        rows = topBarRows();
        bar = topToolbarActionRect(w);
    } else if (hv.kind == HoverKind::BottomBarButton) {
        rows = bottomBarRows();
        bar = bottomToolbarRect(w, h);
    } else {
        return false;
    }
    if (hv.index < 0 || hv.index >= static_cast<int>(rows.size())) return false;

    // computeBarSlots is the same layout draw + hit-test use, so the anchor
    // matches the drawn button exactly.
    const std::vector<ClusterSpec> clusters = actionBarClusters(rows);
    const std::vector<ActionBarSlot> slots = computeBarSlots(bar, clusters, kBarSpec);
    const ActionBarSlot* slot = nullptr;
    for (const ActionBarSlot& s : slots)
        if (s.index == hv.index) { slot = &s; break; }
    if (!slot) return false;

    // fullName is stored prefix-free; strip defensively in case a source ever
    // carries the display prefix. Tooltip carries name + binding when bound
    // (e.g. "capture selected item — F5"), name only when unbound.
    const std::string phrase = stripActionPrefix(rows[static_cast<std::size_t>(hv.index)].fullName,
                                                 actionDisplayPrefix());
    const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(hv.index)]);
    const std::string binding = barBindingText(cmd);
    textOut = binding.empty() ? phrase : phrase + " \xe2\x80\x94 " + binding;  // " — " (em dash, UTF-8)
    ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height;
    return true;
}

// Between the top and bottom toolbars. When the bottom bar collapses on a short
// client, bottomToolbarRect returns its y at the footer top, so the body still
// ends there.
RECT splitBody(int w, int h) {
    RECT rc{};
    rc.left = 0;
    rc.right = w;
    rc.top = kTopToolbarHeight;
    const ActionBarRect bar = bottomToolbarRect(w, h);
    rc.bottom = bar.y;
    if (rc.bottom < rc.top) rc.bottom = rc.top;
    return rc;
}

bool poolShown() { return g_panel.fullHeight != BankPanelFullHeight::BanksOnly; }
bool banksShown() { return g_panel.fullHeight != BankPanelFullHeight::PoolOnly; }

// Empty when hidden.
RECT poolRegionRect(int w, int h) {
    const RECT body = splitBody(w, h);
    if (!poolShown()) return RECT{0, 0, 0, 0};
    if (!banksShown()) return body;
    RECT rc = body;
    rc.bottom = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2;
    if (rc.bottom < rc.top) rc.bottom = rc.top;
    return rc;
}

RECT banksRegionRect(int w, int h) {
    const RECT body = splitBody(w, h);
    if (!banksShown()) return RECT{0, 0, 0, 0};
    if (!poolShown()) return body;  // banks full-height: the whole body
    RECT rc = body;
    rc.top = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2 +
             kSplitDividerHeight;
    if (rc.top > rc.bottom) rc.top = rc.bottom;
    return rc;
}

RECT regionHeaderRect(const RECT& region) {
    RECT rc = region;
    rc.bottom = region.top + kRegionHeaderHeight;
    if (rc.bottom > region.bottom) rc.bottom = region.bottom;
    return rc;
}

TabStripRect banksTabStripRect(const RECT& region) {
    const RECT hdr = regionHeaderRect(region);
    TabStripRect s;
    s.x = region.left;
    s.y = hdr.bottom;
    s.width = region.right - region.left;
    s.height = kTabStripHeight;
    if (s.y + s.height > region.bottom) s.height = region.bottom - s.y;
    if (s.height < 0) s.height = 0;
    return s;
}

// A region's grid viewport (below the header band, and below the tab strip for the
// banks region). This is where cells tile.
RECT regionGridRect(const RECT& region, bool isBanks) {
    RECT rc = region;
    rc.top = region.top + kRegionHeaderHeight;
    if (isBanks) rc.top += kTabStripHeight;
    if (rc.top > rc.bottom) rc.top = rc.bottom;
    return rc;
}

RECT fullHtBtnRect(const RECT& region) {
    const RECT hdr = regionHeaderRect(region);
    RECT rc = hdr;
    rc.right = hdr.right - 4;
    rc.left = rc.right - kFullHtBtnWidth;
    rc.top = hdr.top + 2;
    rc.bottom = hdr.bottom - 2;
    return rc;
}

RECT createBtnRect(const RECT& region) {
    RECT ft = fullHtBtnRect(region);
    RECT rc = ft;
    rc.right = ft.left - 4;
    rc.left = rc.right - kCreateBtnWidth;
    return rc;
}

// orderedSampleIds reconciles the bank's SlotMap against live membership, so a
// freshly-migrated or out-of-band-mutated bank always yields a complete order.
RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg) {
    RegionDisplay d;
    BankBook* b = book();
    if (!b) return d;
    const std::string bankId = bankIdForRegion(reg);
    if (bankId.empty()) return d;
    d.bank = b->bank(bankId);
    if (!d.bank) return d;

    d.orderedIds = b->orderedSampleIds(bankId);  // occupied ids, slot order (reconciles)
    if (d.orderedIds.empty()) return d;

    const RECT grid = regionGridRect(region, isBanks);
    const int w = grid.right - grid.left;
    if (w <= 0) return d;
    d.slotRects = computeSlotRects(d.bank->slots.maxSlot(), w, kGrid);
    for (SlotCellRect& r : d.slotRects) { r.x += grid.left; r.y += grid.top; }
    return d;
}

RegionDisplay focusedDisplay() {
    RECT cr{};
    GetClientRect(g_panel.hwnd, &cr);
    const int w = cr.right - cr.left, h = cr.bottom - cr.top;
    const bool isBanks = g_panel.focusedRegion == Region::Banks;
    const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
    return regionDisplay(region, isBanks, g_panel.focusedRegion);
}

bool regionAt(int x, int y, Region& out) {
    RECT cr{};
    GetClientRect(g_panel.hwnd, &cr);
    const int w = cr.right - cr.left, h = cr.bottom - cr.top;
    if (poolShown()) {
        const RECT r = poolRegionRect(w, h);
        if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) {
            out = Region::Pool; return true;
        }
    }
    if (banksShown()) {
        const RECT r = banksRegionRect(w, h);
        if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) {
            out = Region::Banks; return true;
        }
    }
    return false;
}

int footerToggleSegmentHit(int x, int y, int w, int h) {
    if (!g_panel.session) return -1;
    const FooterBarLayout fb = footerBarLayoutFor(w, h);
    if (fb.toggle.empty()) return -1;
    const HeaderRect th{fb.toggle.x, fb.toggle.y, fb.toggle.width, fb.toggle.height};
    return hitTestSegment(x, y, th, modeCount());
}

int columnsForRegion(Region reg) {
    RECT cr{};
    GetClientRect(g_panel.hwnd, &cr);
    const int w = cr.right - cr.left, h = cr.bottom - cr.top;
    const RECT region = reg == Region::Banks ? banksRegionRect(w, h)
                                             : poolRegionRect(w, h);
    const RECT grid = regionGridRect(region, reg == Region::Banks);
    return columnsForWidth(grid.right - grid.left, kGrid);
}

}  // namespace reasampler::panel

namespace reasampler {

BankPanelFullHeight bankPanelFullHeight() {
    return panel::g_panel.fullHeight;
}

static void setFullHeight(BankPanelFullHeight target) {
    panel::g_panel.fullHeight =
        (panel::g_panel.fullHeight == target) ? BankPanelFullHeight::Split : target;
    if (panel::g_panel.hwnd) InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE);
}

void bankPanelToggledPoolFullHeight() {
    setFullHeight(BankPanelFullHeight::PoolOnly);
}

void bankPanelToggledBanksFullHeight() {
    setFullHeight(BankPanelFullHeight::BanksOnly);
}

}  // namespace reasampler
