#pragma once
// pitch_shift — per-voice pitch shifter and time-stretcher (the Preserve engine's DSP core).
// Time-domain delay-line with correlation-aligned splices (SOLA-style): one active read tap
// chases the write head at the shift ratio; when it drifts out of its safe delay band it is
// relocated by a nominal jump, refined by a cross-correlation search so the new read point is
// waveform-aligned, then old/new taps crossfade (raised-cosine). The nominal jump is a whole
// number of the SOURCE's own periods when setSourcePeriod names one (pitch-synchronous OLA),
// and the fixed window otherwise.
//
// The WRITE rate (how fast source is consumed = duration) and the TAP rate (setShiftRatio =
// pitch) are INDEPENDENT, and only their difference drives the splice cadence. Feeding 1:1 via
// process() holds duration and moves pitch; feeding faster/slower via writeFrame() /
// processNoInput() moves duration at whatever pitch the tap is set to. Nothing here resamples
// to preserve duration — the splice/overlap-add IS the pitch-preserving mechanism, which is
// what the "WDL_Resampler is not a Preserve engine" invariant asks for.
//
// Regression history — do not revert any of these:
// - Correlated splices, vs. the original two-tap OLA (taps hard-locked w/2 apart, Hann
//   crossfaded by write-head distance): that fixed offset gave the two taps a fixed relative
//   phase, so near-anti-phase source frequencies (roughly half of them) nearly cancelled at
//   every crossfade midpoint — a repitched pure sine came out mangled while unity stayed clean.
//   Splices must be phase-aligned (snapped to the best waveform match), not just distance-fired.
// - Hand-rolled, not WDL_SimplePitchShifter: its include chain pulls <windows.h> unconditionally,
//   which cannot enter this REAPER/VST3-free core (sampler_core_tests links neither SDK). Swap
//   to WDL, if ever wanted, happens at the shell, never in this pure core.
// - prime() fills the ring with real upcoming source before streaming starts, not silence: a
//   silence-warmed ring made every early splice land in zeros — burst/gap/burst stutter at
//   note onset. Since the caller owns the whole decoded sample up front, prime() can know the
//   future and gives output frame 0 == source frame 0 with zero structural latency at any ratio.
// - freezeTail() parks the write head once the source is exhausted instead of feeding the last
//   real sample as a DC plateau: splices against a flat plateau are unalignable and produced
//   ring-modulation-like troughs near the note end. Freezing keeps late splices aligned against
//   the ring's real frozen tail.
//
// RT discipline: configure() sizes the ring once, off the audio thread. prime()/warm() only
// copy into the pre-sized ring (bounded, allocation-free). process() does no allocation and no
// locks; the correlation search is a bounded burst that fires once per splice cadence
// (window / |ratio-1| frames), never per frame.

#include <cstddef>
#include <cstdint>
#include <vector>

#include "core/audio/peaks.h"  // AudioSample (float)

namespace reasampler::instrument::engine {

using audio::AudioSample;

// The splice decision made by the most recent process()/processLinked() call — the linked-lag
// stereo contract. A stereo voice runs channel 0 as the master (full correlation search) and
// channel 1 as the follower: after the master's process() for a frame, the caller passes
// master.lastSplice() to the follower's processLinked() for the same frame, and the follower
// applies exactly this decision instead of running its own search. Both channels therefore
// share one lag and one splice schedule — independent per-channel searches drew an inter-channel
// offset of up to +/-maxLag at every splice, causing stereo image wander and comb coloration on
// a mono sum.
struct SpliceEvent {
    bool fired = false;        // a splice was scheduled on this frame
    std::int64_t jump = 0;     // the clamped nominal jump actually applied (signed)
    std::int64_t lag = 0;      // correlation best integer lag
    double frac = 0.0;         // parabolic sub-sample refinement, [-0.5, 0.5]
    std::int64_t fadeLen = 0;  // live (ratio-scaled) crossfade length chosen
};

// The nominal splice jump for a source whose period is known: the multiple of `periodFrames`
// nearest `windowFrames` that still fits `maxJumpFrames`. Falls back to `windowFrames` — the
// pre-PSOLA geometry, exactly — whenever the period is unknown (<= 0) or too long for even one
// whole period to fit, which is the documented degradation for inharmonic, polyphonic,
// percussive and noise sources.
//
// Why this is the whole fix: a splice can only phase-align on a landing point that is a whole
// number of source periods away, and the correlation search only reaches [0.75, 1.25] windows.
// Periods with no multiple in that one interval — f < ~16 Hz, and 26.7-32 Hz at a 50 ms
// window — could never align, however good the search was. Making the NOMINAL a multiple puts
// an aligned point at the centre of the search rather than hoping one falls inside it. The
// jump is rounded to whole frames; the search's own sub-sample refinement absorbs the residue.
std::int64_t periodAlignedJump(std::int64_t windowFrames, std::int64_t maxJumpFrames,
                               double periodFrames);

// A per-channel time-domain splice-aligned pitch shifter. A stereo voice owns two, linked:
// channel 0 is the master, channel 1 follows its splice decisions via processLinked() so the
// two rings stay sample-aligned.
//
// Default-constructed is inert: with no configure() it passes input through unchanged (ratio
// 1.0, empty ring), so a Varispeed voice that never touches it sees no behavior change.
class PitchShifter {
public:
    // Sizes the delay ring for `windowFrames` (the ring is 2x that for splice/search headroom).
    // <= 1 degrades to pass-through. Off the audio thread (allocates); resets all running state.
    // A larger window means fewer splices and a deeper alignment search; a primed shifter has no
    // added latency regardless of window size (see prime()).
    void configure(std::int64_t windowFrames);

