// reasampler_processor.h — the VST3 SingleComponentEffect (Phase S4, Tier 0). Wires the
// pure S3 sampler core into a real VSTi: it declares an event-input bus + a stereo audio
// output bus, marshals host MIDI note-on/off into the VoiceEngine, and renders the
// engine's audio into the output bus — so a chosen bank sample plays chromatically from
// its root note in REAPER's routing/record/render path.
//
// SingleComponentEffect is the SDK's combined processor+controller base — sanctioned
// for a non-distributable, REAPER-only plugin under D5/D6. It gives us
// addAudioOutput/addEventInput, IComponent setState/getState for the instance's own
// state (the selected sample), and the IEditController seat so createView() can hand the
// host our IPlugView LICE editor.
//
// REAL-TIME DISCIPLINE (S4 hard constraint). The audio thread (process) does NO
// allocation, NO file I/O, NO bridge calls, NO locks. Sample loading — bridge ext-state
// read, WAV decode, path resolve, keymap build, VoiceEngine construction — all happens
// OFF the audio thread (reloadFromBank, driven from the main/UI thread) and is handed to
// process via a single atomic pointer swap. See the LoadedInstrument handoff below.

#pragma once

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

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

#include "reaper_bridge.h"
#include "sample_map.h"     // PerformanceMap (the instrument's owned zoned keymap)
#include "sampler_core.h"

namespace reasampler::vst {

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

// One fully-built, ready-to-play instrument snapshot: the decoded keymap and the voice
// engine that plays it. The engine holds a reference into the keymap, so the two MUST
// live and die together at a STABLE address — hence this is heap-allocated and neither
// copyable nor movable. The audio thread only ever reads it through an atomic pointer;
// it is built and destroyed off the audio thread.
//
// installedAt: the reloadGeneration_ value at which this instrument was atomically
// installed into live_. Set on the reload path before the exchange. process() publishes
// this field (not a fresh re-read of reloadGeneration_) so the published generation is
// exactly the generation of the instrument actually in hand for the block.
struct LoadedInstrument {
    Keymap keymap;
    VoiceEngine engine;
    std::uint64_t installedAt = 0;  // reload generation at which this was installed

    LoadedInstrument(Keymap km, std::size_t maxVoices,
                     std::uint64_t gen, std::size_t preserveVoiceCap = 0,
                     std::int64_t preserveWindowFrames = 0)
        : keymap(std::move(km)),
          engine(maxVoices, keymap, preserveVoiceCap, preserveWindowFrames),
          installedAt(gen) {}

    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 (held by unique_ptr, forward-declared here)
    // is a complete type 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 (D-B: a performance choice the
    // instrument owns; NEVER written back to the bank). Component-state, so a saved
    // REAPER 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;

    // S7 channel-mode bus negotiation. The instrument has ONE canonical output arrangement
    // determined by its per-instance channel mode (mono -> kMono, stereo -> kStereo). We
    // accept the host's proposal only when it matches that arrangement; otherwise we reject
    // (kResultFalse) but keep the mode's arrangement, so getBusArrangement / getBusInfo always
    // report the mode's channel count and REAPER routes accordingly. A runtime mode change
    // updates the output bus + calls restartComponent(kIoChanged) to trigger re-negotiation.
    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;

    // Override queryInterface to additionally expose REAPER's IReaperUIEmbedInterface (S6):
    // REAPER queries the IEditController for it to drive the inline TCP/MCP embed surface.
    // All other iids delegate to SingleComponentEffect's implementation unchanged.
    Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
                                                 void** obj) override;

    // The embedded-strip activity level (0..1), read by the S6 embed shell on the UI thread.
    // Backed by embedPeak_, the per-block mono peak the audio thread stores relaxed — a
    // lock-free advisory readout, never touched with a lock the audio thread could contend.
    double embedActivityLevel() const {
        return static_cast<double>(embedPeak_.load(std::memory_order_relaxed));
    }

    // Called by the editor (main/UI thread) when the user picks a sample, and internally
    // on load. Reads the live bank over the bridge, resolves+decodes the selected WAV
    // OFF the audio thread, and publishes the built instrument to process() via an
    // atomic swap. Safe to call with no bridge / no bank (leaves silence). Returns the
    // resolved selection id ("" if nothing was loaded) for the editor to reflect.
    std::string reloadFromBank();

