#include "DeepPluckDelay.h"
#include "DeepPluckEffectMix.h"

#include <algorithm>
#include <cmath>

namespace deeppluck
{
void DeepPluckDelay::prepare (double sampleRate, int maximumBlockSize)
{
    currentSampleRate = std::isfinite (sampleRate) && sampleRate > 0.0 ? sampleRate : 44100.0;
    const auto capacity = juce::jmax (
        8, static_cast<int> (std::ceil (maximumDelaySeconds * currentSampleRate))
               + juce::jmax (1, maximumBlockSize) + 4);
    for (auto& channel : delayBuffer)
        channel.assign (static_cast<std::size_t> (capacity), 0.0f);

    delaySamples.reset (currentSampleRate, 0.040);
    feedback.reset (currentSampleRate, 0.020);
    toneCoefficient.reset (currentSampleRate, 0.030);
    mix.reset (currentSampleRate, 0.020);
    feedGain.reset (currentSampleRate, 0.020);
    bypassDry.reset (currentSampleRate, 0.020);
    reset();
}

void DeepPluckDelay::clearSignalState() noexcept
{
    for (auto& channel : delayBuffer)
        std::fill (channel.begin(), channel.end(), 0.0f);
    toneState = {};
    writePosition = 0;
    quietBlockCount = 0;
    quietHoldSamples = 0;
}

void DeepPluckDelay::reset() noexcept
{
    clearSignalState();
    targetDelaySeconds = 0.375;
    delaySamples.setCurrentAndTargetValue (targetDelaySeconds * currentSampleRate);
    feedback.setCurrentAndTargetValue (0.32f);
    const auto coefficient = 1.0f - std::exp (
        -juce::MathConstants<float>::twoPi * 6000.0f / static_cast<float> (currentSampleRate));
    toneCoefficient.setCurrentAndTargetValue (coefficient);
    mix.setCurrentAndTargetValue (0.22f);
    feedGain.setCurrentAndTargetValue (0.0f);
    bypassDry.setCurrentAndTargetValue (1.0f);
    active = false;
    bypassed = true;
}

void DeepPluckDelay::setParameters (const DeepPluckParameterSnapshot& snapshot,
                                    double lastValidTempoBpm) noexcept
{
    bypassed = snapshot.delayBypass >= 0.5f;
    const auto targetMix = juce::jlimit (0.0f, 1.0f, snapshot.delayMix);
    const auto tempo = std::isfinite (lastValidTempoBpm)
                           ? juce::jlimit (20.0, 400.0, lastValidTempoBpm) : 120.0;
    const auto division = juce::jlimit (0, 6, static_cast<int> (std::lround (
        std::isfinite (snapshot.delaySyncDivision) ? snapshot.delaySyncDivision : 1.0f)));
    targetDelaySeconds = snapshot.delaySync >= 0.5f
                             ? (60.0 / tempo) * beatsPerDelay[static_cast<std::size_t> (division)]
                             : static_cast<double> (snapshot.delayTimeSeconds);
    targetDelaySeconds = juce::jlimit (0.010, maximumDelaySeconds, targetDelaySeconds);
    delaySamples.setTargetValue (targetDelaySeconds * currentSampleRate);
    feedback.setTargetValue (juce::jlimit (0.0f, maximumFeedback, snapshot.delayFeedback));
    const auto toneHz = juce::jlimit (800.0f, 18000.0f, snapshot.delayToneHz);
    const auto coefficient = 1.0f - std::exp (
        -juce::MathConstants<float>::twoPi * toneHz / static_cast<float> (currentSampleRate));
    toneCoefficient.setTargetValue (juce::jlimit (0.0f, 1.0f, coefficient));
    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 DeepPluckDelay::readDelayedSample (int channel, double delayInSamples) const noexcept
{
    const auto size = static_cast<int> (delayBuffer[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 = delayBuffer[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 DeepPluckDelay::process (juce::AudioBuffer<float>& buffer,
                              int startSample,
                              int numSamples) noexcept
{
    if (! active || numSamples <= 0)
        return;

    auto blockPeak = 0.0f;
    const auto ringSize = static_cast<int> (delayBuffer[0].size());
    for (auto sample = 0; sample < numSamples; ++sample)
    {
        const auto currentDelay = juce::jlimit (
            1.0, maximumDelaySeconds * currentSampleRate, delaySamples.getNextValue());
        const auto currentFeedback = juce::jlimit (
            0.0f, maximumFeedback, feedback.getNextValue());
        const auto currentTone = juce::jlimit (
            0.0f, 1.0f, toneCoefficient.getNextValue());
        const auto currentMix = juce::jlimit (0.0f, 1.0f, mix.getNextValue());
        const auto currentFeed = juce::jlimit (0.0f, 1.0f, feedGain.getNextValue());
        const auto currentBypassDry = juce::jlimit (0.0f, 1.0f, bypassDry.getNextValue());
        const auto gains = calculateEffectMixGains (
            currentMix, wetCalibration, currentBypassDry);
        const auto index = startSample + sample;

        for (auto channel = 0; channel < 2; ++channel)
        {
            const auto input = buffer.getSample (channel, index);
            const auto delayed = readDelayedSample (channel, currentDelay);
            auto& filtered = toneState[static_cast<std::size_t> (channel)];
            filtered += currentTone * (delayed - filtered);
            if (! std::isfinite (filtered))
                filtered = 0.0f;
            const auto write = input * currentFeed + filtered * currentFeedback;
            delayBuffer[static_cast<std::size_t> (channel)][static_cast<std::size_t> (writePosition)] =
                std::isfinite (write) ? write : 0.0f;
            const auto output = input * gains.dry + filtered * gains.wet;
            buffer.setSample (channel, index, std::isfinite (output) ? output : 0.0f);
            blockPeak = juce::jmax (blockPeak, std::abs (filtered), std::abs (write));
            if (std::abs (write) >= tailSilenceThreshold
                || std::abs (filtered) >= tailSilenceThreshold)
                quietHoldSamples = juce::jmax (
                    quietHoldSamples, static_cast<int> (std::ceil (currentDelay)) + 1);
        }
        if (quietHoldSamples > 0)
            --quietHoldSamples;
        writePosition = (writePosition + 1) % ringSize;
    }

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

bool DeepPluckDelay::isIdle() const noexcept { return ! active; }
double DeepPluckDelay::getTargetDelaySeconds() const noexcept { return targetDelaySeconds; }
}
