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

namespace gjalla
{

// Solid-state preamp: two cascaded gain-plus-waveshaper stages, no tube
// modelling anywhere. The reference is a cheap transistor head of the sort a
// 1993 rehearsal room actually had, not a boutique valve amp. So the knee is
// firmer than a tube's, there is no sag, and the distortion character barely
// changes with playing dynamics.
//
// Signal order:
//   tight HPF -> gain 1 -> shaper -> HPF 40 -> LPF 12k -> gain 2 -> shaper -> DC block
//
// Each shaper gets a small DC offset pushed in ahead of it and its rest value
// taken back off afterwards (see biasedShape). Clipping an offset signal
// squashes the two halves of the wave by different amounts, which is what puts
// even-order harmonics in; without it the preamp is purely odd-order and sounds
// like a fuzz. The final 20 Hz blocker then catches the offset the *signal*
// generates as it drives the curve asymmetrically, which is the part that
// cannot be cancelled analytically.
//
// Runs inside the oversampling block. Sample-outer / channel-inner so a single
// smoother drives both channels.
class Preamp
{
public:
    struct Settings
    {
        float gain = 6.0f;      // 0..10
        float tightHz = 90.0f;  // 20..300
    };

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

    void reset() noexcept
    {
        tightHpf.reset();
        interHpf.reset();
        interLpf.reset();
        dcBlocker.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 stageGain = stageGainFor(s.gain);
        const auto compTarget = compensationFor(s.gain);

        tightHpf.setCoeffs(makeHighPass(sampleRate, s.tightHz, 0.70710678f));
        interHpf.setCoeffs(makeHighPass(sampleRate, 40.0f, 0.70710678f));
        interLpf.setCoeffs(makeLowPass(sampleRate, 12000.0f, 0.70710678f));

        compSm.setTimeConstant(sampleRate, 0.02f);
        if (! primed)
        {
            compSm.snap(compTarget);
            primed = true;
        }

        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 comp = compSm.next(compTarget);

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

                x = biasedShape(x * stageGain, bias, knee);
                x = interLpf.processSample(ch, interHpf.processSample(ch, x));
                x = biasedShape(x * stageGain, bias, knee);

                data[ch][i] = dcBlocker.processSample(ch, x) * comp;
            }
        }
    }

    // Exposed for the tests: the two shaper stages with the filters taken out.
    static float staticCurve(float gain, float x) noexcept
    {
        const auto g = stageGainFor(gain);
        return biasedShape(biasedShape(x * g, bias, knee) * g, bias, knee);
    }

private:
    // The brief's "about +36 dB total" split evenly over the two stages. Both
    // stages moving together is what makes the second one start clipping only
    // once the first already is, which is where the tightness comes from.
    static float stageGainFor(float gain) noexcept
    {
        return juce::Decibels::decibelsToGain(juce::jlimit(0.0f, 10.0f, gain) * 0.1f * 18.0f);
    }

    // Level compensation off the static curve, so the Gain knob buys distortion
    // rather than volume and Master stays the level control.
    // Adapted from Gala's Drive, src/Drive.h @ 1.1.0.
    static float compensationFor(float gain) noexcept
    {
        constexpr float ref = 0.1f;   // the curve passes through the origin, so
        const auto up = std::abs(staticCurve(gain, ref));   // no rest value to
        const auto dn = std::abs(staticCurve(gain, -ref));  // subtract here
        return ref / juce::jmax(0.5f * (up + dn), 0.02f);
    }

    // TUNE: solid-state knee. 0 is a bare hard clip, 1 a bare tanh; 0.55 keeps
    // a couple of dB of softness above unity and then flattens, which is the
    // transistor sound. Raise it for a rounder, more tube-like break-up.
    static constexpr float knee = 0.55f;

    // TUNE: pre-clip DC offset. Big enough to hear the even harmonics, small
    // enough that the wave does not go visibly lopsided at low gain.
    static constexpr float bias = 0.06f;

    Biquad tightHpf, interHpf, interLpf;
    DcBlocker dcBlocker;
    OnePoleSmoother compSm;
    int channels = 2;
    bool primed = false;
};

} // namespace gjalla
