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

#include "shell/instrument/reaper_bridge.h"

#include <optional>
#include <string>
#include <vector>

#include "core/instrument/map/bridge_marshal.h"
#include "core/wire/ext_state_read.h"  // readProjExtStateGrowing (grow-loop policy)
#include "core/capture/capture_paths.h"  // projectDirOfRpp (shared project-dir derivation)
#include "ext_keys.h"       // kProjExtNamespace (shared wire contract)

// VST3 base types must be included before REAPER's VST3 interface header, which uses
// unqualified Steinberg types (FUnknown, CStringA, uint32, DECLARE_CLASS_IID, PLUGIN_API).
#include "pluginterfaces/base/funknown.h"
#include "pluginterfaces/base/ftypes.h"

// REAPER's VST3-side bridge interface (vendored): IReaperHostApplication is the
// IHostApplication REAPER passes to IComponent::initialize, exposing getReaperApi
// (resolve-by-name) and getReaperParent (host context). Pulled into namespace Steinberg
// (the header's unqualified types), 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 the iid; this is the only TU that
// queries for the interface, so the DEFINE lives with its sole use.
DEF_CLASS_IID(Steinberg::IReaperHostApplication)

// The ext-state namespace is the shared wire contract with the extension — ext_keys.h's
// kProjExtNamespace() (pure, REAPER-free), channel-derived so both artifacts read one
// symbol and cannot drift.

namespace reasampler::vst {

using capture::projectDirOfRpp;
using instrument::map::decodeGetProjExtState;

bool ReaperBridge::connect(Steinberg::FUnknown* context) {
    getProjExtState_ = nullptr;
    enumProjExtState_ = nullptr;
    enumProjects_ = nullptr;
    setProjExtState_ = nullptr;
    getTrackGuid_ = nullptr;
    guidToString_ = nullptr;
    namedCommandLookup_ = nullptr;
    mainOnCommandEx_ = nullptr;
    getCursorPositionEx_ = nullptr;
    timeMapGetTimeSigAtTime_ = nullptr;
    hostApp_ = nullptr;
    if (!context) return false;

    // 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();

    // getReaperApi returns the same function pointers the extension resolves via
    // rec->GetFunc; a null return means the symbol is unavailable (very old REAPER).
    getProjExtState_ = reinterpret_cast<GetProjExtStateFn>(
        reaper->getReaperApi("GetProjExtState"));
    enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>(
        reaper->getReaperApi("EnumProjExtState"));
    // EnumProjects(-1, ...) yields the active project + its .rpp path — same convention
    // the persist shell uses, so the instrument derives the project directory identically.
    enumProjects_ = reinterpret_cast<EnumProjectsFn>(
        reaper->getReaperApi("EnumProjects"));
    // The (prefix-guarded) usage publish write + the track-identity pair it stamps. All
    // degrade to null gracefully — an old REAPER never publishes usage.
    setProjExtState_ = reinterpret_cast<SetProjExtStateFn>(
        reaper->getReaperApi("SetProjExtState"));
    getTrackGuid_ = reinterpret_cast<GetTrackGuidFn>(
        reaper->getReaperApi("GetTrackGUID"));
    guidToString_ = reinterpret_cast<GuidToStringFn>(
        reaper->getReaperApi("guidToString"));
    // The bake crossing: resolve the extension's action id by name, then fire it against
    // this instance's own project. All degrade to null gracefully — an unresolvable pair
    // simply leaves the bake affordance disabled.
    namedCommandLookup_ = reinterpret_cast<NamedCommandLookupFn>(
        reaper->getReaperApi("NamedCommandLookup"));
    mainOnCommandEx_ = reinterpret_cast<MainOnCommandExFn>(
        reaper->getReaperApi("Main_OnCommandEx"));
    getCursorPositionEx_ = reinterpret_cast<GetCursorPositionExFn>(
        reaper->getReaperApi("GetCursorPositionEx"));
    timeMapGetTimeSigAtTime_ = reinterpret_cast<TimeMapGetTimeSigAtTimeFn>(
        reaper->getReaperApi("TimeMap_GetTimeSigAtTime"));

    return getProjExtState_ != nullptr;
}

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

    // getReaperParent(3) reads the live "reasampler" ext-state against the active project
    // the instrument was instantiated in, so it follows project switches for free. A null
    // project is legitimate (REAPER treats it as the current project) — pass it through
    // rather than bailing; a fruitless read still yields nullopt to the caller.
    auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
    void* proj = reaper->getReaperParent(3);

    // The bank blob can be large, so grow the buffer until it fits rather than risk a
    // silent truncation. The shared wire::readProjExtStateGrowing loop keeps this bridge
    // read and the extension's persist/usage reads from drifting.
    const auto read = wire::readProjExtStateGrowing(
        [&](char* buf, int cap) {
            return getProjExtState_(proj, kProjExtNamespace(), key.c_str(), buf, cap);
        });
    if (read.status != wire::GrowingExtStateRead::Status::Complete)
        return std::nullopt;  // absent / empty key, or pathologically large (>16 MB)
    return decodeGetProjExtState(read.apiReturn, read.value);
}

