// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 Andrea De Murtas

#pragma once

#include <juce_audio_basics/juce_audio_basics.h>
#include <cmath>

namespace gjalla
{

// ---------------------------------------------------------------------------
// Biquads
//
// Hand-rolled rather than juce::dsp::IIR::Filter on purpose: the JUCE
// coefficient factories return a reference-counted object, so every coefficient
// update allocates. Gjalla recomputes coefficients once per block from smoothed
// parameter values, on the audio thread, and the real-time rules forbid that.
// These are the RBJ cookbook forms, transposed direct form II, up to 2 channels.
// ---------------------------------------------------------------------------

struct BiquadCoeffs
{
    float b0 = 1.0f, b1 = 0.0f, b2 = 0.0f, a1 = 0.0f, a2 = 0.0f;
};

namespace detail
{
    struct RbjTerms
    {
        float cosw, alpha;
    };

    inline RbjTerms rbjTerms(double sampleRate, float freqHz, float q)
    {
        // Nyquist guard: coefficients blow up as w0 approaches pi.
        const auto f = juce::jlimit(10.0f, (float) (sampleRate * 0.49), freqHz);
        const auto w0 = juce::MathConstants<float>::twoPi * f / (float) sampleRate;
        return { std::cos(w0), std::sin(w0) / (2.0f * juce::jmax(0.05f, q)) };
    }

    inline BiquadCoeffs normalise(float b0, float b1, float b2,
                                  float a0, float a1, float a2)
    {
        const auto inv = 1.0f / a0;
        return { b0 * inv, b1 * inv, b2 * inv, a1 * inv, a2 * inv };
    }
}

inline BiquadCoeffs makeLowPass(double sampleRate, float freqHz, float q)
{
    const auto [cw, alpha] = detail::rbjTerms(sampleRate, freqHz, q);
    return detail::normalise((1.0f - cw) * 0.5f, 1.0f - cw, (1.0f - cw) * 0.5f,
                             1.0f + alpha, -2.0f * cw, 1.0f - alpha);
}

inline BiquadCoeffs makeHighPass(double sampleRate, float freqHz, float q)
{
    const auto [cw, alpha] = detail::rbjTerms(sampleRate, freqHz, q);
    return detail::normalise((1.0f + cw) * 0.5f, -(1.0f + cw), (1.0f + cw) * 0.5f,
                             1.0f + alpha, -2.0f * cw, 1.0f - alpha);
}

inline BiquadCoeffs makePeak(double sampleRate, float freqHz, float q, float gainDb)
{
    const auto [cw, alpha] = detail::rbjTerms(sampleRate, freqHz, q);
    const auto a = std::pow(10.0f, gainDb / 40.0f);
    return detail::normalise(1.0f + alpha * a, -2.0f * cw, 1.0f - alpha * a,
                             1.0f + alpha / a, -2.0f * cw, 1.0f - alpha / a);
}

inline BiquadCoeffs makeLowShelf(double sampleRate, float freqHz, float q, float gainDb)
{
    const auto [cw, alpha] = detail::rbjTerms(sampleRate, freqHz, q);
    const auto a = std::pow(10.0f, gainDb / 40.0f);
    const auto tsa = 2.0f * std::sqrt(a) * alpha;
    return detail::normalise(a * ((a + 1.0f) - (a - 1.0f) * cw + tsa),
                             2.0f * a * ((a - 1.0f) - (a + 1.0f) * cw),
                             a * ((a + 1.0f) - (a - 1.0f) * cw - tsa),
                             (a + 1.0f) + (a - 1.0f) * cw + tsa,
                             -2.0f * ((a - 1.0f) + (a + 1.0f) * cw),
                             (a + 1.0f) + (a - 1.0f) * cw - tsa);
}

inline BiquadCoeffs makeHighShelf(double sampleRate, float freqHz, float q, float gainDb)
{
    const auto [cw, alpha] = detail::rbjTerms(sampleRate, freqHz, q);
    const auto a = std::pow(10.0f, gainDb / 40.0f);
    const auto tsa = 2.0f * std::sqrt(a) * alpha;
    return detail::normalise(a * ((a + 1.0f) + (a - 1.0f) * cw + tsa),
                             -2.0f * a * ((a - 1.0f) + (a + 1.0f) * cw),
                             a * ((a + 1.0f) + (a - 1.0f) * cw - tsa),
                             (a + 1.0f) - (a - 1.0f) * cw + tsa,
                             2.0f * ((a - 1.0f) - (a + 1.0f) * cw),
                             (a + 1.0f) - (a - 1.0f) * cw - tsa);
}

// The two Qs that make a pair of cascaded biquads a 4th-order Butterworth,
// i.e. the 24 dB/oct slopes the cabinet voicings ask for.
inline constexpr float butterworthQ4[2] { 0.54119610f, 1.30656296f };

class Biquad
{
public:
    void setCoeffs(const BiquadCoeffs& c) noexcept { co = c; }

