// bank_actions.cpp — see bank_actions.h.
//
// Each mutating handler is a thin UX skin — text prompts, name resolution, console
// feedback — over the promptless bankOp* verbs in shell/bank_ops (model op +
// persistBankOp, one bank op = one Ctrl-Z). Pool privileges / collapse-by-hash /
// active-fallback-to-pool live in bank_book; handlers only drive the verbs.
//
// REFERENCE-INVALIDATION GUARDRAIL: book().activeIndex() / bank()->index return a
// reference INTO the book's internal vector, which a create/delete can reallocate.
// No handler caches a BankModel&/Bank* across a structural mutation — ids are
// resolved to strings up front and re-resolved after any create/delete.
//
// main.cpp owns the API pointers; this TU gets them extern. Action ids are minted
// from FOREVER-STABLE strings — never change one after ship.

#include "shell/actions/bank_actions.h"

#include <string>
#include <vector>

#include "shell/actions/action_registry.h"  // channelIdFor / registerAction (shared plumbing)
#include "shell/actions/prune_action.h"     // doBankPruneFolder — the guarded prune body

#include "core/model/bank_book.h"        // BankBook, nextBankId, kPoolBankId (B1)
#include "shell/bank_ops/bank_ops.h"     // bankOp* promptless verbs (Q-W6 non-UI seam)
#include "shell/panel/panel_bank_ops.h"  // promptBankName + the panel selection seam
#include "shell/panel/panel_layout.h"    // full-height toggles (B3)
#include "shell/persist/session.h"       // ReaSamplerSession (owns book())

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_ShowMessageBox
#include "reaper_plugin_functions.h"

