#pragma once
// Design View's per-FX offline surface: the FX-identity read that snapshot,
// park and restore all address FX through, the deferred intent queue that keeps
// TrackFX_SetOffline off the mode switch's synchronous path, and the idle-tick
// drain that applies it. See src/shell/view/CLAUDE.md's FX-parking caveat.

#include <string>
#include <vector>

#include "core/view/fx_offline.h"

// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
class MediaTrack;
class ReaProject;

namespace reasampler {

// The chain as it stands now: identity by current slot. Snapshot, park and
// restore all address FX through this one plain 0..TrackFX_GetCount-1
// enumeration — never the 0x1000000/0x2000000 input-FX or container forms — so
// whatever it covers, all three cover identically.
std::vector<std::string> liveFxGuids(MediaTrack* tr);

// One deferred per-FX intent for one track. A park carries no ops (every live
// slot goes offline); a restore carries the planned ops verbatim.
struct FxParkIntent {
    std::string              guid;
    bool                     park = false;
    std::vector<FxOfflineOp> restoreOps;
};

// The queue's re-entrancy rule, pure so it can be asserted without a DAW: at
// most ONE intent per track GUID, and the latest one wins. Park and restore are
// inverses, so an intent landing on its own pending inverse CANCELS it rather
// than stacking — the queued work never ran, so the track already holds the
// state the newcomer asks for, and replaying both would be both slower and
// observably wrong.
class FxParkQueue {
public:
    // Returns the ops of a pending restore this park CANCELLED, empty otherwise.
    // The caller needs them: that restore never ran, so the live chain still
    // reads the PARKED offline states and is no longer a source for a fresh
    // pre-park snapshot (see preParkFxFromCancelledRestore).
    //
    // The empty return is a LOAD-BEARING sentinel that deliberately conflates
    // "nothing was cancelled" with "cancelled a restore carrying no ops": a
    // zero-op restore held no FX state to hand back, so falling through to a
    // live chain read is the same answer, not a worse one.
    std::vector<FxOfflineOp> park(const std::string& guid) {
        FxParkIntent* held = find(guid);
        if (!held) {
            pending_.push_back(FxParkIntent{guid, true, {}});
            return {};
        }
        if (held->park) return {};
        std::vector<FxOfflineOp> cancelled = std::move(held->restoreOps);
        erase(held);
        return cancelled;
    }

    // A restore cancelling a pending park is COMPLETE at that point — the park
    // never ran, so no drain will ever come for this GUID.
    void restore(const std::string& guid, std::vector<FxOfflineOp> ops) {
        FxParkIntent* held = find(guid);
        if (!held) {
            pending_.push_back(FxParkIntent{guid, false, std::move(ops)});
        } else if (held->park) {
            erase(held);
        } else {
            held->restoreOps = std::move(ops);
        }
    }

    // Enqueue order, which is apply order: park before restore within one
    // switch, as the synchronous body already orders them.
    const std::vector<FxParkIntent>& pending() const { return pending_; }
    bool empty() const { return pending_.empty(); }
    void clear() { pending_.clear(); }

    // Detaches everything pending, leaving the queue able to accept intents
    // enqueued WHILE the caller applies what it took. [verify — DAW] applying
    // loads/unloads plugins, which is ASSUMED to pump the message loop, so a
    // re-entrant switch can enqueue mid-apply: iterating the live queue would
    // dangle on the push_back, and clearing it afterwards would discard
    // whatever arrived during the apply. The detach is correct either way; only
    // the need for it is unconfirmed.
    std::vector<FxParkIntent> take() {
        std::vector<FxParkIntent> taken;
        taken.swap(pending_);
        return taken;
    }

private:
    FxParkIntent* find(const std::string& guid) {
        for (FxParkIntent& i : pending_)
            if (i.guid == guid) return &i;
        return nullptr;
    }
    void erase(FxParkIntent* held) {
        pending_.erase(pending_.begin() + (held - pending_.data()));
    }