    void reset() noexcept { s1[0] = s1[1] = s2[0] = s2[1] = 0.0f; }

    forcedinline float processSample(int channel, float x) noexcept
    {
        const auto y = co.b0 * x + s1[channel];
        s1[channel] = co.b1 * x - co.a1 * y + s2[channel];
        s2[channel] = co.b2 * x - co.a2 * y;
        return y;
    }

private:
    BiquadCoeffs co;
    float s1[2] { 0.0f, 0.0f }, s2[2] { 0.0f, 0.0f };
};

// ---------------------------------------------------------------------------
// One-pole DC blocker. Cheaper than a biquad and all the preamp needs to undo
// the deliberate pre-clip offset that generates the even harmonics.
// ---------------------------------------------------------------------------
class DcBlocker
{
public:
    void prepare(double sampleRate, float cutoffHz = 20.0f) noexcept
    {
        r = 1.0f - juce::MathConstants<float>::twoPi * cutoffHz / (float) sampleRate;
        r = juce::jlimit(0.0f, 0.9999f, r);
        reset();
    }

    void reset() noexcept { x1[0] = x1[1] = y1[0] = y1[1] = 0.0f; }

    forcedinline float processSample(int channel, float x) noexcept
    {
        const auto y = x - x1[channel] + r * y1[channel];
        x1[channel] = x;
        y1[channel] = y;
        return y;
    }

private:
    float r = 0.997f;
    float x1[2] { 0.0f, 0.0f }, y1[2] { 0.0f, 0.0f };
};

// ---------------------------------------------------------------------------
// Waveshapers
// ---------------------------------------------------------------------------

inline float hardClip(float x) noexcept
{
    return juce::jlimit(-1.0f, 1.0f, x);
}

// Solid-state knee: neither the round tanh of a tube nor the razor corner of a
// comparator. Blending the two gives a knee that stays soft for a few dB above
// unity and then flattens hard, which is what a cheap op-amp / diode-feedback
// preamp does. blend = 0 is pure hard clip, 1 is pure tanh.
inline float solidStateShape(float x, float blend) noexcept
{
    return blend * std::tanh(x) + (1.0f - blend) * hardClip(x);
}

// The same curve with a DC offset pushed in ahead of it, then shifted back so
// that it passes through the origin. Clipping an offset signal squashes the two
// halves of the wave by different amounts, which is where the even harmonics
// come from; subtracting the curve's own value at rest keeps that asymmetry
// while making sure a silent input produces a silent output.
//
// Doing it here rather than leaving it to a downstream DC blocker matters for
// more than tidiness. A constant offset holds every filter state after it at a
// non-zero operating point, and a biquad whose states are parked away from zero
// is exactly the condition under which single-precision rounding sustains a
// limit cycle instead of decaying. Measured: with the offset left in and taken
// out by a blocker, the chain settled into a permanent 2 kHz tone at -121 dBFS
// once it had been driven hard at 4x. Shifting the curve removes it at source.
inline float biasedShape(float x, float bias, float blend) noexcept
{
    return solidStateShape(x + bias, blend) - solidStateShape(bias, blend);
}

// Asymmetric silicon-diode pair, the shape a cheap 90s distortion box gets from
// clipping diodes in the op-amp feedback loop with an odd diode count: the
// positive half compresses earlier and harder than the negative one. The
// asymmetry is what puts even harmonics in.
inline float diodeShape(float x) noexcept
{
    constexpr float posKnee = 1.0f;   // TUNE: diode asymmetry, positive half
    constexpr float negKnee = 0.7f;   // TUNE: diode asymmetry, negative half
    return x >= 0.0f ? (1.0f - std::exp(-x * posKnee)) / posKnee
                     : -(1.0f - std::exp(x * negKnee)) / negKnee;
}

// Hard clip with different rails per half, for the fuzz: a starved transistor
// stage runs out of headroom towards the supply long before it does towards
// ground, so the waveform squares off lopsided.
inline float asymHardClip(float x, float posRail, float negRail) noexcept
{
    return juce::jlimit(-negRail, posRail, x);
}

// ---------------------------------------------------------------------------
// One-pole tilt EQ: lows and highs get opposite gains around a pivot.
//
// Adapted from Gala (https://github.com/andreademurtas/gala), src/GalaDSP.h
// @ 1.1.0. Deviations: the pivot is a constructor-time setting rather than
// hard-coded, because Gjalla needs it at 800 Hz (Dist pedal tone) and at
// 1 kHz (frost).
// ---------------------------------------------------------------------------
class TiltEq
{
public:
    void prepare(double sampleRate, float pivotHz) noexcept
    {
        pivot = pivotHz;
        setRate(sampleRate);
        reset();
    }

