// Standard RBJ Audio EQ Cookbook math + closed-form Butterworth / Linkwitz-Riley.
using System;

namespace DansoriEQ.Core.Eq;

/// <summary>A single filter band. Fc in Hz, Gain in dB, Q dimensionless.
/// Slope is dB/oct for LSC/HSC or an S value for the Q-as-slope shelves.
/// Order (even) is used by the Butterworth / Linkwitz-Riley types.</summary>
public sealed class Filter
{
    public FilterType Type { get; set; } = FilterType.Peaking;
    public double Fc { get; set; } = 1000;
    public double GainDb { get; set; } = 0;
    public double Q { get; set; } = 1.0;
    public double Slope { get; set; } = 12;   // dB/oct (LSC/HSC) or S (Q-as-slope)
    public int Order { get; set; } = 4;        // even order for Butterworth / Linkwitz-Riley
    public bool Enabled { get; set; } = true;

    public Filter Clone() => (Filter)MemberwiseClone();

    /// <summary>Linear magnitude of this filter at frequency f (Hz), sample rate fs (Hz).</summary>
    public double MagnitudeAt(double f, double fs)
    {
        if (!Enabled) return 1.0;

        switch (Type)
        {
            // ── Pure biquad (RBJ) types ──
            case FilterType.Peaking:   return BiquadMag(PeakingCoeffs(fs), f, fs);
            case FilterType.LowPass:   return BiquadMag(LowPassCoeffs(fs, Q), f, fs);
            case FilterType.HighPass:  return BiquadMag(HighPassCoeffs(fs, Q), f, fs);
            case FilterType.BandPass:  return BiquadMag(BandPassCoeffs(fs, Q), f, fs);
            case FilterType.Notch:     return BiquadMag(NotchCoeffs(fs, Q), f, fs);
            case FilterType.AllPass:   return 1.0;   // flat magnitude, phase only

            // ── Shelves ──
            case FilterType.LowShelf:  return BiquadMag(LowShelfCoeffs(fs, AlphaFromQ(fs)), f, fs);
            case FilterType.HighShelf: return BiquadMag(HighShelfCoeffs(fs, AlphaFromQ(fs)), f, fs);

            case FilterType.LowShelfSlope:
            case FilterType.LowShelfQSlope:
            case FilterType.LowShelfCornerQSlope:
                return BiquadMag(LowShelfCoeffs(fs, AlphaFromSlope(fs)), f, fs);

            case FilterType.HighShelfSlope:
            case FilterType.HighShelfQSlope:
            case FilterType.HighShelfCornerQSlope:
                return BiquadMag(HighShelfCoeffs(fs, AlphaFromSlope(fs)), f, fs);

            // ── Butterworth (closed form, order N) ──
            case FilterType.LowPassButterworth:
                return 1.0 / Math.Sqrt(1 + Math.Pow(f / Fc, 2 * EvenOrder));
            case FilterType.HighPassButterworth:
                return 1.0 / Math.Sqrt(1 + Math.Pow(Fc / Math.Max(f, 1e-6), 2 * EvenOrder));

            // ── Linkwitz-Riley (= Butterworth^2, order N) ──
            case FilterType.LowPassLinkwitzRiley:
                return 1.0 / (1 + Math.Pow(f / Fc, EvenOrder));
            case FilterType.HighPassLinkwitzRiley:
                return 1.0 / (1 + Math.Pow(Fc / Math.Max(f, 1e-6), EvenOrder));

            default:
                return BiquadMag(PeakingCoeffs(fs), f, fs);
        }
    }

    /// <summary>Gain in dB of this band at frequency f.</summary>
    public double GainDbAt(double f, double fs) => 20.0 * Math.Log10(Math.Max(MagnitudeAt(f, fs), 1e-9));

    // ── helpers ──────────────────────────────────────────────────────────────
    private int EvenOrder
    {
        get { int n = Math.Clamp(Order, 2, 8); return (n % 2 == 0) ? n : n + 1; }
    }

    private double W0(double fs) => 2.0 * Math.PI * Fc / fs;

    // alpha for Q-parameterised biquads
    private double AlphaFromQ(double fs) => Math.Sin(W0(fs)) / (2.0 * Math.Max(Q, 1e-4));

    // alpha for slope-parameterised shelves (RBJ shelf S form).
    // For "(Slope in dB)" we treat Slope as dB/oct and map to S; for "(Q as Slope)" Slope IS S.
    private double AlphaFromSlope(double fs)
    {
        double s = Type is FilterType.LowShelfSlope or FilterType.HighShelfSlope
            ? Math.Clamp(Slope / 12.0, 0.05, 2.0)   // 12 dB/oct ≈ S=1
            : Math.Clamp(Slope, 0.05, 2.0);
        double A = Math.Pow(10.0, GainDb / 40.0);
        double sinw = Math.Sin(W0(fs));
        return sinw / 2.0 * Math.Sqrt((A + 1.0 / A) * (1.0 / s - 1.0) + 2.0);
    }

