#pragma once
// sample_map — turns the live "reasampler" bank ext-state + a decoded WAV into the plain
// data the sampler core plays, and resolves the instance's one capture + one parameter set.
// The bank is read over the live-state seam, audio over the file seam; both raw inputs
// cross the bridge/file boundary in the shell, everything after (bank parse via the shared
// bank_book JSON path, sample pick, channel policy, SampleData build) is pure and
// unit-tested here. Links bank_book, wav_codec, play_params, and play_seconds (all pure) —
// deliberately NOT the voice engine: the build's product is plain SampleData.

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

#include "core/model/bank_book.h"       // BankBook::deserialize (shared bank JSON parse)
#include "core/instrument/engine/play_params.h"  // SampleData, SampleLoop, PlayParams
#include "core/instrument/map/play_seconds.h"    // PlaySeconds (the stored seconds value layer)
#include "core/instrument/note/musical_division.h"  // Division (the bake hold's value domain)
#include "core/capture/wav_codec.h"        // parseWavLayout, extractFloatFrames (shared WAV parse)

namespace reasampler::instrument::map {

using audio::AudioSample;
using instrument::engine::VelocityCurve;
using instrument::engine::VelocityPoint;

// The bank sample this instance is bound to, distilled from the live "banks" blob: the
// project-relative WAV path the file seam resolves+decodes, plus the bank intrinsics the
// core repitches/loops by. A pure value — no host, no PCM yet.
struct SelectedSample {
    std::string relativePath;   // project-relative; the shell resolves it
    int rootNote = 60;          // defaults to middle C when the bank left it empty
    SampleLoop loop;            // hasLoop=false when the bank left it empty
    int channelCount = 0;       // capture channel count; 0 = unknown (older bank entries) —
                                // the channel-mode auto-default skips it
};

// `banksJson` is the raw "banks" ext-state value the bridge read (may be empty/malformed —
// an unsaved or pre-bank project); `sampleId` is this instance's stored selection.
//
// Precedence: empty/malformed banksJson -> nullopt. Empty sampleId -> nullopt (no selection
// is SILENCE, not the bank's first sample — deliberate: the metric is time-to-first-note via
// an explicit pick, and mystery auto-play of sample #1 was the anti-pattern). sampleId found
// in any bank -> that sample. sampleId set but not found (stale) -> nullopt, same as no
// selection — the editor shows its "pick a capture" empty state rather than masking it.
std::optional<SelectedSample> selectSample(const std::string& banksJson,
                                           const std::string& sampleId);

// Auto-default rule: given the capture's channel count, current mode, and whether the user
// explicitly toggled it, return the mode to apply. Explicit choice is never overridden;
// channelCount == 0 (unknown) leaves the current mode; >= 2 -> Stereo; == 1 -> Mono.
ChannelMode channelModeFor(int channelCount, ChannelMode current, bool isExplicit);

// --- Instance-owned sample references (self-contained playback) -------------
//
// The instrument must never go silent just because the extension's ext-state hasn't parsed
// yet (or the extension is absent). So the instance persists, in its OWN component state, a
// table of everything needed to PLAY each referenced bank sample: path + decode intrinsics
// (root, loop, channel count), keyed by bank sample id. The shell decodes straight from
// these refs; the bank blob is a browser source that refreshes the table opportunistically
// when readable, never a runtime lifeline.
//
// Consequence: a sample deleted from the bank no longer silences an instance that carries
// its ref — it keeps playing while the file exists (normal sampler behavior; prune deleting
// the file yields the defined no-play).
struct SampleRefEntry {
    std::string sampleId;  // the bank sample id this ref was copied from (the seam key)
    SelectedSample ref;    // path + intrinsics, sufficient to decode + play without a bank
    // Bank display name at copy time — DISPLAY ONLY (editor label fallback when the bank
    // snapshot is unavailable); never consulted by resolution.
    std::string displayName;
};
using SampleRefs = std::vector<SampleRefEntry>;

// Find the ref for `sampleId` (nullptr on miss). Pointer into `refs` — do not outlive it.
const SelectedSample* findRef(const SampleRefs& refs, const std::string& sampleId);

// Every bank sample id this instance plays. One capture = at most one id; the list form is
// kept because the refs-table helpers below are id-set operations.
std::vector<std::string> referencedSampleIds(const std::string& selectionId);

// Upsert a ref for each id in `ids` that resolves in the live bank blob, copying the display
// name alongside the decode intrinsics. A miss leaves any existing entry untouched — the
// instance owns its copy; a bank deletion never strips a ref. Empty/malformed blob -> no-op.
void refreshRefsFromBank(SampleRefs& refs, const std::string& banksJson,
                         const std::vector<std::string>& ids);

// True when two refs would build the same SampleData: path plus every intrinsic
// resolveCapture folds. displayName is excluded on purpose — it is a label, never a decode
// input. Exists so a caller holding an ALREADY-DECODED sample can ask whether a refresh moved
// what that sample was decoded from; comparing the fields at the call site instead would go
// stale the first time this struct gains one.
bool sameDecodeSource(const SelectedSample& a, const SelectedSample& b);

// Legacy-lift terminating decision: can a refs lift make progress against this bank blob
// for the ids the instance references?
//   * Retry — blob absent/empty/unparseable: not readable yet, keep retrying.
//   * Lift  — blob parses and at least one id resolves: copy a ref in (never re-fires once
//     the refs table is non-empty).
//   * Stale — blob parses and no id resolves: provably stale, nothing to lift, ever — the
//     shell latches this and stops retrying (no per-tick churn).
enum class LegacyLiftDecision { Retry, Lift, Stale };
LegacyLiftDecision legacyLiftDecision(const std::optional<std::string>& banksJson,
                                      const std::vector<std::string>& ids);

// Keep only the entries whose id is in `ids` (getState hygiene: the persisted table cannot
// grow with browsing history).
void retainRefs(SampleRefs& refs, const std::vector<std::string>& ids);

// One entry in the capture browser's card list: stable id + display name + intrinsics +
// bank, for a card (peak thumbnail + name + root/key badge, filterable by bank). Peaks are
// NOT here — computed shell-side from the decoded PCM (reasampler_editor's thumbnail
// cache). rootNote is nullopt when the bank left it empty (badge shows no root, never a
// guessed value). Pure projection over the shared parse — the UI never parses JSON itself.
struct SampleChoice {
    std::string id;
    std::string displayName;
    std::optional<int> rootNote;
    std::optional<std::string> key;
    std::string bankId;
};
std::vector<SampleChoice> listSamples(const std::string& banksJson);

// One bank the filter tab strip offers: its stable id + display name, in ordinal order
// (pool first). The browser prepends an "All" tab (no id) shell-side. Empty for an empty /
// malformed blob. Pure projection over the shared parse.
struct BankChoice {
    std::string id;
    std::string displayName;
};
std::vector<BankChoice> listBanks(const std::string& banksJson);

// Downmix interleaved float frames ([f0c0,f0c1,...,f1c0,...]) to ONE channel by AVERAGING
// channels per frame (`channelCount` is the interleave stride, >= 1) — not "take L", not
// summing: a centered mono source stays unity, a hard-panned source is attenuated rather
// than silenced or doubled. Empty/zero-stride in -> empty out. Pure.
std::vector<AudioSample> downmixToMono(const std::vector<AudioSample>& interleaved,
                                       int channelCount);

// Deinterleave one channel (`which`, 0-based). `which` clamps to a valid channel (a request
// past the last channel reads the last channel, so a mono source asked for channel 1 yields
// channel 0 — the dual-mono building block). Empty/zero-stride in -> empty out. Pure.
std::vector<AudioSample> extractChannel(const std::vector<AudioSample>& interleaved,
                                        int channelCount, int which);

// --- Seconds -> frames --------------------------------------------------------

// Resolve a stored seconds bundle to the engine's frame-domain PlayParams against a live
// sample rate (frames = round(seconds * rate)). Source-timeline fields carry through
// unchanged. `sampleRate` must be > 0 (the caller guards this).
PlayParams resolvePlay(const PlaySeconds& stored, int sampleRate);

// --- The instrument's ONE parameter set (its OWN state) -----------------------
//
// One loaded capture, one set of playback parameters governing it across the whole
// keyboard. A performance choice, so it lives in the instrument (VST3 component state),
// never written back to the bank. Pure value type: names no sample (the ComponentState's
// selection id is the capture) and holds no PCM — the shell resolves + decodes the WAV, and
// the pure build stitches the decoded frames + this set into one SampleData.
//
// rootOverride absent -> repitch from the capture's own rootNote intrinsic (or middle C when
// the bank left it empty). loopOverride/startPoint mirror it: the sustain loop and initial
// read position are facts about the file, but the instrument may override them without
// writing back to the bank (loopOverride wins when set; startPoint sets the voice's initial
// read frame, absent -> 0). resolveCapture folds override-beats-intrinsic into the effective
// ResolvedCapture.
struct InstrumentParams {
    std::optional<int> rootOverride;  // instrument-owned override; absent -> bank intrinsic
    std::optional<SampleLoop> loopOverride;  // instrument-owned sustain loop; absent -> intrinsic
    std::optional<std::int64_t> startPoint;  // instrument-owned initial read frame; absent -> 0

