// editor_session.cpp — the ReaSamplerEditor's session/bridge state: construction, the
// live-bank snapshot (refreshFromBank / rebuildVisible), the sync and meter ticks, the
// commit-and-reload seam, selection loading, the loaded capture's marker resolution, and
// the decoded-PCM + peak thumbnail caches. UI thread only; every edit commits off the audio
// thread via the processor's reloadInstrument.

#include "shell/instrument/reasampler_editor.h"

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>

#include "core/audio/peaks.h"            // computeEnvelope (the cached peak thumbnail)
#include "core/capture/capture_paths.h"  // resolveBankFile (shared path resolution)
#include "core/capture/wav_codec.h"       // parseWavLayout, extractFloatFrames
#include "core/ui/bank_grid.h"           // ThumbnailKey / thumbnailKeyString (the pure key)
#include "core/util/file_bytes.h"        // shared whole-file loader
#include "ext_keys.h"
#include "core/instrument/bake/bake_plan.h"     // bakeWindowNeedsHold (the Hold predicate)
#include "core/instrument/ui/browser_scroll.h"  // nameMatchesQuery (type-to-filter)
#include "shell/instrument/instrument_bake.h"  // the deferred bake the sync tick runs
#include "shell/instrument/reaper_bridge.h"
#include "shell/instrument/reasampler_processor.h"

using namespace Steinberg;

