// See view_fx_park.h. Compiled into the reaper_reasampler module; includes
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
// The queue's rule is pure (header, test_view_fx_park.cpp); this file owns the
// REAPER reads/writes and the ordering against the live enumeration.

#include "shell/view/view_fx_park.h"

#include <string>
#include <unordered_map>
#include <vector>

#include "shell/capture/track_guid.h"

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_CountTracks
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetTrack
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetFXGUID
#define REAPERAPI_WANT_TrackFX_GetOffline
#define REAPERAPI_WANT_TrackFX_SetOffline
#define REAPERAPI_WANT_ValidatePtr2
#define REAPERAPI_WANT_guidToString
#include "reaper_plugin_functions.h"

namespace reasampler {

namespace {

FxParkQueue g_queue;
ReaProject* g_owner = nullptr;  // the project the pending intents were enqueued against
bool        g_draining = false;

// Makes "one drain at a time" explicit rather than implied by the call sites.
// RAII because an apply can throw and a stuck flag would silence the queue for
// the rest of the session.
struct DrainScope {
    DrainScope() { g_draining = true; }
    ~DrainScope() { g_draining = false; }
    DrainScope(const DrainScope&) = delete;
    DrainScope& operator=(const DrainScope&) = delete;
};

ReaProject* currentProject() { return EnumProjects(-1, nullptr, 0); }

void adoptOwner(ReaProject* proj) {
    ReaProject* p = proj ? proj : currentProject();
    if (p == g_owner) return;
    g_queue.clear();  // intents planned against another project are never replayed here
    g_owner = p;
}

// [verify — DAW] TrackFX_SetOffline unloads and re-instantiates the plugin, so
// writing a state that already holds is assumed to cost the same unload/reload
// as a real change — the header documents no internal short-circuit either way.
// The compare is cheap and correct regardless; only the size of what it saves is
// unconfirmed.
void setOfflineIfChanged(MediaTrack* tr, int fx, bool offline) {
    if (TrackFX_GetOffline(tr, fx) == offline) return;
    TrackFX_SetOffline(tr, fx, offline);
}

void applyPark(MediaTrack* tr) {
    const int fxCount = TrackFX_GetCount(tr);
    for (int fx = 0; fx < fxCount; ++fx) setOfflineIfChanged(tr, fx, true);
}

// Restores per-FX offline from the plan verbatim — never a blanket "online".
// Which live FX each captured state belongs to is resolveFxRestore's call, and
// what it could not place comes back for the caller to report.
FxRestoreDrops applyRestore(MediaTrack* tr, const std::vector<FxOfflineOp>& ops) {
    const FxRestoreResolution res = resolveFxRestore(ops, liveFxGuids(tr));
    for (const FxOfflineWrite& w : res.writes) setOfflineIfChanged(tr, w.fxIndex, w.offline);
    return res.drops;
}

// The FX's own durable identity, braced exactly like the track GUID keys. Empty
// when REAPER reports none — an FX we cannot name is one we cannot restore, and
// fx_offline treats it that way rather than guessing at its slot. Lifetime is
// settled: the string copy is taken immediately and the GUID* is never held
// past this call (reaper_plugin_functions.h:7348 documents no null contract for
// TrackFX_GetFXGUID; treating null as "no identity" is the safe read).
//
// [verify — DAW] STABILITY across a chain mutation is not settled the same way:
// confirm the GUID for one FX instance survives a native drag-reorder, an SWS
// move (SNM_MoveOrRemoveTrackFX — SWS issue #802 reports the FXID lines do not
// follow the plugin after that call, i.e. wrong-plugin restores or mass drops
// through fx_offline on that path specifically), a save/reload round trip, and
// two live instances of one plugin type staying distinguishable. See
// src/shell/view/CLAUDE.md's Gotchas for the SWS-path risk this leaves open.
std::string fxGuidString(MediaTrack* tr, int fx) {
    GUID* g = TrackFX_GetFXGUID(tr, fx);
    if (!g) return {};
    char buf[64] = {0};  // guidToString needs a >=64-char destination (SDK contract)
    guidToString(g, buf);
    return std::string(buf);
}

}  // namespace

std::vector<std::string> liveFxGuids(MediaTrack* tr) {
    const int fxCount = TrackFX_GetCount(tr);
    std::vector<std::string> guids;
    guids.reserve(static_cast<std::size_t>(fxCount));
    for (int fx = 0; fx < fxCount; ++fx) guids.push_back(fxGuidString(tr, fx));
    return guids;
}

PreParkFx snapshotFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& cancelled) {
    PreParkFx carried = preParkFxFromCancelledRestore(cancelled);
    if (!carried.states.empty()) return carried;

    // Nothing was cancelled, so the chain reads true. fxKeying stays Identity —
    // a live capture always knows the chain.
    PreParkFx live;
    const std::vector<std::string> guids = liveFxGuids(tr);
    live.states.reserve(guids.size());
    for (std::size_t fx = 0; fx < guids.size(); ++fx)
        live.states.push_back(
            FxOfflineState{guids[fx], TrackFX_GetOffline(tr, static_cast<int>(fx)) ? 1 : 0});
    return live;
}

