// 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 "render_settings.h"

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

namespace reasampler {

double autoTrimEndRatio() {
    // Amplitude ratio = 10^(dB/20). Derived from kAutoTrimThresholdDb so the dB is
    // the single source of truth (header ~3062: RENDER_TRIMEND is an amplitude ratio,
    // "0.5 means -6.02 dB"). 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 no-tail capture. Tail off,
            // disable-all normalize (the current default), no trim.
            t.tailFlag  = kTailFlagNone;
            t.tailMs    = 0.0;
            t.normalize = kNormalizeDisableAll;
            t.trimEnd   = 0.0;
            return t;

        case TailMode::Auto:
            // Generous 8 s tail, then SURGICAL normalize: ONLY the trim-ending-silence
            // bit (32768) — every other postprocessing bit clear. A fixed-threshold
            // trailing-silence trim is a pure boundary decision (it scales/limits/fades
            // nothing), so it re-introduces none of the coloring the disable-all bit
            // guarded against, and two identical requests trim at the identical sample
            // -> bit-identical repeats hold (spec §surgical normalize).
            t.tailFlag  = kTailFlagCustomBounds;
            t.tailMs    = kMaxTailMs;
            t.normalize = kNormalizeTrimEnd;
            t.trimEnd   = autoTrimEndRatio();
            return t;

        case TailMode::Manual:
            // Fixed tail, no trim -> keep the disable-all normalize exactly as the
            // no-tail path does. Clamp to the 8 s cap even here: the runaway guard
            // applies whether the length came from the Auto default or an explicit
            // request (spec §Manual override). Negative requests floor to 0.
            t.tailFlag  = kTailFlagCustomBounds;
            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:
            // Exact — no extra recording (byte-identical to today's realtime capture).
            return rangeEndSeconds;
        case TailMode::Auto:
            // The 8 s runaway cap past the range end; the decay-trim shortens it later.
            return rangeEndSeconds + kMaxTailSeconds;
        case TailMode::Manual:
            // Fixed window: range + the set length, clamped to the 8 s cap (the same
            // runaway guard the offline Manual path applies). Negative floors to 0.
            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` is accepted so CaptureRequest.wetDry remains the seam for future
    // dry work (M10 null test), but it does not affect this mapping. FX scoping is
    // handled by fxBypassPlanFor, not by these render bits.
    RenderSettingsChoice c;

    switch (mode) {
        case SourceMode::MasterMix:
        case SourceMode::TimeSelection:
            // Master IS the mix — wet-only; &(1|2)==0, no source bits.
            c.settings  = kRenderMasterMix;
            c.supported = true;
            return c;

        case SourceMode::SelectedTracks:
            // Selected tracks via master (&128) — wet (post-FX). Header ~3041.
            c.settings  = kRenderSelTracksViaMaster;
            c.supported = true;
            return c;

        case SourceMode::SelectedItems:
            // Selected media items, rendered to ONE file (single-file bit) so a
            // multi-item selection yields a single bank entry, not N wavs.
            c.settings  = kRenderSelItems | kRenderSingleFile;
            c.supported = true;
            return c;

        case SourceMode::RazorArea:
            // Render razor edits to ONE file (same single-file rationale as items).
            c.settings  = kRenderRazorEdits | kRenderSingleFile;
            c.supported = true;
            return c;

        case SourceMode::Realtime:
            // Not an offline-render source — the realtime backend (M8) owns it.
            c.settings  = kRenderMasterMix;
            c.supported = false;
            return c;
    }
    // Unreachable for a valid enum; fail closed (unsupported) rather than render.
    c.supported = false;
    return c;
}

SourceMode sourceModeForScope(CaptureScope scope) {
    switch (scope) {
        case CaptureScope::Item:   return SourceMode::SelectedItems;
        case CaptureScope::Track:  return SourceMode::SelectedTracks;
    }
    return SourceMode::SelectedItems;  // unreachable for a valid enum; fail closed
}

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:
            // Item = take/item FX ONLY. Bypass the item's own track FX, every
            // ancestor's FX, and the master's FX. (Take FX live in the item and
            // are always rendered — there is no track to bypass them from.)
            p.bypassSelfFx     = true;
            p.bypassAncestorFx = true;
            p.bypassMaster     = true;
            return p;
        case CaptureScope::Track:
            // Track = item FX + the selected track's OWN FX. Keep self FX; bypass
            // every ancestor (parent/folder) and the master. Parent/master GAIN
            // still applies (I_FXEN is FX-only) — documented boundary.
            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);

    // The string is space-separated TRIPLES: <start> <end> <envGuidString>.
    // A track-audio area's third token is the literal two-char string `""`; an
    // envelope-lane area's is a GUID `{…}`. We keep only track-audio triples.
    std::string startTok, endTok, guidTok;
    while (in >> startTok >> endTok >> guidTok) {
        // Envelope-lane areas carry a real GUID; skip them (razor captures track audio only).
        // A track-audio area's GUID token is the empty quoted string `""`.
        if (guidTok != "\"\"") continue;

        // Parse the two time tokens. std::stod throws on garbage — guard so one
        // malformed triple does not 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 tokens with trailing garbage (e.g. "1.0x") — a partial parse
            // is a malformed area, not a valid range.
            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() {
    // Built once (function-local static): two SCOPE actions, item + track. Both
    // exact bounds by default; the tail mode a capture applies is read from the
    // docked-panel setting at fire time (tail_control + bank_panel), so tail is NOT
    // a per-action variant. Ids are FOREVER-STABLE — never edit a shipped string.
    // Each action infers its range (razor-else-time) at fire time and enforces its
    // FX-scope invariant via fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET /
    // CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are RETIRED (mirror-unregistered in
    // main.cpp); the CAPTURE_MASTER scope action is REMOVED (its id is likewise
    // mirror-unregistered) — to capture the master you render a track.
    static const std::vector<CaptureActionDef> table = {
        // Item scope — item/take FX only. Suffix + phrase are channel-agnostic; the shell
        // composes the FOREVER-STABLE id (prefix + "CAPTURE_ITEM") and the display name.
        {"CAPTURE_ITEM",
         "capture selected item(s)", "item",
         CaptureScope::Item},

        // Track scope — item FX + the track's own FX.
        {"CAPTURE_TRACK",
         "capture selected track(s)", "track",
         CaptureScope::Track},
    };
    return table;
}

} // namespace reasampler
