// SPDX-FileCopyrightText: 2026 0064ff
// SPDX-License-Identifier: MIT-0

#pragma once
#include <JuceHeader.h>
#include "MyBallisticsFilter.h"

//==============================================================================

enum class CompressorMode
{
    AboveThreshold,
    BelowThreshold
};

//==============================================================================

/** A compressor. */
template <typename SampleType>
class MyCompressor
{
public:
    //==============================================================================
    MyCompressor()
    {
        // set defaults for variables
        setThreshold(SampleType(0));
        setSlew(SampleType(0.5));
        setTransientTime(SampleType(0.125));
        setAttackTime(SampleType(10));
        setReleaseTime(SampleType(100));
        setAdaptability(SampleType(2));
        setLowCompPower(SampleType(0.5));
        setHighCompPower(SampleType(4));
        setLowCompOffset(SampleType(1));
        setHighCompOffset(SampleType(0));
        setResponsePower(SampleType(0.5));
        setRatio(SampleType(2));
        setVariMuMultiplier(SampleType(1));
        setKneeDB(SampleType(18));
        setSmoothTime(SampleType(3));
        setSagTime(SampleType(512));
        setMode(CompressorMode::AboveThreshold);
    }

    //==============================================================================

    /** Sets the compressor's threshold in dB. */
    void setThreshold(SampleType newThreshold)
    {
        if (thresholdDB == newThreshold)
            return;

        thresholdDB = newThreshold;
        oneThirdThresholdDB = newThreshold / SampleType(3);
    }

    /** Sets the envelope's transient time in milliseconds. */
    void setTransientTime(SampleType newTransientTime)
    {
        envelopeFilterLow.setTransientTime(newTransientTime);
        envelopeFilterHigh.setTransientTime(newTransientTime);
    }

    /** Sets the envelope's attack time in milliseconds. */
    void setAttackTime(SampleType newAttackTime)
    {
        envelopeFilterLow.setAttackTime(newAttackTime);
        envelopeFilterHigh.setAttackTime(newAttackTime);
    }

    /** Sets the envelope's release time in milliseconds. */
    void setReleaseTime(SampleType newReleaseTime)
    {
        envelopeFilterLow.setReleaseTime(newReleaseTime);
        envelopeFilterHigh.setReleaseTime(newReleaseTime);
    }

    /** Sets slew as a percentage of the attack and release. Takes a value from 0 to 1. */
    void setSlew(SampleType newSlew)
    {
        envelopeFilterLow.setSlew(newSlew);
        envelopeFilterHigh.setSlew(newSlew);
    }

    /** Sets the program-dependent multiplier for attack and release. */
    void setAdaptability(SampleType newAdaptability)
    {
        envelopeFilterLow.setAdaptability(newAdaptability);
        envelopeFilterHigh.setAdaptability(newAdaptability);
    }

    /** Sets the power to raise the signal to for detection when minimal compression is happening. 1 = Peak. 2 = RMS.
        Expects a power that's 2^k, where k is an integer. */
    void setLowCompPower(SampleType newLowCompPower) {envelopeFilterLow.setDetectionPower(newLowCompPower);}

    /** Sets the power to raise the signal to for detection when maximal compression is happening. 1 = Peak. 2 = RMS.
        Expects a power that's 2^k, where k is an integer. */
    void setHighCompPower(SampleType newHighCompPower) {envelopeFilterHigh.setDetectionPower(newHighCompPower);}

    /** Sets the offset for the signal when minimal compression is occurring. 0 = symmetric (odd harmonics). 1 = asymmetric (even harmonics). */
    void setLowCompOffset(SampleType newLowCompOffset)
    {
        envelopeFilterLow.setLowCompOffset(newLowCompOffset);
        envelopeFilterHigh.setLowCompOffset(newLowCompOffset);
    }

    /** Sets the offset for the signal when maximal compression is occurring. 0 = symmetric (odd harmonics). 1 = asymmetric (even harmonics). */
    void setHighCompOffset(SampleType newHighCompOffset)
    {
        envelopeFilterLow.setHighCompOffset(newHighCompOffset);
        envelopeFilterHigh.setHighCompOffset(newHighCompOffset);
    }

