// voice_engine.cpp — note routing, allocation/stealing, the mono held stack, panic, and the
// block render loops. See voice_engine.h for the contract.
//
// The render loops below call Voice::renderFrame / renderFrameStereo, which are inline in
// voice.h precisely so this TU boundary costs nothing on the per-sample path.

#include "core/instrument/engine/voice_engine.h"

namespace reasampler {

VoiceEngine::VoiceEngine(std::size_t maxVoices, const SampleData& sample,
                         std::size_t preserveVoiceCap,
                         std::int64_t preserveWindowFrames,
                         VoiceMode voiceMode, MonoTrigger monoTrigger,
                         bool takeoverDeclick)
    // MONO always uses voices_[0] only (last-note priority, single voice); size to 1 so
    // the "only voices_[0] is ever driven" invariant is structurally enforced — no latent
    // RT-discipline risk if a future mono path touched voices_[1..]. maxVoices == 0 clamps
    // to 1 (documented degenerate: at least one voice so a note-on is always serviceable).
    : voices_(voiceMode == VoiceMode::Mono ? 1
                                           : (maxVoices == 0 ? 1 : maxVoices)),
      sample_(sample),
      preserveVoiceCap_(preserveVoiceCap),
      voiceMode_(voiceMode), monoTrigger_(monoTrigger),
      takeoverDeclick_(takeoverDeclick) {
    // Pre-size every voice's Preserve shifters HERE (construction is off the audio thread), so
    // note-on never allocates. A 0 window leaves them pass-through (no ring). This is the one
    // allocation point for the shifter rings across the engine's lifetime.
    if (preserveWindowFrames > 1) {
        for (std::size_t i = 0; i < voices_.size(); ++i) {
            voices_[i].presizePreserveShifters(preserveWindowFrames);
        }
    }
}

bool VoiceEngine::refreshLive() {
    const instrument::engine::LiveParams* block = sample_.live;
    if (block == nullptr) return false;  // bare engine: the latched note-on values stand
    instrument::engine::LiveValues observed;
    const std::uint32_t generation = block->read(observed);
    if (generation == 0 || generation == liveGeneration_) return false;
    liveGeneration_ = generation;
    live_ = observed;
    haveLive_ = true;
    return true;
}

void VoiceEngine::applyLiveToActive() {
    if (!refreshLive()) return;
    for (Voice& voice : voices_) {
        if (voice.active()) voice.applyLive(live_, /*snap=*/false);
    }
}

void VoiceEngine::startVoice(Voice& voice, int note, int velocity) {
    refreshLive();
    // THE read of the note-on-latched commit class, and the only one: a published block outranks
    // the snapshot's own copy (a live edit deliberately leaves that stale), and applyLive below
    // touches none of these three — so a move reaches the next note and no sounding one.
    const double rate = haveLive_ ? live_.playRate : sample_.play.playRate;
    const double keyTrack = haveLive_ ? live_.keyTrack : sample_.keyTrack;
    // Already spline-folded in the block; the snapshot branch folds here so the two agree.
    const double lengthFraction =
        haveLive_ ? live_.lengthFraction : effectiveLengthFraction(sample_.play);
    voice.start(note, velocity, sample_, /*declickTakeover=*/takeoverDeclick_, rate, keyTrack,
                lengthFraction);
    if (haveLive_) voice.applyLive(live_, /*snap=*/true);
    voice.setStartOrder(nextStartOrder_++);
}

std::size_t VoiceEngine::activePreserveVoices() const {
    // Count only voices that are SOUNDING A NOTE (playable span still running), not voices
    // that have finished their note but are still ringing out a declick tail. A ramp-only
    // past-end voice must not consume a cap slot — that would cause a new Preserve note-on to
    // be dropped during the narrow ~4 ms window the ramp lives.
    std::size_t n = 0;
    for (const Voice& v : voices_) {
        if (v.soundingNote() && v.pitchEngine() == PitchEngine::Preserve) ++n;
    }
    return n;
}

std::size_t VoiceEngine::allocateVoice() {
    // 1. A free (idle) voice, lowest index for determinism.
    for (std::size_t i = 0; i < voices_.size(); ++i) {
        if (!voices_[i].active()) return i;
    }
    // 2. All busy -> steal. Prefer the oldest voice already in release (a dying tail),
    //    else the oldest voice overall. "Oldest" = smallest startOrder.
    std::size_t bestReleasing = kNoVoice;
    std::uint64_t bestReleasingOrder = 0;
    std::size_t bestOverall = kNoVoice;
    std::uint64_t bestOverallOrder = 0;
    for (std::size_t i = 0; i < voices_.size(); ++i) {
        const std::uint64_t order = voices_[i].startOrder();
        if (voices_[i].releasing()) {
            if (bestReleasing == kNoVoice || order < bestReleasingOrder) {
                bestReleasing = i;
                bestReleasingOrder = order;
            }
        }
        if (bestOverall == kNoVoice || order < bestOverallOrder) {
            bestOverall = i;
            bestOverallOrder = order;
        }
    }
    return bestReleasing != kNoVoice ? bestReleasing : bestOverall;
}

void VoiceEngine::removeHeld(int note) {
    for (std::size_t i = 0; i < heldCount_; ++i) {
        if (heldStack_[i].note == static_cast<std::uint8_t>(note)) {
            // Shift the notes above it down one slot (press order preserved).
            for (std::size_t j = i + 1; j < heldCount_; ++j) heldStack_[j - 1] = heldStack_[j];
            --heldCount_;
            return;
        }
    }
}

std::size_t VoiceEngine::monoNoteOn(int note, int velocity) {
    // Reject out-of-range notes BEFORE touching the held stack: HeldNote stores the note as a
    // uint8, so an unguarded value (e.g. 256, or a negative) would alias mod 256 onto a real
    // held note and corrupt the stack. Mirrored in monoNoteOff.
    if (note < 0 || note > 127) return kNoVoice;
    // Nothing decoded: a defined no-play, and the note must not join the stack (it cannot
    // sound, so it must not later take the voice back on a fallback).
    if (!sample_.playable()) return kNoVoice;

    // The note joins (or moves to) the top of the held stack. Velocity is clamped into the
    // byte for storage only; the voice start below receives the caller's value untouched.
    removeHeld(note);
    if (heldCount_ < heldStack_.size()) {
        const int vclamped = velocity < 0 ? 0 : (velocity > 127 ? 127 : velocity);
        heldStack_[heldCount_++] = HeldNote{static_cast<std::uint8_t>(note),
                                            static_cast<std::uint8_t>(vclamped)};
    }

    Voice& v = voices_[0];
    // LEGATO takeover, keyed on the HELD-STACK DEPTH: after the push above, heldCount_ >= 2
    // means another note was already physically held — the exact "takeover within a phrase"
    // predicate. (The previous guard, `active && !releasing`, broke for TRIGGER: release() is
    // a no-op there, so releasing_ never latches and a one-shot still ringing after the last
    // key-up was silently RETUNED in place instead of re-attacked. NOTE: a one-held-note
    // same-note re-press (heldCount_ becomes 1 after the removeHeld/re-push above — so
    // heldCount_ < 2) re-attacks rather than retuning, the correct fresh-phrase behavior.)
    //
    // soundingNote() (not just active()): a voice whose note has run to its play-end but is
    // still ringing a declick tail must NOT be retuned — that would move the pitch of a dying
    // ramp rather than restarting the new note, producing a silent note on the common
    // "hammer same key while a past-end ring-out is active" path. The tail should keep fading;
    // the new note-on restarts the voice normally (falls through to start() below).
    if (v.soundingNote() && heldCount_ >= 2 && monoTrigger_ == MonoTrigger::Legato) {
        v.retune(note);
        return 0;
    }
    // RETRIGGER takeover / first note of a phrase: (re)start the voice. The declick opt-in
    // rides every mono restart; start() self-gates it on the voice being ACTIVE, so a
    // first-note fresh start never ramps — only a hard cut of a sounding tone.
    startVoice(v, note, velocity);
    return 0;
}

void VoiceEngine::monoNoteOff(int note) {
    // Same range guard as monoNoteOn: removeHeld compares against the uint8-cast note, so an
    // unguarded out-of-range off (e.g. 256 -> 0 mod 256) would evict a legitimately held note.
    if (note < 0 || note > 127) return;
    removeHeld(note);
    Voice& v = voices_[0];
    // Releasing a note that is not the sounding one (a lower held note or an already-released
    // note) changes nothing audible.
    if (!v.active() || v.releasing() || v.note() != note) return;

    if (heldCount_ == 0) {
        v.release();  // last finger up: gate off (Trigger ignores this and plays through).
        return;
    }
    // FALLBACK: the most-recent still-held note takes the voice back (last-note priority).
    const HeldNote fb = heldStack_[heldCount_ - 1];
    if (monoTrigger_ == MonoTrigger::Legato) {
        v.retune(fb.note);  // glide back, no re-attack
        return;
    }
    // Retrigger fallback: re-strike the fallen-back-to note at its own original velocity.
    // Peer restart site of monoNoteOn's takeover — same declick opt-in (the fallback also
    // hard-cuts the sounding tone).
    startVoice(v, fb.note, fb.velocity);
}

std::size_t VoiceEngine::noteOn(int note, int velocity) {
    if (voiceMode_ == VoiceMode::Mono) return monoNoteOn(note, velocity);
    if (!sample_.playable()) return kNoVoice;  // nothing decoded: defined no-play.

    // Preserve voice cap: a Preserve voice is materially heavier than Varispeed (a per-voice
    // OLA shifter). When a cap is set and it is already reached, DROP a new Preserve note-on
    // rather than glitch (a defined no-play — no shifter is allocated). Varispeed notes are
    // unaffected. A voice already sounding is never cut by this cap; only NEW Preserve onsets
    // past the cap are refused.
    if (preserveVoiceCap_ > 0 && sample_.play.pitchEngine == PitchEngine::Preserve &&
        activePreserveVoices() >= preserveVoiceCap_) {
        return kNoVoice;
    }

    // The voice's Preserve shifters were pre-sized at engine construction (off-thread), so
    // start() only reset()s + warm()s them — no allocation on this audio-thread path.
    // The takeover declick rides the STEAL restart too: start() self-gates on the voice being
    // active, so a free-voice start never ramps — only an at-cap steal, which is the same hard
    // cut of a sounding tone as the mono retrig takeover.
    const std::size_t v = allocateVoice();
    startVoice(voices_[v], note, velocity);
    return v;
}

void VoiceEngine::noteOff(int note) {
    if (voiceMode_ == VoiceMode::Mono) { monoNoteOff(note); return; }
    // Release the NEWEST active, non-releasing voice on this note (largest startOrder),
    // so a re-triggered note releases its newest instance first and older tails ring.
    std::size_t target = kNoVoice;
    std::uint64_t bestOrder = 0;
    for (std::size_t i = 0; i < voices_.size(); ++i) {
        if (voices_[i].active() && !voices_[i].releasing() &&
            voices_[i].note() == note) {
            const std::uint64_t order = voices_[i].startOrder();
            if (target == kNoVoice || order > bestOrder) {
                target = i;
                bestOrder = order;
            }
        }
    }
    if (target != kNoVoice) voices_[target].release();
}

void VoiceEngine::allNotesOff() {
    // CC 123. Clear the mono held stack so no fallback can resurrect a phantom note (the
    // stuck-note scenario: a lost note-off leaves an entry that monoNoteOff's fallback
    // restarts and sustains forever with no key held), then gate off every active voice.
    // Gate voices enter their release tail; Trigger one-shots ignore release by design and
    // play through their bounded play length. RT-safe: no allocation, bounded by the pool size.
    heldCount_ = 0;
    for (Voice& v : voices_) {
        if (v.active()) v.release();
    }
}

void VoiceEngine::allSoundsOff() {
    // CC 120. Hard-stop EVERY voice immediately (no release ramp — silences Trigger one-shots
    // that allNotesOff() cannot stop) and clear the mono held stack. RT-safe: no allocation,
    // bounded by the pool size.
    heldCount_ = 0;
    for (Voice& v : voices_) {
        v.hardStop();
    }
}

void VoiceEngine::render(AudioSample* out, std::size_t frameCount) {
    // Real-time safe: no allocation, no resize — mix straight into the caller's buffer.
    // The VST3 process callback hands us the host's output channel buffer here, so the
    // audio thread never touches the heap.
    if (out == nullptr || frameCount == 0) return;
    applyLiveToActive();  // block boundary, once — never inside the frame loop
    for (Voice& voice : voices_) {
        if (!voice.active()) continue;
        for (std::size_t f = 0; f < frameCount; ++f) {
            if (!voice.active()) break;
            out[f] += voice.renderFrame();
        }
    }
}

void VoiceEngine::render(AudioSample* left, AudioSample* right, std::size_t frameCount) {
    // Real-time safe stereo mix: no allocation, no resize. Sum each active voice's per-channel
    // contribution into the caller's two buffers. Mirrors the mono loop exactly (same voice
    // iteration, same mid-block idle short-circuit) so stereo and mono share one stealing/idle
    // discipline; only the per-frame call differs (renderFrameStereo vs renderFrame).
    if (left == nullptr || right == nullptr || frameCount == 0) return;
    applyLiveToActive();  // block boundary, once — never inside the frame loop
    for (Voice& voice : voices_) {
        if (!voice.active()) continue;
        for (std::size_t f = 0; f < frameCount; ++f) {
            if (!voice.active()) break;
            AudioSample l = 0.0f, r = 0.0f;
            voice.renderFrameStereo(l, r);
            left[f] += l;
            right[f] += r;
        }
    }
}

void VoiceEngine::render(std::vector<AudioSample>& out, std::size_t frameCount) {
    // Off-thread / test path: grow the buffer (this allocates — never call under
    // process), zero-fill the appended span, then delegate to the RT mix loop so both
    // overloads share exactly one summation path.
    const std::size_t base = out.size();
    out.resize(base + frameCount, 0.0f);
    render(out.data() + base, frameCount);
}

std::size_t VoiceEngine::activeVoiceCount() const {
    std::size_t n = 0;
    for (const Voice& v : voices_) {
        if (v.active()) ++n;
    }
    return n;
}

}  // namespace reasampler
