#include "DeepPluckEffectsRack.h"

#include <algorithm>
#include <cmath>

namespace deeppluck
{
int getEffectPosition (int order, EffectKind effect) noexcept
{
    const auto safeOrder = juce::jlimit (0, 5, order);
    const auto& permutation = effectOrders[static_cast<std::size_t> (safeOrder)];
    for (auto index = 0; index < 3; ++index)
        if (permutation[static_cast<std::size_t> (index)] == effect)
            return index + 1;
    return 1;
}

int moveEffectToPosition (int order, EffectKind effect, int oneBasedPosition) noexcept
{
    auto permutation = effectOrders[static_cast<std::size_t> (juce::jlimit (0, 5, order))];
    const auto desired = juce::jlimit (0, 2, oneBasedPosition - 1);
    const auto current = getEffectPosition (order, effect) - 1;
    if (current < desired)
        for (auto index = current; index < desired; ++index)
            permutation[static_cast<std::size_t> (index)] =
                permutation[static_cast<std::size_t> (index + 1)];
    else if (current > desired)
        for (auto index = current; index > desired; --index)
            permutation[static_cast<std::size_t> (index)] =
                permutation[static_cast<std::size_t> (index - 1)];
    permutation[static_cast<std::size_t> (desired)] = effect;

    const auto found = std::find (effectOrders.begin(), effectOrders.end(), permutation);
    return found != effectOrders.end()
               ? static_cast<int> (std::distance (effectOrders.begin(), found)) : 0;
}

void DeepPluckEffectsRack::prepare (double sampleRate, int maximumExpectedSamplesPerBlock)
{
    const auto validRate = std::isfinite (sampleRate) && sampleRate > 0.0 ? sampleRate : 44100.0;
    maximumBlockSize = juce::jmax (1, maximumExpectedSamplesPerBlock);
    chorus.prepare (validRate, maximumBlockSize);
    delay.prepare (validRate, maximumBlockSize);
    reverb.prepare (validRate, maximumBlockSize);
    dryBuffer.setSize (2, maximumBlockSize, false, true, false);
    orderBridge.reset (validRate, 0.015);
    reset();
}

void DeepPluckEffectsRack::reset() noexcept
{
    chorus.reset();
    delay.reset();
    reverb.reset();
    dryBuffer.clear();
    orderBridge.setCurrentAndTargetValue (1.0f);
    currentOrder = 0;
    pendingOrder = 0;
    bridgePhase = BridgePhase::stable;
    resetRequested.store (false, std::memory_order_release);
}

void DeepPluckEffectsRack::requestReset() noexcept
{
    resetRequested.store (true, std::memory_order_release);
}

void DeepPluckEffectsRack::processEffect (EffectKind effect,
                                          juce::AudioBuffer<float>& buffer,
                                          int startSample,
                                          int numSamples) noexcept
{
    switch (effect)
    {
        case EffectKind::chorus: chorus.process (buffer, startSample, numSamples); break;
        case EffectKind::delay: delay.process (buffer, startSample, numSamples); break;
        case EffectKind::reverb: reverb.process (buffer, startSample, numSamples); break;
    }
}

void DeepPluckEffectsRack::process (juce::AudioBuffer<float>& buffer,
                                    const DeepPluckParameterSnapshot& snapshot,
                                    double lastValidTempoBpm) noexcept
{
    if (resetRequested.exchange (false, std::memory_order_acq_rel))
        reset();

    chorus.setParameters (snapshot);
    delay.setParameters (snapshot, lastValidTempoBpm);
    reverb.setParameters (snapshot);

    const auto requestedOrder = juce::jlimit (
        0, 5, static_cast<int> (std::lround (snapshot.fxOrder)));
    if (requestedOrder != pendingOrder)
        pendingOrder = requestedOrder;

    if (pendingOrder != currentOrder && bridgePhase != BridgePhase::fadingOut)
    {
        bridgePhase = BridgePhase::fadingOut;
        orderBridge.setTargetValue (0.0f);
    }

    if (bridgePhase == BridgePhase::stable && currentOrder == pendingOrder
        && chorus.isIdle() && delay.isIdle() && reverb.isIdle())
        return;

    for (auto start = 0; start < buffer.getNumSamples(); start += maximumBlockSize)
    {
        const auto count = juce::jmin (maximumBlockSize, buffer.getNumSamples() - start);
        dryBuffer.copyFrom (0, 0, buffer, 0, start, count);
        dryBuffer.copyFrom (1, 0, buffer, 1, start, count);

        const auto& order = effectOrders[static_cast<std::size_t> (currentOrder)];
        for (const auto effect : order)
            processEffect (effect, buffer, start, count);

        constexpr auto halfPi = juce::MathConstants<float>::halfPi;
        for (auto sample = 0; sample < count; ++sample)
        {
            const auto bridge = juce::jlimit (0.0f, 1.0f, orderBridge.getNextValue());
            if (bridge >= 1.0f)
                continue;
            if (bridge <= 0.0f)
            {
                for (auto channel = 0; channel < 2; ++channel)
                {
                    const auto index = start + sample;
                    buffer.setSample (channel, index,
                                      dryBuffer.getSample (channel, sample));
                }
                continue;
            }
            const auto dryGain = std::cos (halfPi * bridge);
            const auto wetGain = std::sin (halfPi * bridge);
            for (auto channel = 0; channel < 2; ++channel)
            {
                const auto index = start + sample;
                buffer.setSample (channel, index,
                                  dryBuffer.getSample (channel, sample) * dryGain
                                      + buffer.getSample (channel, index) * wetGain);
            }
        }

        if (bridgePhase == BridgePhase::fadingOut && ! orderBridge.isSmoothing()
            && orderBridge.getCurrentValue() <= 0.0f)
        {
            currentOrder = pendingOrder;
            bridgePhase = BridgePhase::fadingIn;
            orderBridge.setTargetValue (1.0f);
        }
        else if (bridgePhase == BridgePhase::fadingIn && ! orderBridge.isSmoothing()
                 && orderBridge.getCurrentValue() >= 1.0f)
        {
            bridgePhase = BridgePhase::stable;
            if (pendingOrder != currentOrder)
            {
                bridgePhase = BridgePhase::fadingOut;
                orderBridge.setTargetValue (0.0f);
            }
        }
    }
}

int DeepPluckEffectsRack::getCurrentOrder() const noexcept { return currentOrder; }

bool DeepPluckEffectsRack::isIdle() const noexcept
{
    return bridgePhase == BridgePhase::stable && chorus.isIdle()
        && delay.isIdle() && reverb.isIdle();
}
}
