// scope_resolve.cpp — scope/source resolution for the capture action family. See
// the header.
//
// 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; here they are extern.

#include "shell/capture/scope_resolve.h"

#include <cstring>     // strnlen — bounded read of GetTrackName's buffer
#include <filesystem>  // project-dir derivation for provenance parent resolution
#include <utility>

#include "core/capture/render_window.h"      // itemExtentPrintsWindow
#include "shell/capture/provenance_shell.h"  // fxChainIdentity* / *SourceFiles / bankFileRefs
#include "shell/capture/track_guid.h"        // guidString

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_GetSet_LoopTimeRange
#define REAPERAPI_WANT_CountTracks
#define REAPERAPI_WANT_GetTrack
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetSetProjectInfo
#define REAPERAPI_WANT_CountSelectedMediaItems
#define REAPERAPI_WANT_GetSelectedMediaItem
#define REAPERAPI_WANT_GetMediaItem_Track
#define REAPERAPI_WANT_GetMediaItemInfo_Value
#define REAPERAPI_WANT_CountSelectedTracks
#define REAPERAPI_WANT_GetSelectedTrack
#define REAPERAPI_WANT_GetTrackName
#include "reaper_plugin_functions.h"

namespace reasampler::capture {

namespace {

// Time selection -> exact bounds (no rounding). GetSet_LoopTimeRange(isSet=false,
// isLoop=false) reads the current time selection.
bool resolveTimeSelection(double& start, double& end)
{
    start = 0.0; end = 0.0;
    GetSet_LoopTimeRange(false, false, &start, &end, false);
    return end > start;
}

// Maps a capture FX scope onto the pure provenance scope (kept decoupled so the
// pure provenance module does not depend on render_settings).
model::ProvenanceScope provenanceScopeFor(CaptureScope scope)
{
    return scope == CaptureScope::Item ? model::ProvenanceScope::Item
                                       : model::ProvenanceScope::Track;
}

// Collects the tracks that own the selected items (Item scope) into
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
// item capture hears take/item FX only. GUIDs recorded for provenance.
// extentStart/extentEnd come back as the union of the selected items' own extents:
// INFERRED to be the window REAPER's selected-items render source would print
// (SDK ~1990: D_POSITION/D_LENGTH in seconds) — unverified; see
// src/core/capture/CLAUDE.md §Gotchas for what that inference rests on.
bool collectSelectedItemTracks(ResolvedSource& out,
                               double& extentStart, double& extentEnd)
{
    const int n = CountSelectedMediaItems(nullptr);
    if (n <= 0) return false;
    bool anyExtent = false;
    for (int i = 0; i < n; ++i)
    {
        MediaItem* it = GetSelectedMediaItem(nullptr, i);
        if (!it) continue;
        const double pos = GetMediaItemInfo_Value(it, "D_POSITION");
        const double len = GetMediaItemInfo_Value(it, "D_LENGTH");
        if (!anyExtent) { extentStart = pos; extentEnd = pos + len; anyExtent = true; }
        else {
            if (pos < extentStart)       extentStart = pos;
            if (pos + len > extentEnd)   extentEnd   = pos + len;
        }
        MediaTrack* tr = GetMediaItem_Track(it);
        if (!tr) continue;
        // Dedup: several selected items can share a track.
        bool seen = false;
        for (MediaTrack* t : out.sourceTracks) if (t == tr) { seen = true; break; }
        if (seen) continue;
        out.sourceTracks.push_back(tr);
        out.trackNames.push_back(trackName(tr));
        std::string g = guidString(tr);
        if (!g.empty()) out.trackGuids.push_back(std::move(g));
    }
    return !out.sourceTracks.empty();
}

// The header says only that PROJECT_SRATE is IGNORED unless PROJECT_SRATE_USE is set
// (SDK ~3064); that it READS 0 on a project which never pinned a rate is an
// inference, unverified. Either way a non-positive answer is passed through as
// "unknown", which the pure window comparison handles by falling back to exact
// equality.
int projectSampleRate()
{
    ReaProject* proj = EnumProjects(-1, nullptr, 0);
    if (!proj) return 0;
    return static_cast<int>(GetSetProjectInfo(proj, "PROJECT_SRATE", 0.0, false));
}

}  // namespace

std::string trackName(MediaTrack* tr)
{
    if (!tr) return {};
    // Track names are user-typed and unbounded; 1 KB is far past any real one, and this
    // runs once per capture, not per frame.
    std::vector<char> buf(1024, '\0');
    if (!GetTrackName(tr, buf.data(), static_cast<int>(buf.size()))) return {};
    // Bounded construction: the SDK doesn't document NUL-termination within bufOut_sz, so
    // strnlen over the whole buffer (never past it) rather than trusting one.
    return std::string(buf.data(), strnlen(buf.data(), buf.size()));
}

// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
// start, end, envGuidString) and returns the union of parsed track-audio areas.
// Reads only — never clears the razor selection.
bool resolveRazorRange(double& start, double& end)
{
    std::vector<RazorRange> allRanges;
    const int n = CountTracks(nullptr);
    for (int i = 0; i < n; ++i)
    {
        MediaTrack* tr = GetTrack(nullptr, i);
        if (!tr) continue;
        std::vector<char> buf(8192, '\0');
        if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
            continue;
        std::vector<RazorRange> ranges = parseRazorEdits(std::string(buf.data()));
        for (auto& r : ranges) allRanges.push_back(r);
    }
    if (allRanges.empty()) return false;
    RazorRange u = razorUnionBounds(allRanges);
    start = u.startSeconds;
    end   = u.endSeconds;
    return end > start;
}

bool resolveRange(double& start, double& end, std::string& why)
{
    double rzStart = 0.0, rzEnd = 0.0;
    const bool hasRazor = resolveRazorRange(rzStart, rzEnd);
    if (inferRangeSource(hasRazor) == RangeSource::Razor)
    {
        start = rzStart; end = rzEnd;
        return true;  // resolveRazorRange already verified end > start
    }
    if (resolveTimeSelection(start, end)) return true;
    why = "make a razor area or a time selection first";
    return false;
}

bool collectSelectedTracks(ResolvedSource& out)
{
    const int n = CountSelectedTracks(nullptr);  // nullptr = active project
    if (n <= 0) return false;
    for (int i = 0; i < n; ++i)
    {
        MediaTrack* tr = GetSelectedTrack(nullptr, i);
        if (!tr) continue;
        out.sourceTracks.push_back(tr);
        out.trackNames.push_back(trackName(tr));
        std::string g = guidString(tr);
        if (!g.empty()) out.trackGuids.push_back(std::move(g));
    }
    return !out.sourceTracks.empty();
}

bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& why)
{
    double itemExtentStart = 0.0, itemExtentEnd = 0.0;
    switch (scope)
    {
        case CaptureScope::Item:
            if (!collectSelectedItemTracks(out, itemExtentStart, itemExtentEnd)) {
                why = "select at least one media item"; return false;
            }
            break;
        case CaptureScope::Track:
            if (!collectSelectedTracks(out)) {
                why = "select at least one track"; return false;
            }
            break;
    }
    if (!resolveRange(out.startSeconds, out.endSeconds, why)) return false;

    if (scope == CaptureScope::Item)
        out.itemExtentIsWindow = itemExtentPrintsWindow(
            out.startSeconds, out.endSeconds, itemExtentStart, itemExtentEnd,
            projectSampleRate());
    return true;
}