    // The result of a bank-sync poll (S9/S8): what pollBankSync did this tick, so the editor
    // can react (repaint / re-snapshot its own view) only when something actually changed.
    struct BankSyncResult {
        bool reloaded = false;    // the bank generation changed -> reloadFromBank ran
        bool applied = false;     // a new assignment request was applied -> selection changed
    };

    // Poll the S9 bank-generation counter and the S8 assignment request over the bridge, OFF
    // THE AUDIO THREAD (the editor's UI timer drives this — NEVER process()). Semantics:
    //   * S9: if the bank generation differs from what we last saw, call reloadFromBank() so a
    //     recapture/ingest refreshes playback hands-free (atomic swap, glitch-free).
    //   * S8: if a NEW (generation > last consumed) assignment request names a resolvable
    //     sample AND this instance is the target (isFocusedTarget), apply it as the selection
    //     and reload; an unresolvable request is DROPPED silently (marker advanced, no change);
    //     a non-target instance neither applies nor advances its marker.
    // The consumed marker advances in component state (marked dirty via the host handler) so a
    // re-open does not re-apply. `isFocusedTarget` is the shell's thundering-herd policy input
    // (the editor passes true only for the instance whose editor is open — see the handoff).
    // Idempotent on an idle tick (generation unchanged + no new request -> no work).
    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 current single-capture selection id (main/UI thread reads for the editor). Guarded
    // by selectionMutex_ — never touched on the audio thread. Since S10 this is the ONE picked
    // capture the default face plays chromatically when the performance map is empty; an EMPTY
    // id resolves to SILENCE (no first-sample fallback). A non-empty zoned map supersedes it.
    std::string selectedSampleId();
    void setSelectedSampleId(const std::string& id);

    // The performance map (Tier 1: the zoned keymap the instrument owns; D-B). Read/written
    // by the editor on the UI thread; snapshotted under performanceMutex_. NEVER read on the
    // audio thread — reloadFromBank bakes it into the LoadedInstrument's Keymap off-thread.
    PerformanceMap performanceMap();
    void setPerformanceMap(const PerformanceMap& map);

    // The per-instance channel mode (S7, D-E: mono | stereo). Read/written on the UI thread
    // (the editor toggle) and read off-thread by getState/reloadFromBank; guarded by
    // channelModeMutex_. NEVER read on the audio thread — process() renders against the host's
    // negotiated output channel count, and reloadFromBank bakes the mode into the decode.
    ChannelMode channelMode();
    // Sets the mode. When it CHANGES, updates the output bus arrangement (mono->kMono /
    // stereo->kStereo) and asks the host to re-negotiate I/O via restartComponent(kIoChanged),
    // then reloads the instrument so the next block decodes the new channel count. A no-op set
    // (same mode) does neither. UI thread only.
    void setChannelMode(ChannelMode mode);

private:
    // Apply `mode` to the output audio bus's SpeakerArrangement (kMono / kStereo). Called from
    // initialize (topology) and setChannelMode (runtime change). Does NOT re-negotiate — the
    // caller drives restartComponent when appropriate.
    void applyOutputArrangement(ChannelMode mode);

    ReaperBridge bridge_;

