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

#pragma once

#include <juce_dsp/juce_dsp.h>
#include "GjallaDSP.h"
#include "Params.h"

namespace gjalla
{

// ---------------------------------------------------------------------------
// Analytic cabinet voicings.
//
// v1 ships no binary IR assets, so each cabinet is a fixed biquad cascade. The
// whole tuning surface is the table below: nothing else in this file needs
// touching to revoice a cab.
//
// The numbers started from the brief and were then checked against the
// magnitude curves of real cab IRs. What that comparison teaches, and what the
// table encodes:
//
//   * a guitar speaker is a band-pass, not a full-range driver. Everything
//     below the cabinet's tuning and above the cone break-up is simply gone,
//     which is why the outer sections are 24 dB/oct and not 12.
//   * the 300-500 Hz scoop is the single most recognisable feature of a 4x12
//     measured on axis, and it is what stops a high-gain amp sounding boxy.
//   * the presence peak in the 2-3 kHz region is the cone break-up resonance:
//     narrow, only a few dB, and the thing that makes a riff cut.
//
// TUNE: every frequency, Q and gain here. They are starting points, meant to be
// auditioned against real cabs and moved.
// ---------------------------------------------------------------------------

enum class SectionType { highPass, lowPass, peak, lowShelf, highShelf };

struct CabSection
{
    SectionType type = SectionType::peak;
    float freqHz = 1000.0f;
    float q = 0.70710678f;
    float gainDb = 0.0f;
};

inline constexpr int maxCabSections = 6;

struct CabVoicing
{
    const char* name;
    float preSatDrive;   // 0 = no saturation ahead of the filters
    float makeupDb;      // brings the band-limited result back to sensible level
    int numSections;
    CabSection sections[maxCabSections];
};

// Butterworth Q pairs give a maximally flat 24 dB/oct slope out of two biquads.
inline constexpr CabVoicing cabVoicings[3] =
{
    // 4x12: closed back, greenback-ish. Deep, scooped low mids, cone break-up
    // just above 2 kHz, and nothing at all above 5 kHz.
    { "4x12", 0.0f, 0.0f, 6, {
        { SectionType::highPass,   80.0f, butterworthQ4[0],  0.0f },
        { SectionType::highPass,   80.0f, butterworthQ4[1],  0.0f },
        { SectionType::peak,      400.0f, 1.4f,             -4.0f },
        { SectionType::peak,     2200.0f, 1.2f,              3.0f },
        { SectionType::lowPass,  4800.0f, butterworthQ4[0],  0.0f },
        { SectionType::lowPass,  4800.0f, butterworthQ4[1],  0.0f } } },

    // Practice combo: a single small open-backed speaker. No low end to speak
    // of, a hard honk in the upper mids, and a brick wall where the paper cone
    // gives up. Makeup gain because the pass band is barely two octaves wide.
    { "Practice", 0.0f, 3.0f, 4, {
        { SectionType::highPass,  140.0f, 0.70710678f,       0.0f },
        { SectionType::peak,     1200.0f, 1.5f,              5.0f },
        { SectionType::lowPass,  3800.0f, butterworthQ4[0],  0.0f },
        { SectionType::lowPass,  3800.0f, butterworthQ4[1],  0.0f } } },

    // Boombox: a rehearsal tape played back through something with a paper cone
    // and an overdriven class-AB chip amp. The pre-saturation is the chip amp
    // and it is what makes this voicing sound like a copy rather than a mix;
    // the high shelf is the tape and the plastic grille eating the top.
    { "Boombox", 1.8f, 4.0f, 3, {
        { SectionType::highPass,  220.0f, 0.70710678f,       0.0f },
        { SectionType::highShelf, 2000.0f, 0.70710678f,     -3.0f },
        { SectionType::lowPass,  3000.0f, 0.70710678f,       0.0f } } },
};

// ---------------------------------------------------------------------------
// Cabinet stage: analytic voicing, user IR, or bypassed.
//
// Base rate. The convolution is JUCE's default uniform-partitioned engine,
// which is the zero-latency one. The unit tests assert that, because a cab
// that quietly added latency would break the latency contract every time the
// user switched to IR mode.
// ---------------------------------------------------------------------------
class Cabinet
{
public:
    // The brief's ceiling. Longer files are trimmed rather than refused: a
    // 4 second reverb-tail IR in an amp sim is a user mistake, not a crash.
    static constexpr double maxIrSeconds = 2.0;