// Empty for an unsaved project (EnumProjects writes an empty .rpp path), which
// makes every bank file resolve empty -> no false parentage.
std::string currentProjectDir()
{
    std::vector<char> buf(4096, '\0');
    EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
    const std::string rpp(buf.data());
    if (rpp.empty()) return {};
    namespace fs = std::filesystem;
    std::string dir = fs::path(rpp).parent_path().string();
    for (char& c : dir) if (c == '\\') c = '/';
    if (dir.size() > 1 && dir.back() == '/') dir.pop_back();
    return dir;
}

// Detection rule: the capture's source item media file(s) must all resolve, by
// exact normalized absolute path, to one bank sample's file.
std::optional<model::Provenance> buildCaptureProvenance(
    const BankBook& book, const CaptureRequest& req,
    CaptureScope scope, const ResolvedSource& src)
{
    const std::string projectDir = currentProjectDir();
    const std::vector<model::BankFileRef> bankFiles = bankFileRefs(book, projectDir);

    // What "the source audio" means depends on scope: item scope uses the selected
    // items (the user picked them); track scope uses the range-overlapping items on
    // the source tracks (the user picked the track, not the item).
    const std::vector<std::string> sourceFiles =
        scope == CaptureScope::Item
            ? selectedItemSourceFiles()
            : trackItemSourceFiles(src.sourceTracks, req.startSeconds,
                                   req.endSeconds);

    const std::optional<std::string> parentId =
        model::detectParent(sourceFiles, bankFiles);
    if (!parentId) return std::nullopt;  // not a resample-from-sample — no provenance

    model::CaptureRecipe recipe;
    recipe.scope        = provenanceScopeFor(scope);
    recipe.sourceMode   = static_cast<int>(req.sourceMode);
    recipe.startSeconds = req.startSeconds;
    recipe.endSeconds   = req.endSeconds;
    recipe.tailMode     = static_cast<int>(req.tailMode);
    recipe.tailMs       = req.tailMs;
    recipe.sampleRate   = req.sampleRate;
    recipe.channelCount = req.channelCount;
    recipe.trackGuids   = req.trackGuids;
    // The in-scope FX-chain identity:
    //   Track scope — per-track chains combined in track order (TrackFX_*).
    //   Item scope  — per-item active-take chains combined in item order (TakeFX_*);
    //     the owning track's FX chain is OUT OF SCOPE for an item capture and must
    //     not be fingerprinted here (it is bypassed during render, not heard).
    if (scope == CaptureScope::Item) {
        const int n = CountSelectedMediaItems(nullptr);
        std::vector<MediaItem*> items;
        items.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
        for (int i = 0; i < n; ++i) {
            MediaItem* it = GetSelectedMediaItem(nullptr, i);
            if (it) items.push_back(it);
        }
        recipe.fxChainIdentity = fxChainIdentityForItems(items);
    } else {
        std::vector<std::string> perTrack;
        perTrack.reserve(src.sourceTracks.size());
        for (MediaTrack* tr : src.sourceTracks)
            perTrack.push_back(fxChainIdentityForTrack(tr));
        recipe.fxChainIdentity = model::combineChainIdentities(perTrack);
    }

    model::Provenance prov;
    prov.parentSampleId  = *parentId;
    prov.fxChainSnapshot = model::buildFingerprint(recipe);
    return prov;
}

}  // namespace reasampler::capture
