#pragma once
// capture_paths — the REAPER-free path arithmetic behind offline capture.
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
// vendor/ includes. Standard library only. The capture shell resolves the
// current project directory via REAPER APIs, then hands the raw strings here so
// the fiddly, easy-to-get-wrong path arithmetic (bank subfolder, unique file
// name, absolute render dir, project-relative index path) is unit-tested outside
// the DAW.
//
// Path convention: this module works in forward-slash form and does NOT touch
// the filesystem. The bank subfolder name is a fixed constant so the same
// project always resolves the same bank location (determinism).

#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

namespace reasampler {

// The project-relative bank subfolder. All captured wavs live here so the bank
// travels with the .rpp (CONTEXT.md §Settled decisions: per-project bank).
inline constexpr const char* kBankSubfolder = "reasampler_bank";

// A resolved pair of paths for one capture: where REAPER must be told to write
// (absolute, because RENDER_FILE wants a directory REAPER can create/open) and
// what we store in the BankIndex (project-relative, because the index is
// relative-paths-only — CLAUDE.md precision invariant).
struct BankPaths {
    std::string absoluteDir;    // <projectDir>/reasampler_bank  (forward slash)
    std::string relativePath;   // reasampler_bank/<fileName>    (index value)
    std::string fileName;       // <stem>.wav                    (full file name)
    std::string fileStem;       // <stem>                        (RENDER_PATTERN — REAPER appends the extension)
};

// Computes a deterministic FNV-1a 64-bit content hash over `len` bytes at `data`
// and returns it as a 16-character lowercase hex string. Designed to fill
// Sample::contentHash so the confirm-on-last-reference guardrail
// (BankBook::hashReferencedElsewhere) can distinguish "no other bank holds this
// file" from "another bank holds the same file." An empty buffer returns the bare
// FNV-1a 64-bit offset basis in hex (a stable, non-empty sentinel that two empty
// files would share, but real WAV files are never empty).
std::string hashBytes(const std::uint8_t* data, std::size_t len);

// WAV-aware content hash: hashes only the audio-defining content of a 32-bit-float
// RIFF/WAVE file — the `fmt ` chunk body + the `data` chunk payload — skipping all
// other RIFF chunks (e.g. `bext` origination timestamp, `iXML`, `LIST`/`INFO`, SMED).
//
// WHY: REAPER's offline renderer embeds render-varying metadata chunks (at minimum a
// `bext` chunk containing the origination date/time) even when the format config blob
// requests no BWF metadata. Two renders of identical audio therefore differ in those
// bytes, making whole-file hashes diverge and preventing dedup collapse.
//
// DOMAIN SEPARATION: the FNV-1a input is prefixed with the tag byte 'W' (0x57) before
// the fmt/data bytes are fed in, so a content hash can never equal a whole-file
// hashBytes result for a different file of the same size.
//
// FALLBACK: if `bytes` does not parse as a valid RIFF/WAVE with both a `fmt ` and a
// `data` chunk, the function falls back to whole-file hashBytes (no prefix tag) —
// identical to calling hashBytes(bytes.data(), bytes.size()). This ensures that an
// unrecognized or malformed file still gets a non-empty hash rather than silently
// skipping dedup.
//
// Called by both capture commit paths (offline and realtime) in place of the raw
// hashBytes call.
std::string hashWavContent(const std::vector<std::uint8_t>& bytes);

// Normalizes a path to forward slashes and strips any trailing slash. Empty in
// -> empty out. Pure string transform (does not consult the filesystem).
// Platform case rule: on Windows (_WIN32) the result is also lowercased so that
// paths differing only in drive-letter or component casing compare equal (Windows
// paths are case-insensitive). On macOS/Linux the case is preserved exactly (those
// filesystems are case-sensitive).
std::string normalizeSlashes(const std::string& path);

// Sanitizes a caller-supplied base name into a filesystem-safe stem: keeps
// [A-Za-z0-9._-], replaces every other byte (spaces, slashes, quotes, control)
// with '_', and collapses to "capture" if nothing usable remains. Deterministic:
// the same input always yields the same stem (feeds bit-identical file naming).
std::string sanitizeStem(const std::string& baseName);

// Derives the bank paths for one capture.
//   projectDir : absolute directory of the current .rpp (any slash style)
//   baseName   : human base for the file stem (sanitized)
//   uniqueTag  : caller-supplied disambiguator appended to the stem (e.g. a
//                timestamp or counter) so repeated captures do not collide.
//                Also sanitized. May be empty.
// Produces "<stem>[_<tag>].wav". The relativePath is always project-relative and
// forward-slashed so it satisfies BankIndex::add's relative-only invariant.
BankPaths deriveBankPaths(const std::string& projectDir,
                          const std::string& baseName,
                          const std::string& uniqueTag);

// The project-relative index spelling for a bank file KNOWN ONLY by its file name —
// the forward derivation the Phase R prune shell uses to spell an ENUMERATED folder
// entry the SAME way deriveBankPaths spelled it at capture time. By construction it
// is the identical expression deriveBankPaths().relativePath uses (kBankSubfolder +
// "/" + fileName), so a file the capture path created and a directory listing of that
// same file resolve to the byte-identical relative string — the safety-critical
// spelling-consistency the prune core's exact-string match depends on (a divergence
// here could make a referenced file look like an orphan). fileName is a bare entry
// name (no directory component); the caller supplies forward-slash-free names from the
// folder enumeration. Empty in -> empty out.
std::string bankRelativeForName(const std::string& fileName);

// --- Persist-side path arithmetic (M4) --------------------------------------
//
// The index stores relative paths only; on project load the persist shell must
// turn each entry's relativePath back into an absolute path against the CURRENT
// project directory (so a project opened from a new location still resolves its
// bank). This is the inverse of the relativePath the capture path produced.
//
//   projectDir   : absolute directory of the current .rpp (any slash style)
//   relativePath : a project-relative index entry (e.g. "reasampler_bank/x.wav")
//
// Returns "<projectDir>/<relativePath>" forward-slashed. Returns empty when
// either input is empty (no default-location fallback — CLAUDE.md invariant) so
// a caller that ignores an unsaved/unset project fails loudly rather than
// resolving against CWD.
std::string resolveBankFile(const std::string& projectDir,
                            const std::string& relativePath);

// The project directory that holds a .rpp: its parent directory, forward-slashed,
// trailing slash stripped. Empty in -> empty out (an unsaved project has an empty
// .rpp path, which must stay empty so resolveBankFile refuses to resolve — the
// no-default-location invariant). This is the M4 convention persist uses to place
// the bank alongside the .rpp; extracted here (pure) so the VST3 instrument resolves
// audio paths the SAME way persist does rather than re-implementing the derivation.
std::string projectDirOfRpp(const std::string& rppPath);

// A relocation plan for the physical bank folder on Save-As to a new project
// location. The index's relative paths do NOT change (they are relative to the
// project dir, which is what moved with the .rpp), so relocation is purely a
// folder move: copy/move the whole bank subfolder from the old project dir to
// the new one. Both dirs are absolute, forward-slashed, trailing-slash-stripped.
struct BankRelocation {
    std::string oldBankDir;  // <oldProjectDir>/reasampler_bank
    std::string newBankDir;  // <newProjectDir>/reasampler_bank
    bool        needed = false;  // false when old==new (Save in place, not Save-As)
};

// Derives the relocation plan from the old and new project directories.
//   oldProjectDir : project dir the bank currently sits under (any slash style)
//   newProjectDir : project dir the .rpp was just saved to (any slash style)
// `needed` is true iff the normalized dirs differ (a genuine Save-As-to-new-dir).
// Returns a plan with empty dirs and needed=false when either input is empty.
BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
                                    const std::string& newProjectDir);