    void prepare(double sampleRate, int maxBlockSize, int numChannels)
    {
        sr = sampleRate;
        channels = juce::jlimit(1, 2, numChannels);

        const juce::dsp::ProcessSpec spec { sampleRate, (juce::uint32) maxBlockSize,
                                            (juce::uint32) channels };
        convolution.prepare(spec);
        reset();
    }

    void reset() noexcept
    {
        for (auto& s : sections)
            s.reset();
        convolution.reset();
    }

    // Message thread. juce::dsp::Convolution does the file reading and the
    // partitioning on its own background thread and swaps the result in with a
    // crossfade, lock-free: the only IR loading route the real-time rules
    // allow.
    void loadIr(const juce::File& file)
    {
        convolution.loadImpulseResponse(file,
                                        juce::dsp::Convolution::Stereo::yes,
                                        juce::dsp::Convolution::Trim::no,
                                        (size_t) (maxIrSeconds * sr),
                                        juce::dsp::Convolution::Normalise::yes);
    }

    // Message thread. The engine has no unload call, but its own initial state
    // is a 1-sample unit impulse, so loading one back is an exact unload. Without
    // this, forgetting the IR in the plugin state would leave the old file
    // audibly convolving forever.
    void clearIr()
    {
        juce::AudioBuffer<float> dirac(1, 1);
        dirac.setSample(0, 0, 1.0f);
        convolution.loadImpulseResponse(std::move(dirac), sr,
                                        juce::dsp::Convolution::Stereo::yes,
                                        juce::dsp::Convolution::Trim::no,
                                        juce::dsp::Convolution::Normalise::no);
    }

    void process(juce::AudioBuffer<float>& buffer, int mode) noexcept
    {
        const auto cabMode = (choices::CabMode) juce::jlimit(0, 4, mode);

        if (cabMode == choices::CabMode::off)
            return;

        if (cabMode == choices::CabMode::ir)
        {
            // Always runs, even before any IR has been chosen: the engine's
            // default state is a unit impulse, so this is a transparent pass.
            // And a background-loaded IR is only ever swapped in from inside
            // process(), so skipping the call would also mean the load never
            // completes.
            juce::dsp::AudioBlock<float> block(buffer);
            juce::dsp::ProcessContextReplacing<float> context(block);
            convolution.process(context);
            return;
        }

        processAnalytic(buffer, cabVoicings[(int) cabMode]);
    }

private:
    void processAnalytic(juce::AudioBuffer<float>& buffer, const CabVoicing& v) noexcept
    {
        const auto numSamples = buffer.getNumSamples();
        const auto nCh = juce::jmin(channels, buffer.getNumChannels());

        for (int n = 0; n < v.numSections; ++n)
            sections[n].setCoeffs(coeffsFor(v.sections[n]));

        // Normalising by tanh(drive) keeps the saturation gentle in level as
        // well as in shape, so switching voicings is not a jump in loudness.
        const auto satDrive = v.preSatDrive;
        const auto satNorm = satDrive > 0.0f ? 1.0f / std::tanh(satDrive) : 1.0f;
        const auto makeup = juce::Decibels::decibelsToGain(v.makeupDb);

        for (int ch = 0; ch < nCh; ++ch)
        {
            auto* data = buffer.getWritePointer(ch);
            for (int i = 0; i < numSamples; ++i)
            {
                auto x = data[i];

                if (satDrive > 0.0f)
                    x = std::tanh(x * satDrive) * satNorm;

                for (int n = 0; n < v.numSections; ++n)
                    x = sections[n].processSample(ch, x);

                data[i] = x * makeup;
            }
        }
    }

    BiquadCoeffs coeffsFor(const CabSection& s) const noexcept
    {
        switch (s.type)
        {
            case SectionType::highPass:  return makeHighPass(sr, s.freqHz, s.q);
            case SectionType::lowPass:   return makeLowPass(sr, s.freqHz, s.q);
            case SectionType::lowShelf:  return makeLowShelf(sr, s.freqHz, s.q, s.gainDb);
            case SectionType::highShelf: return makeHighShelf(sr, s.freqHz, s.q, s.gainDb);
            case SectionType::peak:
            default:                     return makePeak(sr, s.freqHz, s.q, s.gainDb);
        }
    }

    juce::dsp::Convolution convolution;
    Biquad sections[maxCabSections];
    double sr = 44100.0;
    int channels = 2;
};

} // namespace gjalla