    /** Sets the power the compression amount multiplier is raised to. Expects a power that's 2^k, where k is an integer. */
    void setResponsePower(SampleType newResponsePower) {responsePower=newResponsePower;}

    /** Sets the compressor's ratio. */
    void setRatio(SampleType newRatio)
    {
        if (ratio == newRatio)
            return;

        ratio = newRatio;
        gainDBSlope = calculateGainDBSlope(newRatio);
    }

    /** Sets the max compression-amount-scaled multiplier for ratio. */
    void setVariMuMultiplier(SampleType newVariMuMultiplier) {variMuMultiplier=newVariMuMultiplier;}

    /** Sets the compressor's knee width in dB. */
    void setKneeDB(SampleType newKneeDB)
    {
        if (kneeDB == newKneeDB)
            return;

        kneeDB = newKneeDB;

        SampleType safeKneeDB = std::max(kneeDB, SampleType(1e-6));
        halfKneeDB = safeKneeDB * SampleType(0.5);
        invTwoKneeDB = SampleType(1) / (safeKneeDB * SampleType(2));
    }

    /** Sets how quickly the compressor smooths gain changes in milliseconds. */
    void setSmoothTime(SampleType newSmoothTime)
    {
        if (smoothTime == newSmoothTime)
            return;

        smoothTime = newSmoothTime;
        updateSmoothTimeCache();
    }

    /** Sets the time period for measuring sag amount in milliseconds. */
    void setSagTime(SampleType newSagTime)
    {
        if (sagTime == newSagTime)
            return;

        sagTime = newSagTime;
        updateSagTimeCache();
    }

    /** Sets the compressor's mode (below or above threshold). */
    void setMode(CompressorMode newMode)
    {
        mode = newMode;
        sagDirection = (newMode == CompressorMode::AboveThreshold) ? SampleType(1) : SampleType(-1);
    }

    /** Sets values for testing. */
    void setTestValues(SampleType newTestValue1, SampleType newTestValue2, SampleType newTestValue3)
    {
        testValue1 = newTestValue1;
        testValue2 = newTestValue2;
        testValue3 = newTestValue3;
        envelopeFilterLow.setTestValues(newTestValue1, newTestValue2, newTestValue3);
        envelopeFilterHigh.setTestValues(newTestValue1, newTestValue2, newTestValue3);
    }

    //==============================================================================
    /** Initialises the processor. */
    void prepare(juce::dsp::ProcessSpec const& spec)
    {
        jassert (spec.sampleRate > 0);
        jassert (spec.numChannels > 0);

        // resize vectors with channel count
        auto resize = [spec](auto&... vectors) {
            (vectors.resize(spec.numChannels), ...);
        };
        resize(gainSmoothedState, prevGainDBSlopeState, sagState);

        // envelopes need to be prepared with proper sample rate
        envelopeFilterLow.prepare(spec);
        envelopeFilterHigh.prepare(spec);

        // caches need to be updated with proper sample rate
        updateSmoothTimeCache();
        updateSagTimeCache();

        // initialize state vectors
        reset();
    }

    //==============================================================================
    /** Resets the internal state variables of the processor. */
    void reset()
    {
        auto initGainDBSlope = calculateGainDBSlope(ratio);

        for (auto& sample : gainSmoothedState)    sample = SampleType(1);
        for (auto& sample : prevGainDBSlopeState) sample = initGainDBSlope;
        for (auto& sample : sagState)             sample = SampleType(0);

        envelopeFilterLow.reset();
        envelopeFilterHigh.reset();
    }

    //==============================================================================
    /** Performs the processing operation on a single sample at a time. */
    SampleType processSample(int channel, SampleType inputValue) noexcept
        {return processSample(channel, inputValue, inputValue);}