bool ReaperBridge::writeGuarded(const std::string& key, const std::string& value,
                                const char* requiredPrefix) {
    // getProjExtState_ is required too: without it the write could not be proven, and an
    // unprovable write may not be reported as a landed one.
    if (!setProjExtState_ || !getProjExtState_ || !hostApp_) return false;
    // Read-only-BANK guard: this module writes the two sanctioned per-instance prefixes
    // and nothing else. Any other key is refused rather than widening the instrument's
    // write surface (banks/view/tail/assign stay extension-owned).
    const std::string prefix = requiredPrefix;
    if (key.compare(0, prefix.size(), prefix) != 0) return false;

    auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
    void* proj = reaper->getReaperParent(3);  // null = current project (same as reads)
    // The return is DELIBERATELY discarded: it describes the whole extname's state, not
    // this key, so testing it was a guard that could not fire for the bake publish
    // (wire::extStateWriteLanded owns the reasoning). The read-back below is the proof.
    setProjExtState_(proj, kProjExtNamespace(), key.c_str(), value.c_str());

    const auto back = wire::readProjExtStateGrowing([&](char* buf, int cap) {
        return getProjExtState_(proj, kProjExtNamespace(), key.c_str(), buf, cap);
    });
    if (back.status == wire::GrowingExtStateRead::Status::Overflow)
        return false;  // could not check -> report unconfirmed, never a claimed success
    return wire::extStateWriteLanded(
        value, back.status == wire::GrowingExtStateRead::Status::Complete
                   ? std::optional<std::string>(back.value)
                   : std::nullopt);
}

bool ReaperBridge::writeUsageExtState(const std::string& usageKey,
                                      const std::string& value) {
    return writeGuarded(usageKey, value, kProjExtUsageKeyPrefix);
}

bool ReaperBridge::writeBakeExtState(const std::string& bakeKey,
                                     const std::string& value) {
    return writeGuarded(bakeKey, value, kProjExtBakeKeyPrefix);
}

int ReaperBridge::lookupCommand(const std::string& commandName) {
    if (!namedCommandLookup_ || commandName.empty()) return 0;
    return namedCommandLookup_(commandName.c_str());
}

bool ReaperBridge::extensionActionAvailable(const std::string& commandName) {
    return mainOnCommandEx_ != nullptr && lookupCommand(commandName) != 0;
}

bool ReaperBridge::invokeExtensionAction(const std::string& commandName) {
    if (!mainOnCommandEx_ || !hostApp_) return false;
    const int command = lookupCommand(commandName);
    if (command == 0) return false;
    auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
    // getReaperParent(3), not the focused tab — the ask is that an instance in a background
    // tab bakes into ITS OWN project's bank (see the header on what that does and does not
    // guarantee).
    void* proj = reaper->getReaperParent(3);
    // flag 0 is the conventional "no modifier" value; the header documents no other, and
    // the extension's hookcommand ignores it.
    mainOnCommandEx_(command, 0, proj);
    return true;
}

double ReaperBridge::projectTempoBpm() {
    if (!timeMapGetTimeSigAtTime_ || !getCursorPositionEx_ || !hostApp_) return 0.0;
    auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
    void* proj = reaper->getReaperParent(3);
    int num = 0, denom = 0;
    double tempo = 0.0;
    timeMapGetTimeSigAtTime_(proj, getCursorPositionEx_(proj), &num, &denom, &tempo);
    // The meter is read but not applied: the note model's beat is a quarter note by
    // ruling (core/instrument/note/CLAUDE.md), so the undivided BPM is the right one.
    return tempo;
}

std::string ReaperBridge::currentTrackGuid() {
    if (!hostApp_ || !getTrackGuid_ || !guidToString_) return {};
    auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
    void* track = reaper->getReaperParent(1);  // the hosting MediaTrack*
    if (!track) return {};                     // no track context (unusual host state)
    void* guid = getTrackGuid_(track);
    if (!guid) return {};
    char buf[64] = {0};  // guidToString's documented destNeed64 contract
    guidToString_(guid, buf);
    return std::string(buf);
}

std::string ReaperBridge::activeProjectDir() {
    if (!enumProjects_) return {};
    // idx=-1 is the current project tab; the out-buffer is empty for a never-saved
    // project. projectDirOfRpp keeps that empty (no default-location fallback).
    std::vector<char> buf(4096, '\0');
    enumProjects_(-1, buf.data(), static_cast<int>(buf.size()));
    return projectDirOfRpp(std::string(buf.data()));
}

}  // namespace reasampler::vst
