// reasampler_processor.cpp — see reasampler_processor.h. This TU is the VST3 lifecycle +
// the real-time process() path only: factory/queryInterface, initialize/terminate/
// setActive, bus setup, and the block render. Component-state I/O + parameter accessors
// live in processor_state.cpp; the off-thread reload/publish family lives in
// processor_reload.cpp. process() and its per-block work stay one TU on purpose — no
// virtual seam, no cross-TU call on the per-sample path.

#include "shell/instrument/reasampler_processor.h"

#include <cstdint>
#include <memory>
#include <mutex>

#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivstevents.h"
#include "pluginterfaces/vst/ivstmidicontrollers.h"  // kCtrlAllNotesOff / kCtrlAllSoundsOff (panic)
#include "pluginterfaces/vst/vstspeaker.h"

#include "core/instrument/engine/master_gain.h"   // the automation write of the gain's own atomic
#include "core/instrument/param/param_merge.h"    // the block-boundary merge decision
#include "shell/instrument/reasampler_editor.h"  // createView hands the host our IPlugView editor
#include "shell/instrument/reasampler_embed.h"   // embed shell + IReaperUIEmbedInterface (its iid DEF'd there)

using namespace Steinberg;
using namespace Steinberg::Vst;

namespace reasampler::vst {

namespace {

// Post-mixer gain ramp time (wall-clock): gainRampStep_ = 1/(kGainRampSeconds *
// sampleRate_), per the no-hardcoded-rate ruling — ~20 ms full ramp at every host rate.
constexpr double kGainRampSeconds = 0.020;

}  // namespace

FUnknown* ReaSamplerProcessor::createInstance(void* /*context*/) {
    // The host owns the returned reference; cast to IAudioProcessor so the FUnknown
    // refcount is correctly rooted.
    return static_cast<IAudioProcessor*>(new ReaSamplerProcessor());
}

// Out-of-line so unique_ptr<ReaSamplerEmbed> sees the complete type here.
ReaSamplerProcessor::~ReaSamplerProcessor() = default;

tresult PLUGIN_API ReaSamplerProcessor::queryInterface(const TUID iid, void** obj) {
    // REAPER queries the IEditController for IReaperUIEmbedInterface; hand it our
    // lazily-created embed shell (the processor outlives the borrowed reference).
    if (FUnknownPrivate::iidEqual(iid, IReaperUIEmbedInterface::iid)) {
        if (!embed_) embed_ = std::make_unique<ReaSamplerEmbed>(this);
        embed_->addRef();
        *obj = static_cast<IReaperUIEmbedInterface*>(embed_.get());
        return kResultOk;
    }
    return SingleComponentEffect::queryInterface(iid, obj);
}

tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) {
    tresult result = SingleComponentEffect::initialize(context);
    if (result != kResultOk) return result;

    // Connect the REAPER bridge. Non-fatal if it fails (non-REAPER host): the
    // instrument still loads, it just has no live bank to play.
    bridge_.connect(context);

    // One event input (MIDI, 16 channels), one audio output, no audio input. The output
    // bus is fixed stereo regardless of channel mode (mono renders dual-mono, centered).
    // Do not reintroduce per-mode bus renegotiation: flipping kMono<->kStereo via
    // restartComponent previously panned a dual-mono capture hard right in the host's pin
    // re-routing (see testDualMonoStereoSampleRendersCentered).
    addEventInput(STR16("MIDI In"), 16);
    addAudioOutput(STR16("Audio Out"), SpeakerArr::kStereo);

    buildParameterList();

    return kResultOk;
}

tresult PLUGIN_API ReaSamplerProcessor::terminate() {
    // process() is guaranteed stopped at terminate: free the live + draining instruments
    // and drain the graveyard.
    std::lock_guard<std::mutex> lock(reloadMutex_);
    delete live_.exchange(nullptr);
    delete draining_.exchange(nullptr);
    graveyard_.clear();
    dormantSample_.reset();
    return SingleComponentEffect::terminate();
}

tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool state) {
    // Activation governs ONE thing — whether the audio thread may run. The decoded
    // SampleData has its own lifetime and survives the cycle (dormantSample_); only the
    // voice state is built and destroyed here. Main/UI-thread call.
    //
    // Neither branch is idempotent on its own — a repeated deactivate would park an empty
    // optional over a still-valid sample, and a repeated activate would reset the limiter over
    // a live delay line — and the SDK base is an empty stub that guards neither.
    // `[verify — DAW]` whether any host actually repeats the call.
    if (static_cast<bool>(state) == active_) return kResultOk;
    active_ = static_cast<bool>(state);
    if (state) {
        // Rebuild the voices around the sample the deactivate parked — no WAV decode — so a
        // host-driven cycle (a kLatencyChanged restart, an offline-render bracket) costs no
        // disk read. With nothing to activate from, the full reload runs: it resolves +
        // decodes from the instance-owned refs (no bank read required, so it plays regardless
        // of PROJEXTSTATE parse state) and doubles as the non-editor legacy-lift trigger for a
        // pre-v10 blob, whose opportunistic refreshRefsFromBank copies refs in when the bank
        // blob is readable by now. Nothing can shadow that lift: a pre-v10 blob resolves
        // nothing, so it has neither a parked sample nor a published instrument.
        // Residual load-order race (DAW-verifiable only): if the host activates before the
        // project's ext-state parses, nothing retries until the next activation or editor
        // tick — open a pre-v10 instrument once after upgrading if it restores silent.
        if (!resumeDormantInstrument()) reloadInstrument();
        // The host performs this deactivate/reactivate whenever it acts on a kLatencyChanged
        // request, so the limiter starts each activation with an empty delay line and snapped
        // to its persisted state — no transition mute, because the deactivate destroyed every
        // voice and the rebuild above starts with none sounding.
        limiter_.reset();
    } else {
        std::lock_guard<std::mutex> lock(reloadMutex_);
        // Park the decoded PCM; free EVERYTHING else, live_ included. Its voices are frozen
        // mid-flight, and if it survived deactivation the reactivate would displace it into
        // the drain slot, resurrecting stale sustained voices as ghosts.
        std::unique_ptr<LoadedInstrument> dying(live_.exchange(nullptr));
        // Moved out ahead of the destruction: `sample` is declared before `engine`, so the
        // engine — the only holder of a reference to it — dies first and never reads the
        // moved-from value. Empty when nothing was loaded, which is what routes the next
        // activation back through the full reload.
        dormantSample_ = dying ? std::optional<SampleData>(std::move(dying->sample))
                               : std::nullopt;
        dying.reset();
        delete draining_.exchange(nullptr);
        graveyard_.clear();
    }
    return kResultOk;
}

uint32 PLUGIN_API ReaSamplerProcessor::getLatencySamples() {
    if (!limiterEnabled_.load(std::memory_order_relaxed)) return 0;
    return static_cast<uint32>(
        instrument::engine::limiterLookaheadSamples(sampleRate_));
}

tresult PLUGIN_API ReaSamplerProcessor::setupProcessing(ProcessSetup& setup) {
    sampleRate_ = setup.sampleRate;
    maxBlockSize_ = setup.maxSamplesPerBlock;
    // Resolve the gain-ramp step against the live host rate (host guarantees
    // setupProcessing never overlaps process).
    if (sampleRate_ > 0.0) {
        gainRampStep_ = static_cast<float>(1.0 / (kGainRampSeconds * sampleRate_));
    }
    // Every limiter allocation and transcendental happens here, off the audio thread.
    limiter_.prepare(sampleRate_);
    return SingleComponentEffect::setupProcessing(setup);
}

tresult PLUGIN_API ReaSamplerProcessor::setBusArrangements(
    SpeakerArrangement* inputs, int32 numIns,
    SpeakerArrangement* outputs, int32 numOuts) {
    // Fixed stereo output bus (channel mode is a decode policy, never a bus fact); no audio
    // input, so any inputs are rejected. Accept only a single stereo output proposal;
    // otherwise reject and keep stereo (per the VST3 contract, a plug-in that can't honor a
    // proposal keeps a valid arrangement of its own) — the host adapts to us.
    if (numIns < 0 || numOuts < 0) return kInvalidArgument;
    if (numIns > 0) return kResultFalse;  // no audio input bus to arrange
    if (numOuts == 1 && outputs && outputs[0] == SpeakerArr::kStereo) return kResultTrue;
    return kResultFalse;
}

tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
    // Real-time: no allocation, no IO, no locks. Load live + draining once for the whole
    // block (two atomic acquires), then publish the minimum installedAt over the pointers
    // held so the off-thread graveyard pruner knows which generations this block holds (see
    // the header's drain-slot proof). We publish installedAt rather than re-reading
    // reloadGeneration_ to close an ordering race: a fresh read could observe a generation
    // newer than the pointers actually held, letting the pruner free an instrument still
    // in use.
    //
    // The drain instrument is the previously-live snapshot displaced by the last reload:
    // its already-sounding voices keep rendering (and receive note-offs) so an edit never
    // cuts a ringing note; it receives no note-ons. A racing reload can briefly leave the
    // same pointer in both slots (live_ loaded before the swap, draining_ after); collapse
    // that to live-only so one engine is never advanced twice per frame.
    LoadedInstrument* inst = live_.load(std::memory_order_acquire);
    LoadedInstrument* drain = draining_.load(std::memory_order_acquire);
    if (drain == inst) drain = nullptr;
    std::uint64_t heldGen = 0;
    if (inst && drain) {
        heldGen = inst->installedAt < drain->installedAt ? inst->installedAt
                                                         : drain->installedAt;
    } else if (inst) {
        heldGen = inst->installedAt;
    } else if (drain) {
        heldGen = drain->installedAt;
    }
    processGeneration_.store(heldGen, std::memory_order_release);

    // Publish whether the drain snapshot is fully idle, naming its own installedAt (0 = no
    // drain / still sounding). Idleness is monotone for a drain (no note-ons), so a
    // snapshot observed idle here stays idle. Bounded scan, relaxed store — RT-safe.
    drainIdleGeneration_.store(
        (drain && drain->fullyIdle()) ? drain->installedAt : 0,
        std::memory_order_relaxed);

    // Host automation, merged into the engine-facing block BEFORE the note marshalling below, so
    // a note-on in this block latches this block's values. See automationLive_ (header) for why
    // the merge is here rather than in the model's own publisher.
    {
        const bool dirty = drainInputParameterChanges(data.inputParameterChanges);
        if (dirty || liveParams_.generation() != seenModelGeneration_) {
            // BEFORE the block is read, and the ordering is load-bearing — automation_channel.h
            // owns why.
            automation_.refreshReleases();
            // Declared INSIDE the branch: LiveValues carries default member initializers, so a
            // block where nothing moved must not pay to construct one.
            instrument::engine::LiveValues merged;
            // The generation ACTUALLY observed, not the one sampled above: a publish landing
            // between the two would otherwise leave this thread re-merging an identical block
            // every following quiet one.
            const std::uint32_t observed = liveParams_.read(merged);
            if (observed != 0) {
                seenModelGeneration_ = observed;
                instrument::param::mergeAutomation(
                    merged, automation_.slots(), kDeckParamSlots,
                    builtSampleRate_.load(std::memory_order_relaxed));
                // The second gate, and a different window from the drain's own: this one catches
                // a model republish that changed nothing (a knob committed to the value it
                // already held, a released hold whose value the model now carries). Field-wise
                // — live_params.h owns why it must never become a memcmp.
                if (!haveMergedLive_ || merged != lastMergedLive_) {
                    lastMergedLive_ = merged;
                    haveMergedLive_ = true;
                    automationLive_.publish(merged);
                }
            }
        }
    }

    // Marshal MIDI note-on/off at block granularity (no per-event sample-offset split;
    // sample-accurate scheduling is a later tier). Note-offs also route to the drain
    // engine so a note held across a reload releases its old-snapshot voice too.
    if (data.inputEvents) {
        const int32 count = data.inputEvents->getEventCount();
        for (int32 i = 0; i < count; ++i) {
            Event e;
            if (data.inputEvents->getEvent(i, e) != kResultOk) continue;
            if (e.type == Event::kNoteOnEvent) {
                // A note-on with velocity 0 is a note-off by MIDI convention.
                const int vel = static_cast<int>(e.noteOn.velocity * 127.0f + 0.5f);
                if (vel <= 0) {
                    if (inst) inst->engine.noteOff(e.noteOn.pitch);
                    if (drain) drain->engine.noteOff(e.noteOn.pitch);
                } else if (inst) {
                    inst->engine.noteOn(e.noteOn.pitch, vel);
                }
            } else if (e.type == Event::kNoteOffEvent) {
                if (inst) inst->engine.noteOff(e.noteOff.pitch);
                if (drain) drain->engine.noteOff(e.noteOff.pitch);
            } else if (e.type == Event::kLegacyMIDICCOutEvent) {
                // Panic: REAPER delivers raw input MIDI CC as kLegacyMIDICCOut events on the
                // INPUT event list (a REAPER-ism, DAW-verified). CC 123 (All Notes Off):
                // release semantics (Gate -> release tail; Trigger plays through). CC 120
                // (All Sounds Off): immediate hard silence, including Trigger. Both apply to
                // live + drain and cover a ringing preview note.
                const auto cc = static_cast<int>(e.midiCCOut.controlNumber);
                if (cc == kCtrlAllSoundsOff) {
                    if (inst) inst->engine.allSoundsOff();
                    if (drain) drain->engine.allSoundsOff();
                } else if (cc == kCtrlAllNotesOff) {
                    if (inst) inst->engine.allNotesOff();
                    if (drain) drain->engine.allNotesOff();
                }
            }
        }
    }

    // Preview mailbox: drain off-thread preview-trigger requests (one relaxed atomic load
    // each). A request is new when its packed sequence differs from the last consumed; fire
    // once, then latch the sequence. Drives the main VoiceEngine — same noteOn/noteOff as
    // host MIDI, so a preview note is a real voice. Consume even when inst is null so a
    // note-on posted while nothing is loaded does not re-fire stale later.
    {
        const std::uint32_t on = previewOnRequest_.load(std::memory_order_acquire);
        const std::uint16_t onSeq = static_cast<std::uint16_t>(on >> 16);
        if (onSeq != 0 && onSeq != previewOnConsumed_) {
            previewOnConsumed_ = onSeq;
            if (inst) {
                const int vel = static_cast<int>((on >> 8) & 0xFF);
                const int note = static_cast<int>(on & 0xFF);
                if (vel > 0) inst->engine.noteOn(note, vel);
            }
        }
    }
    {
        const std::uint32_t off = previewOffRequest_.load(std::memory_order_acquire);
        const std::uint16_t offSeq = static_cast<std::uint16_t>(off >> 16);
        if (offSeq != 0 && offSeq != previewOffConsumed_) {
            // Consume unconditionally (mirror of the on path) so a stale off does not
            // survive to release the NEXT preview press. Routes to both engines: a preview
            // held across a reload must release the old-snapshot voice too. NOTE: preview
            // shares the host-MIDI note space, so a release can release a host-held note at
            // the same pitch (inherent to routing preview through the real note path).
            previewOffConsumed_ = offSeq;
            if (inst) inst->engine.noteOff(static_cast<int>(off & 0xFF));
            if (drain) drain->engine.noteOff(static_cast<int>(off & 0xFF));
        }
    }

    if (data.numOutputs <= 0 || !data.outputs || data.numSamples <= 0) {
        publishSilentMeterBlock();
        return kResultOk;
    }
    AudioBusBuffers& out = data.outputs[0];
    const int32 frames = data.numSamples;

    // 64-bit host processing is not supported by the mono float core; emit silence
    // rather than mis-render. REAPER runs 32-bit float by default.
    if (data.symbolicSampleSize != kSample32) {
        publishSilentMeterBlock();
        for (int32 ch = 0; ch < out.numChannels; ++ch) {
            if (double* buf = out.channelBuffers64[ch]) {
                for (int32 i = 0; i < frames; ++i) buf[i] = 0.0;
            }
        }
        out.silenceFlags = (out.numChannels >= 64)
                               ? ~0ULL
                               : ((1ULL << out.numChannels) - 1);
        return kResultOk;
    }

    // Render per the host's negotiated channel count (mode was baked into the decode
    // off-thread, so the mode itself is never read here): >=2 channels -> stereo into
    // ch0/ch1 (then mirror extras); exactly 1 -> mono. Adds into a cleared buffer.
    float* ch0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
    float* ch1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr;
    if (ch0 && ch1) {
        // A mono sample plays dual-mono via the engine's stereo path, so a mono capture in
        // stereo mode is centered, not silent. The drain engine's ringing tails add on top.
        for (int32 i = 0; i < frames; ++i) { ch0[i] = 0.f; ch1[i] = 0.f; }
        if (inst) inst->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
        if (drain) drain->engine.render(ch0, ch1, static_cast<std::size_t>(frames));
        // Post-mixer master gain: ramp gainCurrent_ toward the atomic target per-sample so
        // knob drags produce no zipper noise. Early-out when already at target.
        {
            const float gTarget = masterGain_.load(std::memory_order_relaxed);
            const float gStep = gainRampStep_;          // rate-derived per-sample step
            const float gSnap = 0.5f * gStep;
            const float diff = gTarget - gainCurrent_;
            if (diff < -gSnap || diff > gSnap) {
                // Ramp toward target: step per sample, then apply the per-sample gain.
                for (int32 i = 0; i < frames; ++i) {
                    const float d = gTarget - gainCurrent_;
                    if (d > gStep) gainCurrent_ += gStep;
                    else if (d < -gStep) gainCurrent_ -= gStep;
                    else gainCurrent_ = gTarget;
                    ch0[i] *= gainCurrent_;
                    ch1[i] *= gainCurrent_;
                }
            } else {
                gainCurrent_ = gTarget;
                if (gTarget != 1.f) {
                    for (int32 i = 0; i < frames; ++i) { ch0[i] *= gTarget; ch1[i] *= gTarget; }
                }
            }
        }
        // The chain's last stage before the bus, after the gain above.
        const float minGain = limiter_.process(ch0, ch1, frames);
        instrument::engine::foldMinGain(meterMinGain_, minGain);
        // Channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2).
        for (int32 ch = 2; ch < out.numChannels; ++ch) {
            if (float* buf = out.channelBuffers32[ch]) {
                for (int32 i = 0; i < frames; ++i) buf[i] = ch0[i];
            }
        }
        // Meter tap: the bus output, post-limiter. Raw per-channel block peaks only.
        float peakL = 0.f, peakR = 0.f;
        for (int32 i = 0; i < frames; ++i) {
            const float a0 = ch0[i] < 0.f ? -ch0[i] : ch0[i];
            const float a1 = ch1[i] < 0.f ? -ch1[i] : ch1[i];
            if (a0 > peakL) peakL = a0;
            if (a1 > peakR) peakR = a1;
        }
        instrument::engine::foldPeak(meterPeakL_, peakL);
        instrument::engine::foldPeak(meterPeakR_, peakR);
        advisoryPeak_.store(peakL > peakR ? peakL : peakR, std::memory_order_relaxed);
        if (peakL >= 1.f || peakR >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
    } else if (ch0) {
        // Mono: render into channel 0, replicate to any extra channels (defensive).
        for (int32 i = 0; i < frames; ++i) ch0[i] = 0.f;
        if (inst) inst->engine.render(ch0, static_cast<std::size_t>(frames));
        if (drain) drain->engine.render(ch0, static_cast<std::size_t>(frames));
        // Same gain-ramp contract as the stereo branch above.
        {
            const float gTarget = masterGain_.load(std::memory_order_relaxed);
            const float gStep = gainRampStep_;          // rate-derived per-sample step
            const float gSnap = 0.5f * gStep;
            const float diff = gTarget - gainCurrent_;
            if (diff < -gSnap || diff > gSnap) {
                for (int32 i = 0; i < frames; ++i) {
                    const float d = gTarget - gainCurrent_;
                    if (d > gStep) gainCurrent_ += gStep;
                    else if (d < -gStep) gainCurrent_ -= gStep;
                    else gainCurrent_ = gTarget;
                    ch0[i] *= gainCurrent_;
                }
            } else {
                gainCurrent_ = gTarget;
                if (gTarget != 1.f) {
                    for (int32 i = 0; i < frames; ++i) ch0[i] *= gTarget;
                }
            }
        }
        const float minGain = limiter_.process(ch0, nullptr, frames);
        instrument::engine::foldMinGain(meterMinGain_, minGain);
        float peak = 0.f;
        for (int32 i = 0; i < frames; ++i) {
            const float a = ch0[i] < 0.f ? -ch0[i] : ch0[i];
            if (a > peak) peak = a;
        }
        instrument::engine::foldPeak(meterPeakL_, peak);
        instrument::engine::foldPeak(meterPeakR_, peak);
        advisoryPeak_.store(peak, std::memory_order_relaxed);
        if (peak >= 1.f) meterClip_.store(true, std::memory_order_relaxed);
        for (int32 ch = 1; ch < out.numChannels; ++ch) {
            if (float* buf = out.channelBuffers32[ch]) {
                for (int32 i = 0; i < frames; ++i) buf[i] = ch0[i];
            }
        }
    } else {
        publishSilentMeterBlock();
    }

    // Report silence only when nothing is loaded (lets the host optimize when idle); with
    // a drain snapshot still ringing out, clear the flag so it is not skipped.
    out.silenceFlags = (inst || drain) ? 0
                                       : ((out.numChannels >= 64)
                                              ? ~0ULL
                                              : ((1ULL << out.numChannels) - 1));
    return kResultOk;
}

IPlugView* PLUGIN_API ReaSamplerProcessor::createView(FIDString name) {
    if (name && FIDStringsEqual(name, ViewType::kEditor)) {
        return new ReaSamplerEditor(this);
    }
    return nullptr;
}

}  // namespace reasampler::vst
