// processor_state.cpp — ReaSamplerProcessor's component-state I/O (setState/getState
// against the component_state_io codec) and its UI-thread parameter accessors/setters
// (selection, the one parameter set, channel mode, preview velocity, voice-system params,
// master gain, preview-note mailbox posts). Everything here runs off the audio thread;
// setters hand work to the reload family (processor_reload.cpp) or store atomics
// process() picks up at block start.

#include "shell/instrument/reasampler_processor.h"

#include <cstdint>
#include <mutex>
#include <vector>

#include "pluginterfaces/base/ibstream.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"  // RestartFlags::kLatencyChanged

#include "core/instrument/engine/master_gain.h"      // masterGainMaxLinear (post-mixer gain clamp)
#include "core/instrument/map/component_state_io.h"  // the ComponentState codec
#include "core/instrument/map/sample_map.h"          // retainRefs / referencedSampleIds

using namespace Steinberg;
using namespace Steinberg::Vst;

namespace reasampler::vst {

using namespace instrument::map;  // the codec + resolution vocabulary this TU marshals
using instrument::engine::masterGainMaxLinear;  // taper ceiling

tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
    if (!state) return kResultFalse;
    // The blob is small; read it in one shot into a growable buffer.
    std::vector<std::uint8_t> bytes;
    std::uint8_t chunk[256];
    int32 got = 0;
    while (state->read(chunk, sizeof(chunk), &got) == kResultOk && got > 0) {
        bytes.insert(bytes.end(), chunk, chunk + got);
    }
    // Component state is {loaded capture id, one parameter set}. deserializeComponentState
    // lifts older blobs cleanly — including the retired zone payloads, which adopt zone
    // one's capture into cs.selectionId. sampleRate_ is the real host rate here (REAPER
    // calls setupProcessing before setState on load), which the legacy v3 payload's
    // frames->seconds conversion needs.
    const ComponentState cs = deserializeComponentState(bytes, sampleRate_);
    // A LOAD is not an edit: the SDK is explicit that a controller must never pass a restored
    // value back to the host through IComponentHandler. The push into the controller happens
    // once at the tail instead, through syncParamsFromModel.
    paramNotifySuppressed_ = true;
    setSelectedSampleId(cs.selectionId);
    setInstrumentParams(cs.params);
    // Restore the last-consumed assignment generation so a re-open does not re-apply a
    // stale assign_request.
    {
        std::lock_guard<std::mutex> lock(assignMarkerMutex_);
        lastConsumedAssignGeneration_ = cs.lastConsumedAssignGeneration;
    }
    // The output bus is fixed stereo (see initialize) — the mode only governs decode below.
    {
        std::lock_guard<std::mutex> lock(channelModeMutex_);
        channelMode_ = cs.channelMode;
        channelModeExplicit_ = cs.channelModeExplicit;
    }
    {
        std::lock_guard<std::mutex> lock(previewMutex_);
        previewVelocity_ = cs.previewVelocity;
    }
    // Restore before the reload so the rebuilt engine is born with the saved polyphony/mode.
    {
        std::lock_guard<std::mutex> lock(voiceParamsMutex_);
        voiceCount_ = cs.voiceCount;
        voiceMode_ = cs.voiceMode;
        monoTrigger_ = cs.monoTrigger;
    }
    setMasterGainLinear(cs.masterGainLinear);
    // Restore the instance-owned sample refs before the reload so it decodes straight from
    // them — no bank read required. A pre-v10 blob lifts to an empty table; the reload
    // resolves nothing until the bank blob becomes readable (opportunistic refresh, or
    // pollBankSync's legacy lift), after which the next save is self-contained.
    {
        std::lock_guard<std::mutex> lock(refsMutex_);
        sampleRefs_ = cs.sampleRefs;
    }
    // Restore the publish identity (pre-v11 lifts to empty, minted on first publish).
    // usageNonce_ resets: a restored blob is a new lifetime, so this incarnation can never
    // be mistaken for the previous one's writes or a copy-sibling's.
    {
        std::lock_guard<std::mutex> lock(usageMutex_);
        instanceGuid_ = cs.instanceGuid;
        usageNonce_.clear();
    }
    // A new blob is new facts — the legacy lift gets one fresh run per restored state.
    legacyLiftConcluded_.store(false, std::memory_order_relaxed);
    reloadInstrument();
    paramNotifySuppressed_ = false;
    // Every exposed parameter now reads the blob's value. Its ordering against the host's first
    // parameter block does not need to be known: an automation point held by the audio thread is
    // re-applied over every merge, so a lane outranks this restore whichever way round the two
    // arrive. That is VST3's own rule — a written lane outranks anything the plug-in sets — not
    // a race we lost.
    syncParamsFromModel();
    // This caller has no editor to flush for it. At the TAIL on purpose: a host that services the
    // restart synchronously deactivates/reactivates, and our setActive(true) resumes or reloads
    // against the refs above, which are only fully restored once this function has run to here.
    flushLatencyRestart();
    flushGainNotify();
    return kResultOk;
}

tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
    if (!state) return kResultFalse;
    // Before the snapshot, not after: the blob is authoritative, so anything the host's
    // automation wrote must be in the model by the time it is serialised. This is the one drain
    // site that is not an optimisation — a save with no editor open still has to see it.
    drainAutomationToModel();
    // Persists the full instance state — never written to the "reasampler" bank ext-state.
    // No pick serializes to {"", default params}, restoring as silence (never auto-playing
    // sample #1).
    ComponentState state_out;
    state_out.selectionId = selectedSampleId();
    state_out.params = instrumentParams();
    {
        std::lock_guard<std::mutex> lock(channelModeMutex_);
        state_out.channelMode = channelMode_;
        state_out.channelModeExplicit = channelModeExplicit_;
    }
    {
        std::lock_guard<std::mutex> lock(assignMarkerMutex_);
        state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_;
    }
    state_out.previewVelocity = previewVelocity();
    {
        std::lock_guard<std::mutex> lock(voiceParamsMutex_);
        state_out.voiceCount = voiceCount_;
        state_out.voiceMode = voiceMode_;
        state_out.monoTrigger = monoTrigger_;
    }
    state_out.masterGainLinear = masterGainLinear();
    // Persist the owned sample refs — the saved blob decodes + plays with no extension
    // present. Filtered (snapshot copy only) to what the instance currently plays, so the
    // table cannot grow with browsing history.
    state_out.sampleRefs = sampleRefs();
    retainRefs(state_out.sampleRefs, referencedSampleIds(state_out.selectionId));
    // Persist the publish identity so the usage key is stable across sessions.
    {
        std::lock_guard<std::mutex> lock(usageMutex_);
        state_out.instanceGuid = instanceGuid_;
    }
    const std::vector<std::uint8_t> bytes = serializeComponentState(state_out);
    if (!bytes.empty()) {
        const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
                                        static_cast<int32>(bytes.size()), nullptr);
        if (wr != kResultOk) return wr;
    }
    return kResultOk;
}

std::string ReaSamplerProcessor::selectedSampleId() {
    std::lock_guard<std::mutex> lock(selectionMutex_);
    return selectedSampleId_;
}

void ReaSamplerProcessor::setSelectedSampleId(const std::string& id) {
    std::lock_guard<std::mutex> lock(selectionMutex_);
    selectedSampleId_ = id;
}

InstrumentParams ReaSamplerProcessor::instrumentParams() {
    std::lock_guard<std::mutex> lock(paramsMutex_);
    return params_;
}

std::uint32_t ReaSamplerProcessor::instrumentParamsGeneration() {
    std::lock_guard<std::mutex> lock(paramsMutex_);
    return paramsGeneration_;
}

InstrumentParams ReaSamplerProcessor::instrumentParams(std::uint32_t& generation) {
    std::lock_guard<std::mutex> lock(paramsMutex_);
    generation = paramsGeneration_;
    return params_;
}

