// limiter.h — the master bus's lookahead brickwall limiter: true-peak sidechain detection,
// stereo-linked gain, and NO makeup gain of any kind. RT: process() allocates nothing, takes
// no lock and evaluates no transcendental; prepare() owns every allocation and every exp/pow.
// Bypassed and settled, process() returns without touching a sample — that untouched buffer
// is what makes the master bus byte-identical to the bare ramped multiply with the limiter off.

#pragma once

#include <atomic>
#include <cstdint>
#include <vector>

namespace reasampler::instrument::engine {

// The BAKED ceiling. A safety device with no configurable controls, so this is not a
// parameter. dBTP is a TRUE-peak target, which is why the detector oversamples and the
// signal path never does — though the bound is on the detector's 4x-oversampled ESTIMATE,
// not infinite-resolution true peak (normal for any practical TP limiter, and part of why
// this ceiling sits at -0.3 rather than 0).
inline constexpr double kLimiterCeilingDbTp = -0.3;

// The total delay the limiter imposes while engaged, and therefore the plugin's whole reported
// PDC latency. The detector's own group delay is inside this budget, not on top of it.
inline constexpr double kLimiterLookaheadSeconds = 0.002;

// Gain recovery. The min-then-average smoother releases in one lookahead window on its own,
// which distorts low frequencies; this one-pole only ever slows the RISE, so the smoother's
// no-overshoot bound survives it unchanged.
inline constexpr double kLimiterReleaseSeconds = 0.100;

// The transition mute. Long enough that the fade is not itself an edge and that it dwarfs the
// 2 ms delay-line prime it covers; short enough that the whole muted window (prime + fade) is
// ~12 ms rather than a gap. Linear in amplitude, not equal-power: this fades ONE leg to
// silence, it does not cross two.
inline constexpr double kLimiterMuteSeconds = 0.010;

// 4x true-peak oversampling (ITU-R BS.1770's floor at 48 kHz) over an 8-tap-per-phase
// polyphase interpolator. The 33-tap prototype's centre tap makes phase 0 an exact 4-sample
// delay, and that delay is the detector's group delay.
inline constexpr int kLimiterOversample = 4;
inline constexpr int kLimiterOsTaps = 8;
inline constexpr int kLimiterOsDelay = 4;

// kLimiterCeilingDbTp as a linear magnitude.
double limiterCeilingLinear();

// The delay the limiter imposes while engaged, in samples at `sampleRate` — what the plugin
// reports to the host's PDC. 0 at a non-positive rate; never below the detector's own delay.
int limiterLookaheadSamples(double sampleRate);

// The master-bus limiter. One instance per plugin instance; prepare() before the first block.
//
// The gain law is a sliding MINIMUM of the per-sample target gain over the lookahead window,
// then a MOVING AVERAGE of the same width. Every term of that average is a minimum whose own
// window contains the sample being gained, so the smoothed gain is <= the target gain at every
// sample by construction — the ceiling is held structurally rather than by a tuned attack.
//
// SWITCHING IS A MUTE, NOT A BLEND. Unlimited signal is emitted at weight 1 (settled bypass,
// which is the untouched buffer) or at weight 0, never in between — a fraction of an unlimited
// signal is a peak above the ceiling, which is exactly the leak this design forbids. So the
// FADE always rides the limited path (any weight of it is already under the ceiling, since the
// mute only scales down) and the HARD EDGE always lands on the bypassed side, against silence:
// engaging mutes at once, holds while the delay line primes, then fades the limited path in;
// disengaging fades the limited path out and resumes the dry buffer from silence. That
// discontinuity is accepted; a spike is not.
class Limiter {
public:
    // Sizes the delay line, the detector and the smoothers, and snaps to the current enable
    // state. Allocates and evaluates transcendentals: main/UI thread only, never in process().
    void prepare(double sampleRate);

    // Clears the delay line and the detector and snaps to the current enable state, skipping
    // the transition mute — an activation has nothing sounding to be continuous with.
    // Main/UI thread only (the host guarantees process() is stopped at both call sites).
    void reset();

    // The enable target. Set on the UI thread, observed by process() at block start.
    void setEnabled(bool on);
    bool enabled() const { return target_.load(std::memory_order_relaxed); }

    // Applies the limiter in place over `frames` of `left` (and `right`, which may be null for
    // a mono buffer). Returns the SMALLEST gain the LIMITER ITSELF computed this block —
    // smoothGain's output against the real input, at every sample including a muted one — NOT
    // scaled by the transition mute. The mute is a switch, not limiting: scaling by it would
    // report 0.0 (full reduction) on every toggle regardless of program content, which is a
    // meter defect, not a fact about the bus. 1.0 means no detected peak exceeded the ceiling,
    // whether settled bypassed or mid-mute over quiet material.
    float process(float* left, float* right, int frames);

private:
    void clearState();
    // The detector's true-peak estimate for the sample kLimiterOsDelay back, given the newest
    // input frame. Advances the FIR history.
    float detectTruePeak(float xl, float xr, bool stereo);
    // Pushes one target gain through the sliding minimum and the moving average.
    float smoothGain(float target);

    std::atomic<bool> target_{false};

    // --- prepared geometry ---
    int latency_ = 0;   // total delay; also the delay ring's length
    int window_ = 0;    // the minimum/average width, latency_ - kLimiterOsDelay + 1
    float ceiling_ = 1.f;
    float releaseCoeff_ = 1.f;
    float switchStep_ = 1.f;
    float osTaps_[kLimiterOversample][kLimiterOsTaps] = {};  // phase 0 is unused (exact delay)

    // --- audio-thread state ---
    std::vector<float> delayL_, delayR_;
    int delayPos_ = 0;
    float histL_[kLimiterOsTaps] = {};
    float histR_[kLimiterOsTaps] = {};
    int histPos_ = 0;
    // Monotonic wedge over the target gain: values ascending from the front, so the front is
    // the window minimum. Amortized O(1) per sample, bounded by 2 ops per push over a block.
    std::vector<float> wedgeVal_;
    std::vector<std::int64_t> wedgeIdx_;
    int wedgeHead_ = 0, wedgeCount_ = 0;
    std::int64_t pushIndex_ = 0;
    std::vector<float> avgRing_;
    double avgSum_ = 0.0;  // double: the running sum is added to and subtracted from forever
    int avgPos_ = 0;
    float releaseGain_ = 1.f;
    bool active_ = false;       // the limited path is in circuit (engaged, or still fading out)
    float switchGain_ = 0.f;    // the transition mute; only ever scales the LIMITED path
    int primeRemaining_ = 0;    // samples held at silence while the delay line fills
};

}  // namespace reasampler::instrument::engine
