// render_settings.cpp — pure logic for the three-scope capture action family. See header.
// NO REAPER types; unit-tested by tests/test_render_settings.cpp.

#include "core/capture/render_settings.h"

#include <algorithm>
#include <cmath>
#include <sstream>

namespace reasampler::capture {

double autoTrimEndRatio() {
    // Amplitude ratio = 10^(dB/20) (header ~3062). For -72 dB this is ~0.00025119.
    return std::pow(10.0, kAutoTrimThresholdDb / 20.0);
}

TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs) {
    TailRenderSettings t;
    switch (mode) {
        case TailMode::None:
            // Exact bounds — byte-identical to the pre-tail capture.
            t.tailFlag  = kTailFlagNone;
            t.tailMs    = 0.0;
            t.normalize = kNormalizeDisableAll;
            t.trimEnd   = 0.0;
            return t;

        case TailMode::Auto:
            // Surgical normalize: only the trim-ending-silence bit set, every other
            // postprocessing bit clear. A fixed-threshold trim scales/limits/fades
            // nothing, so identical requests trim at the identical sample -> holds
            // the bit-identical-repeats invariant.
            t.tailFlag  = kTailFlagTimeSelection;
            t.tailMs    = kMaxTailMs;
            t.normalize = kNormalizeTrimEnd;
            t.trimEnd   = autoTrimEndRatio();
            return t;

        case TailMode::Manual:
            // Clamped to the cap regardless of source; negative floors to 0.
            t.tailFlag  = kTailFlagTimeSelection;
            t.tailMs    = std::clamp(manualTailMs, 0.0, kMaxTailMs);
            t.normalize = kNormalizeDisableAll;
            t.trimEnd   = 0.0;
            return t;
    }
    // Unreachable for a valid enum; fail closed to exact bounds (never a stray tail).
    return t;
}

double realtimeRecordWindowEnd(TailMode mode, double rangeEndSeconds,
                               double manualTailMs) {
    switch (mode) {
        case TailMode::None:
            return rangeEndSeconds;  // exact, no extra recording
        case TailMode::Auto:
            return rangeEndSeconds + kMaxTailSeconds;  // runaway cap; decay-trim shortens later
        case TailMode::Manual:
            return rangeEndSeconds + std::clamp(manualTailMs, 0.0, kMaxTailMs) / 1000.0;
    }
    // Unreachable for a valid enum; fail closed to exact bounds (never a stray tail).
    return rangeEndSeconds;
}

RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) {
    // wetDry doesn't affect this mapping (seam for future dry work); FX scoping
    // is handled by fxBypassPlanFor, not by these render bits.
    RenderSettingsChoice c;

    switch (mode) {
        case SourceMode::MasterMix:
        case SourceMode::TimeSelection:
            c.settings  = kRenderMasterMix;  // wet-only, no source bits
            c.supported = true;
            return c;

        case SourceMode::SelectedTracks:
            c.settings  = kRenderSelTracksViaMaster;
            c.supported = true;
            return c;

        case SourceMode::SelectedItems:
            // Single-file bit so a multi-item selection yields one bank entry.
            c.settings  = kRenderSelItems | kRenderSingleFile;
            c.supported = true;
            return c;

        case SourceMode::RazorArea:
            c.settings  = kRenderRazorEdits | kRenderSingleFile;
            c.supported = true;
            return c;

        case SourceMode::Realtime:
            c.settings  = kRenderMasterMix;
            c.supported = false;  // not an offline-render source
            return c;
    }
    // Unreachable for a valid enum; fail closed (unsupported) rather than render.
    c.supported = false;
    return c;
}

const char* renderSourceLabel(SourceMode mode) {
    switch (mode) {
        // MasterMix and TimeSelection share this label because they ARE the same
        // render — see the header.
        case SourceMode::MasterMix:
        case SourceMode::TimeSelection:  return "master mix";
        case SourceMode::SelectedTracks: return "selected tracks via master";
        case SourceMode::SelectedItems:  return "selected media items";
        case SourceMode::RazorArea:      return "razor edits";
        case SourceMode::Realtime:       return "realtime record";
    }
    return "unknown";  // unreachable for a valid enum; never claim a source
}

SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
    switch (scope) {
        case CaptureScope::Item:
            return itemExtentIsWindow ? SourceMode::SelectedItems
                                      : SourceMode::SelectedTracks;
        case CaptureScope::Track:  return SourceMode::SelectedTracks;
    }
    // Unreachable for a valid enum; fail closed to the time-bounded render, which
    // honors the requested bounds whatever the selection is.
    return SourceMode::SelectedTracks;
}

bool isMultiTrackStemRender(SourceMode mode, int sourceTrackCount) {
    return mode == SourceMode::SelectedTracks && sourceTrackCount > 1;
}

std::string multiTrackRefusalMessage(CaptureScope scope) {
    // Deliberately does not name realtime capture as a way out, though it is the one
    // action that sums correctly here: realtime is non-deterministic (hardware/performed
    // FX, no bit-identical-repeats guarantee), so pointing an offline refusal at it would
    // trade one invariant for another rather than just naming a substitute. A stated
    // choice, not an oversight.
    switch (scope) {
        case CaptureScope::Item:
            return "This range is narrower than the selected items, so it renders "
                   "through their tracks -- and those items span more than one track, "
                   "which this shape cannot land as a single file. Capture one track's "
                   "items at a time, or make the range match the items' extent.";
        case CaptureScope::Track:
            return "A track capture renders the selected tracks through the master, "
                   "and more than one track cannot land as a single file. Capture one "
                   "track at a time, or route them into a folder/bus track and capture "
                   "that (a folder's own output is its children summed).";
    }
    // Unreachable for a valid enum; a refusal with no way out is still better than a
    // silent one, so fail closed to the scope-agnostic half of the message.
    return "This selection spans more than one track, which cannot land as a single "
           "file. Capture one track at a time.";
}

RangeSource inferRangeSource(bool hasRazorArea) {
    // Razor wins when present; otherwise the time selection. Orthogonal to scope.
    return hasRazorArea ? RangeSource::Razor : RangeSource::TimeSelection;
}

FxBypassPlan fxBypassPlanFor(CaptureScope scope) {
    FxBypassPlan p;
    switch (scope) {
        case CaptureScope::Item:
            // Take FX live in the item and are always rendered — bypass everything else.
            p.bypassSelfFx     = true;
            p.bypassAncestorFx = true;
            p.bypassMaster     = true;
            return p;
        case CaptureScope::Track:
            // Keep self FX; bypass every ancestor (parent/folder) and the master.
            p.bypassSelfFx     = false;
            p.bypassAncestorFx = true;
            p.bypassMaster     = true;
            return p;
    }
    return p;  // unreachable; bypass nothing (fail to full-chain, never over-bypass)
}

std::vector<RazorRange> parseRazorEdits(const std::string& razorString) {
    std::vector<RazorRange> ranges;
    std::istringstream in(razorString);

    std::string startTok, endTok, guidTok;
    while (in >> startTok >> endTok >> guidTok) {
        // Skip envelope-lane areas (real GUID); keep only track-audio (`""`).
        if (guidTok != "\"\"") continue;

        // std::stod throws on garbage — guard so one malformed triple doesn't
        // abort the whole parse.
        double start = 0.0, end = 0.0;
        try {
            std::size_t sp = 0, ep = 0;
            start = std::stod(startTok, &sp);
            end   = std::stod(endTok, &ep);
            // Reject trailing garbage (e.g. "1.0x") — a partial parse is malformed.
            if (sp != startTok.size() || ep != endTok.size()) continue;
        } catch (...) {
            continue;
        }

        if (end > start) ranges.push_back({start, end});  // drop empty/inverted
    }
    return ranges;
}

RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges) {
    if (ranges.empty()) return {0.0, 0.0};
    RazorRange u = ranges.front();
    for (const RazorRange& r : ranges) {
        if (r.startSeconds < u.startSeconds) u.startSeconds = r.startSeconds;
        if (r.endSeconds   > u.endSeconds)   u.endSeconds   = r.endSeconds;
    }
    return u;
}

const std::vector<CaptureActionDef>& captureActionTable() {
    // FOREVER-STABLE ids — never edit a shipped string. No master capture
    // action (its id was retired; do not reintroduce it).
    static const std::vector<CaptureActionDef> table = {
        {"CAPTURE_ITEM",
         "capture selected item(s)", "item",
         CaptureScope::Item},

        {"CAPTURE_TRACK",
         "capture selected track(s)", "track",
         CaptureScope::Track},
    };
    return table;
}

} // namespace reasampler::capture