    // Pre-fills the ring with the first `count` frames of the upcoming source stream and parks
    // the tap on src[0]; the caller then feeds process() the stream continuing at src[count].
    // `count` is clamped to [0, window()]. If the playable source is shorter than one window,
    // prime only the real span and call freezeTail() immediately after — never pad with silence
    // and declare it valid; padded zeros are splice targets and reintroduce the onset gap.
    // RT-safe: bounded copy, no allocation. No-op when unconfigured; ratio is left untouched.
    void prime(const AudioSample* src, std::int64_t count);

    // Silence-prime: zero the ring, park the tap one window behind the writer, declare that
    // window silence as valid history. Kept for callers with no access to the upcoming stream
    // (a silence-primed up-shift plays ~a window of silence before speaking; the Voice path uses
    // prime() instead). At unity a warmed shifter is a bit-exact window() delay.
    void warm();

    // 2^((note - root)/12) plus any per-frame pitch-envelope bias; 1.0 = no shift, no splices
    // ever fire. Cheap enough to set per frame. Values <= 0 are ignored (kept at the last valid
    // ratio) so a bad input never runs the tap backward or stalls it.
    void setShiftRatio(double ratio);

    // Source frames written per output frame — 1.0 unless the caller is stretching. Used ONLY
    // to size a splice crossfade safely: the outgoing tap closes on the write head at
    // (ratio - feedRate) per frame, so a fade sized against an assumed 1.0 overruns when the
    // source is fed slower than the output runs and the tail of the fade reads lapped content.
    // Values <= 0 are ignored. Exactly 1.0 reproduces the 1:1 geometry bit for bit.
    void setFeedRate(double rate);

    // The period of the source being fed, in SOURCE frames, making every splice jump a whole
    // number of it — <= 0 means "unknown" (see periodAlignedJump for the exact fallback); the
    // default, so a caller that never calls this sees no change.
    // Detection itself is off-thread and elsewhere (period_detect, which the engine deliberately
    // does not link); this is a couple of divisions and is safe to call at note-on.
    // Cleared by configure()/reset(); NOT by prime()/warm(), which do not change the source.
    void setSourcePeriod(double periodFrames);

    // The nominal jump splices currently use — window() unless a source period retuned it to
    // the nearest whole-period multiple, which can land either narrower or wider than window().
    std::int64_t spliceJump() const { return jump_; }

    // Transforms one input frame into one output frame (1 in, 1 out). RT-safe: reads/writes the
    // pre-sized ring only, no allocation, no lock. Unconfigured returns `in` unchanged. Otherwise
    // writes `in` at the write head, reads the active tap (crossfading against the outgoing tap
    // during a splice fade), then advances the write head and tap(s). When the active tap leaves
    // its safe delay band, a correlation-aligned splice is scheduled.
    AudioSample process(AudioSample in);

    // Follower-mode process: identical to process() except the splice decision isn't computed
    // here — when `master.fired` is true this frame splices with exactly the master's
    // jump/lag/frac/fadeLen. The caller must process the master channel first each frame and
    // pass its lastSplice() here; both shifters must be configured/primed/ratio'd identically so
    // their ring state advances in lockstep. RT-safe: same guarantees as process().
    AudioSample processLinked(AudioSample in, const SpliceEvent& master);

    // Writes one source frame WITHOUT producing an output frame — the stretch path's surplus
    // input when the source is consumed faster than the output runs. No splice can fire here:
    // splices are decided on the read side. No-op while unconfigured or tail-frozen, and it
    // deliberately leaves lastSplice_ alone so a linked follower's schedule is unaffected.
    // RT-safe.
    void writeFrame(AudioSample in);

    // Produces one output frame WITHOUT consuming a source frame — the stretch path's starved
    // output frame when the source is consumed slower than the output runs. Identical to
    // process()/processLinked() in every other respect. Returns 0 while unconfigured (there is
    // no input to pass through). RT-safe.
    AudioSample processNoInput();
    AudioSample processNoInputLinked(const SpliceEvent& master);

    const SpliceEvent& lastSplice() const { return lastSplice_; }

