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

// Envelope-follower noise gate, sitting between the input trim and the
// distortion so it never has to fight a wall of preamp gain: the detector sees
// the raw pickup level, where the difference between a muted string and a
// played one is tens of dB rather than the couple of dB it becomes after
// +36 dB of preamp.
//
// Attack, release and hysteresis are fixed by the brief; only the threshold is
// exposed. Hysteresis stops the gate chattering on a decaying note, and the
// gain is smoothed with one-pole ramps so it can never click.
//
// Adapted from Gala (https://github.com/andreademurtas/gala), src/Gate.h
// @ 1.1.0. Deviations: no lookahead (Gjalla must not add latency for a gate),
// no ratio (a black metal noise gate is a gate, not an expander), no sidechain
// high-pass, and a hard snap to zero once the gain is inaudible so that silence
// in gives bit-exact silence out.
class NoiseGate
{
public:
    static constexpr float attackMs     = 0.5f;
    static constexpr float releaseMs    = 80.0f;
    static constexpr float hysteresisDb = 4.0f;

    void prepare(double sampleRate, int numChannels) noexcept
    {
        sr = sampleRate;
        channels = juce::jlimit(1, 2, numChannels);
        detAttack  = coeff(0.0001f);
        detRelease = coeff(0.005f);
        gainAttack  = coeff(attackMs * 0.001f);
        gainRelease = coeff(releaseMs * 0.001f);
        reset();
    }

    // Starts closed: a freshly instantiated plugin fed silence must output
    // silence from the very first sample, not fade into it.
    void reset() noexcept
    {
        env = 0.0f;
        gain = 0.0f;
        open = false;
    }

    // Deepest gain reduction of the block, positive dB, for the meter.
    float process(juce::AudioBuffer<float>& buffer, float thresholdDb, bool enabled) noexcept
    {
        const auto numSamples = buffer.getNumSamples();
        const auto nCh = juce::jmin(channels, buffer.getNumChannels());
        const auto closeDb = thresholdDb - hysteresisDb;
        const auto scale = 1.0f / (float) juce::jmax(1, nCh);

        auto minGain = 1.0f;

        for (int i = 0; i < numSamples; ++i)
        {
            auto mono = 0.0f;
            for (int ch = 0; ch < nCh; ++ch)
                mono += buffer.getSample(ch, i);

            const auto det = std::abs(mono * scale);
            env += (det - env) * (det > env ? detAttack : detRelease);
            const auto envDb = juce::Decibels::gainToDecibels(env, -120.0f);

            if (! enabled)
                open = true;
            else if (open)
                open = envDb >= closeDb;
            else
                open = envDb > thresholdDb;

            const auto target = open ? 1.0f : 0.0f;
            gain += (target - gain) * (target > gain ? gainAttack : gainRelease);

            // Below -100 dB the ramp is inaudible; snapping to a true zero is
            // what makes "silence in, silence out" exact instead of asymptotic,
            // and it keeps the closed gate from feeding denormals downstream.
            if (target == 0.0f && gain < 1.0e-5f)
                gain = 0.0f;

            minGain = juce::jmin(minGain, gain);

            for (int ch = 0; ch < nCh; ++ch)
                buffer.setSample(ch, i, buffer.getSample(ch, i) * gain);
        }

        return -juce::Decibels::gainToDecibels(juce::jmax(minGain, 1.0e-5f), -100.0f);
    }

private:
    float coeff(float seconds) const noexcept
    {
        return seconds <= 0.0f ? 1.0f : 1.0f - std::exp(-1.0f / (seconds * (float) sr));
    }

    double sr = 44100.0;
    int channels = 2;
    float detAttack = 0.1f, detRelease = 0.01f;
    float gainAttack = 0.1f, gainRelease = 0.01f;
    float env = 0.0f, gain = 0.0f;
    bool open = false;
};

} // namespace gjalla