std::vector<FxOfflineOp> deferFxPark(ReaProject* proj, const std::string& guid) {
    adoptOwner(proj);
    return g_queue.park(guid);
}

void deferFxRestore(ReaProject* proj, const std::string& guid, std::vector<FxOfflineOp> ops) {
    adoptOwner(proj);
    g_queue.restore(guid, std::move(ops));
}

void discardDeferredFxParks() {
    g_queue.clear();
    g_owner = nullptr;
}

void drainDeferredFxParks() {
    if (g_queue.empty()) return;
    if (g_draining) return;  // re-entered mid-apply: those intents are the outer drain's next pass

    if (g_owner != currentProject()) {
        // The project the intents were planned against was closed or switched
        // away from: its tracks are not ours to write and its handles may be
        // gone. Discard rather than apply. This catches only a DIFFERENT live
        // pointer; a REAPER-recycled address is caught upstream, by the caller
        // discarding on the session's own load transition.
        discardDeferredFxParks();
        return;
    }

    ReaProject* const proj = g_owner;
    const DrainScope scope;

    // Detached before the first write: [verify — DAW] applying is ASSUMED to pump
    // the message loop (plugins load and unload), so a re-entrant switch can
    // enqueue while this runs. Everything defensive below rests on that one
    // assumption; each piece is correct regardless of whether it holds.
    const std::vector<FxParkIntent> draining = g_queue.take();

    std::unordered_map<std::string, MediaTrack*> byGuid;
    const int count = CountTracks(proj);
    byGuid.reserve(static_cast<std::size_t>(count));
    for (int i = 0; i < count; ++i) {
        MediaTrack* tr = GetTrack(proj, i);
        if (!tr) continue;
        std::string guid = guidString(tr);
        if (!guid.empty()) byGuid.emplace(std::move(guid), tr);
    }

    FxRestoreDrops drops;
    int dropTracks = 0;
    for (const FxParkIntent& intent : draining) {
        auto it = byGuid.find(intent.guid);
        if (it == byGuid.end()) continue;  // track deleted since the switch — prune

        // Re-validated PER INTENT, not once above: under the same pumping
        // assumption, a project closed or a track deleted between two applies
        // leaves the handle resolved above dangling — a use-after-free, not a
        // pruned intent. Same gate capture_realtime_shell's teardown uses; a null
        // first argument validates the ReaProject* itself (SDK: proj is ignored
        // when the pointer is a project).
        if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return;
        if (!ValidatePtr2(proj, it->second, "MediaTrack*")) continue;

        if (intent.park) {
            applyPark(it->second);
            continue;
        }
        const FxRestoreDrops d = applyRestore(it->second, intent.restoreOps);
        if (d.total() > 0) {
            drops.add(d);
            ++dropTracks;
        }
    }

    // Captured FX state that could not be applied is REPORTED — silence would read
    // to the user as "restore worked" while an FX sat at whatever state the park
    // left it in. The "!SHOW:" prefix (reaper_plugin_functions.h:6536) keeps it from
    // force-opening the console: this drain also runs behind an unattended
    // project-load reapply, and cannot tell that case from an interactive toggle.
    const std::string fxDropMsg = describeFxRestoreDrops(drops, dropTracks);
    if (!fxDropMsg.empty()) ShowConsoleMsg(("!SHOW:" + fxDropMsg).c_str());
}

}  // namespace reasampler
