// panel_bank_ops.cpp — the bank-CRUD-UX + menus seam of the docked bank panel. The
// promptless bank verbs live in shell/bank_ops (bankOp* + persistBankOp); this TU is
// the panel's THIN UX SKIN over them — menu handlers, book/bank accessors, popup
// menus, and the selection-id / OS-drag path resolvers. `bank_actions` is the
// sibling bindable-action skin.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers. DAW-verified, not
// unit-tested.

#include <cstdio>
#include <filesystem>
#include <string>
#include <vector>

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

#include "shell/actions/package_export_action.h"  // doBankPackageExport — the export skin
#include "shell/actions/package_import_action.h"  // doImportBankPackage — the menu's import row
#include "shell/bank_ops/bank_ops.h"    // bankOp* promptless verbs
#include "shell/persist/session.h"      // ReaSamplerSession — the live session the ops mutate

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetUserInputs
#define REAPERAPI_WANT_ShowMessageBox
#define REAPERAPI_WANT_Main_OnCommand
#include "reaper_plugin_functions.h"

namespace reasampler::panel {

namespace fs = std::filesystem;

// Mirrors the persist shell's derivation (ext_state_io.cpp).
std::string currentProjectDir() {
    std::vector<char> buf(4096, '\0');
    EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
    std::string rpp(buf.data());
    if (rpp.empty()) return {};
    return normalizeSlashes(fs::path(rpp).parent_path().string());
}

BankBook* book() { return g_panel.session ? &g_panel.session->book() : nullptr; }

// The BankModel a region currently displays. Pool region -> the pool; banks region ->
// the shown tab's bank (or nullptr when no named banks / the id went stale). Resolved
// FRESH every call (never cached across a mutation).
const BankModel* indexForRegion(Region r) {
    BankBook* b = book();
    if (!b) return nullptr;
    if (r == Region::Pool) return &b->pool().index;
    if (g_panel.shownBankId.empty()) return nullptr;
    return b->index(g_panel.shownBankId);
}

// The bank id a region displays (pool id, or the shown tab's id; "" when none).
std::string bankIdForRegion(Region r) {
    if (r == Region::Pool) return std::string(kPoolBankId);
    return g_panel.shownBankId;
}

// The named banks in ordinal order (pool excluded) — the tabs. Resolved fresh.
std::vector<const Bank*> namedBanks() {
    std::vector<const Bank*> out;
    BankBook* b = book();
    if (!b) return out;
    for (const Bank& bk : b->banks())
        if (!bk.isPool()) out.push_back(&bk);
    return out;
}

// Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs). After a
// STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we
// resolve fresh, pass ids, and let the next refreshFingerprint repaint.

void doCreateBank() {
    if (!book()) return;
    std::string name;
    if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return;
    const std::string id = bankOpCreate(*g_panel.session, name);
    if (id.empty()) {
        ShowMessageBox("A bank with that name already exists.",
                       "ReaSampler: create bank", 0);
        return;
    }
    g_panel.shownBankId = id;             // show the freshly-created bank
    g_panel.focusedRegion = Region::Banks;
    invalidatePanel();
}

namespace {

void doRenameBank(const std::string& bankId) {
    if (!book()) return;
    const Bank* bk = book()->bank(bankId);
    if (!bk || bk->isPool()) return;
    const std::string current = bk->displayName;  // copy before any mutation
    std::string newName;
    if (!promptBankName("ReaSampler: rename bank", "New name:", current, newName)) return;
    if (!bankOpRename(*g_panel.session, bankId, newName)) {
        ShowMessageBox("Another bank already uses that name.",
                       "ReaSampler: rename bank", 0);
        return;
    }
    invalidatePanel();
}

// Delete with a confirm-on-non-empty affordance: the confirm names the member count
// and offers evacuate as the one-click alternative (Yes=delete anyway,
// No=evacuate-then-keep, Cancel=abort).
void doDeleteBank(const std::string& bankId) {
    if (!book()) return;
    const Bank* bk = book()->bank(bankId);
    if (!bk || bk->isPool()) return;
    const std::size_t members = bk->index.size();  // read BEFORE any mutation
    const std::string name = bk->displayName;

    if (members > 0) {
        const std::string msg =
            "\"" + name + "\" holds " + std::to_string(members) +
            (members == 1 ? " sample" : " samples") +
            ".\n\nYes  -- delete the bank AND drop its samples (files are kept on disk "
            "but no bank references them until prune).\nNo   -- Evacuate them to the "
            "pool first, then delete the empty bank (keeps the samples).\nCancel -- "
            "do nothing.";
        // 3 == MB_YESNOCANCEL. 6=Yes, 7=No, 2=Cancel (SDK).
        const int r = ShowMessageBox(msg.c_str(),
                                     "ReaSampler: delete non-empty bank", 3);
        if (r == 2) return;                 // Cancel
        if (r == 7) {                       // No -> evacuate, then delete empty
            if (!book()->evacuate(bankId)) return;
            // book() may have reallocated; re-resolve nothing (we pass the id again).
        }
        // r == 6 (Yes) falls through to a plain delete (drops members).
    }
    // Bump generation when the bank held samples — an empty-bank delete is purely
    // organizational. The ORIGINAL member count decides (the No-path already evacuated them).
    if (!bankOpDelete(*g_panel.session, bankId, /*bumpGeneration=*/members > 0)) return;
    // If no named banks remain, nudge focus to the pool so the selection has a valid home.
    if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
    invalidatePanel();
}

void doEvacuateBank(const std::string& bankId) {
    if (!book()) return;
    const Bank* bk = book()->bank(bankId);
    if (!bk || bk->isPool()) return;
    if (!bankOpEvacuate(*g_panel.session, bankId)) return;
    invalidatePanel();
}

void doActivateBank(const std::string& bankId) {
    if (!book()) return;  // no live session — nothing to activate against
    if (!bankOpActivate(*g_panel.session, bankId)) return;  // rejects an unknown id
    invalidatePanel();
}

}  // namespace

// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Thin panel
// skin over bankOpTransfer; clears the stale selection and repaints on an actual mutation.
void transferSamples(const std::vector<std::string>& sampleIds,
                     const std::string& srcBankId, const std::string& destBankId,
                     bool copy) {
    if (!book()) return;  // no live session — nothing to transfer within
    if (!bankOpTransfer(*g_panel.session, sampleIds, srcBankId, destBankId, copy))
        return;  // nothing changed — no persist, no undo point
    // Selection indexed into the source; after a move those indices are stale.
    g_panel.selection = Selection{};
    invalidatePanel();
}

// Remove `sampleIds` from `srcBankId` (index-only, this-bank scope). Thin panel skin
// over bankOpRemove. Clears the stale selection and repaints on an actual removal.
void removeSamples(const std::vector<std::string>& sampleIds,
                   const std::string& srcBankId) {
    if (!book()) return;  // no live session — nothing to remove from
    if (!bankOpRemove(*g_panel.session, sampleIds, srcBankId))
        return;  // nothing changed — no persist, no undo point
    g_panel.selection = Selection{};
    invalidatePanel();
}

// The selection's sample ids resolved against the FOCUSED region's bank (source of a
// move/copy). Selection ordinals index the DISPLAY (slot) order, not BankModel
// insertion order. Returns ids in bank order; empty when nothing selected.
std::vector<std::string> focusedSelectionIds() {
    std::vector<std::string> ids;
    const RegionDisplay disp = focusedDisplay();
    const int count = disp.occupiedCount();
    for (int i : g_panel.selection.indices)
        if (i >= 0 && i < count) ids.push_back(disp.orderedIds[static_cast<std::size_t>(i)]);
    return ids;
}

// Resolves the ARMED drag payload to the absolute, existing-file path list for a native
// OS drag-out. Reuses resolveBankFile (audition/insert's path machinery) — no temp
// copies. Each id is looked up in its SOURCE bank's index (not the focused region, which
// can differ once the pointer roams), then handed to drag_out::assemblePathList for
// dedupe + skip-missing/unresolved policy. Read-only.
std::vector<std::string> resolveDragPathsForOs() {
    std::vector<ResolvedSample> resolved;
    BankBook* b = book();
    if (!b) return {};
    const BankModel* idx = b->index(g_panel.dragSourceBankId);
    if (!idx) return {};

    const std::string projectDir = currentProjectDir();
    resolved.reserve(g_panel.dragSampleIds.size());
    for (const std::string& sid : g_panel.dragSampleIds) {
        const Sample* s = idx->query(sid);
        if (!s) continue;  // stale id — the pure layer would skip it anyway; nothing to resolve
        ResolvedSample rs;
        rs.absolutePath = resolveBankFile(projectDir, s->relativePath);
        rs.fileExists = !rs.absolutePath.empty() && fs::exists(fs::path(rs.absolutePath));
        resolved.push_back(std::move(rs));
    }
    return assemblePathList(resolved).paths;
}

// SWELL/Win32 both expose CreatePopupMenu / InsertMenu / TrackPopupMenu(TPM_RETURNCMD) /
// DestroyMenu. Menu command ids below are LOCAL to the popup (not REAPER action ids) —
// TPM_RETURNCMD hands the chosen id straight back, so no hookcommand routing is involved.

namespace {

// Win32 and SWELL both treat pos < 0 as append.
void menuAppend(HMENU menu, unsigned int id, const char* text, bool grayed = false) {
    UINT flags = MF_BYPOSITION | MF_STRING;
    if (grayed) flags |= MF_GRAYED;
    InsertMenu(menu, -1, flags, id, text);
}
void menuSeparator(HMENU menu) {
    InsertMenu(menu, -1, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr);
}

// Menu command ids (local to a popup).
enum : unsigned int {
    kMenuNone = 0,
    kMenuActivate = 100,
    kMenuRename,
    kMenuDelete,
    kMenuEvacuate,
    kMenuCreate,
    kMenuExport,            // export this bank as a .rsbank package
    kMenuRemove,            // remove selected sample(s) from the source bank
    kMenuImportPackage,     // land a .rsbank as a NEW bank (never merges into this one)
    kMenuMoveBase = 1000,   // move-to-bank: kMenuMoveBase + destination index
    kMenuCopyBase = 2000,   // copy-to-bank: kMenuCopyBase + destination index
};

}  // namespace

// Shows the right-click context menu for a named-bank TAB: activate / rename / delete
// / evacuate / export that bank, plus a create entry. Drives the id-keyed ops.
void showTabMenu(int screenX, int screenY, const std::string& bankId) {
    if (!book()) return;
    const Bank* bk = book()->bank(bankId);
    if (!bk || bk->isPool()) return;
    const bool isActive = book()->activeBankId() == bankId;
    const bool nonEmpty = !bk->index.empty();

    HMENU menu = CreatePopupMenu();
    menuAppend(menu, kMenuActivate,
               isActive ? "Active (capture target)" : "Activate (make capture target)",
               /*grayed=*/isActive);
    menuSeparator(menu);
    menuAppend(menu, kMenuRename, "Rename...");
    menuAppend(menu, kMenuEvacuate, "Evacuate to pool", /*grayed=*/!nonEmpty);
    menuAppend(menu, kMenuDelete, "Delete...");
    menuAppend(menu, kMenuExport, "Export as package...");
    menuSeparator(menu);
    menuAppend(menu, kMenuCreate, "New bank...");
    menuAppend(menu, kMenuImportPackage, "Import bank package...");

    const int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, screenX, screenY, 0,
                                   g_panel.hwnd, nullptr);
    DestroyMenu(menu);