std::uint32_t ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
    // The exposed values alone, not the whole set: this funnel fires per mouse move on every live
    // knob and node drag, and InstrumentParams owns seven vectors — copying all of them to diff
    // 44 doubles is the cost, and the diff is what the notification actually needs.
    double before[kDeckParamSlots];
    std::uint32_t generation;
    {
        std::lock_guard<std::mutex> lock(paramsMutex_);
        for (const instrument::param::ParamRow& row : instrument::param::exposedParams()) {
            before[static_cast<std::size_t>(row.deck)] = modelParamNormalized(params_, row.deck);
        }
        params_ = params;
        // Bumped inside the lock with the write it names, so a reader taking the pair together
        // can never see a generation that does not describe the set beside it. Read back before
        // the unlock for the same reason — a caller wanting ITS OWN write's generation must not
        // race a second writer's bump between this function's unlock and its return.
        generation = ++paramsGeneration_;
    }
    // Every writer of the parameter set — setState, the editor's commits, the bake's adopt —
    // funnels through here, so mirroring the limiter flag at this one point is what keeps the
    // audio thread's copy and the latency report from ever lagging what is persisted. The MIRROR
    // is inline, because that is the sound the user clicked for; the host notification is not,
    // because this funnel is reachable from inside a mouse handler and restartComponent is not
    // safe there (see this directory's CLAUDE.md).
    publishLimiterEnabled(params.limiterEnabled);
    // Armed AFTER the mirror, so getLatencySamples already answers the new value for the whole
    // window the arm stays outstanding. Compared against the last ANNOUNCED enable rather than
    // against the previous parameter set: off->on->off inside one tick ends at the latency the
    // host already knows, and a restart rebuilds the instance, so announcing a latency that
    // never changed is pure cost. Any number of changes before one flush still cost at most one
    // restart, and this store is the only one that raises OR lowers the arm.
    latencyRestartPending_.store(
        params.limiterEnabled != latencyAnnounced_.load(std::memory_order_relaxed),
        std::memory_order_release);
    // The host-notification obligation, at the same one funnel and for the same reason the
    // limiter mirror sits here: an internal write that skipped it would leave the host
    // displaying — and, on the next touch, re-imposing — the superseded value.
    notifyParamsFromModel(before, params);
    return generation;
}

void ReaSamplerProcessor::flushLatencyRestart() {
    // Cleared only once it can actually be delivered — an arm raised before the host connected
    // its handler waits for a later flush instead of evaporating.
    if (!componentHandler) return;
    if (!latencyRestartPending_.exchange(false, std::memory_order_acquire)) return;
    // Latched BEFORE the call: a host that services the restart synchronously re-enters this
    // object inside it, so the next commit must compare against the value the host is about to
    // read, not against the one it held before.
    const bool previouslyAnnounced = latencyAnnounced_.exchange(
        limiterEnabled_.load(std::memory_order_relaxed), std::memory_order_relaxed);
    // The SDK requires this on the UI thread and answers getLatencySamples only after the host's
    // own deactivate/reactivate — so the flag is long committed by the time the host asks. This
    // is a kLatencyChanged restart with the bus untouched, NOT the retired per-mode kIoChanged
    // bus renegotiation (see initialize()); do not conflate.
    if (componentHandler->restartComponent(kLatencyChanged) == kResultOk) return;
    // A refused restart leaves the host's delay compensation on the OLD value, so the latch has
    // to come back off it: announcing a value the host never took would let a later toggle BACK
    // to that value arm nothing, stranding the host's view permanently. Re-armed instead, which
    // costs one retry per drain in a host that always refuses. The SDK documents no refusal
    // semantics, so whether any host returns non-kResultOk here is `[verify — DAW]`.
    latencyAnnounced_.store(previouslyAnnounced, std::memory_order_relaxed);
    latencyRestartPending_.store(true, std::memory_order_release);
}

void ReaSamplerProcessor::publishLimiterEnabled(bool on) {
    limiterEnabled_.store(on, std::memory_order_relaxed);
    limiter_.setEnabled(on);
}