    std::vector<FxParkIntent> pending_;
};

// The FX half of a snapshot, with the keying it must be read back under.
struct PreParkFx {
    std::vector<FxOfflineState> states;
    FxKeying                    keying = FxKeying::Identity;
};

// The FX half a fresh pre-park snapshot must carry when the park CANCELLED a
// pending restore: those ops are the only surviving record of the pre-park
// state, because the chain still reads the parked values until that restore
// drains — and it never will, the cancel dropped it. Empty in (nothing was
// cancelled) means the caller reads the live chain instead. The restore's own
// keying travels with it so a slot-keyed snapshot lifted from a legacy
// view_state does not silently become an identity-keyed one with no identities.
//
// FAITHFUL TO THE SNAPSHOT, NOT THE CHAIN. The ops describe the chain as it was
// at the ORIGINAL park; applyPark enumerates it again at drain time. So an FX
// added while the track was parked (a floating FX-chain window, ReaScript) is
// absent from this reconstruction yet IS offlined by the cancelling park's
// drain — and so never comes back online. Rare, and recoverable by hand in the
// FX chain, but specific to the deferral.
//
// `offline` is already boolean by the time it arrives: makeRestorePlan narrowed
// FxOfflineState's defensive int to FxOfflineOp's bool, so this widening back to
// int restores the type, not lost information.
inline PreParkFx preParkFxFromCancelledRestore(const std::vector<FxOfflineOp>& cancelled) {
    PreParkFx out;
    if (cancelled.empty()) return out;
    out.keying = cancelled.front().keying;
    out.states.reserve(cancelled.size());
    for (const FxOfflineOp& op : cancelled)
        out.states.push_back(FxOfflineState{op.fxGuid, op.offline ? 1 : 0});
    return out;
}

// The FX half of a fresh pre-park snapshot for `tr`: whatever the accompanying
// park cancelled, else a live read of the chain.
PreParkFx snapshotFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& cancelled);

// Enqueue against `proj` (nullptr = current project). An enqueue naming a
// different project than the pending intents discards those unapplied. The park
// returns whatever pending restore it cancelled, per FxParkQueue::park.
std::vector<FxOfflineOp> deferFxPark(ReaProject* proj, const std::string& guid);
void deferFxRestore(ReaProject* proj, const std::string& guid, std::vector<FxOfflineOp> ops);

// Applies every pending intent. Touches NO model state — a restore's snapshot is
// dropped where the restore is planned (applyMode). Discards the queue unapplied
// if the project it was enqueued against is no longer current (close / switch);
// an intent whose track is gone is pruned. Idle cost is one empty-queue test.
// Re-entrant calls early-out: one drain at a time, the outer one owns the queue.
//
// Called on the idle tick AND synchronously before the view model is serialized
// (persistViewState, render_in_place). The second call is not an optimization:
// between a restore's synchronous flag writes and its drain the FX are still
// offline while the model has already dropped the snapshot that would replan
// them, so a save inside that window — deterministic under a custom action chain
// like "toggle mode; save project" — records offline FX beside a snapshot-free
// model, and nothing on reopen brings them back online. The cost is that an
// action-driven switch pays the FX hitch before it returns; its repaint and undo
// block have both closed by then, which is what the deferral was for.
void drainDeferredFxParks();

// Drops every pending intent without applying it. Called when the model the
// intents were planned against has been replaced (project load/switch, undo/redo
// state restore) — applying them then would write the pre-reload plan over the
// project that replaced it.
//
// Not pure loss: the caller reapplies the active mode over the reloaded model
// immediately after, which re-plans a park for every inactive leaf, so parked FX
// converge on the following drain. The case that does NOT self-heal is a track
// whose reloaded model carries no snapshot — nothing plans a restore for it, so
// FX left offline stay offline.
void discardDeferredFxParks();

} // namespace reasampler
