#pragma once
// peaks — waveform min/max envelope (thumbnail) computation from raw interleaved
// PCM. We compute our own thumbnails from the captured file rather than depending
// on REAPER's peak API: we own the file format, so this is simpler, testable, and
// dependency-free. A future bank panel (M5) calls this at whatever bin resolution
// the panel width dictates and draws one min/max envelope per channel.
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
// vendor/ includes. Standard library only. Builds and unit-tests without REAPER.

#include <cstddef>
#include <vector>

namespace reasampler {

// Canonical in-memory audio-sample type. `float` is REAPER's native audio buffer
// format (its render/PCM_source callbacks hand back interleaved 32-bit float), so
// peaks consumes that directly with no lossy conversion. If a capture ever lands
// as a different depth, the caller converts to float at the boundary — the
// thumbnail core stays single-typed.
//
// NAMED AudioSample, not `Sample`: `reasampler::Sample` is already bank_model's
// metadata struct. A `using Sample = float` here would collide at namespace scope
// wherever both headers are visible (the bank_panel module includes both). The
// audio-domain name also reads more precisely — this is one PCM sample value.
using AudioSample = float;

// One bin of a channel's envelope: the extremes of every sample that fell in it.
// min <= max always. For an empty bin (more bins than frames), both are 0.
struct MinMax {
    AudioSample min = 0.0f;
    AudioSample max = 0.0f;

    bool operator==(const MinMax& o) const { return min == o.min && max == o.max; }
};

// One channel's envelope: exactly `binCount` bins, in time order.
using ChannelEnvelope = std::vector<MinMax>;

// Per-channel envelopes: outer index is channel (channelCount entries, order
// preserved — never mixed or folded), inner is that channel's bins.
using Envelope = std::vector<ChannelEnvelope>;

// Computes a per-channel min/max envelope from interleaved PCM.
//
//   interleaved  frame-interleaved samples: [f0c0, f0c1, ..., f1c0, f1c1, ...].
//                Size must be >= frameCount * channelCount; extra is ignored.
//   channelCount channels per frame (the stride). Each channel is enveloped
//                INDEPENDENTLY — no averaging, no stereo fold (precision
//                invariant: channel count preserved).
//   frameCount   frames (samples-per-channel) to consider.
//   binCount     requested bins per channel. Honored exactly for any frameCount.
//
// Frame->bin partition: frames are split into `binCount` contiguous spans as
// evenly as possible; when frameCount does not divide evenly, the remainder is
// spread one-frame-per-bin across the earliest bins (ceil/floor split), so the
// tail is never dropped and no bin reads out of bounds. When binCount > frameCount
// the trailing empty bins are {0, 0}.
//
// Defined behavior for degenerate input (no UB, no throw):
//   binCount == 0     -> per channel: an empty bin vector.
//   channelCount == 0 -> an empty envelope (no channels).
//   frameCount == 0   -> per channel: binCount bins, all {0, 0}.
Envelope computeEnvelope(const std::vector<AudioSample>& interleaved,
                         std::size_t channelCount,
                         std::size_t frameCount,
                         std::size_t binCount);

// Sentinel returned by lastFrameAboveThreshold when NO frame in the scanned range
// peaks above the threshold (pure silence at that level). SIZE_MAX is unambiguous:
// no valid frame index can equal it (a real index is < frameCount <= SIZE_MAX for
// any allocatable buffer), so the caller tests `== kNoFrameAboveThreshold` cleanly.
inline constexpr std::size_t kNoFrameAboveThreshold =
    static_cast<std::size_t>(-1);

// Scans interleaved PCM BACKWARD for the last frame whose per-frame peak (the max
// absolute value across all channels of that frame — NO stereo fold, just the
// loudest channel that frame) exceeds `linearThreshold`, returning that frame index.
// Returns kNoFrameAboveThreshold if no frame exceeds it (or on degenerate input).
//
// This is the boundary primitive behind the realtime tail's decay-scan trim
// (docs/product/capture-tail.md §The realtime path): the recorded tail window is
// scanned back from the end for the last frame still above -72 dB, and the file is
// truncated one frame past it. Deliberately a separate primitive from
// computeEnvelope — that answers "the min/max envelope over bins" (a thumbnail),
// this answers "the last frame above a level" (a boundary). Bending the bin-oriented
// envelope to a frame-exact boundary question is a worse fit (spec §option a).
//
//   interleaved     frame-interleaved samples: [f0c0, f0c1, ..., f1c0, f1c1, ...].
//                   Must hold >= frameCount * channelCount; extra is ignored, and a
//                   short buffer is clamped to what it actually holds (no OOB read).
//   channelCount    channels per frame (the stride). The per-frame test is the max
//                   |sample| over these channels — the frame is "above" if its
//                   loudest channel is above the threshold.
//   frameCount      frames to consider (the scan starts at the last of these).
//   linearThreshold the comparison level as a LINEAR amplitude ratio (e.g. the
//                   -72 dB ratio from render_settings::autoTrimEndRatio), NOT dB.
//                   A frame counts as above when its peak is STRICTLY > this.
//
// Pure, stdlib-only, unit-tested (a synthetic decaying ramp, silence, all-above,
// and degenerate inputs) so the trim boundary math is locked outside the DAW.
std::size_t lastFrameAboveThreshold(const std::vector<AudioSample>& interleaved,
                                    std::size_t channelCount,
                                    std::size_t frameCount,
                                    AudioSample linearThreshold);

} // namespace reasampler