void ReaSamplerProcessor::setLimiterEnabled(bool on) {
    // Rebased off the PROCESSOR's copy rather than taking a caller-supplied set: an editor
    // snapshot may carry edits it has not committed, and writing one back here would clobber
    // them. Everything else is setInstrumentParams', the one funnel every writer agrees through.
    InstrumentParams params = instrumentParams();
    params.limiterEnabled = on;
    setInstrumentParams(params);
}

MasterBusMeter ReaSamplerProcessor::masterBusMeter() {
    MasterBusMeter m;
    // Consuming: each read takes the window and reinstalls its identity element, which is what
    // starts the next one. The audio thread's fold is an unconditional CAS against exactly that
    // (meter_accumulate.h owns the argument), so a fold interleaved with these exchanges lands
    // in one window or the other and is never dropped between them.
    m.peakL = instrument::engine::consumePeak(meterPeakL_);
    m.peakR = instrument::engine::consumePeak(meterPeakR_);
    m.minGain = instrument::engine::consumeMinGain(meterMinGain_);
    // NOT consumed: the clip is a latch the user clears, not a window.
    m.clip = meterClip_.load(std::memory_order_relaxed);
    return m;
}

void ReaSamplerProcessor::clearMasterBusClip() {
    meterClip_.store(false, std::memory_order_relaxed);
}

AutomationChannel::ReleaseProof ReaSamplerProcessor::publishLiveParams() {
    const int rate = builtSampleRate_.load(std::memory_order_relaxed);
    if (rate <= 0) return AutomationChannel::ReleaseProof::noRepublishNeeded();
    const std::uint32_t before = liveParams_.generation();
    const InstrumentParams params = instrumentParams();
    const instrument::engine::LiveValues block =
        instrument::engine::foldLive(resolvePlay(params.play, rate), params.keyTrack);
    {
        // livePublishMutex_ enforces the seqlock's single-writer contract (live_params.h)
        // against reloadInstrument's publish — held for the publish call only, not the fold
        // above.
        std::lock_guard<std::mutex> lock(livePublishMutex_);
        liveParams_.publish(block);
    }
    return AutomationChannel::ReleaseProof::fromPublish(before, liveParams_.generation());
}

SampleRefs ReaSamplerProcessor::sampleRefs() {
    std::lock_guard<std::mutex> lock(refsMutex_);
    return sampleRefs_;
}

ChannelMode ReaSamplerProcessor::channelMode() {
    std::lock_guard<std::mutex> lock(channelModeMutex_);
    return channelMode_;
}

std::uint8_t ReaSamplerProcessor::previewVelocity() {
    std::lock_guard<std::mutex> lock(previewMutex_);
    return previewVelocity_;
}

void ReaSamplerProcessor::setPreviewVelocity(std::uint8_t velocity) {
    // Clamp to [1,127] — 0 would be a note-off by convention, and a preview strike must sound.
    if (velocity < 1) velocity = 1;
    if (velocity > 127) velocity = 127;
    std::lock_guard<std::mutex> lock(previewMutex_);
    previewVelocity_ = velocity;
}

int ReaSamplerProcessor::voiceCount() {
    std::lock_guard<std::mutex> lock(voiceParamsMutex_);
    return voiceCount_;
}

void ReaSamplerProcessor::setVoiceCount(int count) {
    // Clamp to the shared pure-core range so the engine, state bytes, and editor control
    // can never disagree about the legal polyphony span.
    if (count < kMinVoiceCount) count = kMinVoiceCount;
    if (count > kMaxVoiceCount) count = kMaxVoiceCount;
    {
        std::lock_guard<std::mutex> lock(voiceParamsMutex_);
        if (voiceCount_ == count) return;  // no-op: don't churn a rebuild
        voiceCount_ = count;
    }
    // Light rebuild through the drain-slot swap (no bridge re-read, no WAV re-decode) so a
    // voice-param change never cuts a sounding tail. Same contract below.
    rebuildVoiceEngine();
}

VoiceMode ReaSamplerProcessor::voiceMode() {
    std::lock_guard<std::mutex> lock(voiceParamsMutex_);
    return voiceMode_;
}

