#pragma once
// render_settings — the REAPER-free logic behind the capture action family:
// sourceMode -> RENDER_SETTINGS bits, P_RAZOREDITS parsing + range union,
// razor-else-time inference, the FX-scope bypass plan, the capture-action
// table main.cpp iterates, and the multi-track-stem refusal + its user-facing
// message text. Bit MEANINGS below are transcribed verbatim from
// reaper_plugin_functions.h; the CHOICE of which bits each mode sets is tested.

#include <string>
#include <vector>

#include "core/model/bank_model.h"  // SourceMode (pure enum)

namespace reasampler::capture {

using model::SourceMode;

// --- RENDER_SETTINGS source/processing bits (verbatim from SDK header ~3041) --
inline constexpr int kRenderMasterMix          = 0;      // (&(1|2))==0, no source bits
inline constexpr int kRenderSelItems           = 32;     // &32   selected media items
inline constexpr int kRenderSelItemsViaMaster  = 64;     // &64   selected media items via master
inline constexpr int kRenderSelTracksViaMaster = 128;    // &128  selected tracks via master
inline constexpr int kRenderRazorEdits         = 4096;   // &4096 render razor edits
// kRenderPreFaderStems (&8192) is deliberately NOT used — REAPER offline render
// has no true pre-FX "dry" bit. FX scoping is done by the FX-bypass-around-render
// mechanism (see fxBypassPlan below), not by any render bit. All capture actions
// render wet; the scope decides which FX remain enabled.
inline constexpr int kRenderSingleFile         = (4 << 16);  // items/razor -> one file

// --- Render bounds mode -------------------------------------------------------
//
// A capture hands its window over on RENDER_BOUNDSFLAG=2 — the project's own TIME
// SELECTION (value verbatim, header ~3042), written through GetSet_LoopTimeRange.
//
// Custom time bounds (RENDER_BOUNDSFLAG=0, RENDER_STARTPOS/RENDER_ENDPOS, header
// ~3045-3046) must NOT be reintroduced: REAPER resolved a custom-bounds window on a
// whole-millisecond grid AT RENDER TIME, floored the end, wrote the floored value back
// over RENDER_ENDPOS, and rendered exactly the floored frame count — twice, to the
// frame. Re-rendering on this mode came back exact on both edges, including a start
// carrying a sub-millisecond remainder, which is what locates the floor in the
// custom-bounds field rather than downstream in the render engine. This is the one
// narrative home for that; other sites point here.
inline constexpr int kRenderBoundsTimeSelection = 2;

// --- Tail: RENDER_NORMALIZE / RENDER_TRIMEND bits + named constants ----------
//
// RENDER_NORMALIZE (verbatim, header ~3051): &32768 = trim ending silence (Auto
// path); &(4<<16) = disable all render postprocessing (None/Manual path).
inline constexpr int kNormalizeTrimEnd    = 32768;        // &32768 trim ending silence
inline constexpr int kNormalizeDisableAll = (4 << 16);    // &(4<<16) = 262144, disable all

inline constexpr int kTailFlagNone = 0;

// RENDER_TAILFLAG's bits are keyed PER BOUNDS MODE (header ~3047): &4 is the
// time-selection mode's bit, the pair of kRenderBoundsTimeSelection above. A tail set
// under a different mode's bit renders no tail at all, so these two move together.
inline constexpr int kTailFlagTimeSelection = 4;

// Auto-trim trailing-silence threshold; single source of truth (RENDER_TRIMEND
// ratio derives from this dB, never the reverse). Daniel-set.
inline constexpr double kAutoTrimThresholdDb = -72.0;

// Runaway guard: max tail rendered past the range end, so a non-decaying or
// looping signal doesn't render forever. Daniel-set; shared by offline+realtime.
inline constexpr double kMaxTailSeconds = 8.0;
inline constexpr double kMaxTailMs      = 8000.0;

// Derived linear amplitude ratio for RENDER_TRIMEND (header ~3062: an amplitude
// ratio, "0.5 means -6.02 dB", i.e. 10^(dB/20)) from kAutoTrimThresholdDb.
// Function not constant: std::pow isn't constexpr before C++26.
double autoTrimEndRatio();

// The three tail states — see src/core/capture/CLAUDE.md.
enum class TailMode {
    None,
    Auto,
    Manual,
};

// The RENDER_* values a tail mode drives, in addition to the exact STARTPOS/ENDPOS
// the backend already sets. `trimEnd` is meaningful only when the trim-end
// normalize bit is set (Auto). The backend reads these straight onto
// GetSetProjectInfo.
struct TailRenderSettings {
    int    tailFlag  = kTailFlagNone;         // RENDER_TAILFLAG (0 or the bounds mode's bit)
    double tailMs    = 0.0;                    // RENDER_TAILMS
    int    normalize = kNormalizeDisableAll;   // RENDER_NORMALIZE
    double trimEnd   = 0.0;                    // RENDER_TRIMEND (only used when trim bit set)
};

// Maps a tail mode (+ requested manual tail ms, used only for Manual) to its
// RENDER_* values. Manual is clamped to kMaxTailMs regardless of source.
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs);