    switch (cmd) {
        case kMenuActivate: doActivateBank(bankId); break;
        case kMenuRename:   doRenameBank(bankId); break;
        case kMenuEvacuate: doEvacuateBank(bankId); break;
        case kMenuDelete:   doDeleteBank(bankId); break;
        case kMenuExport:   doBankPackageExport(*g_panel.session, bankId); break;
        case kMenuCreate:   doCreateBank(); break;
        // Always a NEW bank, never a merge into the right-clicked one — the row sits
        // here because this is the panel's bank menu, not because it targets this bank.
        case kMenuImportPackage:
            if (g_panel.session) {
                const std::string id = doImportBankPackage(*g_panel.session);
                if (!id.empty()) {  // landed — show the freshly-imported bank
                    g_panel.shownBankId = id;
                    g_panel.focusedRegion = Region::Banks;
                    invalidatePanel();
                }
            }
            break;
        default: break;
    }
}

// Opens the top-toolbar overflow ("⋯" More) popup and fires the chosen rare-capture
// variant's command. Menu ids are LOCAL to the popup (1-based ordinal into
// overflowMenuRows); we resolve + fire the corresponding registered command id via
// the same contract the visible buttons use.
void showMoreMenu() {
    if (!g_panel.hwnd) return;
    const std::vector<ActionBarRow> rows = overflowMenuRows();
    if (rows.empty()) return;

    RECT cr{};
    GetClientRect(g_panel.hwnd, &cr);
    const MenuButtonRect mb = topMenuButtonRect(cr.right - cr.left);
    if (mb.empty()) return;

    HMENU menu = CreatePopupMenu();
    for (std::size_t i = 0; i < rows.size(); ++i) {
        const int cmd = resolveBarCommandId(rows[i]);
        // Grey a variant not registered on this channel (defensive — all three are registered).
        menuAppend(menu, static_cast<unsigned int>(i + 1), rows[i].fullName.c_str(),
                   /*grayed=*/cmd == 0);
    }

    // Anchor the popup at the button's bottom-left, in screen coords.
    POINT pt{mb.x, mb.y + mb.height};
    ClientToScreen(g_panel.hwnd, &pt);
    const int chosen = TrackPopupMenu(menu, TPM_RETURNCMD, pt.x, pt.y, 0, g_panel.hwnd, nullptr);
    DestroyMenu(menu);

    if (chosen >= 1 && chosen <= static_cast<int>(rows.size())) {
        const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(chosen - 1)]);
        if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
    }
}

