#pragma once
// bank_model — the HEART of ReaSampler: the per-project sample bank. `Sample`
// metadata struct + `BankModel` (add/remove/query/tier moves/dedup-by-hash + JSON
// round-trip to/from std::string).

#include <cstdint>
#include <optional>
#include <string>
#include <vector>

namespace reasampler::model {

// How the source audio was obtained; the capture backends map their own notion
// onto these.
enum class SourceMode {
    MasterMix,      // offline render of the master output
    SelectedTracks, // offline render of selected tracks
    SelectedItems,  // offline render of selected media items
    TimeSelection,  // offline render bounded by the time selection
    RazorArea,      // offline render of a razor edit area
    Realtime,       // realtime record of wet output
};

// Retention tier. `Scratch` is auto-prunable working material; `Archive` is kept.
enum class Tier {
    Scratch,
    Archive,
};

// Sample-accurate source bounds, in both project seconds and PPQ (ticks) — both
// stored so capture doesn't re-derive one from the other and risk rounding.
struct SourceRange {
    double startSeconds = 0.0;
    double endSeconds   = 0.0;
    double startPpq     = 0.0;
    double endPpq       = 0.0;

    bool operator==(const SourceRange& o) const;
};

// Present only when a sample was resampled FROM another sample. Carries the
// parent's id and an FX-chain snapshot (a thin drift fingerprint, NOT a restorable
// chunk) — re-capture-from-source uses it to detect chain drift and replay the
// original capture request.
struct Provenance {
    std::string parentSampleId;
    std::string fxChainSnapshot;

    bool operator==(const Provenance& o) const;
};

// Loudness / level metrics measured from the captured file.
struct Levels {
    double peakDb = 0.0;
    double rmsDb  = 0.0;
    double lufs   = 0.0;

    bool operator==(const Levels& o) const;
};

// Sample-accurate sustain-loop bounds, as frame indices into the captured file — a
// bank intrinsic (like sampleRate or length) the MIDI-playback instrument uses to
// hold notes past the recorded length. One optional struct (not two loose
// optionals) so "both points or neither" is structural, not a rule to re-check at
// every boundary. Frame indices, not seconds — the instrument relates them to time
// via the file's sample rate. Invariant (enforced at deserialize): 0 <= start <=
// end; start == end is a valid zero-length loop marker.
struct LoopPoints {
    std::int64_t start = 0;
    std::int64_t end   = 0;

    bool operator==(const LoopPoints& o) const;
};

// The metadata record for one captured sample. The audio itself lives in a
// project-relative file; `relativePath` is ALWAYS relative (enforced at the
// BankModel::add boundary — see AddResult).
struct Sample {
    std::string id;                 // stable unique id (assigned by the caller)
    std::string displayName;
    std::string relativePath;       // project-relative; never absolute (invariant)

    SourceMode  sourceMode = SourceMode::MasterMix;
    SourceRange sourceRange;

    // Track GUID(s) the capture came from, when applicable (empty otherwise).
    std::vector<std::string> trackGuids;

    double wetDry = 1.0;            // 1.0 = fully wet, 0.0 = fully dry

    // Channels in the file this entry names — equal to its `fmt ` count by
    // construction on every path that measures it, which is what makes the
    // instrument's mono/stereo-toggle default agree with the audio (the waveform
    // lane count reads the decoded file directly, not this field). 0 = unknown —
    // a pre-field entry, or a capture whose file could not be parsed to measure it.
    int    channelCount = 0;
    int    sampleRate   = 0;

    double lengthSeconds = 0.0;
    double lengthBeats   = 0.0;
    double captureTempo  = 0.0;     // project tempo (BPM) at capture time

    // Time signature at capture time, stamped alongside captureTempo so the
    // bars.beats.subdivisions read-out is stable under later project meter changes.
    // 0/0 means UNSTAMPED (pre-existing sample, or a capture that could not read the
    // meter); the metadata formatter renders a blank musical read-out then, keeping s.ms.
    int    captureTimeSigNum   = 0;  // meter numerator   (e.g. 4 in 4/4); 0 = unstamped
    int    captureTimeSigDenom = 0;  // meter denominator (e.g. 4 in 4/4); 0 = unstamped

    std::optional<std::string> key;  // musical key, when known

