#include "DeepPluckLfo.h"

#include <array>
#include <cmath>

namespace deeppluck
{
namespace
{
constexpr double rateSmoothingSeconds = 0.020;
constexpr double outputSmoothingSeconds = 0.002;
constexpr double fallbackSampleRate = 44100.0;
constexpr double fallbackTempoBpm = 120.0;
constexpr double minimumTempoBpm = 20.0;
constexpr double maximumTempoBpm = 400.0;
constexpr double minimumRateHz = 0.020;
constexpr double maximumRateHz = 12.0;
constexpr double maximumSynchronisedRateHz = 30.0;

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

int roundedChoice (float value, int fallback, int maximum) noexcept
{
    if (! std::isfinite (value))
        return fallback;

    return juce::jlimit (0, maximum, static_cast<int> (std::round (value)));
}
}

void DeepPluckLfo::prepare (double newSampleRate) noexcept
{
    sampleRate = sanitise (newSampleRate, fallbackSampleRate, 1.0, 768000.0);
    smoothedRateHz.reset (sampleRate, rateSmoothingSeconds);
    smoothedRateHz.setCurrentAndTargetValue (0.25);
    outputSmoothingCoefficient = static_cast<float> (
        1.0 - std::exp (-1.0 / (sampleRate * outputSmoothingSeconds)));
    restart();
}

void DeepPluckLfo::setParameters (DeepPluckLfoParameters newParameters) noexcept
{
    waveform = static_cast<DeepPluckLfoWaveform> (
        roundedChoice (newParameters.waveform, 0, 3));
    restartMode = static_cast<DeepPluckLfoRestartMode> (
        roundedChoice (newParameters.restartMode, 1, 1));

    const auto syncEnabled = std::isfinite (newParameters.tempoSync)
                          && newParameters.tempoSync >= 0.5f;
    const auto division = roundedChoice (newParameters.syncDivision, 4, 6);
    const auto freeRate = sanitise (newParameters.freeRateHz, 0.25,
                                    minimumRateHz, maximumRateHz);
    const auto tempo = sanitise (newParameters.tempoBpm, fallbackTempoBpm,
                                 minimumTempoBpm, maximumTempoBpm);
    const auto targetRate = syncEnabled ? synchronisedRateHz (tempo, division) : freeRate;
    smoothedRateHz.setTargetValue (juce::jlimit (minimumRateHz, maximumSynchronisedRateHz,
                                                targetRate));
}

void DeepPluckLfo::restart() noexcept
{
    phase = 0.0;
    smoothedOutput = waveformValue (waveform, phase);
}

float DeepPluckLfo::processSample() noexcept
{
    const auto rawOutput = waveformValue (waveform, phase);
    smoothedOutput += outputSmoothingCoefficient * (rawOutput - smoothedOutput);

    if (! std::isfinite (smoothedOutput))
        smoothedOutput = 0.0f;

    smoothedOutput = juce::jlimit (-1.0f, 1.0f, smoothedOutput);
    const auto rate = sanitise (smoothedRateHz.getNextValue(), 0.25,
                                minimumRateHz, maximumSynchronisedRateHz);
    phase = wrapPhase (phase + rate / sampleRate);
    return smoothedOutput;
}

float DeepPluckLfo::waveformValue (DeepPluckLfoWaveform selectedWaveform,
                                   double phaseToEvaluate) noexcept
{
    const auto boundedPhase = wrapPhase (phaseToEvaluate);
    float value = 0.0f;

    switch (selectedWaveform)
    {
        case DeepPluckLfoWaveform::sine:
            value = static_cast<float> (std::sin (
                juce::MathConstants<double>::twoPi * boundedPhase));
            break;
        case DeepPluckLfoWaveform::triangle:
            value = static_cast<float> (1.0 - 4.0 * std::abs (boundedPhase - 0.5));
            break;
        case DeepPluckLfoWaveform::saw:
            value = static_cast<float> (2.0 * boundedPhase - 1.0);
            break;
        case DeepPluckLfoWaveform::square:
            value = boundedPhase < 0.5 ? 1.0f : -1.0f;
            break;
    }

    return std::isfinite (value) ? juce::jlimit (-1.0f, 1.0f, value) : 0.0f;
}

double DeepPluckLfo::beatsPerCycle (int divisionIndex) noexcept
{
    constexpr std::array values { 16.0, 8.0, 4.0, 2.0, 1.0, 0.5, 0.25 };
    return values[static_cast<std::size_t> (juce::jlimit (0, 6, divisionIndex))];
}

double DeepPluckLfo::synchronisedRateHz (double tempoBpm, int divisionIndex) noexcept
{
    const auto tempo = sanitise (tempoBpm, fallbackTempoBpm,
                                 minimumTempoBpm, maximumTempoBpm);
    return tempo / (60.0 * beatsPerCycle (divisionIndex));
}

double DeepPluckLfo::wrapPhase (double value) noexcept
{
    if (! std::isfinite (value))
        return 0.0;

    value -= std::floor (value);
    return value < 0.0 ? value + 1.0 : value;
}
}