namespace reasampler::vst {

using namespace reasampler::instrument::map;  // sample_map vocabulary (selectSample / listSamples / …)
using audio::computeEnvelope;
using capture::WavLayout;
using capture::extractFloatFrames;
using capture::parseWavLayout;
using capture::resolveBankFile;
using instrument::ui::nameMatchesQuery;
using ui::ThumbnailKey;
using ui::thumbnailKeyString;
using util::readFileBytes;

// Sync ticks a bake outcome stays in the title band. Long enough to read, short enough
// that it does not shadow the identity readout.
constexpr int kBakeMessageTicks = 20;

ReaSamplerEditor::ReaSamplerEditor(ReaSamplerProcessor* processor)
    : CPluginView(nullptr), processor_(processor) {
    // The default IS the enforced floor (checkSizeConstraint) — the face opens at the size its
    // band stack is laid out for and can only be grown from there.
    ViewRect r(0, 0, instrument::ui::kEditorMinWidth, instrument::ui::kEditorMinHeight);
    setRect(r);
}

void ReaSamplerEditor::refreshFromBank() {
    // Main/UI thread only — reads the live bank over the bridge (allocates, calls REAPER).
    thumbCache_.clear();  // a bank edit may have re-captured/removed a sample; drop stale peaks
    pcmCache_.clear();    // and its decoded PCM (the waveform + snap source)
    holdNeedValid_ = false;  // …and the bake-Hold answer derived from the bank's loop intrinsic
    marksValid_ = false;     // …and pickedMarkers' own memo of the same intrinsic
    channelPcmId_.clear();
    channelPcm_ = ChannelPcm{};
    if (!processor_) {
        samples_.clear();
        banks_.clear();
        visible_.clear();
        selectedId_.clear();
        params_ = InstrumentParams{};
        return;
    }
    auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
    samples_ = banksJson ? listSamples(*banksJson) : std::vector<SampleChoice>{};
    banks_ = banksJson ? listBanks(*banksJson) : std::vector<BankChoice>{};
    selectedId_ = processor_->selectedSampleId();
    params_ = processor_->instrumentParams(seenParamsGeneration_);
    channelMode_ = processor_->channelMode();
    voiceCount_ = processor_->voiceCount();
    voiceMode_ = processor_->voiceMode();
    monoTrigger_ = processor_->monoTrigger();
    // A refresh that emptied the selection closes the curve popup — an open-but-invisible
    // modal would otherwise swallow clicks on the empty state.
    if (selectedId_.empty()) closeCurvePopup();
    // Drop a filter that names a bank no longer present.
    if (!activeFilterBankId_.empty()) {
        bool found = false;
        for (const BankChoice& b : banks_) if (b.id == activeFilterBankId_) found = true;
        if (!found) activeFilterBankId_.clear();
    }
    rebuildVisible();
}

void ReaSamplerEditor::rebuildVisible() {
    // Bank filter first, then type-to-filter search narrows by name substring.
    visible_.clear();
    for (const SampleChoice& s : samples_) {
        const bool inBank = activeFilterBankId_.empty() || s.bankId == activeFilterBankId_;
        if (!inBank) continue;
        const std::string& name = s.displayName.empty() ? s.id : s.displayName;
        if (nameMatchesQuery(name, searchQuery_)) visible_.push_back(s);
    }
    // scrollOffset_ is clamped at paint/wheel time (where layout is known); this runs on
    // the sync-timer refresh too, so it must not reset the user's scroll here.
}

#ifdef _WIN32
// Windows-only (the WM_TIMER cadence + invalidate() are the Win32 child-window path).
void ReaSamplerEditor::onSyncTimer() {
    // UI thread (WM_TIMER). Never while a drag is in flight: a reload mid-drag would
    // rebuild the instrument and repaint under the cursor, yanking the edit — the next
    // tick picks up the change after release.
    if (!processor_) return;

    if (drag_ != DragKind::kNone) return;  // defer past the in-flight edit

    // A parameter commit that flipped the limiter already changed the sound; what waits for this
    // tick is only telling the host to re-ask for the latency. Past the drag guard with the bake,
    // because the restart makes the host rebuild this instance — mid-drag that would yank the
    // edit surface exactly as a reload would. Unconditional: it self-cancels when nothing is
    // armed, so no commit site has to remember to ask for it.
    processor_->flushLatencyRestart();
    // Peers of it: both deliver work the originating thread could not do where it stood — a host
    // callback from inside reloadMutex_, and a model write from the audio thread.
    processor_->flushGainNotify();
    processor_->drainAutomationToModel();

    // params_ is a CACHE of the processor's model, authoritative only for the duration of a
    // gesture — which is why this sits past the drag guard. Re-seed it whenever the model has
    // moved under it: the automation fold just above, the host's own generic panel, a state
    // restore. Without this, commitLive writes the WHOLE stale set back and notifyParamsFromModel
    // diffs it as a real edit, performEdit-ing superseded values the user never touched — which a
    // lane in latch or write mode records.
    if (processor_->instrumentParamsGeneration() != seenParamsGeneration_) {
        params_ = processor_->instrumentParams(seenParamsGeneration_);
        invalidate();
    }

    // Resolve the bake affordance's availability on the SAME tick that paints it, so it
    // can never be enabled on one tick and refuse on the next.
    const bool available = bakeAvailable(processor_->bridge());
    if (available != bakeAvailable_) {
        bakeAvailable_ = available;
        invalidate();
    }
    // Same cadence for the Hold control's applicability, so it too can never paint live on
    // one tick and be inert on the next.
    const bool holdNeeded = resolveBakeHoldNeeded();
    if (holdNeeded != bakeHoldNeeded_) {
        bakeHoldNeeded_ = holdNeeded;
        invalidate();
    }

    // The click armed it; this is where it runs — same thread, one tick later, no mouse
    // capture held, and no REAPER action nested inside a mouse handler.
    if (bakePending_) {
        bakePending_ = false;
        const BakeChainResult result = runBake(*processor_);
        bakeMessage_ = result.message;
        bakeMessageTicks_ = kBakeMessageTicks;
        // A landed bake re-pointed the instance and reset the parameter set; re-snapshot
        // so the face draws the new capture and its neutral controls.
        if (result.ok) refreshFromBank();
        invalidate();
    }
    if (bakeMessageTicks_ > 0 && --bakeMessageTicks_ == 0) invalidate();

    // An open editor is the focused assignment target (thundering-herd policy); instances
    // with no editor open never poll (the timer is bound to the child window).
    const ReaSamplerProcessor::BankSyncResult r = processor_->pollBankSync(/*isFocusedTarget=*/true);

    // Re-snapshot only when something changed.
    if (r.reloaded || r.applied) {
        refreshFromBank();
        invalidate();
    }

    // Decay the drop-affordance banner so it auto-dismisses a few ticks after a drop.
    if (dropHintTicks_ > 0) {
        --dropHintTicks_;
        invalidate();
    }
}

void ReaSamplerEditor::onMeterTimer() {
    // UI thread (WM_TIMER). Deliberately outside the sync tick's in-flight-drag guard rather
    // than merely ahead of it: a drag suppresses the reload poll, but the bus keeps sounding
    // and a frozen bar would misreport it.
    if (!processor_) return;

    const auto now = std::chrono::steady_clock::now();
    const bool first = meterTick_ == std::chrono::steady_clock::time_point{};
    const double elapsed =
        first ? 0.0 : std::chrono::duration<double>(now - meterTick_).count();
    // A zero delta advances nothing, and the drain below is CONSUMING — returning ahead of it
    // keeps the window intact for the next tick instead of spending it on a frame that would
    // move no pixel. GetTickCount64's ~15.6 ms granularity made that the common case at this
    // rate; a steady clock makes it rare, not impossible.
    if (!first && elapsed <= 0.0) return;
    meterTick_ = now;

    // THE one call site. masterBusMeter() exchanges the accumulators to identity as it reads,
    // so a second clock reading it would steal windows from this one.
    const MasterBusMeter bus = processor_->masterBusMeter();
    // The accumulators have exactly one consumer — this tick — so with no editor open they
    // hold everything since the instance was created. The first read is therefore session
    // history, not a window: showing it would put the bar at the loudest peak of the session
    // (instantaneous rise, then a 1.5 s hold) and light the GR lamp off a catch minutes old,
    // with the limiter possibly off since. Discard it and start the window here. The CLIP
    // survives, because it is a latch the user clears rather than a window — it is still set in
    // the processor and the next tick reports it.
    if (first) return;

    const instrument::ui::MasterMeterUi advanced = instrument::ui::advanceMasterMeter(
        masterMeter_, {bus.peakL, bus.peakR, bus.minGain, bus.clip}, elapsed);
    const bool changed = !instrument::ui::meterDrawEqual(advanced, masterMeter_);
    // The GR lamp sits in the deck group's caption row, outside the bar field a meter frame
    // repaints, and is drawn straight onto the group's own gradient — so its rare transitions
    // take the whole-client repaint rather than growing the fast path a second rect.
    const bool lampMoved =
        instrument::ui::grLampLit(advanced) != instrument::ui::grLampLit(masterMeter_);
    masterMeter_ = advanced;
    if (!changed) return;
    if (lampMoved) invalidate();
    else invalidateMeter();
}
#endif  // _WIN32

void ReaSamplerEditor::commitAndReload() {
    // UI thread only. Publishes the edited selection + parameter set, then rebuilds off the
    // audio thread. The reload also copies the loaded capture's file ref + intrinsics into
    // the instance-owned refs table — a browser load is the moment the instance becomes
    // self-contained for that sample.
    if (!processor_) return;
    processor_->setSelectedSampleId(selectedId_);
    // This copy IS the model now, so adopt the generation IT produced (the return, not a second
    // separate query — a write landing between the two would make this adopt a generation newer
    // than the copy just sent) rather than re-seeding off it on the next tick. Same reason at
    // commitLive.
    seenParamsGeneration_ = processor_->setInstrumentParams(params_);
    processor_->reloadInstrument();
    // The reload may have auto-defaulted the channel mode (implicit only) — re-read so the
    // toggle draws what the engine actually decoded with.
    channelMode_ = processor_->channelMode();
#ifdef _WIN32
    invalidate();
#endif
}

void ReaSamplerEditor::commitLive() {
    // UI thread only. See the declaration for why this still writes the parameter set.
    if (!processor_) return;
    seenParamsGeneration_ = processor_->setInstrumentParams(params_);
    processor_->publishLiveParams();
}

bool ReaSamplerEditor::deckKnobIsProcessorSide(int paramId) {
    return paramId == -2 ||
           paramId == static_cast<int>(ParamControl::kVoiceCount) ||
           paramId == static_cast<int>(ParamControl::kMasterGain);
}

bool ReaSamplerEditor::dragCommitsLive(DragKind kind, int paramId) const {
    // The decision itself is the pure liveCommitFor's; this is only the shell's drag-kind
    // vocabulary mapped onto it, so the routing is pinned by deck_groups' tests rather than
    // by inspection of this file.
    using instrument::ui::LiveDragKind;
    const LiveDragKind k = kind == DragKind::kDeckKnob ? LiveDragKind::kDeckKnob
                         : kind == DragKind::kEnvNode  ? LiveDragKind::kEnvNode
                                                       : LiveDragKind::kOther;
    // Live and NoteOnLatched take the SAME route out of here — one publish, no reload — so the
    // shell's question is only "does this reload?". Which voices then read the published value
    // is the engine's business (deck_groups.h).
    return instrument::ui::liveCommitFor(k, paramId) != instrument::ui::LiveCommit::Reload;
}

void ReaSamplerEditor::closeCurvePopup() {
    curvePopup_ = CurveTarget::kNone;
    if (drag_ == DragKind::kCurveNode) {
        drag_ = DragKind::kNone;
        curvePointIndex_ = -1;
    }
}

void ReaSamplerEditor::loadSelection(const std::string& id) {
    // A load REPLACES the loaded sound. The shaping parameters (play mode, envelopes, pitch
    // engine, key-track, velocity curve) are NOT reset — the one set governs whatever is
    // loaded, so a load swaps the sound and keeps the settings. The three CAPTURE-ANCHORED
    // overrides are: a root, a loop span and a start frame all name positions in the
    // OUTGOING capture and mean nothing in the new one, so they clear and the new capture
    // plays from its own bank intrinsics. The loop crossfade goes with the span it belongs
    // to — it is a length in the outgoing capture's frames.
    selectedId_ = id;
    params_.rootOverride.reset();
    params_.loopOverride.reset();
    params_.loopCrossfadeFrames = 0;
    params_.startPoint.reset();
    commitAndReload();
}

ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t frames) const {
    // Answer once per distinct input; refreshFromBank drops the memo along with holdNeed's,
    // which answers the same "is there a bank read to pay" question for the bake-Hold predicate
    // — hoverWaveform calls this on every WM_MOUSEMOVE inside the band, not just marker grabs,
    // so the bridge read + JSON parse below is not a cost to pay per pixel of mouse travel. Frames
    // is not part of the key: it is a function of selectedId_ alone (monoPcmFor's own cache),
    // busted by the same refreshFromBank that busts this memo.
    const HoldNeedKey key{selectedId_, params_.loopOverride, params_.loopCrossfadeFrames,
                          params_.startPoint};
    if (marksValid_ && key == marksKey_) return marksCache_;

    instrument::ui::StoredLoop stored;
    stored.override_ = params_.loopOverride;
    stored.crossfade = params_.loopCrossfadeFrames;
    stored.startPoint = params_.startPoint;
    // Read the loop intrinsic from the live bank blob (the same path selectSample uses); when
    // that is not readable (extension absent / not yet parsed) the instance-owned ref carries
    // the same intrinsics. Skipped entirely once an override is already set — the resolve would
    // discard it, and the bridge read + JSON parse it costs is real.
    if (processor_ && !params_.loopOverride) {
        std::optional<SelectedSample> sel;
        auto banksJson =
            processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
        if (banksJson) sel = selectSample(*banksJson, selectedId_);
        if (!sel) {
            const SampleRefs refs = processor_->sampleRefs();
            if (const SelectedSample* r = findRef(refs, selectedId_)) sel = *r;
        }
        if (sel) stored.intrinsic = sel->loop;
    }
    const SetupMarkers m = instrument::ui::resolveLoopMarks(stored, frames);
    marksKey_ = key;
    marksCache_ = m;
    marksValid_ = true;
    return m;
}

