#include "DeepPluckOscillator.h"

#include <cmath>

namespace deeppluck
{
namespace
{
constexpr double waveformCrossfadeSeconds = 0.005;
constexpr double pulseWidthSmoothingSeconds = 0.020;

std::size_t waveformIndex (DeepPluckOscillatorWaveform waveform) noexcept
{
    return static_cast<std::size_t> (waveform);
}

float sanitisePulseWidth (float value) noexcept
{
    return std::isfinite (value) ? juce::jlimit (0.05f, 0.95f, value) : 0.5f;
}
}

void DeepPluckOscillator::prepare (double newSampleRate) noexcept
{
    sampleRate = std::isfinite (newSampleRate) && newSampleRate > 0.0 ? newSampleRate : 44100.0;
    for (auto& mix : waveformMix)
        mix.reset (sampleRate, waveformCrossfadeSeconds);
    waveformRampLengthSamples = juce::jmax (
        1, static_cast<int> (std::round (sampleRate * waveformCrossfadeSeconds)));
    waveformRampSamplesRemaining = 0;
    pulseWidth.reset (sampleRate, pulseWidthSmoothingSeconds);
    initialiseWaveformMix (waveform);
    pulseWidth.setCurrentAndTargetValue (pulseWidthTarget);
    frequencyHz = std::isfinite (frequencyHz)
                    ? juce::jlimit (0.0, sampleRate * 0.49, frequencyHz) : 0.0;
    phaseIncrement = frequencyHz / sampleRate;
    oneMinusPhaseIncrement = 1.0 - phaseIncrement;
    inversePhaseIncrement = phaseIncrement > 0.0 ? 1.0 / phaseIncrement : 0.0;
}

void DeepPluckOscillator::reset (double newPhase) noexcept
{
    reset (newPhase, waveform, pulseWidthTarget);
}

void DeepPluckOscillator::reset (double newPhase,
                                 DeepPluckOscillatorWaveform newWaveform,
                                 float newPulseWidth) noexcept
{
    if (! std::isfinite (newPhase))
        newPhase = 0.0;

    phase = wrapPhase (newPhase);
    waveform = waveformFromParameter (static_cast<float> (newWaveform));
    pulseWidthTarget = sanitisePulseWidth (newPulseWidth);
    initialiseWaveformMix (waveform);
    waveformRampSamplesRemaining = 0;
    pulseWidth.setCurrentAndTargetValue (pulseWidthTarget);
    triangleState = triangleInitialState();
}

void DeepPluckOscillator::setWaveform (DeepPluckOscillatorWaveform newWaveform) noexcept
{
    const auto bounded = waveformFromParameter (static_cast<float> (newWaveform));
    if (bounded == waveform)
        return;

    waveform = bounded;
    waveformRampSamplesRemaining = waveformRampLengthSamples;
    for (auto index = std::size_t { 0 }; index < waveformMix.size(); ++index)
        waveformMix[index].setTargetValue (index == waveformIndex (waveform) ? 1.0f : 0.0f);
}

void DeepPluckOscillator::setPulseWidth (float newPulseWidth) noexcept
{
    const auto bounded = sanitisePulseWidth (newPulseWidth);
    if (bounded == pulseWidthTarget)
        return;

    pulseWidthTarget = bounded;
    pulseWidth.setTargetValue (pulseWidthTarget);
}

float DeepPluckOscillator::getWaveformMix (DeepPluckOscillatorWaveform selected) const noexcept
{
    return waveformMix[waveformIndex (waveformFromParameter (
        static_cast<float> (selected)))].getCurrentValue();
}

DeepPluckOscillatorWaveform DeepPluckOscillator::waveformFromParameter (float value) noexcept
{
    const auto bounded = std::isfinite (value)
                           ? juce::jlimit (0, 4, static_cast<int> (std::round (value)))
                           : 0;
    return static_cast<DeepPluckOscillatorWaveform> (bounded);
}

double DeepPluckOscillator::polyBlep (double currentPhase, double increment) noexcept
{
    if (currentPhase < increment)
    {
        const auto x = currentPhase / increment;
        return x + x - x * x - 1.0;
    }

    if (currentPhase > 1.0 - increment)
    {
        const auto x = (currentPhase - 1.0) / increment;
        return x * x + x + x + 1.0;
    }

    return 0.0;
}

double DeepPluckOscillator::wrapPhase (double value) noexcept
{
    value -= std::floor (value);
    return value < 0.0 ? value + 1.0 : value;
}

double DeepPluckOscillator::pulseSample (double currentPhase, double increment,
                                         double width, double wrapCorrection) noexcept
{
    auto sample = currentPhase < width ? 1.0 : -1.0;
    sample += wrapCorrection;
    const auto edgePhase = currentPhase >= width
                             ? currentPhase - width : currentPhase - width + 1.0;
    sample -= polyBlep (edgePhase, increment);
    sample -= 2.0 * width - 1.0;
    return sample / (2.0 * juce::jmax (width, 1.0 - width));
}

double DeepPluckOscillator::triangleInitialState() const noexcept
{
    const auto idealTriangle = 1.0 - 4.0 * std::abs (phase - 0.5);
    return 0.25 * idealTriangle;
}

void DeepPluckOscillator::initialiseWaveformMix (
    DeepPluckOscillatorWaveform newWaveform) noexcept
{
    for (auto index = std::size_t { 0 }; index < waveformMix.size(); ++index)
        waveformMix[index].setCurrentAndTargetValue (
            index == waveformIndex (newWaveform) ? 1.0f : 0.0f);
}
}