// The realtime record-window end (project seconds): realtime does NOT drive
// RENDER_*, it records a generous window and trims later, so this is where the
// transport actually stops. None -> exact rangeEndSeconds; Auto -> +8s runaway
// cap; Manual -> + clamp(manualTailMs, kMaxTailMs)/1000.
double realtimeRecordWindowEnd(TailMode mode, double rangeEndSeconds,
                               double manualTailMs);

// The RENDER_SETTINGS value for a given source mode. `supported` is false only
// for SourceMode::Realtime (that backend doesn't use offline render).
struct RenderSettingsChoice {
    int  settings  = kRenderMasterMix;
    bool supported = true;  // false => not an offline-render source (e.g. Realtime)
};

// Maps a source mode to its RENDER_SETTINGS value (which content the render
// covers); FX scoping is orthogonal (done by fxBypassPlan). `wetDry` is
// accepted but ignored — retained as the seam for future dry work. CONFIRMED
// (SDK header ~3041): MasterMix/TimeSelection -> 0; SelectedTracks -> &128;
// SelectedItems -> &32|single-file; RazorArea -> &4096|single-file.
RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry);

// The render source a mode drives, in words. Exists for the offline backend's
// bounds refusal: the two ways a render can miss its window — a source that
// derives its own bounds (selected items, razor edits) versus a time-bounded
// render that came up short — are indistinguishable from a frame count alone,
// and naming the source is what tells them apart in a bug report.
//
// MasterMix and TimeSelection deliberately answer the SAME words: they map to the
// same RENDER_SETTINGS value and render identically, so naming them apart would
// assert a render distinction that does not exist.
const char* renderSourceLabel(SourceMode mode);

// --- Capture scope: the FX-scope invariant ------------------------------------
//
// See src/core/capture/CLAUDE.md for the scope contract. There is NO master
// scope; the master track's FX/gain/pan are still NEUTRALIZED as part of the
// out-of-scope chain (bypassMaster below) — master is a bypass target only.
enum class CaptureScope {
    Item,
    Track,
};

// The render source mode each scope drives. Track scope always captures its
// selected tracks (via master), time-bounded by RENDER_STARTPOS/ENDPOS.
//
// Item scope captures the selected items ONLY when `itemExtentIsWindow` — i.e.
// when those items' own extent already prints the requested window (see
// render_window::itemExtentPrintsWindow). REAPER's selected-items render source is
// INFERRED to derive its bounds from the item extents, so a window strictly inside
// (or wider than) a selected item cannot be expressed through it; that case renders
// time-bounded through the items' own tracks. The inference is unverified — see
// src/core/capture/CLAUDE.md §Gotchas for what it rests on.
//
// The FX SCOPE is unaffected by the swap (fxBypassPlanFor is keyed on CaptureScope,
// not on the source mode, so an item capture still hears take/item FX only), but the
// CONTENT reaching the render is not: the selected-tracks source prints everything
// upstream of the track — its folder children and its receives — which the shell
// transiently silences (shell/capture/render_isolation). An overlapping item on the
// track ITSELF is deliberately not isolated; see src/shell/capture/CLAUDE.md.
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow);