bool ReaSamplerEditor::HoldNeedKey::operator==(const HoldNeedKey& o) const {
    if (sampleId != o.sampleId || crossfade != o.crossfade || startPoint != o.startPoint)
        return false;
    if (loopOverride.has_value() != o.loopOverride.has_value()) return false;
    if (!loopOverride) return true;
    return loopOverride->hasLoop == o.loopOverride->hasLoop &&
           loopOverride->start == o.loopOverride->start &&
           loopOverride->end == o.loopOverride->end;
}

bool ReaSamplerEditor::resolveBakeHoldNeeded() {
    // effectivePlayMode, not the raw field: the bake reads the mode AFTER the drawn-EG fold
    // (resolvePlay, via bakeSnapshot), so a restored or foreign blob carrying Gate together
    // with an active spline would otherwise paint a control for a sound that bakes as Trigger.
    // The mode test comes first so the common Trigger case never pays the resolve below.
    const PlayMode mode = effectivePlayMode(params_.play);
    if (selectedId_.empty() || mode != PlayMode::Gate) return false;

    // Everything past here costs a WAV decode and — with no loop override set — a bridge read
    // plus a bank parse, and the sync tick asks twice a second for as long as an editor is
    // open. Answer once per distinct input; refreshFromBank drops the memo along with the rest
    // of the bank-derived caches, which is the same edge a bank generation bump would give.
    // The mode is not part of the key: the resolve below always asks about Gate, and the test
    // above is what keeps a Trigger sound from reaching it.
    const HoldNeedKey key{selectedId_, params_.loopOverride, params_.loopCrossfadeFrames};
    if (holdNeedValid_ && key == holdNeedKey_) return holdNeedAnswer_;
    holdNeedValid_ = true;
    holdNeedKey_ = key;
    holdNeedAnswer_ = false;

    const auto frames = static_cast<std::int64_t>(monoPcmFor(selectedId_).size());
    if (frames <= 0) return false;
    // `m.hasLoop` alone is not the answer — the engine's fold also refuses a span that reaches
    // outside the PCM, which is exactly what the pure predicate is asked for.
    const SetupMarkers m = pickedMarkers(frames);
    holdNeedAnswer_ = instrument::bake::bakeWindowNeedsHold(
        PlayMode::Gate, SampleLoop{m.hasLoop, m.loopStart, m.loopEnd}, m.crossfade, frames);
    return holdNeedAnswer_;
}