    // Pre-seam crossfade at the loop reset, in SOURCE frames — a source-timeline quantity
    // like the loop points, so it needs no rate to resolve and cannot be rescaled by a
    // project/file rate mismatch. 0 is the hard seam a blob predating the field lifts to.
    // Never a bank fact: the fade is a performance choice, the loop points are the file's.
    std::int64_t loopCrossfadeFrames = 0;

    // Key-tracking scalar: how far playback pitch tracks the keyboard around the root. 1.0
    // (100%, standard 12-tone-ET) is the default — a blob predating this field lifts to
    // exactly 1.0, so already-saved instances are bit-identical. 0.0 = no tracking (every
    // key plays root pitch); 2.0 = double. Applied in keyTrackedRatio inside both repitch
    // engines.
    double keyTrack = kKeyTrackDefault;

    // Velocity->amp transfer curve: maps note-on MIDI velocity (0..127) to voice amp gain,
    // replacing the old fixed linear velocity/127. Default = flat y=1 (Daniel-approved):
    // every velocity plays at unity. DELIBERATE non-back-compat behavior change — a blob
    // predating this field lifts to flat y=1, so an already-saved instance's soft hits play
    // LOUDER than under the old linear map. Do NOT preserve the linear response. Eval'd in
    // Voice::start.
    VelocityCurve velocityCurve = VelocityCurve::flat();

