#include "DeepPluckTransientNoise.h"

#include <cmath>

namespace deeppluck
{
namespace
{
constexpr float envelopeFloor = 0.001f;
constexpr float transientCalibration = 0.45f;
constexpr float noiseCalibration = 0.30f;
constexpr float transientMinimumFrequencyHz = 650.0f;
constexpr float transientMaximumFrequencyHz = 8000.0f;
constexpr float noiseMinimumCutoffHz = 500.0f;
constexpr float noiseMaximumCutoffHz = 16000.0f;
constexpr float dcBlockFrequencyHz = 60.0f;
constexpr double transientMicroAttackSeconds = 0.00025;
constexpr double noiseMicroAttackSeconds = 0.00050;
constexpr double transientSmoothingSeconds = 0.010;
constexpr double noiseSmoothingSeconds = 0.020;
constexpr double layerBypassSmoothingSeconds = 0.005;
}

DeepPluckTransientNoise::DeepPluckTransientNoise (std::uint32_t voiceIndexToUse) noexcept
    : baseSeed (mixSeed (0x9e3779b9u ^ ((voiceIndexToUse + 1u) * 0x85ebca6bu))),
      randomState (baseSeed)
{
}

void DeepPluckTransientNoise::prepare (double newSampleRate) noexcept
{
    sampleRate = std::isfinite (newSampleRate) && newSampleRate > 0.0
                   ? newSampleRate : 44100.0;
    transientEnvelope.prepare (sampleRate, static_cast<float> (transientMicroAttackSeconds));
    noiseEnvelope.prepare (sampleRate, static_cast<float> (noiseMicroAttackSeconds));
    transientGain.reset (sampleRate, transientSmoothingSeconds);
    transientFrequency.reset (sampleRate, transientSmoothingSeconds);
    transientMix.reset (sampleRate, layerBypassSmoothingSeconds);
    noiseGain.reset (sampleRate, noiseSmoothingSeconds);
    noiseFilterCoefficient.reset (sampleRate, noiseSmoothingSeconds);
    noiseMix.reset (sampleRate, layerBypassSmoothingSeconds);
    dcBlockCoefficient = std::exp (-juce::MathConstants<float>::twoPi
                                   * dcBlockFrequencyHz / static_cast<float> (sampleRate));
    noteCounter = 0;
    setParameters (parameters);
    synchroniseSmoothers();
    reset();
}

void DeepPluckTransientNoise::reset() noexcept
{
    transientEnvelope.reset();
    noiseEnvelope.reset();
    transientPhase = 0.0;
    noiseInputHistory = 0.0f;
    noiseHighPassHistory = 0.0f;
    noiseLowPassState1 = 0.0f;
    noiseLowPassState2 = 0.0f;
    randomState = baseSeed;
    synchroniseSmoothers();
}

void DeepPluckTransientNoise::setParameters (
    DeepPluckTransientNoiseParameters newParameters) noexcept
{
    const auto previous = parameters;
    parameters.transientEnabled = sanitise (newParameters.transientEnabled, 1.0f, 0.0f, 1.0f);
    parameters.transientLevelDb = sanitise (newParameters.transientLevelDb, -18.0f, -60.0f, 0.0f);
    parameters.transientDecaySeconds = sanitise (
        newParameters.transientDecaySeconds, 0.012f, 0.001f, 0.100f);
    parameters.transientTone = sanitise (newParameters.transientTone, 0.55f, 0.0f, 1.0f);
    parameters.noiseEnabled = sanitise (newParameters.noiseEnabled, 1.0f, 0.0f, 1.0f);
    parameters.noiseLevelDb = sanitise (newParameters.noiseLevelDb, -27.0f, -60.0f, 0.0f);
    parameters.noiseDecaySeconds = sanitise (
        newParameters.noiseDecaySeconds, 0.120f, 0.005f, 1.500f);
    parameters.noiseTone = sanitise (newParameters.noiseTone, 0.35f, 0.0f, 1.0f);

    transientEnvelope.setDecaySeconds (parameters.transientDecaySeconds,
                                       0.001f, 0.100f, 0.012f);
    noiseEnvelope.setDecaySeconds (parameters.noiseDecaySeconds,
                                   0.005f, 1.500f, 0.120f);

    if (parameters.transientLevelDb != previous.transientLevelDb)
        transientGain.setTargetValue (levelGain (parameters.transientLevelDb,
                                                 transientCalibration));

    if (parameters.transientTone != previous.transientTone)
        transientFrequency.setTargetValue (toneFrequency (parameters.transientTone,
                                                           transientMinimumFrequencyHz,
                                                           transientMaximumFrequencyHz));

    if (parameters.transientEnabled != previous.transientEnabled)
        transientMix.setTargetValue (parameters.transientEnabled >= 0.5f ? 1.0f : 0.0f);

    if (parameters.noiseLevelDb != previous.noiseLevelDb)
        noiseGain.setTargetValue (levelGain (parameters.noiseLevelDb, noiseCalibration));

    if (parameters.noiseTone != previous.noiseTone)
    {
        const auto maximumNoiseCutoff = juce::jmax (
            noiseMinimumCutoffHz,
            juce::jmin (noiseMaximumCutoffHz, static_cast<float> (sampleRate) * 0.45f));
        const auto cutoff = toneFrequency (parameters.noiseTone,
                                           noiseMinimumCutoffHz, maximumNoiseCutoff);
        noiseFilterCoefficient.setTargetValue (
            1.0f - std::exp (-juce::MathConstants<float>::twoPi
                             * cutoff / static_cast<float> (sampleRate)));
    }

    if (parameters.noiseEnabled != previous.noiseEnabled)
        noiseMix.setTargetValue (parameters.noiseEnabled >= 0.5f ? 1.0f : 0.0f);
}

void DeepPluckTransientNoise::noteOn (int midiNoteNumber) noexcept
{
    synchroniseSmoothers();
    transientPhase = 0.0;
    noiseInputHistory = 0.0f;
    noiseHighPassHistory = 0.0f;
    noiseLowPassState1 = 0.0f;
    noiseLowPassState2 = 0.0f;

    const auto note = static_cast<std::uint32_t> (juce::jlimit (0, 127, midiNoteNumber) + 1);
    randomState = mixSeed (baseSeed ^ (note * 0xc2b2ae35u)
                           ^ ((++noteCounter) * 0x27d4eb2du));
    if (randomState == 0)
        randomState = baseSeed;

    transientEnvelope.trigger();
    noiseEnvelope.trigger();
}

float DeepPluckTransientNoise::processSample() noexcept
{
    const auto currentTransientGain = transientGain.getNextValue();
    const auto currentTransientFrequency = transientFrequency.getNextValue();
    const auto currentTransientMix = transientMix.getNextValue();
    const auto currentNoiseGain = noiseGain.getNextValue();
    const auto currentNoiseFilterCoefficient = noiseFilterCoefficient.getNextValue();
    const auto currentNoiseMix = noiseMix.getNextValue();

    auto transient = 0.0f;
    const auto transientEnvelopeValue = transientEnvelope.getNextSample();
    if (transientEnvelopeValue > 0.0f)
    {
        transient = static_cast<float> (std::sin (
                        juce::MathConstants<double>::twoPi * transientPhase))
                    * transientEnvelopeValue * currentTransientGain * currentTransientMix;
        transientPhase += static_cast<double> (currentTransientFrequency) / sampleRate;
        transientPhase -= std::floor (transientPhase);
    }

    auto noise = 0.0f;
    const auto noiseEnvelopeValue = noiseEnvelope.getNextSample();
    if (noiseEnvelopeValue > 0.0f)
    {
        const auto input = nextNoiseSample();
        const auto highPassed = input - noiseInputHistory
                                + dcBlockCoefficient * noiseHighPassHistory;
        noiseInputHistory = input;
        noiseHighPassHistory = highPassed;
        noiseLowPassState1 += currentNoiseFilterCoefficient
                              * (highPassed - noiseLowPassState1);
        noiseLowPassState2 += currentNoiseFilterCoefficient
                              * (noiseLowPassState1 - noiseLowPassState2);
        noise = noiseLowPassState2 * noiseEnvelopeValue * currentNoiseGain * currentNoiseMix;
    }

    const auto output = transient + noise;
    if (std::isfinite (output))
        return output;

    reset();
    return 0.0f;
}

bool DeepPluckTransientNoise::isTransientActive() const noexcept
{
    return transientEnvelope.isActive();
}

bool DeepPluckTransientNoise::isNoiseActive() const noexcept
{
    return noiseEnvelope.isActive();
}

float DeepPluckTransientNoise::getTransientEnvelopeLevel() const noexcept
{
    return transientEnvelope.getCurrentLevel();
}

float DeepPluckTransientNoise::getNoiseEnvelopeLevel() const noexcept
{
    return noiseEnvelope.getCurrentLevel();
}

void DeepPluckTransientNoise::OneShotEnvelope::prepare (double newSampleRate,
                                                        float microAttackSeconds) noexcept
{
    sampleRate = std::isfinite (newSampleRate) && newSampleRate > 0.0
                   ? newSampleRate : 44100.0;
    const auto safeAttack = std::isfinite (microAttackSeconds)
                              ? juce::jlimit (0.0001f, 0.0005f, microAttackSeconds)
                              : 0.00025f;
    attackSamples = juce::jmax (1, static_cast<int> (
        std::round (sampleRate * static_cast<double> (safeAttack))));
    reset();
}

void DeepPluckTransientNoise::OneShotEnvelope::reset() noexcept
{
    currentLevel = 0.0f;
    decayCoefficient = 0.0f;
    attackSamplesRemaining = 0;
    decaySamplesRemaining = 0;
    stage = Stage::idle;
}

void DeepPluckTransientNoise::OneShotEnvelope::setDecaySeconds (
    float newDecaySeconds, float minimum, float maximum, float fallback) noexcept
{
    const auto validated = DeepPluckTransientNoise::sanitise (
        newDecaySeconds, fallback, minimum, maximum);
    if (juce::approximatelyEqual (validated, decaySeconds))
        return;

    decaySeconds = validated;
    if (stage == Stage::decay)
        updateDecayCoefficient();
}

void DeepPluckTransientNoise::OneShotEnvelope::trigger() noexcept
{
    currentLevel = 0.0f;
    decayCoefficient = 0.0f;
    attackSamplesRemaining = attackSamples;
    decaySamplesRemaining = 0;
    stage = Stage::attack;
}

float DeepPluckTransientNoise::OneShotEnvelope::getNextSample() noexcept
{
    if (stage == Stage::idle)
        return 0.0f;

    if (stage == Stage::attack)
    {
        const auto completed = attackSamples - attackSamplesRemaining + 1;
        currentLevel = static_cast<float> (completed) / static_cast<float> (attackSamples);
        --attackSamplesRemaining;
        if (attackSamplesRemaining <= 0)
            beginDecay();
        return juce::jlimit (0.0f, 1.0f, currentLevel);
    }

    currentLevel *= decayCoefficient;
    --decaySamplesRemaining;
    if (! std::isfinite (currentLevel) || decaySamplesRemaining <= 0
        || currentLevel <= envelopeFloor)
    {
        reset();
        return 0.0f;
    }

    return currentLevel;
}

void DeepPluckTransientNoise::OneShotEnvelope::beginDecay() noexcept
{
    currentLevel = 1.0f;
    stage = Stage::decay;
    updateDecayCoefficient();
}

void DeepPluckTransientNoise::OneShotEnvelope::updateDecayCoefficient() noexcept
{
    if (currentLevel <= envelopeFloor)
    {
        reset();
        return;
    }

    decaySamplesRemaining = juce::jmax (1, static_cast<int> (
        std::round (sampleRate * static_cast<double> (decaySeconds))));
    decayCoefficient = std::exp (std::log (envelopeFloor / currentLevel)
                                 / static_cast<float> (decaySamplesRemaining));
}

float DeepPluckTransientNoise::sanitise (float value, float fallback,
                                         float minimum, float maximum) noexcept
{
    return std::isfinite (value) ? juce::jlimit (minimum, maximum, value) : fallback;
}

float DeepPluckTransientNoise::levelGain (float decibels, float calibration) noexcept
{
    return calibration * juce::Decibels::decibelsToGain (
        sanitise (decibels, -60.0f, -60.0f, 0.0f));
}

float DeepPluckTransientNoise::toneFrequency (float tone, float minimumHz,
                                              float maximumHz) noexcept
{
    const auto normalised = sanitise (tone, 0.5f, 0.0f, 1.0f);
    const auto safeMinimum = juce::jmax (1.0f, minimumHz);
    const auto safeMaximum = juce::jmax (safeMinimum, maximumHz);
    return safeMinimum * std::exp2 (normalised * std::log2 (safeMaximum / safeMinimum));
}

std::uint32_t DeepPluckTransientNoise::mixSeed (std::uint32_t value) noexcept
{
    value ^= value >> 16u;
    value *= 0x7feb352du;
    value ^= value >> 15u;
    value *= 0x846ca68bu;
    value ^= value >> 16u;
    return value == 0 ? 1u : value;
}

float DeepPluckTransientNoise::nextNoiseSample() noexcept
{
    randomState ^= randomState << 13u;
    randomState ^= randomState >> 17u;
    randomState ^= randomState << 5u;
    const auto normalized = static_cast<float> (randomState >> 8u)
                            * (1.0f / 16777215.0f);
    return normalized * 2.0f - 1.0f;
}

void DeepPluckTransientNoise::synchroniseSmoothers() noexcept
{
    transientGain.setCurrentAndTargetValue (
        levelGain (parameters.transientLevelDb, transientCalibration));
    transientFrequency.setCurrentAndTargetValue (
        toneFrequency (parameters.transientTone,
                       transientMinimumFrequencyHz, transientMaximumFrequencyHz));
    transientMix.setCurrentAndTargetValue (parameters.transientEnabled >= 0.5f ? 1.0f : 0.0f);
    noiseGain.setCurrentAndTargetValue (levelGain (parameters.noiseLevelDb, noiseCalibration));

    const auto maximumNoiseCutoff = juce::jmax (
        noiseMinimumCutoffHz,
        juce::jmin (noiseMaximumCutoffHz, static_cast<float> (sampleRate) * 0.45f));
    const auto cutoff = toneFrequency (parameters.noiseTone,
                                       noiseMinimumCutoffHz, maximumNoiseCutoff);
    noiseFilterCoefficient.setCurrentAndTargetValue (
        1.0f - std::exp (-juce::MathConstants<float>::twoPi
                         * cutoff / static_cast<float> (sampleRate)));
    noiseMix.setCurrentAndTargetValue (parameters.noiseEnabled >= 0.5f ? 1.0f : 0.0f);
}
}
