// reasampler_processor.h — VST3 SingleComponentEffect wiring the pure sampler core into
// a playable instrument: event-input + stereo output bus, MIDI -> VoiceEngine, render.
// Self-contained playback: component state owns per-sample WAV path + decode intrinsics
// (SampleRefs); the bank blob is an opportunistic browser source, never a playback
// dependency. Audio thread (process()) does no allocation/file-IO/bridge calls/locks;
// loading happens off-thread (reloadInstrument) and hands off via one atomic pointer swap.

#pragma once

#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <vector>

#include "public.sdk/source/vst/vstsinglecomponenteffect.h"

#include "shell/instrument/reaper_bridge.h"
#include "core/instrument/map/sample_map.h"     // InstrumentParams (the one parameter set)
#include "core/instrument/map/component_state_io.h"  // ComponentState codec
#include "core/instrument/engine/voice_engine.h"

namespace reasampler::vst {

using instrument::map::ComponentState;
using instrument::map::InstrumentParams;
using instrument::map::SampleRefs;
using instrument::map::kPreviewVelocityDefault;

class ReaSamplerEmbed;  // embedded TCP/MCP UI shell (owned below; see queryInterface)

// The decoded capture + the voice engine playing it. The engine holds a reference to the
// sample, so both must live/die together at a stable address — heap-allocated,
// non-copyable, non-movable. process() only ever reads this through an atomic pointer.
struct LoadedInstrument {
    SampleData sample;
    VoiceEngine engine;
    std::uint64_t installedAt = 0;  // reloadGeneration_ at which this was installed into live_

    // Takeover declick is on by default here (product default; the pure core defaults it
    // off): any voice restart (mono retrigger, legato, poly steal, preview) ramps instead
    // of clicking.
    LoadedInstrument(SampleData sd, std::size_t maxVoices,
                     std::uint64_t gen, std::size_t preserveVoiceCap = 0,
                     std::int64_t preserveWindowFrames = 0,
                     VoiceMode voiceMode = VoiceMode::Poly,
                     MonoTrigger monoTrigger = MonoTrigger::Retrigger)
        : sample(std::move(sd)),
          engine(maxVoices, sample, preserveVoiceCap, preserveWindowFrames,
                 voiceMode, monoTrigger, /*takeoverDeclick=*/true),
          installedAt(gen) {}

    // True when nothing in this snapshot is sounding; lets the off-thread retirer park an
    // idle drain early. Bounded scan (<= maxVoices).
    bool fullyIdle() const { return engine.activeVoiceCount() == 0; }

    LoadedInstrument(const LoadedInstrument&) = delete;
    LoadedInstrument& operator=(const LoadedInstrument&) = delete;
};

class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect {
public:
    ReaSamplerProcessor() = default;
    // Out-of-line so the owned ReaSamplerEmbed (unique_ptr, forward-declared here) is
    // complete at the destruction point (defined in the .cpp).
    ~ReaSamplerProcessor() override;

    // The factory create function (registered in vst_entry.cpp).
    static Steinberg::FUnknown* createInstance(void* /*context*/);

