// voice_filter.h — per-voice TPT state-variable filter with a continuous HP->BP->LP morph and
// an in-loop drive stage. Concrete type, no vtable: this sits on the per-voice per-sample path,
// so process() is header-inline. No allocation, no virtual dispatch, no I/O in process().

#pragma once

#include <cassert>
#include <type_traits>

#include "core/instrument/engine/filter/filter_coeffs.h"
#include "core/instrument/engine/filter/filter_morph.h"
#include "core/instrument/engine/filter/filter_params.h"
#include "core/instrument/engine/filter/filter_saturate.h"

namespace reasampler::instrument::engine::filter {

// Normalized control positions, as the editor moves them and the persisted state carries them.
// morphLaw is the one discrete control here — a two-value selector, not a normalized position —
// because its two values are characters to choose between, not points on a continuum.
struct FilterSettings {
    float cutoffNorm = 1.0f;
    float resonanceNorm = 0.0f;
    float morphNorm = 1.0f;  // 0 = high-pass, 1 = low-pass; the centre is set by morphLaw
    float driveNorm = 0.0f;
    MorphLaw morphLaw = MorphLaw::HighBandLow;
};

// Below this the recursion has decayed past -600 dB. Flushing keeps the state out of the
// subnormal range, where a ringing-out voice would otherwise stall the FPU for thousands of
// samples. Chosen well above FLT_MIN so a flushed state can never re-enter that range.
inline constexpr float kFilterDenormalFloor = 1e-30f;

class VoiceFilter {
public:
    // The instrument's output bus is permanently stereo; one integrator pair per channel.
    static constexpr int kMaxChannels = 2;

    struct State {
        float ic1 = 0.0f;  // band-pass integrator
        float ic2 = 0.0f;  // low-pass integrator
    };

    // Recomputes coefficients from the control positions. State is deliberately preserved so a
    // live parameter move glides instead of clicking; call reset() at note-on.
    void prepare(const FilterSettings& settings, double sampleRate);

    // Re-solves ONLY the cutoff-dependent coefficients, for a cutoff that moves per frame under
    // envelope modulation — cheap enough that the corner never needs quantizing (see
    // filter/CLAUDE.md for the numbers). Neither Q's parabola nor the morph's cos/sin enters
    // `g`, and the folded mix is a function of the weights and k alone, so a cutoff move
    // re-derives none of them. State preserved, exactly as prepare(). `sampleRate` must be the
    // one the last prepare() ran at: prepare() owns the non-positive-rate bypass mix, and this
    // deliberately leaves that mix alone.
    void setCutoffNorm(float cutoffNorm, double sampleRate);

    void reset();

    // Hot path. `channel` must be in [0, kMaxChannels).
    float process(int channel, float x) {
        assert(channel >= 0 && channel < kMaxChannels);
        State& s = state_[channel];

        const float v3 = x - s.ic2;
        const float v1 = coeffs_.a1 * s.ic1 + coeffs_.a2 * v3;
        const float v2 = s.ic2 + coeffs_.a2 * s.ic1 + coeffs_.a3 * v3;

        // The drive stage, and the only nonlinearity. It shapes the BAND-PASS integrator state
        // rather than the input because that state IS the resonance: in the passband and at DC
        // it sits at zero, so drive colours the resonance and leaves the passband transparent.
        // Placing it on the state rather than inside the zero-delay loop keeps a1/a2/a3 an exact
        // algebraic solve — a nonlinearity inside the loop would need per-sample Newton
        // iteration. softLimit is a contraction, so this cannot destabilize the filter.
        //
        // Gated on driven_ rather than called unconditionally: sqrt and div sit on this
        // recursive dependency chain, so out-of-order execution can't hide them, and at drive 0
        // (the default) that cost buys nothing — softLimit(x, 0) == x algebraically. Measured:
        // 11.2 ns/sample unconditional vs 4.1 ns gated, matching the limiter-removed floor.
        // driven_ only changes at prepare(), so the branch predicts perfectly. Bit-identity at
        // drive 0 holds either way, by algebra — the gate is a perf optimization, not what makes
        // it exact.
        const float u = 2.0f * v1 - s.ic1;
        s.ic1 = driven_ ? softLimit(u, driveDepth_) : u;
        s.ic2 = 2.0f * v2 - s.ic2;

        // Snap the state once the whole resonator has decayed past -600 dB. isSilent() means
        // "both integrators are exactly zero," so both must reach zero for that check to be
        // meaningful — the conjunctive test is the cheapest guarantee of that, not a defense
        // against a demonstrated limit cycle on this topology (measured: a per-variable flush
        // and an either-below-zero-both flush both go silent here too, no limit cycle, no
        // subnormals). That risk was real on the retired Direct Form I state, where a per-sample
        // flush zeroed y1/y2 — the actual OUTPUT — injecting a step the resonance then amplified.
        // ic1/ic2 are integrator STATE, not output; zeroing one only removes energy, a
        // contraction rather than an injection. The only demonstrable hazard here is no flush at
        // all, which never reaches exact zero and stalls in subnormals for thousands of samples.
        if (s.ic1 > -kFilterDenormalFloor && s.ic1 < kFilterDenormalFloor &&
            s.ic2 > -kFilterDenormalFloor && s.ic2 < kFilterDenormalFloor) {
            s.ic1 = 0.0f;
            s.ic2 = 0.0f;
        }

        return mix_.m0 * x + mix_.m1 * v1 + mix_.m2 * v2;
    }

    void processFrame(float* samples, int channelCount) {
        assert(channelCount >= 0 && channelCount <= kMaxChannels);
        for (int c = 0; c < channelCount; ++c) samples[c] = process(c, samples[c]);
    }

    // True once every integrator has flushed to exact zero — the voice's filter has stopped
    // ringing and cannot contribute further output.
    bool isSilent() const;

    const State& state(int channel) const {
        assert(channel >= 0 && channel < kMaxChannels);
        return state_[channel];
    }
    const SvfCoeffs& coeffs() const { return coeffs_; }
    const MorphMix& mix() const { return mix_; }

private:
    SvfCoeffs coeffs_{};
    MorphMix mix_{};
    float q_ = 1.0f;  // cached at prepare() so setCutoffNorm need not re-solve Q's parabola
    float driveDepth_ = 0.0f;
    bool driven_ = false;  // driveDepth_ != 0, cached so process() branches on a bool, not a float compare
    State state_[kMaxChannels]{};
};

// The port's whole point, enforced by the compiler rather than by review: the source was a
// virtual hierarchy dispatching per channel per sample, and this type must never grow one
// back. Trivially copyable also means nothing here is heap-owned.
static_assert(!std::is_polymorphic_v<VoiceFilter>, "no vtable on the per-sample path");
static_assert(std::is_trivially_copyable_v<VoiceFilter>, "state is plain values, never owned");

}  // namespace reasampler::instrument::engine::filter