void ReaSamplerProcessor::setVoiceMode(VoiceMode mode) {
    {
        std::lock_guard<std::mutex> lock(voiceParamsMutex_);
        if (voiceMode_ == mode) return;
        voiceMode_ = mode;
    }
    rebuildVoiceEngine();
}

MonoTrigger ReaSamplerProcessor::monoTrigger() {
    std::lock_guard<std::mutex> lock(voiceParamsMutex_);
    return monoTrigger_;
}

void ReaSamplerProcessor::setMonoTrigger(MonoTrigger trigger) {
    {
        std::lock_guard<std::mutex> lock(voiceParamsMutex_);
        if (monoTrigger_ == trigger) return;
        monoTrigger_ = trigger;
    }
    rebuildVoiceEngine();
}

bool ReaSamplerProcessor::publishMasterGainLinear(double linear) {
    // Clamp to the master_gain taper (0 = silence, cap = +24 dB). One relaxed atomic
    // store — no rebuild, no lock (a post-sum trim is not a keymap fact).
    if (!(linear >= 0.0)) linear = 0.0;  // also catches NaN
    const double maxLin = masterGainMaxLinear();
    if (linear > maxLin) linear = maxLin;
    const float value = static_cast<float>(linear);
    return masterGain_.exchange(value, std::memory_order_relaxed) != value;
}

void ReaSamplerProcessor::setMasterGainLinear(double linear) {
    const bool moved = publishMasterGainLinear(linear);
    // Gain's own notification funnel — it is the one exposed control that does not ride the
    // parameter set, so setInstrumentParams' diff cannot see it. Compared for a real change so a
    // reload's republish of an unmoved gain writes nothing into a host's automation lane.
    if (paramNotifySuppressed_ || !moved) return;
    notifyParamChanged(instrument::param::kParamMasterGain,
                       instrument::engine::masterGainNormFromLinear(masterGainLinear()));
}

void ReaSamplerProcessor::flushGainNotify() {
    if (!componentHandler) return;  // an arm raised before the handler connected waits
    if (!gainNotifyPending_.exchange(false, std::memory_order_acquire)) return;
    if (paramNotifySuppressed_) return;
    notifyParamChanged(instrument::param::kParamMasterGain,
                       instrument::engine::masterGainNormFromLinear(masterGainLinear()));
}

void ReaSamplerProcessor::previewNoteOn(int note) {
    if (note < 0) note = 0;
    if (note > 127) note = 127;
    const std::uint8_t vel = previewVelocity();  // latch the current knob value into the request
    // Advance the sequence (wrapping; process compares for inequality — 16 bits gives 65535
    // posts between collisions, unreachable at UI-click rates).
    const std::uint16_t seq = ++previewOnSeq_ == 0 ? ++previewOnSeq_ : previewOnSeq_;
    const std::uint32_t packed = (static_cast<std::uint32_t>(seq) << 16) |
                                 (static_cast<std::uint32_t>(vel) << 8) |
                                 static_cast<std::uint32_t>(note & 0xFF);
    previewOnRequest_.store(packed, std::memory_order_release);
}

void ReaSamplerProcessor::previewNoteOff(int note) {
    if (note < 0) note = 0;
    if (note > 127) note = 127;
    const std::uint16_t seq = ++previewOffSeq_ == 0 ? ++previewOffSeq_ : previewOffSeq_;
    const std::uint32_t packed = (static_cast<std::uint32_t>(seq) << 16) |
                                 static_cast<std::uint32_t>(note & 0xFF);
    previewOffRequest_.store(packed, std::memory_order_release);
}

void ReaSamplerProcessor::setChannelMode(ChannelMode mode) {
    {
        std::lock_guard<std::mutex> lock(channelModeMutex_);
        // A deliberate choice either way: latch explicit even on a same-mode click so
        // auto-default stops fighting it.
        channelModeExplicit_ = true;
        if (channelMode_ == mode) return;  // no decode change: don't churn a reload
        channelMode_ = mode;
    }
    // The output bus is fixed stereo (no bus repoint): reloading re-decodes off-thread
    // under the new mode and the RT path just keeps rendering.
    reloadInstrument();
}

}  // namespace reasampler::vst