    //--- from IComponent / IPluginBase -------------------------------------
    // Connects the REAPER bridge (context is REAPER's IHostApplication) and declares
    // the instrument bus topology.
    Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown* context) override;
    Steinberg::tresult PLUGIN_API terminate() override;
    Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) override;

    // Instance state = the selected bank sample id (a performance choice the instrument
    // owns; never written back to the bank). Component-state, so a saved project restores
    // which sample each instance plays.
    Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream* state) override;
    Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream* state) override;

    //--- from IAudioProcessor ----------------------------------------------
    Steinberg::tresult PLUGIN_API setupProcessing(
        Steinberg::Vst::ProcessSetup& setup) override;
    // Marshals MIDI -> VoiceEngine -> audio output. Real-time safe (no alloc/IO/lock).
    Steinberg::tresult PLUGIN_API process(
        Steinberg::Vst::ProcessData& data) override;

    // Fixed stereo output bus — channel mode is a decode policy, never a bus fact; mono
    // renders dual-mono through it. Do not reintroduce per-instance bus renegotiation.
    // Accepts only a single stereo output proposal; otherwise rejects and keeps stereo.
    Steinberg::tresult PLUGIN_API setBusArrangements(
        Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns,
        Steinberg::Vst::SpeakerArrangement* outputs, Steinberg::int32 numOuts) override;

    //--- from IEditController -----------------------------------------------
    // Hands the host our LICE IPlugView editor.
    Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override;

    // Additionally exposes REAPER's IReaperUIEmbedInterface (queried by REAPER to drive the
    // inline TCP/MCP embed); all other iids delegate to SingleComponentEffect unchanged.
    Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
                                                 void** obj) override;

    // The embedded-strip activity level (0..1) for the embed shell, UI thread. Backed by
    // embedPeak_, a lock-free relaxed atomic the audio thread writes each block.
    double embedActivityLevel() const {
        return static_cast<double>(embedPeak_.load(std::memory_order_relaxed));
    }

    // Resolves the selection against the instance-owned SampleRefs, decodes its WAV
    // off-thread, and publishes the built instrument via atomic swap — no bank read
    // required. When the bank blob is readable it's first folded into the refs table
    // (refreshRefsFromBank; the browser's copy-the-ref-in + recapture-sync mechanism). A
    // missing/unreadable WAV is the defined no-play (silence, no retry). Returns the
    // resolved selection id ("" if nothing loaded).
    std::string reloadInstrument();

    // What pollBankSync did this tick, so the editor can react only when something changed.
    struct BankSyncResult {
        bool reloaded = false;    // bank generation changed (or a legacy lift landed) -> reloaded
        bool applied = false;     // a new assignment request was applied -> selection changed
    };

    // Off-thread poll (editor's UI timer only) of the bank generation + assignment request;
    // playback never depends on it. Generation change -> reload; a resolvable NEW assignment
    // targeting this instance (isFocusedTarget) -> apply as selection + reload (unresolvable
    // ones drop silently, marker still advances); pre-v10 legacy blobs retry the bank read
    // until the refs lift in, then stop (legacyLiftShouldRun). The consumed marker persists
    // so a re-open does not re-apply. Idempotent on an idle tick.
    BankSyncResult pollBankSync(bool isFocusedTarget);

    // The bridge, for the editor's live-state readout + sample list. Owned here; the
    // editor borrows it (outlives the editor).
    ReaperBridge& bridge() { return bridge_; }

    // The live host sample rate latched from setupProcessing; the editor's envelope overlay
    // shares this time base. 0.0 before setupProcessing runs.
    double sampleRate() const { return sampleRate_; }
    // The loaded capture's id (guarded by selectionMutex_, never read on the audio thread).
    // Empty id -> silence, no first-sample fallback.
    std::string selectedSampleId();
    void setSelectedSampleId(const std::string& id);

    // The one parameter set governing that capture. UI thread, guarded by paramsMutex_;
    // never read on the audio thread — reloadInstrument bakes it into the SampleData
    // off-thread.
    InstrumentParams instrumentParams();
    void setInstrumentParams(const InstrumentParams& params);

    // Per-instance channel mode (mono | stereo), guarded by channelModeMutex_, never read
    // on the audio thread. Decode policy only (downmix vs L/R split) — the output bus is
    // fixed stereo, so a mode change never renegotiates host I/O.
    ChannelMode channelMode();
    // Editor toggle: latches the mode explicit (auto-default stops fighting it) and
    // reloads so the next block decodes the new channel count. UI thread only.
    void setChannelMode(ChannelMode mode);

    // Per-instance preview-trigger velocity (MIDI 1..127), guarded by previewMutex_, not
    // read on the audio thread.
    std::uint8_t previewVelocity();
    void setPreviewVelocity(std::uint8_t velocity);

    // Voice-system parameters (per-instance), guarded by voiceParamsMutex_, not read on the
    // audio thread — each setter rebuilds via rebuildVoiceEngine (already-decoded SampleData,
    // no bridge/WAV re-read) through the same drain-slot swap, so a change never cuts a tail.
    int voiceCount();
    void setVoiceCount(int count);        // clamped to kMinVoiceCount..kMaxVoiceCount
    VoiceMode voiceMode();
    void setVoiceMode(VoiceMode mode);
    MonoTrigger monoTrigger();
    void setMonoTrigger(MonoTrigger trigger);

    // Post-mixer master gain, linear in [0, masterGainMaxLinear()] (0 = true silence, 1 =
    // unity, cap +24 dB). Atomic — the audio thread applies it as a per-block post-sum
    // multiply, no lock, no rebuild.
    double masterGainLinear() const {
        return static_cast<double>(masterGain_.load(std::memory_order_relaxed));
    }
    void setMasterGainLinear(double linear);  // clamped to [0, masterGainMaxLinear()]

    // Fires a one-shot preview note-on/off through the live VoiceEngine — the same
    // noteOn/noteOff host MIDI uses, so a preview is a real voice (counts against voice
    // count, can steal/be stolen, respects Poly/Mono + Retrigger/Legato). Off the audio
    // thread; handed to process() via a lock-free single-slot mailbox drained at block
    // start. Never captures, never inserts a timeline item.
    void previewNoteOn(int note);
    void previewNoteOff(int note);

    // Snapshot copy of the instance-owned sample refs, for the editor's waveform/loop
    // fallback when the bank blob is unreadable. Guarded by refsMutex_.
    SampleRefs sampleRefs();