// True for the one render shape that cannot land as a single capture: a selected-tracks
// render covering more than one track — a ranged item capture whose items span several
// tracks, or any multi-track track capture. That source is read as rendering one file
// per selected track — the single-file bit is documented for item/razor sources only
// (SDK header ~3041), which is the whole basis for the reading and is DAW-unverified.
// If it holds, N tracks collapse N stems onto one literal render pattern and whichever
// file survived would land as a successful capture carrying one track's audio. The
// caller refuses instead.
//
// Scope is deliberately NOT a parameter: the exposure comes from the render SOURCE,
// which both scopes reach.
bool isMultiTrackStemRender(SourceMode mode, int sourceTrackCount);

// The refusal text for the shape above. Keyed on scope because only the way OUT differs:
// an item capture can also widen its range to the items' own extent, which a track
// capture has no analog for. Kept beside the predicate so the two read as siblings.
std::string multiTrackRefusalMessage(CaptureScope scope);

// --- Range inference: razor-else-time (orthogonal to scope) -------------------
//
// Razor-present -> razor union; otherwise time selection. Razor is a range
// source, not a capture mode.
enum class RangeSource {
    Razor,          // a razor area is present -> use its union bound
    TimeSelection,  // no razor -> use the time selection
};

// Picks the range source. Pure so "razor wins when present" is tested without
// a DAW; the shell supplies whether any razor area was found.
RangeSource inferRangeSource(bool hasRazorArea);

// --- FX-bypass plan: which tracks' FX to bypass for a scope -------------------
//
// Given a CaptureScope, returns three boolean flags: bypass (a) the captured
// track's OWN FX, (b) every ancestor (parent/folder) track's FX, (c) the
// master FX. The caller (FxBypassGuard, shell) walks the ancestor chain via
// GetParentTrack, clears I_FXEN on each flagged track (RAII restore), and also
// neutralizes D_VOL/D_PAN/D_WIDTH/D_PANLAW/I_PANMODE to unity/center on the
// same set (I_PANMODE is load-bearing: in pan mode 6, D_PAN/D_WIDTH are
// ignored entirely, so forcing it is what makes the other neutralizations
// take effect) — I_FXEN alone doesn't touch a track's volume/pan. This plan
// selects the set; the guard applies both the FX bypass and the neutralize.
struct FxBypassPlan {
    bool bypassSelfFx     = false;  // the captured track's own FX
    bool bypassAncestorFx = false;  // every ancestor (parent/folder) track's FX
    bool bypassMaster     = false;  // the master track's FX
};

FxBypassPlan fxBypassPlanFor(CaptureScope scope);

// A single razor-edit area: a time range on one track (envelope GUID ignored —
// razor captures target track-audio areas, not envelope lanes).
struct RazorRange {
    double startSeconds = 0.0;
    double endSeconds   = 0.0;
};

// Parses ONE track's P_RAZOREDITS string (SDK header ~2899): space-separated
// TRIPLES of <start> <end> <envGuidString>, envGuid == `""` for a track-audio
// area vs a GUID for an envelope-lane area. Returns only track-audio ranges
// (envelope-lane triples skipped); malformed trailing tokens are ignored, not
// fatal; a range with end <= start is dropped.
std::vector<RazorRange> parseRazorEdits(const std::string& razorString);

// The union bound (min start, max end) of a set of razor ranges — the exact
// window the offline render must cover. {0,0} for empty input ("no razor area").
RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges);

// --- Capture-action taxonomy (the bindable set main.cpp registers) -----------
//
// One row per bindable scope action (item/track); range inference and tail
// mode are read at fire time, not baked into the row. The row stores only the
// channel-agnostic command-id SUFFIX + description PHRASE; the registering
// shell composes the full channel-qualified id/name via app_version.
//
// commandSuffix is FOREVER-STABLE (user keybindings key off the composed id).
struct CaptureActionDef {
    const char*  commandSuffix;      // e.g. "CAPTURE_TRACK" — FOREVER-STABLE (composed w/ prefix)
    const char*  descriptionPhrase;  // e.g. "capture selected track(s)" — Actions-list phrase
    const char*  baseName;           // file-stem FALLBACK; the source track normally names the capture
    CaptureScope scope;              // FX scope (item / track)
};

// The capture-action table. Iterated by main.cpp to register the family and
// route each fired command back to its definition.
//
// Two rows: CAPTURE_ITEM / CAPTURE_TRACK. There is no master capture — to
// capture the master you render a track.
const std::vector<CaptureActionDef>& captureActionTable();

} // namespace reasampler::capture
