// reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin.

#include "reaper_bridge.h"

#include <vector>

#include "bridge_marshal.h"
#include "capture_paths.h"  // projectDirOfRpp (shared M4 project-dir derivation)
#include "ext_keys.h"       // kProjExtNamespace (shared wire contract)

// The VST3 base types must be included before REAPER's VST3 interface header, which
// uses FUnknown / CStringA / uint32 / DECLARE_CLASS_IID / PLUGIN_API from
// pluginterfaces/base — all in namespace Steinberg.
#include "pluginterfaces/base/funknown.h"
#include "pluginterfaces/base/ftypes.h"

// REAPER's VST3-side bridge interface (vendored). IReaperHostApplication is what REAPER
// passes (as an IHostApplication) to IComponent::initialize; it exposes getReaperApi
// (resolve-by-name) and getReaperParent (host context). The header uses UNQUALIFIED
// Steinberg types (FUnknown, CStringA, uint32, FUID, DECLARE_CLASS_IID, PLUGIN_API), so
// it must be pulled into the Steinberg namespace — the same way REAPER's own VST3
// examples include it.
namespace Steinberg {
#include "reaper_vst3_interfaces.h"
}  // namespace Steinberg

// DECLARE_CLASS_IID in the REAPER header only DECLARES IReaperHostApplication::iid; some
// TU must DEFINE it. We do it here — this is the only place that queries for the
// interface (FUnknownPtr uses the iid), so the definition lives with its sole use.
DEF_CLASS_IID(Steinberg::IReaperHostApplication)

// The ext-state namespace is the SHARED wire contract between the extension (writer)
// and this instrument (reader); it lives in ext_keys.h (pure, REAPER-free) —
// reasampler::kProjExtNamespace() — so the two artifacts read one symbol and cannot
// drift. Channel-derived (Phase V, V4): the accessor returns "reasampler" (stable) or
// "reasampler_beta" (beta), matching whatever the extension wrote. The S1 spike
// duplicated it locally; that duplication is retired.

namespace reasampler::vst {

bool ReaperBridge::connect(Steinberg::FUnknown* context) {
    getProjExtState_ = nullptr;
    enumProjExtState_ = nullptr;
    enumProjects_ = nullptr;
    hostApp_ = nullptr;
    if (!context) return false;

    // Query the host context for REAPER's bridge interface. In a non-REAPER host this
    // query fails and we stay unconnected — the instrument still loads.
    Steinberg::FUnknownPtr<Steinberg::IReaperHostApplication> reaper(context);
    if (!reaper) return false;
    hostApp_ = reaper.get();

    // Resolve the ext-state functions by name. getReaperApi returns the same function
    // pointers the extension resolves via rec->GetFunc; a null return means the symbol
    // is unavailable (very old REAPER) — degrade gracefully.
    getProjExtState_ = reinterpret_cast<GetProjExtStateFn>(
        reaper->getReaperApi("GetProjExtState"));
    enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>(
        reaper->getReaperApi("EnumProjExtState"));
    // EnumProjects(-1, ...) yields the active project AND its .rpp path — the same call
    // persist.cpp uses, so the instrument derives the project directory identically.
    enumProjects_ = reinterpret_cast<EnumProjectsFn>(
        reaper->getReaperApi("EnumProjects"));

    return getProjExtState_ != nullptr;
}

std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::string& key) {
    if (!getProjExtState_ || !hostApp_) return std::nullopt;

    // Fetch the host project (getReaperParent(3) — project). Reads that live "reasampler"
    // ext-state against the ACTIVE project the instrument was instantiated in, so it
    // follows project switches for free (D6).
    auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
    void* proj = reaper->getReaperParent(3);
    // A null project is legitimate (e.g. instantiated before a project context exists);
    // REAPER treats null as the current project for these calls, so we pass it through
    // rather than bailing — but if the read yields nothing the caller sees nullopt.

    // GetProjExtState writes into a caller buffer; the bank blob can be large (many
    // samples), so grow the buffer until the value fits rather than risk a silent
    // truncation — mirrors persist.cpp's getProjExtStateString growing strategy. The
    // return value is the value length; if it fits strictly inside the buffer it is
    // complete, else grow and retry up to a 16 MB ceiling.
    for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
        std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
        const int rv = getProjExtState_(proj, kProjExtNamespace(), key.c_str(),
                                        buf.data(), cap);
        if (rv <= 0) return std::nullopt;  // absent / empty key
        std::string s(buf.data());
        if (static_cast<int>(s.size()) + 1 < cap) {
            return decodeGetProjExtState(rv, s);
        }
        // else: possibly truncated -> grow and retry.
    }
    return std::nullopt;  // pathologically large (>16 MB) — give up rather than loop
}

std::string ReaperBridge::activeProjectDir() {
    if (!enumProjects_) return {};
    // idx=-1 is the current project tab; the out-buffer receives the full .rpp path,
    // EMPTY for a never-saved project. Same call + convention as persist.cpp; the pure
    // projectDirOfRpp turns the .rpp path into the project directory (parent, forward-
    // slashed) and keeps an unsaved project's empty path empty (no default-location
    // fallback — the tool's invariant).
    std::vector<char> buf(4096, '\0');
    enumProjects_(-1, buf.data(), static_cast<int>(buf.size()));
    return projectDirOfRpp(std::string(buf.data()));
}

}  // namespace reasampler::vst
