#pragma once
// loop_span.h — the sustain loop's ONE validity/clamp rule plus its pre-seam crossfade
// geometry. The resolve is cold (note-on, editor); crossfadeWeight is header-inline because
// it sits on the per-voice-per-sample read.

#include <cstdint>
#include <vector>

#include "core/audio/peaks.h"                     // AudioSample
#include "core/instrument/engine/play_params.h"  // SampleLoop

namespace reasampler::instrument::engine::loop {

using audio::AudioSample;

// The crossfade's own bound: it cannot outrun the material ahead of the loop (`start` source
// frames precede it) nor the loop's own length (the incoming tap is one loop length behind the
// head). Shared by resolveLoop's clamp and the editor's drag clamp so the two cannot diverge.
inline std::int64_t maxCrossfade(std::int64_t start, std::int64_t length) {
    return start < length ? start : length;
}

// A sustain loop folded against one capture: validity, geometry, and the clamped crossfade.
// `active == false` leaves every other field zero, so a caller can wrap on the flag alone.
//
// The fade is PRE-SEAM and its incoming tap is the same read head one loop length earlier —
// `pos - length`, no second position to advance. See CLAUDE.md for why that shape, and for
// the `crossfade <= start` bound it forces.
struct ResolvedLoop {
    bool active = false;
    std::int64_t start = 0;
    std::int64_t end = 0;        // half-open
    std::int64_t length = 0;     // end - start
    std::int64_t crossfade = 0;  // source frames; 0 = hard seam
    double fadeBegin = 0.0;      // end - crossfade
    double fadeInv = 0.0;        // 1 / (crossfade - 1); 0 when crossfade <= 1
};

// Folds a stored loop + crossfade against the decoded sample. Refuses anything the read path
// could not honour — a non-Gate mode, an unset loop, an inverted or empty span, a span
// reaching outside the PCM — by returning an inactive result rather than a repaired one, so a
// corrupt span silently plays through instead of reading out of bounds.
ResolvedLoop resolveLoop(const SampleLoop& loop, std::int64_t crossfadeFrames,
                         std::int64_t frameCount, bool gateMode);

// Weight of the INCOMING (pre-loop-start) tap at source position `pos`: 0 before the fade
// region, reaching exactly 1 at the LAST rendered frame (`end - 1`), not merely approaching it —
// normalizing over `crossfade - 1` rather than `crossfade` is what buys that: d at `end - 1` is
// always exactly `crossfade - 1`, the ceiling's own threshold, for any REAL crossfade
// (`crossfade >= 2`). That last frame is therefore the incoming tap outright, which is exactly
// the value the wrap hands over, so the step across the seam is the material's own natural step
// — not a residual that merely shrinks with a longer fade. `crossfade == 1` degenerates to the
// hard seam instead: its one frame sits at `d == 0`, caught by the `d <= 0` floor below before
// the ceiling ever runs. `pos` must already be wrapped into [start, end) — the ceiling is a belt
// for a caller that has not wrapped yet, not a licence to skip it.
inline double crossfadeWeight(const ResolvedLoop& lp, double pos) {
    if (lp.crossfade <= 0) return 0.0;
    const double d = pos - lp.fadeBegin;
    if (d <= 0.0) return 0.0;
    return d < static_cast<double>(lp.crossfade - 1) ? d * lp.fadeInv : 1.0;
}

// Linear-interpolated read at a plain (non-wrapping) fractional source position. The crossfade
// tap sits one loop length behind the head, i.e. BEFORE the loop start, so it never needs the
// wrap partner the main read uses.
inline double lerpSource(const std::vector<AudioSample>& pcm, std::int64_t frameCount,
                         double pos) {
    const std::int64_t i0 = static_cast<std::int64_t>(pos);
    const std::int64_t i1 = i0 + 1;
    const double frac = pos - static_cast<double>(i0);
    const double a = (i0 >= 0 && i0 < frameCount) ? static_cast<double>(pcm[i0]) : 0.0;
    const double b = (i1 >= 0 && i1 < frameCount) ? static_cast<double>(pcm[i1]) : 0.0;
    return a + (b - a) * frac;
}

// One integer source frame with the loop crossfade already blended in — the Preserve path's
// read, and the start()-time ring prime's. `pos` must be a valid index; `xw` is crossfadeWeight
// at that position (0 blends nothing).
inline AudioSample crossfadedSource(const std::vector<AudioSample>& pcm, const ResolvedLoop& lp,
                                    std::int64_t pos, double xw) {
    const double v = static_cast<double>(pcm[static_cast<std::size_t>(pos)]);
    if (xw <= 0.0) return static_cast<AudioSample>(v);
    const std::int64_t tap = pos - lp.length;
    if (tap < 0) return static_cast<AudioSample>(v);
    const double in = static_cast<double>(pcm[static_cast<std::size_t>(tap)]);
    return static_cast<AudioSample>(v + xw * (in - v));
}

// Where the editor parks the loop handles for a capture that has none — the last quarter,
// where a sustain loop actually goes. Lives here, next to the validity rule, so the span a
// user is offered and the span the engine will accept are one definition.
struct LoopBounds {
    std::int64_t start = 0;
    std::int64_t end = 0;
};
LoopBounds defaultLoopBounds(std::int64_t frameCount);

}  // namespace reasampler::instrument::engine::loop