    /** Performs the processing operation on a single sample at a time. */
    SampleType processSample(int channel, SampleType inputValue, SampleType detectionValue) noexcept
    {
        // bypass if processing below threshold and threshold is at noise floor
        if (mode == CompressorMode::BelowThreshold && thresholdDB <= noiseFloorDB + SampleType(1e-3))
            return inputValue;

        // get values in vectors for processing
        SampleType gainSmoothed = gainSmoothedState[static_cast<std::size_t>(channel)];
        SampleType prevGainDBSlope = prevGainDBSlopeState[static_cast<std::size_t>(channel)];
        SampleType sag = sagState[static_cast<std::size_t>(channel)];

        //--------------------------------------------------------------------------
        // compression amount calculation

        // compression amount ranges from 0 to 1 depending on hard the compressor is working
        SampleType compressionAmount;

        // with upwards compression, gain is unbounded, so we have to normalize by
        // establishing a noise floor to calculate max possible gain
        if (mode == CompressorMode::BelowThreshold) // processing below threshold
        {
            SampleType const gainSmoothedDB = std::abs(linearToDB(std::max(gainSmoothed, noiseFloorLinear)));
            SampleType const maxGainDB = std::max(std::abs(prevGainDBSlope) * (thresholdDB-noiseFloorDB),
                                                  SampleType(1));
            compressionAmount = std::clamp(gainSmoothedDB/maxGainDB,
                                           SampleType(0), SampleType(1));
        }
        else if (ratio > SampleType(0) && ratio < SampleType(1)) // expansion above threshold
        {
            // with expansion above threshold, compression amount increases with gain
            compressionAmount = SampleType(1) - SampleType(1)/std::max(gainSmoothed, SampleType(1));
        }
        else // compression above threshold
        {
            compressionAmount = std::clamp(std::abs(SampleType(1) - gainSmoothed),
                                           SampleType(0), SampleType(1));
        }

        // adjust compression amount curve (response power)
        SampleType const compressionAmountMultiplier =
            MyBallisticsFilter<SampleType>::quickPow(compressionAmount, responsePower);

        // propagate to envelopes
        envelopeFilterLow.setCompressionAmountMultiplier(compressionAmountMultiplier);
        envelopeFilterHigh.setCompressionAmountMultiplier(compressionAmountMultiplier);

        //--------------------------------------------------------------------------
        // sag

        // sag is compression amount over time
        sag = MyBallisticsFilter<SampleType>::onePoleSmoother(sag, compressionAmountMultiplier, sagCoeff);

        // propagate to envelopes
        envelopeFilterLow.setSag(sag);
        envelopeFilterHigh.setSag(sag);

        // sag affects threshold by up to 1/3 its value, weakening compression and expansion
        SampleType const saggedThresholdDB = thresholdDB - sag * oneThirdThresholdDB * sagDirection;

        //--------------------------------------------------------------------------
        // detection

        // blend between envelopes based on compression amount
        SampleType env = envelopeFilterLow.processSample(channel, detectionValue) *
                            (SampleType(1) - compressionAmountMultiplier) +
                         envelopeFilterHigh.processSample(channel, detectionValue) *
                            (compressionAmountMultiplier);
        env = std::max(env, SampleType(1e-6));

        //--------------------------------------------------------------------------
        // gain calculation

        // work in db domain
        SampleType const envDB = linearToDB(env);
        SampleType const diffDB = envDB - saggedThresholdDB;

        SampleType effectiveGainDBSlope = gainDBSlope;

        // vari-mu (don't apply to negation or when vari-mu is equal to 1)
        if (ratio!=SampleType(-1) && variMuMultiplier!=SampleType(1))
        {
            SampleType const effectiveRatio =
                ratio * (SampleType(1)+(variMuMultiplier-SampleType(1))*compressionAmountMultiplier);
            effectiveGainDBSlope = calculateGainDBSlope(effectiveRatio);
        }

        prevGainDBSlope = effectiveGainDBSlope;

        // lambda for calculating gain in knee
        auto calculateKneeGainDB = [&](SampleType kneeInputDB) -> SampleType
            {return effectiveGainDBSlope * kneeInputDB * kneeInputDB * invTwoKneeDB;};

        // calculate gain
        SampleType gainDB;
        if (mode == CompressorMode::AboveThreshold) // above threshold
        {
            if (diffDB < -halfKneeDB) // no gain below knee
                gainDB = SampleType(0);
            else if (diffDB > halfKneeDB) // standard gain above knee
                gainDB = -effectiveGainDBSlope * diffDB;
            else // knee-modified gain in knee
                gainDB = -calculateKneeGainDB(diffDB + halfKneeDB);
        }
        else // below threshold
        {
            if (diffDB > halfKneeDB) // no gain above knee
                gainDB = SampleType(0);
            else if (diffDB < -halfKneeDB) // standard gain below knee
                gainDB = effectiveGainDBSlope * -diffDB;
            else // knee-modified gain in knee
                gainDB = calculateKneeGainDB(halfKneeDB - diffDB);
        }

        // convert back to linear domain
        SampleType const gain = (gainDB==SampleType(0)) ? SampleType(1) : dBToLinear(gainDB);

        // smooth gain
        gainSmoothed = MyBallisticsFilter<SampleType>::onePoleSmoother(gainSmoothed, gain, gainSmoothCoeff);

        // apply gain
        SampleType const output = inputValue * gainSmoothed;

        //--------------------------------------------------------------------------

        // update vectors
        gainSmoothedState[static_cast<std::size_t>(channel)] = gainSmoothed;
        prevGainDBSlopeState[static_cast<std::size_t>(channel)] = prevGainDBSlope;
        sagState[static_cast<std::size_t>(channel)] = sag;

        return output;
    }

