#include "DeepPluckFilter.h"

#include <cmath>

namespace deeppluck
{
namespace
{
constexpr float minimumCutoffHz = 20.0f;
constexpr float maximumCutoffHz = 20000.0f;
constexpr float maximumCutoffRatio = 0.45f;
constexpr float maximumFeedback = 3.8f;
constexpr float ladderOutputGain = 1.2f;
constexpr float maximumResonanceCompensation = 2.50f;
constexpr double resonanceCompensationSmoothingSeconds = 0.020;

float resonanceCompensationFor (float resonance) noexcept
{
    const auto boundedResonance = juce::jlimit (0.0f, 1.0f, resonance);
    return 1.0f + (maximumResonanceCompensation - 1.0f)
                      * boundedResonance * boundedResonance;
}
}

void DeepPluckFilter::prepare (double newSampleRate) noexcept
{
    sampleRate = std::isfinite (newSampleRate) && newSampleRate > 0.0
                   ? newSampleRate : 44100.0;
    setCutoffFrequencyHz (cutoffHz);
    resonanceCompensation.reset (sampleRate, resonanceCompensationSmoothingSeconds);
    setResonance (resonance);
    resonanceCompensation.setCurrentAndTargetValue (resonanceCompensationFor (resonance));
    driveShape.reset (sampleRate, 0.020);
    const auto linearGain = std::pow (10.0f, driveDb / 20.0f);
    driveShape.setCurrentAndTargetValue ((linearGain - 1.0f) * 0.35f);
    reset();
}

void DeepPluckFilter::reset() noexcept
{
    state.fill (0.0f);
    driveShape.setCurrentAndTargetValue (driveShape.getTargetValue());
    resonanceCompensation.setCurrentAndTargetValue (
        resonanceCompensation.getTargetValue());
}

void DeepPluckFilter::setCutoffFrequencyHz (float newCutoffHz) noexcept
{
    const auto upperLimit = juce::jmax (
        minimumCutoffHz,
        juce::jmin (maximumCutoffHz, static_cast<float> (sampleRate) * maximumCutoffRatio));
    cutoffHz = std::isfinite (newCutoffHz)
                 ? juce::jlimit (minimumCutoffHz, upperLimit, newCutoffHz)
                 : 1800.0f;
}

void DeepPluckFilter::setResonance (float newResonance) noexcept
{
    resonance = std::isfinite (newResonance)
                  ? juce::jlimit (0.0f, 1.0f, newResonance) : 0.18f;
    resonanceCompensation.setTargetValue (resonanceCompensationFor (resonance));
}

void DeepPluckFilter::setDriveDb (float newDriveDb) noexcept
{
    driveDb = std::isfinite (newDriveDb)
                ? juce::jlimit (0.0f, 18.0f, newDriveDb) : 3.0f;
    const auto linearGain = std::pow (10.0f, driveDb / 20.0f);
    driveShape.setTargetValue ((linearGain - 1.0f) * 0.35f);
}

float DeepPluckFilter::processSample (float input) noexcept
{
    if (! std::isfinite (input))
        input = 0.0f;

    const auto pole = std::exp (static_cast<float> (-juce::MathConstants<double>::twoPi)
                                * cutoffHz / static_cast<float> (sampleRate));
    const auto smoothing = 1.0f - pole;
    const auto forward = smoothing * 0.76923076923f;
    const auto history = smoothing * 0.23076923076f;
    const auto driven = saturateInput (input);
    const auto feedback = maximumFeedback * resonance * std::tanh (state[4]);
    const auto a = driven - feedback;
    const auto b = history * state[0] + pole * state[1] + forward * a;
    const auto c = history * state[1] + pole * state[2] + forward * b;
    const auto d = history * state[2] + pole * state[3] + forward * c;
    const auto e = history * state[3] + pole * state[4] + forward * d;

    state = { a, b, c, d, e };
    const auto output = ladderOutputGain * e * resonanceCompensation.getNextValue();

    if (! std::isfinite (output))
    {
        reset();
        return 0.0f;
    }

    return output;
}

float DeepPluckFilter::saturateInput (float input) noexcept
{
    const auto shape = driveShape.getNextValue();
    if (shape <= 1.0e-6f)
        return input;

    return input * (1.0f + shape) / (1.0f + shape * std::abs (input));
}
}
