#pragma once
// voice_engine.h — the COLD half of the sampler engine: note routing, voice allocation and
// stealing, the mono held-note stack, the two-tier panic, and the block render loops. The
// per-voice per-sample work it drives is inline in voice.h, so render's inner loop keeps its
// present inline shape across this seam.

#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>

#include "core/audio/peaks.h"
#include "core/instrument/engine/live_params.h"
#include "core/instrument/engine/play_params.h"
#include "core/instrument/engine/voice.h"

namespace reasampler {

using audio::AudioSample;

// The polyphonic voice engine: a fixed pool of voices over ONE loaded capture, note-on
// allocation with bounded voice stealing, note-off routing, and block rendering (sum of
// voices).
//
// Voice-stealing policy (deterministic, documented): when all voices are busy and a new
// note-on arrives, steal in this priority order:
//   1. the oldest voice already in release (finishing anyway — cheapest to cut),
//   2. else the oldest voice overall (longest-held note gives way to the new one).
// "Oldest" = smallest startOrder (assigned monotonically at note-on) — the standard
// hardware-sampler policy.
class VoiceEngine {
public:
    // Builds an engine with `maxVoices` voices playing `sample` (must outlive the engine —
    // held by reference, never copies PCM). Every playback parameter rides on the sample; the
    // engine holds no parameters of its own beyond the voice-system config below.
    // `preserveVoiceCap` bounds how many Preserve-engine voices may sound at once (the
    // shifter is materially heavier than Varispeed) — a Preserve note-on beyond the cap is
    // dropped rather than glitching; 0 means no separate cap (bounded only by maxVoices).
    // `preserveWindowFrames` is the OLA window every voice's Preserve shifters are pre-sized
    // to at construction (off the audio thread), so note-on never allocates; 0 leaves them
    // pass-through. The processor derives it from the host sample rate.
    //
    // `voiceMode`: POLY is the pool-with-stealing engine above; MONO drives a single voice
    // (voices_[0]) with last-note priority over the held-note stack, per `monoTrigger`
    // (Retrigger restarts the envelopes on every takeover/fallback; Legato retunes without a
    // re-attack). The engine's config is immutable — a mode/count change rebuilds the engine
    // off-thread through the processor's drain-slot reload, so ringing tails survive the swap.
    //
    // `takeoverDeclick`: when true, every restart of a sounding voice (mono retrigger
    // takeover/fallback, poly at-cap steal) seeds the per-voice declick ramp (see
    // kDeclickDecay) so the hard cut doesn't click. start() self-gates on the voice being
    // active, so a fresh start never ramps. Default false keeps the bare core byte-identical
    // to the pre-fix engine; the processor shell opts in.
    VoiceEngine(std::size_t maxVoices, const SampleData& sample,
                std::size_t preserveVoiceCap = 0, std::int64_t preserveWindowFrames = 0,
                VoiceMode voiceMode = VoiceMode::Poly,
                MonoTrigger monoTrigger = MonoTrigger::Retrigger,
                bool takeoverDeclick = false);

    // MIDI note-on. Allocates a free voice, or steals one per the policy above. Returns the
    // index of the voice used, or kNoVoice when nothing is playable (no decoded PCM, an
    // out-of-range note, or a Preserve note-on past the cap) — a defined no-play, not an error.
    std::size_t noteOn(int note, int velocity);

    // MIDI note-off. Releases the most-recently-started active, non-releasing voice
    // playing `note` (so a re-triggered same note releases the newest first, leaving
    // the older tail to ring — matches hardware behavior). No-op if none match.
    void noteOff(int note);

    // CC 123 (All-Notes-Off): clears the mono held stack and releases every active voice
    // (Gate enters AHDSR release; Trigger ignores release and plays through). The mono
    // stack's only reset path — a phantom entry left by a lost note-off would otherwise be
    // resurrected by the fallback and sustain forever with no key held. RT-safe.
    void allNotesOff();

    // CC 120 (All-Sounds-Off): hard-stops every voice immediately, clears the mono held
    // stack, silences even Trigger one-shots that would ignore a release. Panic; CC 123 is
    // the softer "let gates release." RT-safe, callable from the audio thread.
    void allSoundsOff();

    // Sums all active voices into the caller-provided buffer `out[0..frameCount)`, adding
    // to whatever is there — never allocates (the audio-thread entry point; the VST3
    // process callback passes the host's own output buffer). Voices that finish mid-block
    // go idle. `out` must point at least `frameCount` writable samples; null/zero is a no-op.
    void render(AudioSample* out, std::size_t frameCount);