    //==============================================================================

    // noise floor determines max possible gain with upwards compression
    // it also sets the threshold for automatically bypassing below-threshold processing
    // and appears in other areas such as the minimum volume control setting
    // these should be set to balance gain range with click prevention
    // when the upwards compressor responds after silence
    static SampleType constexpr noiseFloorLinear = static_cast<SampleType>(1e-4);
    static SampleType constexpr noiseFloorDB = -80.0;

    /** Converts linear gain to dB. */
    static SampleType linearToDB(SampleType linear) noexcept
        {return std::log(std::max(linear, noiseFloorLinear)) * dbPerNeper;}

    /** Converts dB to linear gain. */
    static SampleType dBToLinear(SampleType dB) noexcept {return std::exp(dB * neperPerDB);}

    //==============================================================================
private:
    /** Caches expensive operations related to smooth time. Use this in prepare() to bypass the early return in setSmoothTime(). */
    void updateSmoothTimeCache() noexcept {gainSmoothCoeff = envelopeFilterLow.msToCoeff(smoothTime);}

    /** Caches expensive operations related to sag time. Use this in prepare() to bypass the early return in setSagTime(). */
    void updateSagTimeCache() noexcept {sagCoeff = envelopeFilterLow.msToCoeff(sagTime);}

    //==============================================================================

    /** Calculates the slope of compression in decibels to gain when given a ratio r. */
    static SampleType calculateGainDBSlope(SampleType r) noexcept {return SampleType(1) - (SampleType(1)/r);}

    //==============================================================================

    // working with nepers allows for more efficient math functions when converting between dB and linear gain
    static inline SampleType const dbPerNeper = SampleType(20.0 / std::log(10.0));
    static inline SampleType const neperPerDB = SampleType(std::log(10.0) / 20.0);

    //==============================================================================

    std::vector<SampleType> gainSmoothedState, prevGainDBSlopeState, sagState;

    MyBallisticsFilter<SampleType> envelopeFilterLow, envelopeFilterHigh;

    static constexpr SampleType qNAN = std::numeric_limits<SampleType>::quiet_NaN();

    SampleType sagCoeff=qNAN, gainSmoothCoeff=qNAN;

    CompressorMode mode = CompressorMode::AboveThreshold;
    SampleType sagDirection=qNAN;
    SampleType thresholdDB=qNAN, responsePower=qNAN, ratio=qNAN, variMuMultiplier=qNAN, kneeDB=qNAN;
    SampleType smoothTime=qNAN, sagTime=qNAN;
    SampleType gainDBSlope=qNAN, halfKneeDB=qNAN, invTwoKneeDB=qNAN, oneThirdThresholdDB=qNAN;

    SampleType testValue1=qNAN, testValue2=qNAN, testValue3=qNAN;
};