// --- Project-identity transition (W12 combined identity fix) -----------------
//
// What the persist timer must do on each tick. Identity rests on TWO facts,
// layered GUID-PRIMARY:
//   1. the minted GUID — content-based identity of record, stored in ext state.
//      It is IMMUNE to REAPER recycling a closed project's ReaProject* address,
//      so it is checked FIRST.
//   2. sameProjectObject — did the same live ReaProject* stay active across the
//      two ticks (computed in poll() as `proj == lastProject_`)? Used ONLY to
//      disambiguate the same-GUID case: a forked sibling (Save-As copied our GUID
//      onto a distinct object) vs a genuine Save-As (one object, new path).
//
// This fix layers both prior designs, GUID-primary. M4 (GUID-only) broke Save-As
// forks: Save-As copies the whole .rpp incl. our stored GUID, so a fork and its
// parent share a GUID on disk. W10 (pointer-primary, GUID voided) broke pointer
// RECYCLING: REAPER reuses a closed project's address, so a reopened/new project
// can present the previous project's pointer with a different stored GUID —
// pointer-primary read that as NoOp/SaveAsRelocate and the bank never reloaded.
// Checking the GUID first catches recycling; the pointer then separates a fork
// (same GUID, different object -> Load) from a Save-As (same GUID, same object,
// new path -> relocate).
//
// The load-bearing rule: a DIFFERENT record identity (GUID) is always a Load; a
// DIFFERENT project object with the same GUID is a fork Load, never a relocate.
enum class ProjectTransition {
    NoOp,             // same object, same GUID, same location — nothing to do
    Load,             // a different project is active — load ITS index from ext state
    SaveAsRelocate,   // SAME object + SAME GUID, new .rpp location — relocate the bank
};

// Classifies what a poll tick observed.
//   sameProjectObject : true iff the SAME ReaProject* stayed active across the two
//                       ticks (poll() computes `proj == lastProject_`). The pure
//                       classifier takes the bool, not the raw pointer, to stay
//                       REAPER-free and testable.
//   lastGuid    : the GUID of the project persist last acted on ("" if none/unsaved)
//   lastPath    : that project's .rpp path when last seen  ("" if unsaved)
//   currentGuid : the GUID stored in the now-active project's ext state ("" if
//                 unsaved or never written)
//   currentPath : the now-active project's .rpp path        ("" if unsaved)
//
// Rules (evaluated in EXACTLY this order):
//   1. currentGuid != lastGuid            -> Load   (different record identity:
//                                            recycled pointer w/ different GUID,
//                                            new/unsaved<->saved, or two distinct
//                                            saved projects)
//   2. !sameProjectObject                 -> Load   (same GUID, different object:
//                                            forked sibling, or two unsaved projects)
//   3. currentPath != lastPath            -> SaveAsRelocate (same object + same GUID,
//                                            new path: genuine Save-As, or first save
//                                            of an unsaved project — relocate no-ops
//                                            on the empty old dir, poll() mints a GUID)
//   4. otherwise                          -> NoOp   (same object, same GUID, same path)
//
// The GUID (identity of record) leads; the pointer only disambiguates the same-GUID
// case (fork-Load in step 2 vs Save-As in step 3). The empty-GUID safety (unsaved
// projects never physically relocate) is preserved because an empty old project dir
// makes deriveRelocationPlan's `needed` false.
ProjectTransition classifyProjectTransition(bool sameProjectObject,
                                            const std::string& lastGuid,
                                            const std::string& lastPath,
                                            const std::string& currentGuid,
                                            const std::string& currentPath);

} // namespace reasampler
