// instrument_drop_win.cpp — see instrument_drop_win.h. main.cpp owns the API
// pointers; this TU gets them extern via the WANT list.

#include "shell/actions/instrument_drop_win.h"

#include <atomic>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <vector>

#include "core/version/app_version.h"      // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
#include "core/wire/instrument_drop.h"  // classifyReaperSurface — the PURE, unit-tested classifier

#include "reaper_plugin.h"

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_GetThingFromPoint
#define REAPERAPI_WANT_TrackFX_AddByName
#define REAPERAPI_WANT_TrackFX_Delete
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_SetPreset
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"

namespace reasampler {

// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using version::vstPluginName;
using wire::decideDropOutcome;
using wire::DropAttempt;
using wire::DropOutcome;
using wire::classifyReaperSurface;

namespace {

// Writes `bytes` to a fresh uniquely-named .vstpreset in the OS temp dir; empty path
// on any failure. The .vstpreset extension is load-bearing — TrackFX_SetPreset's
// full-path form is documented for .vstpreset files (VST3). Transient: the caller
// deletes it right after the SetPreset call.
//
// Non-throwing: every std::filesystem call uses the error_code overload, and the
// whole body is try/catch-wrapped so no exception crosses the REAPER callback
// boundary. Returns the path object (not a narrow string) so the caller can pass
// path.u8string() to TrackFX_SetPreset (UTF-8, not ACP-converted) and delete via the
// same retained path — never a re-parsed narrow string.
std::filesystem::path writeTempPreset(const std::vector<std::uint8_t>& bytes) {
    try {
        static std::atomic<unsigned> counter{0};
        std::error_code ec;
        const std::filesystem::path dir = std::filesystem::temp_directory_path(ec);
        if (ec) return {};
        // PID in the name: two concurrent REAPER instances (stable + beta) cannot
        // collide in the shared temp dir.
        const std::string name =
            "reasampler_drop_" + std::to_string(GetCurrentProcessId()) +
            "_" + std::to_string(counter.fetch_add(1)) + ".vstpreset";
        const std::filesystem::path path = dir / name;
        std::ofstream out(path, std::ios::binary | std::ios::trunc);
        if (!out) return {};
        out.write(reinterpret_cast<const char*>(bytes.data()),
                  static_cast<std::streamsize>(bytes.size()));
        out.close();
        if (!out) {  // short write / flush failure -> don't hand REAPER a truncated preset
            std::filesystem::remove(path, ec);
            return {};
        }
        return path;
    } catch (...) {
        return {};
    }
}

}  // namespace

DropProbe probeDropTarget(int screenX, int screenY) {
    DropProbe out;
    char info[256] = {0};
    // GetThingFromPoint may return a null track together with a valid info string (its own
    // doc-comment says so), so the track and the surface are two independent facts and both
    // are reported. The pure classifier owns every token rule.
    out.track = GetThingFromPoint(screenX, screenY, info, sizeof(info));
    out.surface = classifyReaperSurface(info, out.track != nullptr);
    return out;
}

bool loadInstrumentOntoTrack(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes) {
    if (!track || presetBytes.empty()) return false;

    // Materialize the .vstpreset FIRST so an I/O failure leaves the track untouched
    // (no FX added yet — nothing to roll back).
    const std::filesystem::path presetPath = writeTempPreset(presetBytes);
    if (presetPath.empty()) return false;

    // Channel-correct FX name ("VST3:ReaSampler 9000[ beta]") sourced from the same
    // accessor the VST factory display name derives from, so the pairing invariant
    // (beta extension <-> beta VST) has no literal to drift.
    const std::string fxName = "VST3:" + vstPluginName();

    // An EXPLICIT top-level insertion position (instantiate <= -1000 IS the position, -1000
    // = first in chain), not the bare -1 — this form is documented in the SDK header. Both
    // always create a new instance; the bare form additionally leaves placement to REAPER's
    // ambient FX-chain insert point, which a drop onto an FX container/chain-window is
    // suspected (unconfirmed by experiment) to move — if so, the index handed to
    // TrackFX_SetPreset and the instance just created would stop denoting the same FX and the
    // capture would never land. The bare-form retry keeps the reference path alive if the
    // positional form is ever refused.
    // recFX = false: a normal track FX chain instance, not a record/monitoring FX.
    const int insertPos = TrackFX_GetCount(track);
    int fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false,
                                    /*instantiate=*/-1000 - insertPos);
    // Only retry with the bare form when the chain is PROVABLY unchanged (count still
    // insertPos): a negative return with the count grown means the positional add DID create
    // an instance and just reported -1 — retrying then would add a SECOND instance, leaving
    // the first orphaned (no preset applied, unreachable for rollback), which is exactly the
    // all-or-nothing violation this contract forbids.
    if (fxIndex < 0 && TrackFX_GetCount(track) == insertPos)
        fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false, /*instantiate=*/-1);

    // u8string() gives UTF-8 bytes on MSVC (not ACP-converted), so a temp dir under
    // an accented or CJK user-name is handled correctly by REAPER's path APIs.
    DropAttempt attempt;
    attempt.addedFxIndex = fxIndex;
    if (fxIndex >= 0)
        attempt.presetApplied = TrackFX_SetPreset(track, fxIndex, presetPath.u8string().c_str());

    std::error_code ec;
    std::filesystem::remove(presetPath, ec);  // transient regardless of outcome

    const DropOutcome outcome = decideDropOutcome(attempt);
    if (outcome.rollbackFxIndex >= 0) TrackFX_Delete(track, outcome.rollbackFxIndex);
    return outcome.loaded;
}

bool performInstrumentDrop(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes) {
    if (!track || presetBytes.empty()) return false;

    Undo_BeginBlock2(nullptr);
    const bool ok = loadInstrumentOntoTrack(track, presetBytes);
    Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1);
    return ok;
}

}  // namespace reasampler