    // Bank intrinsics for the MIDI-playback instrument, additive like `provenance`.
    // Both default cleanly empty: pre-existing samples deserialize without them and
    // re-serialize without inventing values.
    //  - rootNote: MIDI note (0..127) the sample was recorded at, so the instrument
    //    can repitch it across the keyboard. DISTINCT from the musical `key` above:
    //    `key` is a human label ("F#m"); `rootNote` is the exact pitch for repitch.
    //    Populated only where derivable — never guessed when the source isn't a
    //    single played note.
    //  - loop: sustain-loop bounds, populated only where explicitly set.
    std::optional<int>        rootNote;
    std::optional<LoopPoints> loop;

    Levels levels;
    bool   clipped = false;

    Tier tier = Tier::Scratch;

    std::string contentHash;        // dedup key (see BankModel)

    std::optional<Provenance> provenance;  // set only when resampled

    std::int64_t createdTimestamp = 0;     // unix epoch seconds

    bool operator==(const Sample& o) const;
    bool operator!=(const Sample& o) const { return !(*this == o); }

    // A scratch-tier sample is auto-prunable; archive is kept.
    bool isAutoPrunable() const { return tier == Tier::Scratch; }
};

// Outcome of BankModel::add. `add` rejects rather than silently mutating:
//  - RejectedAbsolutePath: relativePath was absolute (precision invariant).
//  - RejectedEmptyId:      id was empty (the collection is keyed by id).
//  - Collapsed:            content hash matched an existing entry; the existing
//                          entry is kept and the add is a no-op (dedup).
//  - Added:                inserted as a new entry.
enum class AddResult {
    Added,
    Collapsed,
    RejectedAbsolutePath,
    RejectedEmptyId,
};

// An ordered, id-keyed collection of Samples with content-hash dedup, tier
// moves/filtering, and lossless JSON round-trip. Insertion order is preserved
// so a panel can iterate in stable order.
class BankModel {
public:
    // Adds a sample. Enforces the relative-paths-only invariant and dedups by
    // content hash (an equal-hash add collapses onto the existing entry rather
    // than duplicating). See AddResult for the full outcome set.
    AddResult add(const Sample& sample);

    // Removes the sample with `id`. Returns true if one was removed.
    bool remove(const std::string& id);

    // Replaces the sample carrying `id` IN PLACE (preserving its position in
    // insertion order) with `updated`. Used by re-capture-from-source: a
    // provenanced sample's file is regenerated and its metadata refreshed while
    // its identity (id) and slot are kept, so the panel shows the same tile
    // updated rather than a reordered new entry. `updated.id` should equal `id`;
    // a differing id is written through as given. Does NOT dedup — an in-place
    // refresh is not a new insert, so collapse-by-hash (which guards inserts)
    // does not apply. Returns false (no mutation) if `id` is absent or
    // `updated.relativePath` is absolute (relative-paths-only still holds here).
    bool updateInPlace(const std::string& id, const Sample& updated);

    // Returns the sample with `id`, or nullptr if absent. The pointer is
    // invalidated by any mutating call.
    const Sample* query(const std::string& id) const;

    // Returns the sample whose contentHash matches, or nullptr. Empty hashes are
    // never matched (they do not participate in dedup).
    const Sample* findByHash(const std::string& contentHash) const;

    // Moves the sample with `id` to `tier`. Returns true if the sample existed.
    bool moveTier(const std::string& id, Tier tier);

    // Returns copies of all samples in the given tier, in insertion order.
    std::vector<Sample> byTier(Tier tier) const;

    // All samples in insertion order.
    const std::vector<Sample>& all() const { return samples_; }

    std::size_t size() const { return samples_.size(); }
    bool empty() const { return samples_.empty(); }

    bool operator==(const BankModel& o) const { return samples_ == o.samples_; }

    // Serializes the whole index to a JSON string (lossless round-trip).
    std::string serialize() const;

    // Parses a JSON string produced by serialize(). Returns std::nullopt on
    // malformed / truncated input (error signaled, never UB). On success the
    // returned index satisfies deserialize(serialize(x)) == x.
    static std::optional<BankModel> deserialize(const std::string& json);

private:
    std::vector<Sample> samples_;  // insertion order preserved
};

} // namespace reasampler::model