void ReaSamplerEditor::applyMarkers(const SetupMarkers& m) {
    // Write the edited markers into the parameter set as the loop/start override; the fold
    // itself is the pure loop_marks module's. The bank intrinsic is never written (read-only
    // bank consumer).
    const instrument::ui::LoopWrite w = instrument::ui::applyLoopMarks(m);
    params_.loopOverride = w.loop;
    params_.loopCrossfadeFrames = w.crossfade;
    params_.startPoint = w.start;
}

int ReaSamplerEditor::effectiveRoot() const {
    if (params_.rootOverride) return *params_.rootOverride;
    for (const SampleChoice& s : samples_) {
        if (s.id == selectedId_ && s.rootNote) return *s.rootNote;
    }
    return 60;
}

std::string ReaSamplerEditor::samplePathFor(const std::string& sampleId) const {
    // SampleChoice is the browser's metadata projection and does not carry the WAV path, so
    // resolve it from the live bank blob (selectSample).
    if (!processor_) return {};
    auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
    if (banksJson) {
        // An empty relativePath (found the entry, but it carries no path) falls through to
        // the refs fallback below rather than short-circuiting on it.
        if (auto sel = selectSample(*banksJson, sampleId); sel && !sel->relativePath.empty()) {
            return sel->relativePath;
        }
    }
    // Fallback: the bank blob is not readable (extension absent / not yet parsed) or the id
    // went stale there — the instance-owned ref still carries the path, so a self-contained
    // instance draws its loaded sound's waveform regardless.
    const SampleRefs refs = processor_->sampleRefs();
    if (const SelectedSample* r = findRef(refs, sampleId)) return r->relativePath;
    return {};
}