// Shows the move/copy menu for the current selection (the SOURCE is the focused
// region's bank). Lists every OTHER bank as a move destination, then the same list
// as a copy destination. Move is the default (listed first); copy is secondary.
void showSelectionMenu(int screenX, int screenY) {
    const std::vector<std::string> sel = focusedSelectionIds();
    if (sel.empty()) return;
    const std::string srcId = bankIdForRegion(g_panel.focusedRegion);

    // Destinations: pool + named banks, excluding the source. Ordinal order.
    struct Dest { std::string id; std::string name; };
    std::vector<Dest> dests;
    if (srcId != std::string(kPoolBankId))
        dests.push_back({std::string(kPoolBankId), std::string(kPoolBankName)});
    for (const Bank* bk : namedBanks())
        if (bk->id != srcId) dests.push_back({bk->id, bk->displayName});

    const std::string label = std::to_string(sel.size()) +
                              (sel.size() == 1 ? " sample" : " samples");

    HMENU menu = CreatePopupMenu();
    // Move/copy blocks appear only when there is another bank to transfer to; Remove is
    // always offered (it needs no destination — it drops the entry from the source).
    if (!dests.empty()) {
        menuAppend(menu, kMenuNone, ("Move " + label + " to:").c_str(), /*grayed=*/true);
        for (std::size_t i = 0; i < dests.size(); ++i)
            menuAppend(menu, kMenuMoveBase + static_cast<unsigned int>(i),
                       ("  " + dests[i].name).c_str());
        menuSeparator(menu);
        menuAppend(menu, kMenuNone, ("Copy " + label + " to:").c_str(), /*grayed=*/true);
        for (std::size_t i = 0; i < dests.size(); ++i)
            menuAppend(menu, kMenuCopyBase + static_cast<unsigned int>(i),
                       ("  " + dests[i].name).c_str());
        menuSeparator(menu);
    }
    menuAppend(menu, kMenuRemove, ("Remove " + label + "...").c_str());

    const int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, screenX, screenY, 0,
                                   g_panel.hwnd, nullptr);
    DestroyMenu(menu);
    if (cmd == static_cast<int>(kMenuRemove)) {
        removeSamples(sel, srcId);
    } else if (cmd >= static_cast<int>(kMenuMoveBase) &&
        cmd < static_cast<int>(kMenuMoveBase + dests.size())) {
        transferSamples(sel, srcId, dests[cmd - kMenuMoveBase].id, /*copy=*/false);
    } else if (cmd >= static_cast<int>(kMenuCopyBase) &&
               cmd < static_cast<int>(kMenuCopyBase + dests.size())) {
        transferSamples(sel, srcId, dests[cmd - kMenuCopyBase].id, /*copy=*/true);
    }
}

}  // namespace reasampler::panel