namespace reasampler {

namespace {

// FOREVER-STABLE action-id SUFFIXES: the channel prefix is prepended at register
// (channelCommandId); NEVER change a shipped suffix — user keybindings key off the
// composed id.
constexpr const char* kIdBankCreate       = "BANK_CREATE";
constexpr const char* kIdBankRename       = "BANK_RENAME";
constexpr const char* kIdBankDelete       = "BANK_DELETE";
constexpr const char* kIdBankEvacuate     = "BANK_EVACUATE";
constexpr const char* kIdBankActivateNext = "BANK_ACTIVATE_NEXT";
constexpr const char* kIdBankActivatePool = "BANK_ACTIVATE_POOL";
constexpr const char* kIdBankMoveSel      = "BANK_MOVE_SELECTED";
constexpr const char* kIdBankCopySel      = "BANK_COPY_SELECTED";
constexpr const char* kIdBankRemoveSel    = "BANK_REMOVE_SELECTED";
constexpr const char* kIdBankPoolFull     = "BANK_POOL_FULLHEIGHT";
constexpr const char* kIdBankBanksFull    = "BANK_BANKS_FULLHEIGHT";
constexpr const char* kIdBankPruneFolder  = "BANK_PRUNE_FOLDER";

// Not owned here; set once by bankRegisterActions. bankHandleCommand guards it
// non-null before any handler runs.
ReaSamplerSession* g_session = nullptr;

int g_cmdBankCreate = 0;
int g_cmdBankRename = 0;
int g_cmdBankDelete = 0;
int g_cmdBankEvacuate = 0;
int g_cmdBankActivateNext = 0;
int g_cmdBankActivatePool = 0;
int g_cmdBankMoveSel = 0;
int g_cmdBankCopySel = 0;
int g_cmdBankRemoveSel = 0;
int g_cmdBankPoolFull = 0;
int g_cmdBankBanksFull = 0;
int g_cmdBankPruneFolder = 0;

// gaccel storage must outlive registration — REAPER holds each pointer until we
// mirror-unregister it. One per action.
gaccel_register_t g_accelBankCreate{};
gaccel_register_t g_accelBankRename{};
gaccel_register_t g_accelBankDelete{};
gaccel_register_t g_accelBankEvacuate{};
gaccel_register_t g_accelBankActivateNext{};
gaccel_register_t g_accelBankActivatePool{};
gaccel_register_t g_accelBankMoveSel{};
gaccel_register_t g_accelBankCopySel{};
gaccel_register_t g_accelBankRemoveSel{};
gaccel_register_t g_accelBankPoolFull{};
gaccel_register_t g_accelBankBanksFull{};
gaccel_register_t g_accelBankPruneFolder{};

// Resolves a user-typed display name to a bank id ("" if none matches). UI name
// resolution, not a model rule — kept here rather than the model. Unambiguous by
// construction: the model enforces unique display names.
std::string bankIdByDisplayName(const std::string& name) {
    for (const Bank& b : g_session->book().banks())
        if (b.displayName == name) return b.id;
    return {};
}

// The new bank is NOT auto-activated (create and activate are distinct acts,
// mirroring capture/placement separation).
void doBankCreate() {
    std::string name;
    if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return;
    if (bankOpCreate(*g_session, name).empty()) {
        ShowConsoleMsg(
            ("ReaSampler: could not create bank \"" + name +
             "\" (a bank with that name already exists).\n")
                .c_str());
    }
}

// Two prompts (which bank, then the new name) keep this bindable form
// self-contained; the panel renames in place on a tab instead.
void doBankRename() {
    std::string which;
    if (!promptBankName("ReaSampler: rename bank", "Bank to rename (current name):", "",
                    which))
        return;
    const std::string id = bankIdByDisplayName(which);
    if (id.empty()) {
        ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
        return;
    }
    std::string newName;
    if (!promptBankName("ReaSampler: rename bank", "New name:", which, newName)) return;
    if (!bankOpRename(*g_session, id, newName)) {
        ShowConsoleMsg("ReaSampler: cannot rename that bank (the pool is un-renamable, "
                       "or another bank already uses that name).\n");
    }
}

// If the bank holds members, confirm first (a plain delete orphans those members'
// files until prune); an empty bank deletes with no prompt.
void doBankDelete() {
    std::string which;
    if (!promptBankName("ReaSampler: delete bank", "Bank to delete:", "", which)) return;
    const std::string id = bankIdByDisplayName(which);
    if (id.empty()) {
        ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
        return;
    }
    // Catch the pool BEFORE the non-empty confirm, so typing "Pool" never shows a
    // misleading "delete anyway?" for an operation the model will refuse regardless.
    if (id == kPoolBankId) {
        ShowConsoleMsg("ReaSampler: the pool cannot be deleted.\n");
        return;
    }
    // Read member count before deleting — the Bank* is invalidated by the delete.
    const Bank* b = g_session->book().bank(id);
    if (!b) return;  // race-safe: id resolved above but re-check
    const std::size_t members = b->index.size();
    if (members > 0) {
        const std::string msg =
            "\"" + which + "\" holds " + std::to_string(members) +
            (members == 1 ? " sample" : " samples") +
            ".\n\nDeleting drops them from every bank (their files are NOT deleted, "
            "but no bank will reference them until prune).\n\nTo keep the samples, "
            "cancel and Evacuate the bank to the pool first.\n\nDelete anyway?";
        const int r = ShowMessageBox(msg.c_str(), "ReaSampler: delete non-empty bank", 4);
        if (r != 6) return;  // 6 == YES; anything else cancels (SDK ~6544)
    }
    // Bump only when the deleted bank held samples — dropping them changes what a
    // live instance referencing one could play.
    if (!bankOpDelete(*g_session, id, /*bumpGeneration=*/members > 0)) {
        ShowConsoleMsg("ReaSampler: cannot delete that bank (the pool is un-deletable).\n");
    }
}

// The "keep the samples" companion to delete: moves every member back to the pool.
void doBankEvacuate() {
    std::string which;
    if (!promptBankName("ReaSampler: evacuate bank", "Bank to evacuate to the pool:", "",
                    which))
        return;
    const std::string id = bankIdByDisplayName(which);
    if (id.empty()) {
        ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
        return;
    }
    if (!bankOpEvacuate(*g_session, id)) {
        ShowConsoleMsg("ReaSampler: cannot evacuate that bank (the pool is the "
                       "destination, not a source).\n");
    }
}

// Cycles the active bank (pool -> named -> ... -> pool). Activating changes the
// CAPTURE TARGET only — never touches the timeline.
void doBankActivateNext() {
    std::vector<std::string> ids;
    ids.reserve(g_session->book().size());
    for (const Bank& b : g_session->book().banks()) ids.push_back(b.id);
    const std::string target = nextBankId(ids, g_session->book().activeBankId());
    if (target.empty()) return;  // degenerate (no banks) — cannot happen (pool seeded)
    bankOpActivate(*g_session, target);
}

void doBankActivatePool() {
    bankOpActivate(*g_session, kPoolBankId);
}

// SOURCE is the bank the selection lives in (bankPanelSelectedSourceBankId), which is
// NOT necessarily the active/capture-target bank — the vertical split can show a
// different bank than the one active for capture.
void doBankTransferSelected(bool copy) {
    const std::vector<std::string> selected = bankPanelSelectedSampleIds();
    if (selected.empty()) {
        ShowConsoleMsg("ReaSampler: nothing selected in the bank panel to "
                       "move/copy.\n");
        return;
    }
    const char* verb = copy ? "copy" : "move";
    const std::string title = std::string("ReaSampler: ") + verb + " selected samples";
    std::string destName;
    if (!promptBankName(title.c_str(), "Destination bank:", "", destName)) return;
    const std::string destId = bankIdByDisplayName(destName);
    if (destId.empty()) {
        ShowConsoleMsg(("ReaSampler: no bank named \"" + destName + "\".\n").c_str());
        return;
    }
    const std::string srcId = bankPanelSelectedSourceBankId();
    if (srcId == destId) {
        ShowConsoleMsg("ReaSampler: source and destination are the same bank.\n");
        return;
    }
    bankOpTransfer(*g_session, selected, srcId, destId, copy);
}

// Index-only and non-destructive to the file (orphaned until prune); silent, with
// the batched undo as recovery.
void doBankRemoveSelected() {
    const std::vector<std::string> selected = bankPanelSelectedSampleIds();
    if (selected.empty()) {
        ShowConsoleMsg("ReaSampler: nothing selected in the bank panel to remove.\n");
        return;
    }
    const std::string srcId = bankPanelSelectedSourceBankId();
    if (g_session->book().bank(srcId) == nullptr) {
        ShowConsoleMsg("ReaSampler: the selection's bank no longer exists.\n");
        return;
    }
    bankOpRemove(*g_session, selected, srcId);
}

}  // namespace

void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
    g_session = session;  // same live session as the Design View family

