#include "DeepPluckReverb.h"
#include "DeepPluckEffectMix.h"

#include <algorithm>
#include <cmath>

namespace deeppluck
{
void DeepPluckReverb::prepare (double sampleRate, int maximumBlockSize)
{
    currentSampleRate = std::isfinite (sampleRate) && sampleRate > 0.0 ? sampleRate : 44100.0;
    const auto validBlock = juce::jmax (1, maximumBlockSize);
    const juce::dsp::ProcessSpec spec {
        currentSampleRate, static_cast<juce::uint32> (validBlock), 2
    };
    reverb.prepare (spec);
    wetBuffer.setSize (2, validBlock, false, true, false);
    const auto capacity = juce::jmax (
        8, static_cast<int> (std::ceil (maximumPreDelaySeconds * currentSampleRate))
               + validBlock + 4);
    for (auto& channel : preDelayBuffer)
        channel.assign (static_cast<std::size_t> (capacity), 0.0f);
    preDelaySamples.reset (currentSampleRate, 0.030);
    mix.reset (currentSampleRate, 0.020);
    feedGain.reset (currentSampleRate, 0.020);
    bypassDry.reset (currentSampleRate, 0.020);
    reset();
}

void DeepPluckReverb::clearSignalState() noexcept
{
    reverb.reset();
    for (auto& channel : preDelayBuffer)
        std::fill (channel.begin(), channel.end(), 0.0f);
    wetBuffer.clear();
    writePosition = 0;
    quietBlockCount = 0;
}

void DeepPluckReverb::reset() noexcept
{
    clearSignalState();
    preDelaySamples.setCurrentAndTargetValue (0.018 * currentSampleRate);
    mix.setCurrentAndTargetValue (0.18f);
    feedGain.setCurrentAndTargetValue (0.0f);
    bypassDry.setCurrentAndTargetValue (1.0f);
    active = false;
    bypassed = true;
}

void DeepPluckReverb::setParameters (const DeepPluckParameterSnapshot& snapshot) noexcept
{
    bypassed = snapshot.reverbBypass >= 0.5f;
    const auto targetMix = juce::jlimit (0.0f, 1.0f, snapshot.reverbMix);
    juce::dsp::Reverb::Parameters parameters;
    parameters.roomSize = juce::jlimit (0.10f, 0.92f, snapshot.reverbSize);
    parameters.damping = juce::jlimit (0.0f, 1.0f, snapshot.reverbDamping);
    parameters.wetLevel = 1.0f / 3.0f;
    parameters.dryLevel = 0.0f;
    parameters.width = 1.0f;
    parameters.freezeMode = 0.0f;
    reverb.setParameters (parameters);
    preDelaySamples.setTargetValue (juce::jlimit (
        0.0, maximumPreDelaySeconds,
        static_cast<double> (snapshot.reverbPreDelaySeconds)) * currentSampleRate);
    if (! active && ! bypassed)
        mix.setCurrentAndTargetValue (targetMix);
    else
        mix.setTargetValue (targetMix);
    feedGain.setTargetValue (bypassed ? 0.0f : 1.0f);
    bypassDry.setTargetValue (bypassed ? 1.0f : 0.0f);
    if (! bypassed)
        active = true;
}

float DeepPluckReverb::readPreDelay (int channel, double delayInSamples) const noexcept
{
    const auto size = static_cast<int> (preDelayBuffer[static_cast<std::size_t> (channel)].size());
    auto read = static_cast<double> (writePosition) - delayInSamples;
    while (read < 0.0)
        read += static_cast<double> (size);
    while (read >= static_cast<double> (size))
        read -= static_cast<double> (size);
    const auto first = static_cast<int> (read);
    const auto second = (first + 1) % size;
    const auto fraction = static_cast<float> (read - static_cast<double> (first));
    const auto& data = preDelayBuffer[static_cast<std::size_t> (channel)];
    return data[static_cast<std::size_t> (first)]
         + fraction * (data[static_cast<std::size_t> (second)]
                     - data[static_cast<std::size_t> (first)]);
}

void DeepPluckReverb::process (juce::AudioBuffer<float>& buffer,
                               int startSample,
                               int numSamples) noexcept
{
    if (! active || numSamples <= 0)
        return;

    jassert (numSamples <= wetBuffer.getNumSamples());
    const auto ringSize = static_cast<int> (preDelayBuffer[0].size());
    for (auto sample = 0; sample < numSamples; ++sample)
    {
        const auto currentFeed = juce::jlimit (0.0f, 1.0f, feedGain.getNextValue());
        const auto currentDelay = juce::jlimit (
            0.0, maximumPreDelaySeconds * currentSampleRate,
            preDelaySamples.getNextValue());
        for (auto channel = 0; channel < 2; ++channel)
        {
            const auto input = buffer.getSample (channel, startSample + sample) * currentFeed;
            preDelayBuffer[static_cast<std::size_t> (channel)][static_cast<std::size_t> (writePosition)] = input;
            wetBuffer.setSample (channel, sample, readPreDelay (channel, currentDelay));
        }
        writePosition = (writePosition + 1) % ringSize;
    }

    auto block = juce::dsp::AudioBlock<float> (wetBuffer).getSubBlock (
        0, static_cast<std::size_t> (numSamples));
    juce::dsp::ProcessContextReplacing<float> context (block);
    reverb.process (context);

    auto blockPeak = 0.0f;
    for (auto sample = 0; sample < numSamples; ++sample)
    {
        const auto currentMix = juce::jlimit (0.0f, 1.0f, mix.getNextValue());
        const auto currentBypassDry = juce::jlimit (0.0f, 1.0f, bypassDry.getNextValue());
        const auto gains = calculateEffectMixGains (
            currentMix, wetCalibration, currentBypassDry);
        for (auto channel = 0; channel < 2; ++channel)
        {
            const auto wet = wetBuffer.getSample (channel, sample);
            const auto index = startSample + sample;
            const auto output = buffer.getSample (channel, index) * gains.dry + wet * gains.wet;
            buffer.setSample (channel, index, std::isfinite (output) ? output : 0.0f);
            blockPeak = juce::jmax (blockPeak, std::abs (wet));
        }
    }

    if (bypassed && ! feedGain.isSmoothing() && feedGain.getCurrentValue() <= 0.0f)
    {
        quietBlockCount = blockPeak < tailSilenceThreshold ? quietBlockCount + 1 : 0;
        if (quietBlockCount >= requiredQuietBlocks)
        {
            clearSignalState();
            active = false;
        }
    }
    else
    {
        quietBlockCount = 0;
    }
}

bool DeepPluckReverb::isIdle() const noexcept { return ! active; }
}
