#pragma once // bank_book — pure multi-bank registry: wraps N BankModel instances (bank_model // itself is untouched — additive, no bankId on Sample). Movement between banks is // index-only; files never relocate, banks are logical groupings over one shared pool. // // The pool is bank-zero (fixed id/name, ordinal 0), privileged and enforced HERE: // always exists, un-deletable, un-renamable, un-evacuable (evacuate's destination // only). createBank takes a caller-supplied id — REAPER GUID minting stays in the // shell so this model stays pure and deterministic; the model still enforces // non-empty/unique/not-reserved. #include #include #include #include #include "core/model/bank_model.h" #include "core/model/slot_map.h" namespace reasampler { // Interim: this module re-namespaces later; the model types it wraps live in // reasampler::model. using namespace model; // The pool's fixed identity: createBank rejects this id; renameBank rejects this name. inline constexpr const char* kPoolBankId = "pool"; inline constexpr const char* kPoolBankName = "Pool"; // One bank: id/display/ordinal/BankModel/SlotMap. The pool is the bank whose // id == kPoolBankId. struct Bank { std::string id; // stable, persisted; the pool's is kPoolBankId std::string displayName; // mutable for named banks; fixed "Pool" for the pool int ordinal = 0; // display order; pool is 0, named banks 1..N BankModel index; // this bank's samples SlotMap slots; // display positions of this bank's samples (gap-preserving) bool isPool() const { return id == kPoolBankId; } bool operator==(const Bank& o) const { return id == o.id && displayName == o.displayName && ordinal == o.ordinal && index == o.index && slots == o.slots; } }; // Outcome of a cross-bank move/copy — reports what happened rather than mutating // silently on a bad request. // - Moved / Copied: transferred to the destination as a new entry. // - Collapsed: destination already held the hash, collapsed onto it (move // still removes the source; copy keeps it, as always). // - RejectedUnknownBank / RejectedSampleAbsent / RejectedSameBank: no-op guards. enum class TransferResult { Moved, Copied, Collapsed, RejectedUnknownBank, RejectedSampleAbsent, RejectedSameBank, }; // Scope of a sample-remove. ThisBank is the only behavior surfaced in the UI; // AllBanks is a tested latent seam, not wired to any affordance. // - ThisBank: drop the entry from the one named source bank only (no cross-bank // cascade — dedup is per-bank). // - AllBanks: drop the sample's entry from every bank holding it ("purge from // the library"). enum class RemoveScope { ThisBank, AllBanks, }; // Outcome of BankBook::removeSample — same honesty as TransferResult. // - Removed: at least one index entry was dropped. // - RejectedUnknownBank: the source bank id named no bank (ThisBank scope only). // - RejectedSampleAbsent: the sample id was in no bank in scope (nothing removed). enum class RemoveResult { Removed, RejectedUnknownBank, RejectedSampleAbsent, }; // An ordered registry of banks with the pool seeded as bank-zero, per-bank sample // indices, an active-bank pointer, and lossless JSON round-trip. class BankBook { public: BankBook(); // seeds the pool (id kPoolBankId, name kPoolBankName, ordinal 0); // active bank = pool; zero named banks. // -- Bank lifecycle ------------------------------------------------------ // Creates a named bank with a caller-supplied id/display name (next ordinal // assigned automatically). Rejects (false, no mutation) an empty/duplicate id, // the reserved pool id, or a duplicate display name (trimmed + case-insensitive, // ASCII — "Drums"/"drums"/" Drums " collide, including against the pool's "Pool"). bool createBank(const std::string& id, const std::string& displayName); // Renames a named bank. Rejects (false, no mutation) an unknown id, the pool, or // a name already used by another bank. Renaming to its own current name is a // no-op success. bool renameBank(const std::string& id, const std::string& displayName); // The first name in the sequence `seed`, "seed 2", "seed 3", … whose fold is free // in this book — what a caller that must not be rejected (the package import) asks // for before createBank. First-FREE-ascending, not highest-plus-one, so it fills a // gap ("Drums" + "Drums 3" present yields "Drums 2") and is a pure function of the // current name set. The seed is returned verbatim when free and is NEVER re-parsed: // a bare trailing integer cannot be told from a user's own name, so "Kit 808" would // become "Kit 2" under a stripping rule. Terminates by pigeonhole (one of the first // N+1 candidates is free for N banks), so it needs no cap. A blank seed comes back // blank — what a missing name should become is the caller's policy, not the model's. std::string uniqueDisplayName(const std::string& seed) const; // Deletes a named bank and its member entries (files untouched — a shell/prune // concern). Rejects (false, no mutation) an unknown id or the pool. Remaining // ordinals compact after; if the deleted bank was active, falls back to the pool. bool deleteBank(const std::string& id); // Reorders a named bank to newOrdinal (clamped into range, others shift to stay // contiguous). The pool is pinned at 0. Rejects an unknown id or the pool. bool reorderBank(const std::string& id, int newOrdinal); // Moves every member of a named bank into the pool (index-only, same // destination-collapse-by-hash as a move). Rejects an unknown id or the pool // (the pool is only ever a destination). Returns true even if already empty. bool evacuate(const std::string& id); // -- Active bank --------------------------------------------------------- // The active bank's id (the capture target). Defaults to the pool. const std::string& activeBankId() const { return activeBankId_; } // Sets the active bank. Rejects (false, no change) an id that names no bank. bool setActiveBank(const std::string& id); // The active bank's BankModel — always valid (falls back to the pool). BankModel& activeIndex(); const BankModel& activeIndex() const; // -- Sample movement (index-only; files never relocate) ------------------ // Moves a sample by id between banks (destination collapse-by-hash observed). // See TransferResult for the full outcome set. TransferResult moveSample(const std::string& sampleId, const std::string& fromBankId, const std::string& toBankId); // Copies a sample by id between banks, source retained (destination // collapse-by-hash observed). Cross-bank dedup is NOT enforced — the same hash // may then live in both banks. TransferResult copySample(const std::string& sampleId, const std::string& fromBankId, const std::string& toBankId); // -- Sample removal (index-only; the file is NEVER touched — orphaned until prune) -- // Drops a sample's index entry — non-destructive to the file (a last-reference // remove leaves it on disk, orphaned until prune reclaims it). See RemoveScope/ // RemoveResult for scope and outcome. No mutation on any Rejected outcome. RemoveResult removeSample(const std::string& sampleId, const std::string& fromBankId, RemoveScope scope = RemoveScope::ThisBank); // -- Sample display order (index membership untouched) ------------------- // The bank's sample ids in display (slot) order, reconciled against live index // membership first (drops stale markers, appends unmapped samples densely). An // unknown bank id yields an empty vector. std::vector orderedSampleIds(const std::string& bankId); // Ensures every bank's SlotMap is consistent with its index membership (seeds a // dense order, or reconciles a partial map). Idempotent — call after deserialize // or any out-of-band membership change. void reconcileSlots(); // Reorders sample `id` within `bankId` to targetSlot (gap-preserving; index/file/ // metadata untouched). Returns false (no mutation) on an unknown bank or id. bool reorderSample(const std::string& id, const std::string& bankId, int targetSlot); // Alt-replace: the dragged `newId` (already a member of bankId) takes the slot of // `oldId`, and `oldId` is removed from the index (same semantics as removeSample // ThisBank). Position is preserved; only the occupant changes. // // Applies the same pool guard as removeSample — per-sample removal from the pool // is allowed (the pool's guards are un-delete/rename/evacuate, never per-sample // remove). Rejects (false, no mutation of either index or slots) an unknown bank, // a newId/oldId the bank doesn't hold, or newId == oldId. bool replaceSample(const std::string& newId, const std::string& oldId, const std::string& bankId); // Refreshes a sample in place wherever it lives (re-capture): finds the bank // holding sampleId and replaces its entry with `updated` (order-preserving, no // dedup). Updates the FIRST holder in ordinal order if the id lives in multiple // banks via copy. Returns false (no mutation) if no bank holds the id or the // replacement's path is absolute. bool updateSampleInPlace(const std::string& sampleId, const Sample& updated); // Does any bank other than exceptBankId still hold an entry whose contentHash // == hash? Backs the confirm-on-last-reference guardrail: two entries sharing a // hash share one file, so this answers "would removing here orphan the file." // An empty hash never matches (mirrors findByHash) — reads as // referenced-nowhere-else, the safe confirm-eliciting default. bool hashReferencedElsewhere(const std::string& hash, const std::string& exceptBankId) const; // Every project-relative path referenced by any bank (pool included) — the union // prune subtracts against. Paths are verbatim (no normalization), first-seen // order across banks in ordinal then insertion order, de-duplicated. An empty // relativePath is skipped. std::vector referencedPaths() const; // -- Query --------------------------------------------------------------- // The bank with `id`, or nullptr. Pointer invalidated by any mutating call. Bank* bank(const std::string& id); const Bank* bank(const std::string& id) const; // The bank's BankModel by id, or nullptr. Convenience over bank()->index. BankModel* index(const std::string& id); const BankModel* index(const std::string& id) const; // The pool (always present). Never null. Bank& pool(); const Bank& pool() const; // All banks in ordinal order (pool first). The pool is always banks()[0]. const std::vector& banks() const { return banks_; } std::size_t size() const { return banks_.size(); } // >= 1 (the pool) bool operator==(const BankBook& o) const { return banks_ == o.banks_ && activeBankId_ == o.activeBankId_; } // -- Persistence --------------------------------------------------------- // Serializes the whole book to JSON (lossless): pool as bank-zero + named banks // + per-bank indices + ordinals + active id. deserialize(serialize(x)) == x. std::string serialize() const; // Parses a book JSON produced by serialize(). std::nullopt on malformed input. // // A bare legacy bank_index JSON (pre-multi-bank shape: a "samples" array, no // "banks" key) is promoted into the pool's index — one-way, lossless — yielding // a book of { pool } with zero named banks. static std::optional deserialize(const std::string& json); // Resolves a BankBook from the two persisted ext-state values a project may // carry: the authoritative `banksJson` and the retired legacy `bank_index` blob. // 1. non-empty banksJson -> deserialize it. If malformed, do NOT fall back to // legacy — a corrupt banks blob is an error, not an absence; returns an // empty book so a stale legacy key can never resurrect superseded state. // 2. else non-empty legacyJson -> deserialize it (pool migration). // 3. else -> a fresh empty book (pool only). // Never returns nullopt — an unloadable input degrades to the empty book. static BankBook loadFromPersisted(const std::string& banksJson, const std::string& legacyJson); private: std::vector banks_; // ordinal order; banks_[0] is always the pool std::string activeBankId_; // always names a live bank; defaults to pool // Folds a display name to its uniqueness key: strip leading/trailing ASCII // whitespace, lower-case ASCII letters — so "Drums"/"drums"/" Drums " share one // key. ASCII-only by design — the pure core carries no locale facility. Private // static because both halves of the split implementation (rules + JSON) need the // one folding rule; a drifted second copy would let a parsed book violate the // create/rename uniqueness invariant. static std::string nameKey(const std::string& s); // True if a bank other than exceptId already carries name's uniqueness key. // Backs the create/rename uniqueness check; pass exceptId=id to let a bank keep // (or re-case/-space) its own name. bool displayNameTaken(const std::string& name, const std::string& exceptId) const; // Re-sorts banks_ by ordinal (pool pinned first) and rewrites ordinals to a // contiguous 0..N-1. Called after any structural change (create/delete/reorder). void normalizeOrdinals(); // Replaces the book's banks with a parsed set, normalizes ordinals, and resolves // the active bank (falling back to the pool if the id names no bank). Used only // by deserialize. void adoptBanks(std::vector&& banks, const std::string& activeBank); }; // The next bank id to activate when cycling the active bank forward, in ordinal // order (pool -> named -> ... -> pool, wraps). Free function (not a member) so it's // unit-testable against a bare id vector without a full book. // * empty list -> "" (nothing to cycle to) // * single id (pool-only) -> that id (a one-bank book stays put) // * currentBankId not present -> the first id (a sane home to jump to) std::string nextBankId(const std::vector& orderedBankIds, const std::string& currentBankId); } // namespace reasampler