    g_cmdBankCreate = registerAction(rec, kIdBankCreate, g_accelBankCreate,
                                     "create bank");
    g_cmdBankRename = registerAction(rec, kIdBankRename, g_accelBankRename,
                                     "rename bank");
    g_cmdBankDelete = registerAction(rec, kIdBankDelete, g_accelBankDelete,
                                     "delete bank");
    g_cmdBankEvacuate = registerAction(rec, kIdBankEvacuate, g_accelBankEvacuate,
                                       "evacuate bank to pool");
    g_cmdBankActivateNext = registerAction(rec, kIdBankActivateNext, g_accelBankActivateNext,
                                           "activate next bank (cycle)");
    g_cmdBankActivatePool = registerAction(rec, kIdBankActivatePool, g_accelBankActivatePool,
                                           "activate pool");
    g_cmdBankMoveSel = registerAction(rec, kIdBankMoveSel, g_accelBankMoveSel,
                                      "move selected samples to bank");
    g_cmdBankCopySel = registerAction(rec, kIdBankCopySel, g_accelBankCopySel,
                                      "copy selected samples to bank");
    g_cmdBankRemoveSel = registerAction(rec, kIdBankRemoveSel, g_accelBankRemoveSel,
                                        "remove selected samples");
    g_cmdBankPoolFull = registerAction(rec, kIdBankPoolFull, g_accelBankPoolFull,
                                       "toggle pool full-height");
    g_cmdBankBanksFull = registerAction(rec, kIdBankBanksFull, g_accelBankBanksFull,
                                        "toggle banks full-height");
    g_cmdBankPruneFolder = registerAction(rec, kIdBankPruneFolder, g_accelBankPruneFolder,
                                          "prune bank folder");
}

bool bankHandleCommand(int command) {
    if (command == 0 || !g_session) return false;

    if (command == g_cmdBankCreate)       { doBankCreate(); return true; }
    if (command == g_cmdBankRename)       { doBankRename(); return true; }
    if (command == g_cmdBankDelete)       { doBankDelete(); return true; }
    if (command == g_cmdBankEvacuate)     { doBankEvacuate(); return true; }
    if (command == g_cmdBankActivateNext) { doBankActivateNext(); return true; }
    if (command == g_cmdBankActivatePool) { doBankActivatePool(); return true; }
    if (command == g_cmdBankMoveSel)      { doBankTransferSelected(false); return true; }
    if (command == g_cmdBankCopySel)      { doBankTransferSelected(true);  return true; }
    if (command == g_cmdBankRemoveSel)    { doBankRemoveSelected(); return true; }
    if (command == g_cmdBankPoolFull)     { bankPanelToggledPoolFullHeight(); return true; }
    if (command == g_cmdBankBanksFull)    { bankPanelToggledBanksFullHeight(); return true; }
    if (command == g_cmdBankPruneFolder)  { doBankPruneFolder(*g_session); return true; }

    return false;  // not ours — caller's hookcommand keeps looking
}

int bankPruneCommandId() { return g_cmdBankPruneFolder; }

void bankUnregisterActions(reaper_plugin_info_t* rec) {
    // Reverse of registration order; each '-command_id' re-presents the same
    // interned id (channelIdFor).
    rec->Register("-gaccel", (void*)&g_accelBankPruneFolder);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankPruneFolder));
    rec->Register("-gaccel", (void*)&g_accelBankBanksFull);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankBanksFull));
    rec->Register("-gaccel", (void*)&g_accelBankPoolFull);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankPoolFull));
    rec->Register("-gaccel", (void*)&g_accelBankRemoveSel);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankRemoveSel));
    rec->Register("-gaccel", (void*)&g_accelBankCopySel);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankCopySel));
    rec->Register("-gaccel", (void*)&g_accelBankMoveSel);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankMoveSel));
    rec->Register("-gaccel", (void*)&g_accelBankActivatePool);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankActivatePool));
    rec->Register("-gaccel", (void*)&g_accelBankActivateNext);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankActivateNext));
    rec->Register("-gaccel", (void*)&g_accelBankEvacuate);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankEvacuate));
    rec->Register("-gaccel", (void*)&g_accelBankDelete);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankDelete));
    rec->Register("-gaccel", (void*)&g_accelBankRename);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankRename));
    rec->Register("-gaccel", (void*)&g_accelBankCreate);
    rec->Register("-command_id", (void*)channelIdFor(kIdBankCreate));

    g_session = nullptr;
}

}  // namespace reasampler
