// bake_plan — the programmed note resolved against a concrete sample rate: the frames the
// offline pass renders, the slice of them the capture keeps, and the two event frames.
//
// Separate from bake_render because the plan is what a preview and a bake must agree on;
// the render is only one consumer of it.

#pragma once

#include <cstdint>
#include <optional>

#include "core/instrument/engine/play_params.h"  // SampleData (the dialed sound)
#include "core/instrument/note/note_program.h"

namespace reasampler::instrument::bake {

// The render's frame ceiling, refused like any other degenerate window. A legal offset
// magnitude reaches ~11.6 days, and renderBake allocates two channel buffers plus an
// interleaved one from the window — an unbounded one is a bad_alloc inside a UI tick, not a
// long bake. ~5.5 minutes at 48 kHz, past any musical programmed note.
inline constexpr std::int64_t kMaxBakeFrames = 16'000'000;

// Whether the window needs a user-supplied hold. A Gate voice over an ACTIVE sustain loop
// sounds for as long as it is held, by definition — there is no intrinsic end to derive, and
// this is the ONLY case in which there isn't. Answered by the engine's own loop fold, so the
// control that collects the hold cannot appear for a loop the voice would refuse.
bool bakeWindowNeedsHold(PlayMode mode, const SampleLoop& loop, std::int64_t crossfadeFrames,
                         std::int64_t frameCount);
bool bakeWindowNeedsHold(const SampleData& dialed);

// The bake's programmed note, DERIVED from the dialed sound at `renderSampleRate` (the rate
// the bake renders at, which is what the engine's frame counts are consumed against):
//
//   Trigger      — the note IS the play span (note-off is ignored anyway), stretched by the
//                  slowest read the dialed voice can reach: Rate under BOTH engines, plus the
//                  deepest downward pitch offset under Varispeed.
//   Gate, loop   — `hold` is the note length; the end offset is the release.
//   Gate, no loop— the read head runs off the source and frees the voice whatever the gate is
//                  doing, so the note is the whole post-start span, stretched the same way.
//
// Both derived lengths are EXACT durations, not ladder rungs: a source longer than the
// ladder's top rung has no rung that covers it, and quantizing up to one overshoots every
// other source (see note/CLAUDE.md). `hold` alone stays musical — it is a picker.
//
// Every case is padded by the voice's terminal declick ramp (kDeclickFrames): trailing
// silence is free, and closing the window on the frame the ramp starts is a hard cut.
// `hold` is read only in the Gate-with-loop case; `velocity` is the velocity the note fires
// at, and it feeds the Varispeed half of that stretch as well as the render.
//
// Takes no tempo: nothing derived here is beat-denominated. The one field that is — `hold` —
// meets the tempo in resolveNote, with the rest of the program's beat-denominated fields.
note::NoteProgram defaultBakeProgram(const SampleData& dialed, int renderSampleRate,
                                     note::Division hold, note::Velocity velocity);

// The render window in frames. TWO domains meet here: `totalFrames` is the captured FILE's
// length, everything else counts RENDER frames from whichever comes first, note-on or the
// capture opening. A positive start offset (legal — it trims the attack) puts note-on at
// render frame 0 and the file's frame 0 `leadInFrames` later; a negative one does the
// reverse, and the file opens on silence before the note. Either event frame may sit past
// the render, which then closes before the note ever fires — a legal empty capture.
struct BakePlan {
    std::int64_t totalFrames = 0;   // frames in the captured file
    std::int64_t leadInFrames = 0;  // rendered ahead of the file's frame 0, then discarded
    std::int64_t noteOnFrame = 0;   // both in render frames
    std::int64_t noteOffFrame = 0;
    // The capture's root: rendering AT root is what makes the root survivable, which is
    // why the root parameter is the one processing control a bake does not reset.
    int note = 60;
    int velocity = 100;
    int sampleRate = 0;

    std::int64_t renderFrames() const { return leadInFrames + totalFrames; }
};

// Why a window was refused. The two are different user problems and read as different
// sentences: an empty window is a programming mistake, a window past the ceiling is a legal
// dialed sound that simply cannot be held in one pass.
enum class BakeRefusal : std::uint8_t {
    None,
    EmptyWindow,       // collapsed, a non-positive rate, or a window that rounds to no frames
    PastFrameCeiling,  // representable but longer than kMaxBakeFrames
};

// The one `ResolvedNote` + rate -> frames resolution. A degenerate or unholdable window is
// refused rather than rendered; `refusal` is None iff `plan` holds one. `rootNote` and the
// resolved velocity are clamped into MIDI range.
struct PlannedBake {
    std::optional<BakePlan> plan;
    BakeRefusal refusal = BakeRefusal::None;
};

PlannedBake planBake(const note::ResolvedNote& resolved, int sampleRate, int rootNote);

}  // namespace reasampler::instrument::bake