private:
    // If process() published that the drain instrument is fully idle, move it into the
    // graveyard and prune — so an edited-away snapshot stops costing memory as soon as its
    // tails die. Off the audio thread only (driven by pollBankSync); safe against a racing
    // process() because idleness is monotone and the publication names the drain's own
    // installedAt (a stale value can never retire a newer occupant).
    void retireIdleDrain();

    // Light voice-param rebuild: rebuilds the engine around a copy of the live instrument's
    // already-decoded SampleData (no bridge/disk) and publishes through the same drain-slot
    // swap as a full reload. No-op when nothing is loaded. Off the audio thread only.
    void rebuildVoiceEngine();

    // Pre-v10 legacy-lift gate: true when a lift attempt this tick could make progress
    // (see legacyLiftConcluded_). Off the audio thread only (bridge read + bank parse).
    bool legacyLiftShouldRun();

    // Publishes `built` (null = install silence) into live_: prunes the graveyard by the
    // last process()-published generation, swaps `built` into live_, displaces the previous
    // live into the drain slot, and parks the evicted drain instrument in the graveyard.
    // Requires reloadMutex_ held — shared by reloadInstrument and rebuildVoiceEngine.
    void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);

    // Publishes this instance's held captures to its per-instance ext-state key
    // ("rsusage_<instanceGuid>") so the extension's prune can never reclaim them. Called at
    // the tail of every reloadInstrument, off the audio thread. Mints instanceGuid_ on
    // first need; re-mints on a detected clone (FX copy / track duplication).
    void publishUsage(const SampleRefs& refs, const std::vector<std::string>& ids);

    ReaperBridge bridge_;

    // --- The audio-thread handoff (drain slot) ---
    // process() atomically loads live_ + draining_ at block start (two acquires, no lock).
    // reloadInstrument() (off-thread, serialized by reloadMutex_) swaps a new build into
    // live_; the displaced instrument moves to draining_, where process() keeps rendering
    // its already-sounding voices (and routes note-offs to it) so a reload never cuts a
    // ringing note — new note-ons go only to live_. The instrument evicted from draining_
    // (two reloads old) parks in graveyard_ for reclaim.
    //
    // Reclaim: process() publishes the minimum installedAt it holds via processGeneration_
    // (one relaxed store); the reload path frees graveyard entries older than that. Safe
    // because both slots are monotone in installedAt, so the published minimum is monotone
    // and an entry only reaches the graveyard after leaving both slots under reloadMutex_ —
    // an entry below the published minimum can never be loaded again.
    //
    // Graveyard upper bound: reloads since process last ran (typically 0-1). Remaining
    // entries drain at setActive(false) / terminate(), when process is guaranteed stopped.
    std::atomic<LoadedInstrument*> live_{nullptr};
    std::atomic<LoadedInstrument*> draining_{nullptr};  // displaced instrument still rendering its tails
    std::atomic<std::uint64_t> reloadGeneration_{0};   // incremented by each reload (off-thread, under reloadMutex_; read atomically by process)
    std::atomic<std::uint64_t> processGeneration_{0};  // min installedAt held by process (written on audio thread, read off-thread)
    // The installedAt of the drain instrument process() last observed fully idle (0 = none /
    // still sounds). Written relaxed on the audio thread each block; read by retireIdleDrain()
    // off-thread. Naming the generation (not a bool) closes the swap race: a publication about
    // an old drain can never retire its successor.
    std::atomic<std::uint64_t> drainIdleGeneration_{0};
    std::vector<std::unique_ptr<LoadedInstrument>> graveyard_;  // drained on reclaim + setActive(false) + terminate
    std::mutex reloadMutex_;  // serializes off-thread reloads + graveyard access

    // The loaded capture's id ("" = no pick -> silence). Off-thread only, not read on the
    // audio thread.
    std::mutex selectionMutex_;
    std::string selectedSampleId_;

    // The one parameter set. Off-thread only; reloadInstrument bakes it into the SampleData
    // under the reload lock, never read directly on the audio thread.
    std::mutex paramsMutex_;
    InstrumentParams params_;

    // Instance-owned sample refs: path + intrinsics per referenced sample. Refreshed
    // opportunistically from the bank blob when readable; never a bank dependency for
    // playback. Off-thread only.
    std::mutex refsMutex_;
    SampleRefs sampleRefs_;

    // Usage-publish identity (see publishUsage). instanceGuid_ is the persisted per-instance
    // identity; usageNonce_ is this incarnation's per-lifetime owner nonce (never persisted —
    // a persisted nonce would clone with the state on FX copy, letting a sibling clean-
    // replace over another's held paths). Minted lazily; cleared on setState.
    std::mutex usageMutex_;
    std::string instanceGuid_;
    std::string usageNonce_;

    // Per-instance channel mode, default Mono; not read on the audio thread (process
    // renders against the host's negotiated channel count). channelModeExplicit_: false =
    // reloadInstrument may auto-default the mode from the loaded capture; true = the user
    // deliberately toggled it (never fought thereafter).
    std::mutex channelModeMutex_;
    ChannelMode channelMode_ = ChannelMode::Mono;
    bool channelModeExplicit_ = false;

    // The last assignment-request generation consumed, persisted so a re-open does not
    // re-apply a stale request. Default 0 -> a genuinely new first assign (gen >= 1) applies.
    std::mutex assignMarkerMutex_;
    std::int64_t lastConsumedAssignGeneration_ = 0;

    // The bank generation this instance last saw. UI/timer-thread only (pollBankSync's sole
    // reader/writer), not persisted. -1 sentinel baselines the first poll without a
    // redundant reload; a later generation change then drives the reload.
    std::int64_t lastSeenBankGeneration_ = -1;

    // Legacy-lift terminating latch: set once legacyLiftShouldRun proves the referenced ids
    // stale against a readable bank blob, so the lift stops re-firing every tick. Reset by
    // setState (a new blob = new facts).
    std::atomic<bool> legacyLiftConcluded_{false};

    // Preview-trigger velocity (MIDI 1..127, persisted). Default kPreviewVelocityDefault
    // (64). Not read on the audio thread.
    std::mutex previewMutex_;
    std::uint8_t previewVelocity_ = kPreviewVelocityDefault;

    // Voice-system parameters (per-instance, persisted). Defaults {16, Poly, Retrigger}.
    // Not read on the audio thread — reloadInstrument bakes them into the engine off-thread.
    std::mutex voiceParamsMutex_;
    int voiceCount_ = kDefaultVoiceCount;
    VoiceMode voiceMode_ = VoiceMode::Poly;
    MonoTrigger monoTrigger_ = MonoTrigger::Retrigger;

    // Post-mixer master gain (linear, persisted). Lock-free atomic target; the audio thread
    // ramps gainCurrent_ toward it per-sample (~20 ms wall-clock at every host rate) so
    // knob moves produce no zipper noise.
    std::atomic<float> masterGain_{1.0f};
    // Audio-thread running gain value, stepping at most gainRampStep_ per sample toward the
    // target. Written/read exclusively on the audio thread — no atomics needed.
    float gainCurrent_ = 1.0f;
    // Per-sample ramp step derived from kGainRampSeconds against the live host rate in
    // setupProcessing — never a baked-in rate. Default is the 48 kHz value.
    float gainRampStep_ = 1.0f / 960.0f;

    // --- Preview-trigger mailbox (off-thread -> audio thread, lock-free) -----------------
    // One slot per direction, packed as (seq << 16) | (velocity << 8) | note [on] or
    // (seq << 16) | note [off]. process() detects a new request by comparing the packed
    // sequence against the last one consumed — a single relaxed atomic load per block,
    // RT-safe (no alloc, no lock).
    std::atomic<std::uint32_t> previewOnRequest_{0};   // 0 = no request posted yet
    std::atomic<std::uint32_t> previewOffRequest_{0};
    std::uint16_t previewOnSeq_ = 0;   // UI-thread post counter (never 0 after first post)
    std::uint16_t previewOffSeq_ = 0;
    std::uint16_t previewOnConsumed_ = 0;   // audio-thread: last on-seq fired
    std::uint16_t previewOffConsumed_ = 0;  // audio-thread: last off-seq fired

    // Latched from setupProcessing; 0.0 is explicitly invalid (reloadInstrument guards on it).
    double sampleRate_ = 0.0;
    Steinberg::int32 maxBlockSize_ = 4096;

    // The embed shell, created lazily on the first queryInterface and owned here for the
    // processor's lifetime; REAPER's borrowed AddRef'd references are outlived by this
    // unique_ptr, so its own refcount is a no-op.
    std::unique_ptr<ReaSamplerEmbed> embed_;

    // Per-block mono peak the audio thread stores relaxed; embedActivityLevel() reads it
    // for the embed strip's level indicator. Advisory only.
    std::atomic<float> embedPeak_{0.f};
};

}  // namespace reasampler::vst
