// 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
{

// The boost pedal in front of the amp. Three voicings share one set of knobs,
// and each knob means something different per voicing; the meanings are
// documented here and repeated in the README:
//
//   Dist      drive  0..+40 dB into an asymmetric diode clipper
//             tone   tilt around 800 Hz, dark (0) to thin (10)
//   Chainsaw  drive  clip amount, 0..+36 dB into a hard clipper
//             tone   sweeps the upper of the two pre-clip peaks, 700..1500 Hz
//   Fuzz      drive  0..+50 dB into a lopsided hard clipper
//             tone   post low-pass, 1..8 kHz
//
//   all       level  plain output trim, -12..+12 dB
//
// Runs inside the oversampling block, so every filter here is at the
// oversampled rate and its coefficients are recomputed per block from the rate
// in force. That is deliberate: these filters bracket a nonlinearity and belong
// on the same side of it. The "linear filters at base rate" rule in the brief
// covers the tone stack, presence, cab and frost, which are downstream tone
// shaping rather than part of the clipper.
//
// Sample-outer / channel-inner loop, so one smoother drives both channels; a
// smoother per channel would let the two gains drift apart on a fast move.
class Pedal
{
public:
    struct Settings
    {
        int mode = (int) choices::PedalMode::dist;
        float drive = 6.0f;     // 0..10
        float tone = 5.0f;      // 0..10
        float levelDb = 0.0f;   // -12..+12
    };

    void prepare(double maxSampleRate, int numChannels) noexcept
    {
        channels = juce::jlimit(1, 2, numChannels);
        dcBlocker.prepare(maxSampleRate);
        tilt.prepare(maxSampleRate, 800.0f);
        reset();
    }

    void reset() noexcept
    {
        distHpf.reset();
        sawLow.reset();
        sawHigh.reset();
        sawLpf.reset();
        fuzzLpf.reset();
        dcBlocker.reset();
        tilt.reset();
        primed = false;
    }

    void process(juce::dsp::AudioBlock<float>& block, const Settings& s, double sampleRate) noexcept
    {
        const auto numSamples = (int) block.getNumSamples();
        const auto nCh = juce::jmin((int) block.getNumChannels(), channels);
        const auto mode = (choices::PedalMode) juce::jlimit(0, 2, s.mode);

        const auto driveGain = driveGainFor(mode, s.drive);
        const auto postTarget = compensationFor(mode, s)
                                * juce::Decibels::decibelsToGain(s.levelDb);
        const auto tiltLoTarget = TiltEq::lowGain(distTiltDb(s.tone));
        const auto tiltHiTarget = TiltEq::highGain(distTiltDb(s.tone));

        // 20 ms ramps, recomputed from the rate in force this block so that
        // switching quality does not change how fast the knobs move.
        for (auto* sm : { &postSm, &tiltLoSm, &tiltHiSm })
            sm->setTimeConstant(sampleRate, 0.02f);

        // First block after a reset: start on the target instead of ramping to
        // it from nothing, which would be an audible swell on every preset load.
        if (! primed)
        {
            postSm.snap(postTarget);
            tiltLoSm.snap(tiltLoTarget);
            tiltHiSm.snap(tiltHiTarget);
            primed = true;
        }

        // Per-block coefficients: the parameters feeding them are already
        // smoothed, and a block is at most a few ms at the oversampled rate.
        switch (mode)
        {
            case choices::PedalMode::dist:
                distHpf.setCoeffs(makeHighPass(sampleRate, 60.0f, 0.70710678f));
                tilt.setRate(sampleRate);
                break;

            case choices::PedalMode::chainsaw:
                sawLow.setCoeffs(makePeak(sampleRate, 250.0f, 1.0f, lowPeakDb));
                sawHigh.setCoeffs(makePeak(sampleRate, upperPeakHz(s.tone), 1.0f, highPeakDb));
                sawLpf.setCoeffs(makeLowPass(sampleRate, 6000.0f, 0.70710678f));
                break;

            case choices::PedalMode::fuzz:
                fuzzLpf.setCoeffs(makeLowPass(sampleRate, fuzzToneHz(s.tone), 0.70710678f));
                break;
        }

        auto* left = block.getChannelPointer(0);
        auto* right = nCh > 1 ? block.getChannelPointer(1) : nullptr;
        float* const data[2] { left, right };

        for (int i = 0; i < numSamples; ++i)
        {
            const auto post = postSm.next(postTarget);
            const auto gLo = tiltLoSm.next(tiltLoTarget);
            const auto gHi = tiltHiSm.next(tiltHiTarget);

            for (int ch = 0; ch < nCh; ++ch)
            {
                auto x = data[ch][i];

                switch (mode)
                {
                    case choices::PedalMode::dist:
                        x = distHpf.processSample(ch, x) * driveGain;
                        x = dcBlocker.processSample(ch, diodeShape(x));
                        x = tilt.processSample(ch, x, gLo, gHi);
                        break;

                    case choices::PedalMode::chainsaw:
                        x = sawLow.processSample(ch, x);
                        x = hardClip(sawHigh.processSample(ch, x) * driveGain);
                        x = sawLpf.processSample(ch, x);
                        break;

                    case choices::PedalMode::fuzz:
                        x = asymHardClip(x * driveGain, fuzzPosRail, fuzzNegRail);
                        x = fuzzLpf.processSample(ch, dcBlocker.processSample(ch, x));
                        break;
                }

                data[ch][i] = x * post;
            }
        }
    }

    // Exposed for the tests: the static transfer curve the compensation assumes,
    // i.e. the chain with its filters taken out.
    static float staticCurve(choices::PedalMode mode, float drive, float x) noexcept
    {
        const auto g = driveGainFor(mode, drive);
        switch (mode)
        {
            case choices::PedalMode::dist:     return diodeShape(x * g);
            case choices::PedalMode::chainsaw: return hardClip(x * g);
            default:                           return asymHardClip(x * g, fuzzPosRail, fuzzNegRail);
        }
    }

private:
    // TUNE: pedal drive ceilings. Dist and Fuzz are the brief's +40 and +50 dB;
    // Chainsaw's is a judgement call: an HM-2 has less raw gain than a fuzz but
    // squares the wave far earlier thanks to the pre-clip peaks.
    static float driveGainFor(choices::PedalMode mode, float drive) noexcept
    {
        const auto d = juce::jlimit(0.0f, 10.0f, drive) * 0.1f;
        switch (mode)
        {
            case choices::PedalMode::dist:     return juce::Decibels::decibelsToGain(d * 40.0f);
            case choices::PedalMode::chainsaw: return juce::Decibels::decibelsToGain(d * 36.0f);
            default:                           return juce::Decibels::decibelsToGain(d * 50.0f);
        }
    }

    // Static level compensation taken off the transfer curve at a realistic
    // guitar peak, so that turning drive up adds harmonics instead of volume
    // and the amp behind the pedal keeps seeing instrument level.
    // Adapted from Gala's Drive, src/Drive.h @ 1.1.0.
    static float compensationFor(choices::PedalMode mode, const Settings& s) noexcept
    {
        constexpr float ref = 0.1f;   // ~ -20 dBFS, a hot single-coil peak
        const auto up = std::abs(staticCurve(mode, s.drive, ref));
        const auto dn = std::abs(staticCurve(mode, s.drive, -ref));
        return ref / juce::jmax(0.5f * (up + dn), 0.02f);
    }

    // TUNE: tone laws.
    static float distTiltDb(float tone) noexcept
    {
        return (juce::jlimit(0.0f, 10.0f, tone) - 5.0f) * 2.4f;   // -12..+12 dB
    }

    static float upperPeakHz(float tone) noexcept
    {
        return 700.0f + juce::jlimit(0.0f, 10.0f, tone) * 80.0f;   // 700..1500 Hz
    }

    static float fuzzToneHz(float tone) noexcept
    {
        // Logarithmic so the knob feels even across the sweep.
        return 1000.0f * std::pow(8.0f, juce::jlimit(0.0f, 10.0f, tone) * 0.1f);
    }

    // TUNE: HM-2 pre-clip peaks. The brief allows +8..+10 dB at Q ~1; the low
    // one carries the chainsaw's chug and the high one its buzz, and pushing
    // both to +10 made the clipper crush everything between them.
    static constexpr float lowPeakDb = 9.0f;
    static constexpr float highPeakDb = 9.0f;

    // TUNE: fuzz rail asymmetry.
    static constexpr float fuzzPosRail = 1.0f;
    static constexpr float fuzzNegRail = 0.6f;

    Biquad distHpf, sawLow, sawHigh, sawLpf, fuzzLpf;
    DcBlocker dcBlocker;
    TiltEq tilt;
    OnePoleSmoother postSm, tiltLoSm, tiltHiSm;
    int channels = 2;
    bool primed = false;
};

} // namespace gjalla