    // --- The audio-thread handoff (S4 real-time discipline) -----------------
    // process() atomically loads `live_` at block start and marshals/renders against it —
    // a single atomic acquire, no lock, no free on the audio thread.
    //
    // reloadFromBank() (off-thread, serialized by reloadMutex_) builds a new
    // LoadedInstrument and atomically swaps it into `live_`. The DISPLACED instrument is
    // NOT freed on the reload path: process() may still be mid-block reading it, and two
    // rapid reloads could otherwise free a pointer process is using. Instead it is parked
    // in `graveyard_` tagged with the reload generation at which it was displaced.
    //
    // Bounded reclaim: process() publishes inst->installedAt (the generation at which the
    // held instrument was installed) via processGeneration_ — a single atomic store, RT-
    // safe. The reload path prunes graveyard entries where displacedAt <= seen (where seen
    // is the last published processGeneration_).
    //
    // Safety argument: an entry with displacedAt == D was displaced by reload D, which
    // simultaneously installed its successor with installedAt == D. process() publishing
    // seen == D means it holds that successor (or a later one). In either case, the
    // displaced entry is not the pointer process is using, so freeing it is safe. The
    // pruning condition is therefore <= (not strict <): an entry displaced at exactly the
    // published generation is also provably unreachable.
    //
    // The graveyard's upper bound is the number of reloads since process last ran
    // (typically 0–1 in normal use). Remaining entries drain at setActive(false) /
    // terminate(), when the host guarantees process is stopped.
    std::atomic<LoadedInstrument*> live_{nullptr};
    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};  // generation last seen by process (written on audio thread, read off-thread)
    struct GraveyardEntry {
        std::uint64_t displacedAt = 0;                 // reloadGeneration_ value when this was displaced
        std::unique_ptr<LoadedInstrument> instrument;
    };
    std::vector<GraveyardEntry> graveyard_;  // drained on reclaim + setActive(false) + terminate
    std::mutex reloadMutex_;  // serializes off-thread reloads + graveyard access

    // The single-capture selection id (S10: the ONE picked capture; "" = no pick -> silence).
    // Off-thread only; a small mutex guards the string against a getState/editor race. NOT
    // read on the audio thread.
    std::mutex selectionMutex_;
    std::string selectedSampleId_;

    // The performance map (Tier 1: the instrument's owned zoned keymap). Off-thread only;
    // guarded against a getState/editor race. NOT read on the audio thread — reloadFromBank
    // bakes it into the LoadedInstrument's Keymap under the reload lock.
    std::mutex performanceMutex_;
    PerformanceMap performanceMap_;

    // The per-instance channel mode (S7). Off-thread only (UI + getState + reloadFromBank);
    // guarded against a getState/editor race. Default Mono preserves pre-S7 behavior. NOT read
    // on the audio thread — process renders against the host's negotiated output channel count.
    std::mutex channelModeMutex_;
    ChannelMode channelMode_ = ChannelMode::Mono;

    // The last assignment-request generation this instance CONSUMED (S8 reader). Persisted in
    // component state (v5) so a re-open does not re-apply a request the user already got and
    // then changed away from. Written by pollBankSync (UI/timer thread) and getState; read by
    // pollBankSync + getState; seeded by setState. Guarded against a getState/poll race. NEVER
    // read on the audio thread. 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 (S9 reader). UI/timer-thread only (pollBankSync
    // is the sole reader/writer) — no mutex needed, and it is NOT persisted. Initialized to a
    // -1 SENTINEL (no real generation can be negative — parseBankGeneration yields >= 0) so the
    // FIRST poll after an editor open BASELINES the seen value without a redundant reload (setState
    // already loaded the current bank); a subsequent generation CHANGE then drives the reload.
    // NOT read on the audio thread.
    std::int64_t lastSeenBankGeneration_ = -1;

    // Latched from setupProcessing so setActive/reload can size against it. Read
    // off-thread only. 0.0 is explicitly invalid — setupProcessing sets the real host rate
    // before any audio, and reloadFromBank guards on it before use.
    double sampleRate_ = 0.0;
    Steinberg::int32 maxBlockSize_ = 4096;

    // --- S6 embedded TCP/MCP UI ---------------------------------------------
    // The embed shell (IReaperUIEmbedInterface), created lazily on the first queryInterface
    // and owned here for the processor's lifetime. REAPER borrows AddRef'd references from
    // queryInterface; the shell's refcount is a no-op because THIS unique_ptr governs its
    // destruction (the processor always outlives the borrowed references).
    std::unique_ptr<ReaSamplerEmbed> embed_;

    // The per-block mono peak (0..1+) the audio thread stores relaxed; the embed strip's
    // level indicator reads it via embedActivityLevel(). Advisory only — a plain atomic,
    // no ordering coupling, never guarded by a lock the audio thread touches.
    std::atomic<float> embedPeak_{0.f};
};

}  // namespace reasampler::vst