    private struct C { public double b0, b1, b2, a0, a1, a2; }

    private C PeakingCoeffs(double fs)
    {
        double A = Math.Pow(10.0, GainDb / 40.0);
        double cosw0 = Math.Cos(W0(fs)); double alpha = AlphaFromQ(fs);
        return new C {
            b0 = 1 + alpha * A, b1 = -2 * cosw0, b2 = 1 - alpha * A,
            a0 = 1 + alpha / A, a1 = -2 * cosw0, a2 = 1 - alpha / A };
    }

    private C LowPassCoeffs(double fs, double q)
    {
        double cosw0 = Math.Cos(W0(fs)); double alpha = Math.Sin(W0(fs)) / (2 * Math.Max(q, 1e-4));
        double b1 = 1 - cosw0;
        return new C { b0 = b1 / 2, b1 = b1, b2 = b1 / 2, a0 = 1 + alpha, a1 = -2 * cosw0, a2 = 1 - alpha };
    }

    private C HighPassCoeffs(double fs, double q)
    {
        double cosw0 = Math.Cos(W0(fs)); double alpha = Math.Sin(W0(fs)) / (2 * Math.Max(q, 1e-4));
        double b1 = 1 + cosw0;
        return new C { b0 = b1 / 2, b1 = -b1, b2 = b1 / 2, a0 = 1 + alpha, a1 = -2 * cosw0, a2 = 1 - alpha };
    }

    private C BandPassCoeffs(double fs, double q)
    {
        double cosw0 = Math.Cos(W0(fs)); double alpha = Math.Sin(W0(fs)) / (2 * Math.Max(q, 1e-4));
        return new C { b0 = alpha, b1 = 0, b2 = -alpha, a0 = 1 + alpha, a1 = -2 * cosw0, a2 = 1 - alpha };
    }

    private C NotchCoeffs(double fs, double q)
    {
        double cosw0 = Math.Cos(W0(fs)); double alpha = Math.Sin(W0(fs)) / (2 * Math.Max(q, 1e-4));
        return new C { b0 = 1, b1 = -2 * cosw0, b2 = 1, a0 = 1 + alpha, a1 = -2 * cosw0, a2 = 1 - alpha };
    }

    private C LowShelfCoeffs(double fs, double alpha)
    {
        double A = Math.Pow(10.0, GainDb / 40.0);
        double cosw0 = Math.Cos(W0(fs)); double sqrtA = Math.Sqrt(A);
        return new C {
            b0 =     A * ((A + 1) - (A - 1) * cosw0 + 2 * sqrtA * alpha),
            b1 = 2 * A * ((A - 1) - (A + 1) * cosw0),
            b2 =     A * ((A + 1) - (A - 1) * cosw0 - 2 * sqrtA * alpha),
            a0 =         (A + 1) + (A - 1) * cosw0 + 2 * sqrtA * alpha,
            a1 =    -2 * ((A - 1) + (A + 1) * cosw0),
            a2 =         (A + 1) + (A - 1) * cosw0 - 2 * sqrtA * alpha };
    }

    private C HighShelfCoeffs(double fs, double alpha)
    {
        double A = Math.Pow(10.0, GainDb / 40.0);
        double cosw0 = Math.Cos(W0(fs)); double sqrtA = Math.Sqrt(A);
        return new C {
            b0 =      A * ((A + 1) + (A - 1) * cosw0 + 2 * sqrtA * alpha),
            b1 = -2 * A * ((A - 1) + (A + 1) * cosw0),
            b2 =      A * ((A + 1) + (A - 1) * cosw0 - 2 * sqrtA * alpha),
            a0 =          (A + 1) - (A - 1) * cosw0 + 2 * sqrtA * alpha,
            a1 =      2 * ((A - 1) - (A + 1) * cosw0),
            a2 =          (A + 1) - (A - 1) * cosw0 - 2 * sqrtA * alpha };
    }

    // |H(e^{jw})| for a biquad, closed form.
    private static double BiquadMag(C c, double f, double fs)
    {
        double w = 2.0 * Math.PI * f / fs;
        double cosw = Math.Cos(w), cos2w = Math.Cos(2 * w);
        double num = c.b0 * c.b0 + c.b1 * c.b1 + c.b2 * c.b2
                     + 2 * (c.b0 * c.b1 + c.b1 * c.b2) * cosw + 2 * (c.b0 * c.b2) * cos2w;
        double den = c.a0 * c.a0 + c.a1 * c.a1 + c.a2 * c.a2
                     + 2 * (c.a0 * c.a1 + c.a1 * c.a2) * cosw + 2 * (c.a0 * c.a2) * cos2w;
        if (den <= 0) return 1.0;
        return Math.Sqrt(num / den);
    }
}
