// reaper_bridge.h — the REAPER VST-host bridge. Thin shell: resolves REAPER API functions
// by name over the host context and reads the live "reasampler" project ext-state. The
// fiddly decode lives in bridge_marshal (pure).
//
// Bridge mechanism: REAPER passes an IHostApplication as `context` to
// IComponent::initialize; querying it for IReaperHostApplication yields getReaperApi
// (resolve a REAPER API function pointer by name) and getReaperParent(3) (the host
// ReaProject*; 1=track, 2=take, 3=project, 4=fxdsp, 5=trackchan) — not VST2 hostcb opcodes.

#pragma once

#include <optional>
#include <string>

#include "pluginterfaces/base/funknown.h"

namespace reasampler::vst {

// Wraps the REAPER host bridge for a single plugin instance. Constructed cheaply;
// connect() must be called with the initialize() context before any read. All reads
// degrade to nullopt (never crash) when the host is not REAPER or a symbol is absent —
// the instrument must load in non-REAPER hosts too, just without live state.
class ReaperBridge {
public:
    ReaperBridge() = default;

    // Binds to the host (`context` is the FUnknown* IComponent::initialize hands us).
    // Returns true when the host is REAPER and the ext-state API resolved; safe to call
    // with a null or non-REAPER context (returns false).
    bool connect(Steinberg::FUnknown* context);

    bool isConnected() const { return getProjExtState_ != nullptr; }

    // Reads a "reasampler" ext-state value by key from the host's active project.
    // Returns nullopt when unconnected, unresolvable, or the key is absent.
    //
    // NOT REAL-TIME SAFE (allocates + calls into REAPER): audio-thread callers MUST NOT
    // invoke this. The instrument reads on the main/UI thread and hands a snapshot to
    // the process path.
    std::optional<std::string> readReasamplerExtState(const std::string& key);

    // The active project's directory (forward-slashed, no trailing slash) — the same
    // convention persist uses to place the bank alongside the .rpp. Empty for an unsaved
    // project or when unconnected. Not RT-safe.
    std::string activeProjectDir();

    // The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one
    // key prefix and refusing every other key. That structural refusal is what keeps the
    // read-only-BANK invariant intact — banks/view/tail/assign stay unwritable from here —
    // and neither payload is bank state. Neither is RT-safe: the call sites are the
    // off-audio-thread reload path and the editor's UI tick.
    //
    // Both return true iff the key READ BACK as exactly the value written (an empty value
    // is a clear, which lands as an absent-or-empty key). SetProjExtState's own return
    // cannot answer that — wire::extStateWriteLanded owns why, and testing it here was a
    // guard that could never fire. A `false` does not distinguish a write that was never
    // issued (unconnected host, refused prefix) from one that did not take or could not be
    // checked; a caller must not name one of the three.
    //
    // Neither marks the project dirty. A usage change always rides a component-state change
    // that already does; a bake request is transient and is cleared in the same tick.

    // THIS INSTANCE's usage record. `usageKey` MUST carry the "rsusage_" prefix
    // (ext_keys.h's usageKeyFor).
    bool writeUsageExtState(const std::string& usageKey, const std::string& value);

    // THIS INSTANCE's resample-bake request. `bakeKey` MUST carry the "rsbake_" prefix
    // (ext_keys.h's bakeKeyFor). An empty value clears the key.
    bool writeBakeExtState(const std::string& bakeKey, const std::string& value);

    // --- Extension action invocation (the bake crossing) ------------------------------
    //
    // `commandName` is the NamedCommandLookup spelling — the registered command_id string
    // with a leading underscore, which the registration string itself does not carry.

    // Whether the extension is loaded AND has registered this action. Used to paint the
    // affordance: an unavailable action must read Disabled, never enabled-then-refusing.
    bool extensionActionAvailable(const std::string& commandName);

    // Fires the action with THIS INSTANCE's own project tab (getReaperParent(3)) as
    // Main_OnCommandEx's `proj`, rather than leaving it to whichever tab is focused. What
    // REAPER then makes current for the action's duration is NOT verified in the DAW, so
    // this is a request, not a guarantee — the extension side re-derives which project a
    // request came from and refuses one it cannot safely land. Returns false when the action
    // is unregistered; the invocation itself reports nothing, so a caller learns the result
    // from the state the action wrote, never from here. Must NOT be called from a mouse
    // handler: it runs the extension's whole bake landing synchronously, and the action
    // re-points this instance.
    bool invokeExtensionAction(const std::string& commandName);

    // The effective project tempo (BPM, quarter notes per minute) at the edit cursor of
    // this instance's own project. 0.0 when unconnected or unresolvable — the caller
    // refuses rather than substituting a tempo (no hardcoded rates or tempos in src/).
    double projectTempoBpm();

    // The canonical GUID string of the track hosting this FX instance (same rendering as
    // the extension's track_guid::guidString, so usage records compare byte-equal
    // against its live-FX enumeration). Empty when unconnected or no track context (the
    // usage reader then falls back to any-instance liveness). Not RT-safe.
    std::string currentTrackGuid();

private:
    // Resolved REAPER API function pointers (by name via getReaperApi). Signatures
    // verified against reaper_plugin_functions.h.
    using GetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key,
                                      char* valOutNeedBig, int valOutNeedBig_sz);
    using EnumProjExtStateFn = bool (*)(void* proj, const char* extname, int idx,
                                        char* keyOut, int keyOut_sz, char* valOut,
                                        int valOut_sz);
    // EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path. idx=-1 (current
    // tab) follows the active project, same convention as the persist shell.
    using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz);
    // Used ONLY by the two prefix-guarded writers — see the read-only-bank note there.
    using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key,
                                      const char* value);
    using NamedCommandLookupFn = int (*)(const char* commandName);
    using MainOnCommandExFn = void (*)(int command, int flag, void* proj);
    using GetCursorPositionExFn = double (*)(void* proj);
    using TimeMapGetTimeSigAtTimeFn = void (*)(void* proj, double time, int* numOut,
                                               int* denomOut, double* tempoOut);
    // Opaque-pointer signatures so the header stays SDK-type-free; the GUID* is passed
    // straight through, never dereferenced here.
    using GetTrackGuidFn = void* (*)(void* tr);
    using GuidToStringFn = void (*)(const void* g, char* destNeed64);

    void* hostApp_ = nullptr;  // IReaperHostApplication* (opaque here; used in .cpp)
    GetProjExtStateFn getProjExtState_ = nullptr;
    EnumProjExtStateFn enumProjExtState_ = nullptr;
    EnumProjectsFn enumProjects_ = nullptr;
    SetProjExtStateFn setProjExtState_ = nullptr;
    GetTrackGuidFn getTrackGuid_ = nullptr;
    GuidToStringFn guidToString_ = nullptr;
    NamedCommandLookupFn namedCommandLookup_ = nullptr;
    MainOnCommandExFn mainOnCommandEx_ = nullptr;
    GetCursorPositionExFn getCursorPositionEx_ = nullptr;
    TimeMapGetTimeSigAtTimeFn timeMapGetTimeSigAtTime_ = nullptr;

    // The one prefix guard both public writers route through, so the two cannot diverge
    // in how strictly they refuse a key.
    bool writeGuarded(const std::string& key, const std::string& value,
                      const char* requiredPrefix);
    // 0 when the action is not registered (the extension is absent or older). REAPER's
    // documented "not found" return is 0 by convention only — the header does not state
    // it — so every caller treats 0 as unavailable and never as a valid command id.
    int lookupCommand(const std::string& commandName);
};

}  // namespace reasampler::vst