    // Stereo overload: sums per-channel into `left`/`right`, same RT discipline. A mono
    // sample plays dual-mono (same value both channels); a stereo sample plays its two
    // channels. Mono and stereo render are independent output shapes over the same voice
    // pool — the active channel mode picks which one the process callback drives per block.
    void render(AudioSample* left, AudioSample* right, std::size_t frameCount);

    // Test/off-thread convenience: appends `frameCount` summed frames to `out` (grows it —
    // do not call on the audio thread). Delegates to the real-time overload after sizing
    // the buffer. Does not clear existing contents — appends.
    void render(std::vector<AudioSample>& out, std::size_t frameCount);

    // Count of currently active voices (for tests / diagnostics).
    std::size_t activeVoiceCount() const;

    std::size_t maxVoices() const { return voices_.size(); }

    static constexpr std::size_t kNoVoice = static_cast<std::size_t>(-1);

private:
    // Picks a voice to (re)use for a new note-on: a free voice if any, else a stolen
    // one per the documented policy. Always returns a valid index (maxVoices >= 1).
    std::size_t allocateVoice();

    // --- Live-parameter observation (live_params.h) ---
    // The ONE place the seqlock is read: at block start and at each note-on, on the audio
    // thread, never per frame. A torn or never-published read leaves the last good snapshot
    // in place rather than spinning. Returns whether a NEW generation landed.
    bool refreshLive();
    // Block-boundary refresh: pushes a newly-observed generation into every sounding voice,
    // which glides toward it. No-op when nothing changed (and when no block is attached).
    void applyLiveToActive();
    // The one restart path: start the voice, hand it the live values outright (it has nothing
    // to glide from), and stamp its age. Shared by the poly steal and both mono restarts so
    // no restart site can miss the live handoff.
    void startVoice(Voice& voice, int note, int velocity);

    instrument::engine::LiveValues live_{};
    std::uint32_t liveGeneration_ = 0;  // last generation observed; 0 = none yet
    bool haveLive_ = false;

    // Count of active Preserve-engine voices (for the Preserve cap). Rescanned per note-on
    // (cheap: bounded by maxVoices) rather than maintained as a running tally.
    std::size_t activePreserveVoices() const;

    // Mono mode: last-note priority over a held-note stack. The stack holds every
    // currently-held, playable note in press order (top = most recent = the sounding note).
    // Re-pressing a held note moves it to the top. Fixed-capacity (128 distinct MIDI notes) —
    // no allocation on the audio thread. Velocity is kept per held note so a retrigger
    // fallback re-strikes at its original velocity.
    struct HeldNote { std::uint8_t note; std::uint8_t velocity; };

    // Push to the stack and take the voice over (legato retune, else a fresh start). Returns
    // 0 (the mono voice) or kNoVoice for an unplayable/out-of-range note (rejected before the
    // stack, which stores uint8). The Preserve cap is not applied in mono — a single voice
    // runs at most one shifter, inherently within any cap; applying it would wrongly drop a
    // Preserve->Preserve takeover.
    std::size_t monoNoteOn(int note, int velocity);
    // Pop from the stack; if the released note was sounding, fall back to the most-recent
    // still-held note (retrigger or legato per monoTrigger_), else release.
    void monoNoteOff(int note);
    // Drops `note` from the held stack (order of the remaining notes preserved). No-op if absent.
    void removeHeld(int note);

    std::vector<Voice> voices_;
    const SampleData& sample_;
    std::size_t preserveVoiceCap_ = 0;  // max simultaneous Preserve voices (0 = no separate cap)
    std::uint64_t nextStartOrder_ = 1;  // monotonic; 0 reserved for "never started"
    VoiceMode voiceMode_ = VoiceMode::Poly;
    MonoTrigger monoTrigger_ = MonoTrigger::Retrigger;
    bool takeoverDeclick_ = false;  // declick every restart/steal of a sounding voice
    std::array<HeldNote, 128> heldStack_{};  // mono held notes, press order; top = heldCount_-1
    std::size_t heldCount_ = 0;
};

// The editor's preview trigger is a synthetic note-on at the loaded capture's root note
// through the same VoiceEngine host MIDI drives, so preview is a real voice: it counts
// against the voice count, can steal/be stolen, and respects Poly/Mono + Retrigger/Legato.
// There is no dedicated preview voice isolated from the MIDI pool.

}  // namespace reasampler