const ReaSamplerEditor::ChannelPcm& ReaSamplerEditor::channelPcmFor(
    const std::string& sampleId) {
    if (channelPcmId_ == sampleId) return channelPcm_;

    // A failed decode is still cached (channelCount stays 0) so a broken/missing file is not
    // re-read on every paint.
    channelPcmId_ = sampleId;
    channelPcm_ = ChannelPcm{};
    const std::string relativePath = samplePathFor(sampleId);
    if (relativePath.empty()) return channelPcm_;

    const std::string projectDir = processor_->bridge().activeProjectDir();
    const std::vector<std::uint8_t> bytes =
        readFileBytes(resolveBankFile(projectDir, relativePath));  // empty on any failure
    const WavLayout layout = parseWavLayout(bytes);
    if (layout.valid) {
        channelPcm_.interleaved = extractFloatFrames(bytes, layout, 0, layout.frameCount());
        channelPcm_.channelCount = static_cast<int>(layout.channelCount);
    }
    return channelPcm_;
}

const std::vector<AudioSample>& ReaSamplerEditor::monoPcmFor(const std::string& sampleId) {
    auto it = pcmCache_.find(sampleId);
    if (it != pcmCache_.end()) return it->second;

    // Every failure path caches an empty vector so a broken/missing file is not re-decoded on
    // every paint. Keyed by id (width-independent) — the thumbnail bins this at whatever
    // width, the snap scans it directly.
    std::vector<AudioSample> mono;
    const std::string relativePath = samplePathFor(sampleId);
    if (!relativePath.empty()) {
        const std::string projectDir = processor_->bridge().activeProjectDir();
        const std::string abs = resolveBankFile(projectDir, relativePath);
        const std::vector<std::uint8_t> bytes = readFileBytes(abs);  // empty on any failure
        const WavLayout layout = parseWavLayout(bytes);
        if (layout.valid) {
            std::vector<AudioSample> interleaved =
                extractFloatFrames(bytes, layout, 0, layout.frameCount());
            mono = downmixToMono(interleaved, layout.channelCount);
        }
    }
    auto ins = pcmCache_.emplace(sampleId, std::move(mono));
    return ins.first->second;
}