    // Call once the source stream is exhausted — no real frame remains to feed process().
    // Freezes the write head: subsequent process() calls ignore input and write nothing, but
    // read/splice/crossfade as before over the ring's frozen (all-real) final two windows, so
    // every late splice stays waveform-aligned against real content instead of a DC plateau.
    // Idempotent; RT-safe (flag + bounded arithmetic); cleared by reset()/prime()/warm().
    void freezeTail();

    bool tailFrozen() const { return tailFrozen_; }

    // Reset running state to silence (ring zeroed, heads re-seeded mid-band, fill count zeroed)
    // WITHOUT reallocating — for voice reuse without a re-configure. Keeps the current window.
    // Follow with prime() (or warm()) before streaming: a bare reset has no declared history,
    // so an immediate up-shift would starve its splices.
    void reset();

    // True once configure() sized a real ring (window > 1). A pass-through shifter is false.
    bool configured() const { return window_ > 1; }

    std::int64_t window() const { return window_; }

private:
    double readTap(double pos) const;               // fractional ring read, linear interp
    // Relocate the active tap by ~`nominalJump` frames of added delay (clamped to the filled
    // span for up-jumps) and start the crossfade. `delay` is the tap's current delay behind
    // the writer (the caller just computed it for the trigger test). Records the decision in
    // lastSplice_ for a linked follower channel.
    void splice(std::int64_t nominalJump, double delay);
    // Applies a master channel's already-computed splice decision verbatim (no search) —
    // the follower half of the linked-lag contract. Mirrors it into lastSplice_.
    void applySplice(const SpliceEvent& ev);
    // Shared body of process()/processLinked(); `linked` null = master mode (own trigger +
    // search), non-null = follower mode (splice iff linked->fired, with linked's decision).
    // `write` false is the starved stretch frame: read/splice/advance the taps, but consume no
    // input and hold the write head (the same shape tail-freezing already takes).
    AudioSample processImpl(AudioSample in, const SpliceEvent* linked, bool write);

    std::vector<AudioSample> ring_;  // delay line, length `ringLen_` == 2 * window_
    std::int64_t window_ = 0;        // nominal splice jump in frames; <= 1 = pass-through
    std::int64_t ringLen_ = 0;       // ring length (2 * window_): splice + search headroom
    std::int64_t writePos_ = 0;      // integer write head into the ring (source rate)
    double posA_ = 0.0;              // active read tap (advances at the shift ratio)
    double posB_ = 0.0;              // outgoing tap during a splice crossfade
    bool fading_ = false;            // a splice crossfade is in flight
    std::int64_t fadePos_ = 0;       // crossfade progress, [0, fadeLen_)
    std::int64_t fadeFrames_ = 0;    // NOMINAL crossfade length (window_/4)
    std::int64_t fadeLen_ = 0;       // LIVE crossfade length for the in-flight splice —
                                     // ratio-scaled at splice time so an up-shift's outgoing
                                     // tap can never drain into the writer mid-fade
    std::int64_t maxLag_ = 0;        // correlation search half-range (window_/4)
    double period_ = 0.0;            // source period in frames, 0 = unknown (fixed-window)
    std::int64_t jump_ = 0;          // nominal splice jump; window_ unless period_ retunes it
    std::int64_t jumpMax_ = 0;       // largest jump whose post-splice delay stays STRICTLY
                                     // inside [dLow_, dHigh_] at the worst search lag, so a
                                     // period-sized jump can never land back on a trigger and
                                     // thrash (dHigh_-dLow_-maxLag_-1, i.e. 1.25*window_). At
                                     // jump_==jumpMax_ a DOWN-splice's correlation read comes
                                     // within ~41 frames of the write head (measured: the
                                     // exact ring/lag/corrFrames_ geometry at the product
                                     // window, worst case over every lag the search reaches) —
                                     // real margin, not zero, but tight enough that widening
                                     // maxLag_, corrFrames_ or jumpMax_ without re-deriving
                                     // this bound risks reading unwritten ring content.
    std::int64_t corrFrames_ = 0;    // correlation segment length (dLow_-1, capped at 512, so
                                     // the reference read forward from the tap stays behind
                                     // the writer by construction at an up-splice)
    std::int64_t dLow_ = 0;          // splice trigger: active-tap delay below this (up-shift)
    std::int64_t dHigh_ = 0;         // splice trigger: active-tap delay above this (down-shift)
    std::int64_t filled_ = 0;        // frames of valid history behind the writer; splice()
                                     // clamps its up-jump to this so it never lands in
                                     // unwritten silence
    double ratio_ = 1.0;             // current shift ratio (>0)
    double feedRate_ = 1.0;          // source frames written per output frame; splice-fade only
    SpliceEvent lastSplice_{};       // decision of the most recent process*() frame; cleared
                                     // at the top of every frame, set on a splice
    bool tailFrozen_ = false;        // writer frozen (source exhausted); tap recycles the
                                     // frozen real tail, drains toward the writer at ratio_
                                     // (not ratio_-1) per frame — splice() scales the fade
                                     // by that rate
};

}  // namespace reasampler::instrument::engine
