// usage_scan.cpp — see usage_scan.h. The REAPER reads behind the instance-usage
// prune protection; every decision is in the pure sample_usage module, this TU
// only reads.
//
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
// pointers (CLAUDE.md §contract). REAPER symbols used here (EnumProjExtState,
// GetProjExtState, CountTracks/GetTrack/GetMasterTrack, TrackFX_GetCount/
// GetRecCount/GetNamedConfigParm, CountMediaItems/GetMediaItem, CountTakes/
// GetMediaItemTake, GetMediaItemTrack, TakeFX_GetCount/GetNamedConfigParm) are
// verified against vendor/reaper-sdk/sdk/reaper_plugin_functions.h.

#include "shell/persist/usage_scan.h"

#include <cstdlib>
#include <functional>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>

#include "core/version/app_version.h"      // vstPluginName / vstOutputName (channel name needles)
#include "core/wire/ext_state_read.h"      // readProjExtStateGrowing — the shared grow-loop policy
#include "ext_keys.h"         // kProjExtNamespace / kProjExtUsageKeyPrefix
#include "core/wire/instrument_drop.h"  // vstClassIdHex — the frozen channel class-UID hex
#include "core/wire/sample_usage.h"     // identityMatches, foldUsageRecords (the pure decisions)
#include "shell/capture/track_guid.h"       // guidString — the canonical GUID key formatter

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjExtState
#define REAPERAPI_WANT_GetProjExtState
#define REAPERAPI_WANT_CountTracks
#define REAPERAPI_WANT_GetTrack
#define REAPERAPI_WANT_GetMasterTrack
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetRecCount
#define REAPERAPI_WANT_TrackFX_GetNamedConfigParm
#define REAPERAPI_WANT_CountMediaItems
#define REAPERAPI_WANT_GetMediaItem
#define REAPERAPI_WANT_CountTakes
#define REAPERAPI_WANT_GetMediaItemTake
#define REAPERAPI_WANT_GetMediaItemTrack
#define REAPERAPI_WANT_TakeFX_GetCount
#define REAPERAPI_WANT_TakeFX_GetNamedConfigParm
#include "reaper_plugin_functions.h"