    // Play parameters (play mode + AHDSR + Trigger %-length/fades; pitch engine + AD pitch
    // envelope). Instrument-owned, never a bank fact. Wall-clock times stored in SECONDS
    // (rate-free); the build resolves to frames at the live sample rate. Defaults: Gate,
    // tier-0 AHDSR seconds (0.003 attack / 0.060 release), hold 0, no fades, Preserve pitch
    // engine, pitch env off. An older blob lacking this tail lifts to exactly these.
    PlaySeconds play;

    // How long the resample bake holds the gate. Read ONLY when the bake window cannot be
    // derived — a Gate voice over an active sustain loop, which sounds indefinitely
    // (bake_plan.h's bakeWindowNeedsHold is the predicate). Default one bar; a blob predating
    // the field lifts to it, and no other bake changes.
    note::Division bakeHold = note::makeDivision(2, note::DivisionModifier::Straight);
    // The master-bus limiter's single enable. It sits OUTSIDE PlaySeconds deliberately: it is
    // a post-voice-mixer concern the shell applies to the summed output, never a voice
    // parameter, so it must not ride into the live block or the SampleData build. Default off
    // — a blob predating the field lifts to bypassed and sounds identical.
    bool limiterEnabled = false;
};

// The loaded capture resolved for decode + build: project-relative WAV path (file seam)
// plus the effective values after override-beats-intrinsic. Distinct from InstrumentParams
// (which holds optional overrides) — this is the parameter set folded against the capture.
struct ResolvedCapture {
    std::string relativePath;   // project-relative; the shell resolves + decodes it
    int rootNote = 60;          // effective: override, else bank intrinsic, else 60
    double keyTrack = kKeyTrackDefault;
    VelocityCurve velocityCurve = VelocityCurve::flat();
    SampleLoop loop;            // effective: loopOverride, else bank intrinsic
    std::int64_t loopCrossfadeFrames = 0;  // instrument-owned; no bank intrinsic to beat
    std::int64_t startFrame = 0;  // effective initial read frame: startPoint, else 0
    PlaySeconds play;           // stored SECONDS; resolved to frames at build
};

// The ONE override-beats-intrinsic fold, shared by both resolve paths below so they cannot
// drift.
ResolvedCapture resolveCapture(const SelectedSample& ref, const InstrumentParams& params);

// Resolve the selection against the live "banks" ext-state blob. Empty/malformed blob, an
// empty selection, or a stale id -> nullopt.
//
// NOT the live load path — reloadInstrument resolves via resolveFromRefs (the instance-owned
// refs). Retained as the TESTED REFERENCE the refs path is verified against (both share
// resolveCapture, so the drift test keeps the shared fold honest).
std::optional<ResolvedCapture> resolveFromBank(const std::string& banksJson,
                                              const std::string& selectionId,
                                              const InstrumentParams& params);

// The bank-free mirror, against the INSTANCE-OWNED refs table — shares the same fold, so the
// two paths cannot drift. A selection with no ref -> nullopt (the defined no-play).
std::optional<ResolvedCapture> resolveFromRefs(const SampleRefs& refs,
                                              const std::string& selectionId,
                                              const InstrumentParams& params);

// Freshly-decoded PCM under the instance's channel policy, ready for the SampleData build.
struct DecodedPcm {
    std::vector<AudioSample> monoFrames;   // channel 0 (mono, or L of a stereo decode)
    int sampleRate = 0;                    // 0 is explicitly invalid
    std::vector<AudioSample> framesR;      // channel 1 (R); EMPTY for a mono decode
};

// Apply the cross-mode channel policy to freshly-decoded interleaved PCM, yielding the 1- or
// 2-channel DecodedPcm the build consumes. `interleaved` is the WAV's float frames (stride =
// `sourceChannels`); `mode` is the instance's channel mode.
//   * MONO mode             -> downmix to one channel (average all source channels).
//   * STEREO mode, mono src -> dual-mono: channel 0 duplicated into channel 1 (centered).
//   * STEREO mode, stereo+ src -> channels 0 and 1 as-is (no surround fold on >2 channels).
// Empty/zero-channel input -> empty frames (caller plays silence).
DecodedPcm decodeChannels(const std::vector<AudioSample>& interleaved,
                          int sourceChannels, ChannelMode mode, int sampleRate);

// Stitch the resolved parameter set + the decoded PCM into the one SampleData the engine
// plays across the whole keyboard, repitched from the effective root. A second channel is
// carried only when it length-matches channel 0 (SampleData::channelCount() enforces the
// same rule, so a bad pair never half-plays). Resolves the stored wall-clock SECONDS to
// frames against the DECODE's actual rate. Empty PCM or a non-positive rate yields an
// unplayable SampleData (silence, never a crash).
SampleData buildSampleData(const ResolvedCapture& resolved, DecodedPcm decoded);

// The ComponentState envelope + params-payload binary codec lives in component_state_io.h:
// it grows on every envelope bump and is consumed by the extension's preset-blob path too,
// so both artifacts share the codec while only the VST links the voice engine.

}  // namespace reasampler::instrument::map