namespace reasampler {

// GetUserInputs splits returned values on a separator defaulting to ',', so the
// separator is overridden to \x1f (un-typeable) via the documented `separator=X`
// trailing pseudo-caption — any printable name round-trips.
bool promptBankName(const char* title, const char* caption, const std::string& initial,
                    std::string& out) {
    std::vector<char> buf(512, '\0');
    std::snprintf(buf.data(), buf.size(), "%s", initial.c_str());
    const std::string captions = std::string(caption) + ",separator=\x1f";
    if (!GetUserInputs(title, 1, captions.c_str(), buf.data(),
                       static_cast<int>(buf.size())))
        return false;  // user cancelled
    std::string s(buf.data());
    if (s.empty()) return false;  // an empty name is not a valid bank name
    out = std::move(s);
    return true;
}

std::vector<std::string> bankPanelSelectedSampleIds() {
    return panel::focusedSelectionIds();
}

std::string bankPanelSelectedSourceBankId() {
    // The focused region's displayed bank is the move/copy source. Default to the
    // pool (a safe source) when nothing is selected / the panel never opened.
    if (panel::g_panel.selection.empty()) return std::string(kPoolBankId);
    const std::string id = panel::bankIdForRegion(panel::g_panel.focusedRegion);
    return id.empty() ? std::string(kPoolBankId) : id;
}

}  // namespace reasampler