const Envelope& ReaSamplerEditor::heroEnvelope(std::size_t bins, int laneCount) {
    // One lane draws the downmix binned at the band's width, which is exactly what the
    // thumbnail cache already answers — so the single-lane hero costs no second slot and
    // inherits that cache's busting (refreshFromBank, and a resize's clear).
    if (laneCount != 2) return thumbnailFor(selectedId_, static_cast<int>(bins));

    // Two lanes bin the INTERLEAVED source instead, which no thumbnail ever asks for. A resize
    // changes `bins` and misses the memo; a capture change drops the decode it rides in.
    const int channels = channelPcmFor(selectedId_).channelCount;
    if (channelPcm_.heroBins != bins) {
        channelPcm_.heroBins = bins;
        channelPcm_.heroEnv = computeEnvelope(channelPcm_.interleaved,
                                              static_cast<std::size_t>(channels),
                                              static_cast<std::size_t>(channelPcm_.frameCount()),
                                              bins);
    }
    return channelPcm_.heroEnv;
}

const Envelope& ReaSamplerEditor::thumbnailFor(const std::string& sampleId, int binCount) {
    // Key through the pure ThumbnailKey (bank_grid, length-prefixed id — collision-proof) so
    // both thumbnail pipelines share one tested key grammar. The editor invalidates by
    // wholesale clear() on refresh/resize, so the bank generation carries no information here.
    const std::string key =
        thumbnailKeyString(ThumbnailKey{sampleId, binCount, /*generation=*/0});
    auto it = thumbCache_.find(key);
    if (it != thumbCache_.end()) return it->second;

    // Bin the (cached) decoded mono PCM at the requested width — one decode per id, reused by
    // every thumbnail width AND the waveform surface + snap.
    const std::vector<AudioSample>& mono = monoPcmFor(sampleId);
    Envelope env;
    if (!mono.empty()) {
        // Clamp bins to the frame count: computeEnvelope pads binCount > frameCount with
        // trailing empty {0,0} bins, which would render a very short sample as a comb of
        // spikes over flat gaps.
        const std::size_t bins =
            (std::min)(static_cast<std::size_t>((std::max)(1, binCount)), mono.size());
        env = computeEnvelope(mono, 1, mono.size(), bins);
    }
    auto ins = thumbCache_.emplace(key, std::move(env));
    return ins.first->second;
}

ReaSamplerEditor::~ReaSamplerEditor() {
#ifdef _WIN32
    if (childHwnd_) {
        DestroyWindow(childHwnd_);
        childHwnd_ = nullptr;
    }
    releaseBackBuffer();  // removedFromParent normally did; this is the un-detached path
#endif
}

}  // namespace reasampler::vst