namespace reasampler {

// This TU speaks the sample_usage wire vocabulary wholesale (UsageRecord /
// decodeUsageRecord / foldUsageRecords / identityMatches / toUpperAscii) plus
// the channel-identity accessors + the preset class-id hex.
using namespace reasampler::wire;
using version::vstOutputName;
using version::vstPluginName;

namespace {

// The three UPPERCASED channel needles identityMatches (pure, sample_usage) checks
// every FX identity string against. One instance drives the whole scan.
struct FxIdentityNeedles {
    std::string uidHexUpper;      // 32-hex VST3 class UID (may not appear on all builds)
    std::string outputNameUpper;  // "REASAMPLER_9000" — the .vst3 filename base fx_ident embeds
    std::string nameUpper;        // "REASAMPLER 9000" — factory display name
};

// A named-config-parm getter abstracted over the FX-chain kind: track FX and take FX
// share the identical identity walk (fx_ident + original_name + container recursion),
// differing only in which REAPER getter reads the parm.
using FxParmGetter =
    std::function<std::string(int fxId, const char* parm)>;

// True if any FX in the (possibly container-nested) sub-chain rooted at
// `fxId` is a ReaSampler 9000. Both fx_ident and original_name are checked (a
// renamed instance may keep its original_name; fx_ident carries the module
// path and survives a rename). Containers are walked via the documented
// container_count / container_item.X addressing (v7.06+); on a chain kind or
// REAPER version without containers the parm read returns empty and
// recursion is a no-op. `depth` bounds pathological nesting. fx_ident is
// queried per FX — chain enumeration is chunk-level, so OFFLINE instances
// match too (a Design-View-parked instance must keep protecting its holds).
bool fxSubtreeHasInstance(const FxParmGetter& parm, int fxId,
                          const FxIdentityNeedles& id, int depth) {
    if (identityMatches(parm(fxId, "fx_ident"), id.uidHexUpper, id.nameUpper,
                        id.outputNameUpper) ||
        identityMatches(parm(fxId, "original_name"), id.uidHexUpper, id.nameUpper,
                        id.outputNameUpper))
        return true;
    const std::string countStr = parm(fxId, "container_count");
    if (countStr.empty()) return false;  // not a container; no children to miss
    if (depth <= 0) {
        // Descent budget exhausted on a node that IS a container: we cannot
        // prove none of its children is an instance, so treat the incomplete
        // walk as a positive identification (protect direction).
        return true;
    }
    const int n = std::atoi(countStr.c_str());
    for (int k = 0; k < n; ++k) {
        const std::string item =
            parm(fxId, ("container_item." + std::to_string(k)).c_str());
        if (item.empty()) continue;
        const int childId = std::atoi(item.c_str());
        if (childId <= 0) continue;
        if (fxSubtreeHasInstance(parm, childId, id, depth - 1)) return true;
    }
    return false;
}

// Raised from 8 to 32 (defense in depth against truncation). Real-world FX
// containers are typically 2-4 levels deep; 32 is unreachable in practice
// while remaining finite. The truncation->protect-all guard above is the
// primary protection even at this depth.
constexpr int kMaxContainerDepth = 32;

std::string trackFxParm(MediaTrack* tr, int fxId, const char* parm) {
    char buf[2048] = {0};
    if (!TrackFX_GetNamedConfigParm(tr, fxId, parm, buf, static_cast<int>(sizeof(buf))))
        return {};
    return std::string(buf);
}

std::string takeFxParm(MediaItem_Take* take, int fxId, const char* parm) {
    char buf[2048] = {0};
    if (!TakeFX_GetNamedConfigParm(take, fxId, parm, buf, static_cast<int>(sizeof(buf))))
        return {};
    return std::string(buf);
}

// True if `tr` hosts >= 1 ReaSampler 9000 anywhere: normal chain, record/input chain
// (index | 0x1000000), containers recursively.
bool trackHasInstance(MediaTrack* tr, const FxIdentityNeedles& id) {
    const FxParmGetter parm = [tr](int fxId, const char* p) {
        return trackFxParm(tr, fxId, p);
    };
    const int n = TrackFX_GetCount(tr);
    for (int i = 0; i < n; ++i) {
        if (fxSubtreeHasInstance(parm, i, id, kMaxContainerDepth)) return true;
    }
    const int rec = TrackFX_GetRecCount(tr);
    for (int i = 0; i < rec; ++i) {
        if (fxSubtreeHasInstance(parm, 0x1000000 + i, id, kMaxContainerDepth))
            return true;
    }
    return false;
}

// True if any take FX on `item` is a ReaSampler 9000 (all takes, not just
// active — a non-active take's instance still exists and reactivates with
// the take). Same identity walk as the track path.
bool itemHasInstance(MediaItem* item, const FxIdentityNeedles& id) {
    const int takes = CountTakes(item);
    for (int t = 0; t < takes; ++t) {
        MediaItem_Take* take = GetMediaItemTake(item, t);
        if (!take) continue;
        const FxParmGetter parm = [take](int fxId, const char* p) {
            return takeFxParm(take, fxId, p);
        };
        const int n = TakeFX_GetCount(take);
        for (int i = 0; i < n; ++i) {
            if (fxSubtreeHasInstance(parm, i, id, kMaxContainerDepth)) return true;
        }
    }
    return false;
}

// The usage record scales with the hold count, so a fixed buffer risks a
// truncated decode; uses the shared grow-loop policy. Returns nullopt when
// the key cannot be read whole (absent, or > 16 MB give-up). The caller only
// queries keys the enumeration just listed, so nullopt here is a
// present-but-unreadable record: it folds to abortPrune.
std::optional<std::string> readExtStateValue(ReaProject* proj, const char* key) {
    const GrowingExtStateRead read = readProjExtStateGrowing(
        [&](char* buf, int cap) {
            return GetProjExtState(proj, kProjExtNamespace(), key, buf, cap);
        });
    if (read.status != GrowingExtStateRead::Status::Complete) return std::nullopt;
    return read.value;
}

}  // namespace

UsageFoldResult scanInstanceUsage(void* projOpaque) {
    ReaProject* proj = static_cast<ReaProject*>(projOpaque);
    UsageFoldResult result;

    // Enumerate rsusage_* keys, then read+decode via the growing reader
    // (EnumProjExtState's fixed val buffer could truncate a large record).
    std::vector<std::string> usageKeys;
    {
        const std::string prefix = kProjExtUsageKeyPrefix;  // hoisted: one alloc, not N
        char keyBuf[256];
        for (int idx = 0;; ++idx) {
            keyBuf[0] = '\0';
            if (!EnumProjExtState(proj, kProjExtNamespace(), idx, keyBuf,
                                  static_cast<int>(sizeof(keyBuf)), nullptr, 0))
                break;
            const std::string key(keyBuf);
            if (key.compare(0, prefix.size(), prefix) == 0) usageKeys.push_back(key);
        }
    }
    if (usageKeys.empty()) return result;  // no instance ever published — skip the scan

    std::vector<DecodedUsage> decoded;
    decoded.reserve(usageKeys.size());
    for (const std::string& key : usageKeys) {
        const std::optional<std::string> value = readExtStateValue(proj, key.c_str());
        // Unreadable or undecodable both land as a nullopt record; the pure fold
        // turns either into the abort and names the key.
        decoded.push_back(DecodedUsage{
            key, value ? decodeUsageRecord(*value) : std::nullopt});
    }

    // Enumerate live ReaSampler 9000 hosts; a track needs only one instance to
    // keep all its records live.
    FxIdentityNeedles id;
    id.uidHexUpper = toUpperAscii(vstClassIdHex());
    id.outputNameUpper = toUpperAscii(vstOutputName());
    id.nameUpper = toUpperAscii(vstPluginName());
    std::unordered_set<std::string> liveTrackGuids;
    bool anyLive = false;

    if (MediaTrack* master = GetMasterTrack(proj)) {
        if (trackHasInstance(master, id)) {
            liveTrackGuids.insert(guidString(master));
            anyLive = true;
        }
    }
    const int trackCount = CountTracks(proj);
    for (int i = 0; i < trackCount; ++i) {
        MediaTrack* tr = GetTrack(proj, i);
        if (!tr) continue;
        if (trackHasInstance(tr, id)) {
            liveTrackGuids.insert(guidString(tr));
            anyLive = true;
        }
    }
    // Take-FX instances: attributed to the owning track (the VST-side getReaperParent(1)
    // resolves the same track), and they set anyLive for the empty-guid fallback.
    const int itemCount = CountMediaItems(proj);
    for (int i = 0; i < itemCount; ++i) {
        MediaItem* item = GetMediaItem(proj, i);
        if (!item) continue;
        if (itemHasInstance(item, id)) {
            if (MediaTrack* tr = GetMediaItemTrack(item)) {
                liveTrackGuids.insert(guidString(tr));
            }
            anyLive = true;
        }
    }

    // The pure fold decides: abort on any unreadable record; protect-all when
    // zero instances were identified; otherwise the per-record liveness rule.
    return foldUsageRecords(decoded, liveTrackGuids, anyLive);
}

}  // namespace reasampler
