// bank_ops.cpp — see bank_ops.h for the contract. The ONE implementation home of the
// bank verbs: each mutates the given session's book() then persists via
// persistBankOp() (one bank op = one Ctrl-Z; a true index no-op opens NO undo
// point). Index/model + ext-state only — never the arrange, never a file on disk.
// REFERENCE-INVALIDATION GUARDRAIL: after a STRUCTURAL mutation any Bank*/BankModel&
// is invalid — verbs take ids and resolve fresh per model call.
//
// main.cpp owns the API pointers; this TU gets them extern. DAW-verified, not unit tested.

#include "shell/bank_ops/bank_ops.h"

#include <string>
#include <vector>

#include "core/model/bank_book.h"       // BankBook / TransferResult / RemoveScope
#include "shell/persist/session.h"      // ReaSamplerSession — the session the verbs mutate

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_genGuid
#define REAPERAPI_WANT_guidToString
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"

namespace reasampler {

namespace {

// Ids are caller-supplied and stable; the model stays pure and mints none.
std::string mintBankId() {
    GUID g{};
    genGuid(&g);
    char buf[64] = {0};  // guidToString needs >=64 chars (SDK contract)
    guidToString(&g, buf);
    return std::string(buf);
}

}  // namespace

// WHY THIS WRAPS: a bank verb mutates ONLY our project ext-state, which REAPER's
// undo system captures iff UNDO_STATE_MISCCFG is set (the SDK documents MISCCFG as
// covering extensions' project ext-state). We pass exactly UNDO_STATE_MISCCFG, not
// -1/UNDO_STATE_ALL — a bank verb touches no tracks/FX/items, so snapshotting them
// would be both heavier and wrong. Persist runs INSIDE the block so the post-mutation
// ext-state is the block's "after" image.
//
// UNSAVED-PROJECT GUARDRAIL: on an unsaved/no-active project saveToActiveProject()
// no-ops; we still CLOSE the block, but with an empty label + zero flag so REAPER
// discards the point instead of recording a no-effect undo entry. Quiet persist by
// design (mirrors capture, not Design-View) — deliberately no Save-As prompt.
void persistBankOp(ReaSamplerSession& session, const char* label,
                   bool bumpGeneration) {
    Undo_BeginBlock2(nullptr);
    if (bumpGeneration) session.bumpBankGeneration();
    const bool persisted = session.saveToActiveProject();
    if (persisted)
        Undo_EndBlock2(nullptr, label, UNDO_STATE_MISCCFG);
    else
        Undo_EndBlock2(nullptr, "", 0);  // no ext-state write -> discard the empty point
}

// --- Promptless inner bank verbs (one home) ------------------------------------
// Model op + persistBankOp only; NO UX. Callers own prompts/confirms/nudges and the
// session-liveness question. Each verb persists ONLY after the model accepted — a
// rejected op opens no undo point.

std::string bankOpCreate(ReaSamplerSession& session, const std::string& name) {
    const std::string id = mintBankId();
    if (!session.book().createBank(id, name)) return {};  // duplicate display name (model rule)
    persistBankOp(session, "ReaSampler: create bank");
    return id;
}

bool bankOpRename(ReaSamplerSession& session, const std::string& bankId,
                  const std::string& newName) {
    if (!session.book().renameBank(bankId, newName)) return false;  // pool / name in use
    persistBankOp(session, "ReaSampler: rename bank");
    return true;
}

bool bankOpDelete(ReaSamplerSession& session, const std::string& bankId,
                  bool bumpGeneration) {
    if (!session.book().deleteBank(bankId)) return false;  // pool un-deletable (model rule)
    persistBankOp(session, "ReaSampler: delete bank", bumpGeneration);
    return true;
}

bool bankOpEvacuate(ReaSamplerSession& session, const std::string& bankId) {
    if (!session.book().evacuate(bankId)) return false;  // pool is a destination, not a source
    persistBankOp(session, "ReaSampler: evacuate bank", /*bumpGeneration=*/true);
    return true;
}

bool bankOpActivate(ReaSamplerSession& session, const std::string& bankId) {
    if (!session.book().setActiveBank(bankId)) return false;  // rejects an unknown id
    persistBankOp(session, "ReaSampler: activate bank");
    return true;
}

// NO-OP GUARDRAIL, verb-aware: a MOVE collapse still removed the source entry (the
// index DID mutate), but a COPY collapse left the source intact AND the dest already
// held the hash (a true no-op) — so copy counts only real gains, move counts gains
// OR collapses.
bool bankOpTransfer(ReaSamplerSession& session,
                    const std::vector<std::string>& sampleIds,
                    const std::string& srcBankId, const std::string& destBankId,
                    bool copy) {
    BankBook& b = session.book();
    if (sampleIds.empty() || srcBankId == destBankId) return false;
    if (!b.bank(srcBankId) || !b.bank(destBankId)) return false;
    int ok = 0, collapsed = 0;
    for (const std::string& sid : sampleIds) {
        const TransferResult r =
            copy ? b.copySample(sid, srcBankId, destBankId)
                 : b.moveSample(sid, srcBankId, destBankId);
        switch (r) {
            case TransferResult::Moved:
            case TransferResult::Copied:        ++ok; break;
            case TransferResult::Collapsed:     ++collapsed; break;
            case TransferResult::RejectedUnknownBank:
            case TransferResult::RejectedSampleAbsent:
            case TransferResult::RejectedSameBank: break;
        }
    }
    const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
    if (!mutated) return false;  // nothing changed — no persist, no undo point
    persistBankOp(session,
                  copy ? "ReaSampler: copy sample(s)" : "ReaSampler: move sample(s)",
                  /*bumpGeneration=*/true);
    return true;
}

// Index-only, this-bank scope; non-destructive to the file (a last-reference remove
// leaves the file orphaned until prune). Silent: recoverability is the batched undo.
bool bankOpRemove(ReaSamplerSession& session,
                  const std::vector<std::string>& sampleIds,
                  const std::string& srcBankId) {
    BankBook& b = session.book();
    if (sampleIds.empty() || !b.bank(srcBankId)) return false;
    int removed = 0;
    for (const std::string& sid : sampleIds)
        if (b.removeSample(sid, srcBankId, RemoveScope::ThisBank) ==
            RemoveResult::Removed)
            ++removed;
    if (removed == 0) return false;  // every id already absent — no undo point
    persistBankOp(session, "ReaSampler: remove sample(s)", /*bumpGeneration=*/true);
    return true;
}

}  // namespace reasampler