    void reset() noexcept { state[0] = state[1] = 0.0f; }

    // Recomputes the coefficient for a new rate without touching the filter
    // state: the oversampled stages call this every block, because the rate
    // follows the quality setting.
    void setRate(double sampleRate) noexcept
    {
        sr = sampleRate;
        coeffLp = 1.0f - std::exp(-juce::MathConstants<float>::twoPi
                                  * juce::jmin(pivot, (float) (sr * 0.45)) / (float) sr);
    }

    // tiltDb > 0 brightens: highs get +tilt/2 dB, lows get -tilt/2 dB.
    forcedinline float processSample(int channel, float x, float gLo, float gHi) noexcept
    {
        auto& z = state[channel];
        z += coeffLp * (x - z);
        return z * gLo + (x - z) * gHi;
    }

    static float lowGain(float tiltDb) noexcept
    {
        return juce::Decibels::decibelsToGain(-0.5f * tiltDb);
    }

    static float highGain(float tiltDb) noexcept
    {
        return juce::Decibels::decibelsToGain(0.5f * tiltDb);
    }

private:
    double sr = 44100.0;
    float pivot = 1000.0f;
    float coeffLp = 0.1f;
    float state[2] { 0.0f, 0.0f };
};

// ---------------------------------------------------------------------------
// One-pole parameter smoother.
//
// juce::SmoothedValue is the house tool and is used for everything running at
// the base rate. Inside the oversampling block it is the wrong shape: its ramp
// length is baked in at reset() time, and the oversampled rate changes with the
// quality setting. This one takes its coefficient per block from whatever rate
// is current, so a 20 ms ramp stays 20 ms in Eco and in Full.
// ---------------------------------------------------------------------------
class OnePoleSmoother
{
public:
    void setTimeConstant(double sampleRate, float seconds) noexcept
    {
        coeff = seconds <= 0.0f
                    ? 1.0f
                    : 1.0f - std::exp(-1.0f / (seconds * (float) sampleRate));
    }

    void snap(float v) noexcept { value = v; }

    float current() const noexcept { return value; }

    forcedinline float next(float target) noexcept
    {
        value += (target - value) * coeff;
        return value;
    }

private:
    float value = 0.0f, coeff = 1.0f;
};

// ---------------------------------------------------------------------------
// Click guard.
//
// Switching the oversampling factor, the pedal mode or the cabinet mode swaps
// filter states and sample rates under the signal, which is a step
// discontinuity. Rather than run two chains and crossfade (twice the CPU for
// something the user does with the mouse), the whole output is ramped down,
// the switch happens at silence, and it ramps back up. The brief allows either.
// ---------------------------------------------------------------------------
// The switch itself happens at a block boundary, never mid-block: the caller
// keeps processing with the old settings while the ramp runs, and only swaps
// them over on the block after consumeSwitch() reports the output has reached
// silence. Worst case that costs about 6 ms of dip; at a 1-sample block size
// the fade simply spans more blocks.
class ClickGuard
{
public:
    static constexpr float fadeMs = 3.0f;

    void prepare(double sampleRate) noexcept
    {
        step = 1.0f / juce::jmax(1.0f, (float) (fadeMs * 0.001 * sampleRate));
        gain = 1.0f;
        state = State::idle;
    }

    // A setting that cannot change under the signal has changed.
    void requestSwitch() noexcept
    {
        if (state != State::atZero)
            state = State::fadingOut;
    }

    // Call once at the top of each block. Returns true exactly once per
    // request, on the first block where the output has already been ramped to
    // silence: that is the block on which the caller must apply the change.
    bool consumeSwitch() noexcept
    {
        if (state != State::atZero)
            return false;

        state = State::fadingIn;
        return true;
    }

    bool isBusy() const noexcept { return state != State::idle; }

    forcedinline float nextGain() noexcept
    {
        switch (state)
        {
            case State::fadingOut:
                gain -= step;
                if (gain <= 0.0f)
                {
                    gain = 0.0f;
                    state = State::atZero;
                }
                break;

            case State::atZero:
                gain = 0.0f;
                break;

            case State::fadingIn:
                gain += step;
                if (gain >= 1.0f)
                {
                    gain = 1.0f;
                    state = State::idle;
                }
                break;

            case State::idle:
            default:
                break;
        }

        return gain;
    }

private:
    enum class State { idle, fadingOut, atZero, fadingIn };

    float gain = 1.0f, step = 0.01f;
    State state = State::idle;
};

} // namespace gjalla
