#pragma once

#include <juce_audio_basics/juce_audio_basics.h>

#include <array>
#include <bit>
#include <cmath>
#include <cstdint>

namespace deeppluck
{
enum class DeepPluckOscillatorWaveform
{
    saw = 0,
    triangle,
    square,
    sine,
    pulse
};

class DeepPluckOscillator final
{
public:
    void prepare (double newSampleRate) noexcept;
    void reset (double newPhase = 0.0) noexcept;
    void reset (double newPhase, DeepPluckOscillatorWaveform newWaveform,
                float newPulseWidth) noexcept;
    void setFrequency (double newFrequencyHz) noexcept
    {
        if (newFrequencyHz == frequencyHz)
            return;

        const auto boundedFrequency = std::isfinite (newFrequencyHz)
                                        ? juce::jlimit (0.0, sampleRate * 0.49,
                                                        newFrequencyHz)
                                        : 0.0;
        if (boundedFrequency == frequencyHz)
            return;

        frequencyHz = boundedFrequency;
        phaseIncrement = frequencyHz / sampleRate;
        oneMinusPhaseIncrement = 1.0 - phaseIncrement;
        inversePhaseIncrement = phaseIncrement > 0.0 ? 1.0 / phaseIncrement : 0.0;
    }
    void setWaveform (DeepPluckOscillatorWaveform newWaveform) noexcept;
    void setPulseWidth (float newPulseWidth) noexcept;
    [[nodiscard]]
#if defined (_MSC_VER)
    __forceinline
#endif
    float processSample() noexcept
    {
        // The conservative ramp counter (round >= smoother floor) reaches zero
        // only after every mix has settled to its exact one-hot target. No mix
        // state advances then. Still advance every active ramp, even at zero Hz.
        const auto crossfading = waveformRampSamplesRemaining != 0;
        // All reads below share this guard; the steady path needs no temporary
        // mix values or zero stores. Active-ramp arithmetic/order is unchanged.
        std::array<float, 5> mixValues;
        if (crossfading)
        {
            mixValues = {
                waveformMix[0].getNextValue(), waveformMix[1].getNextValue(),
                waveformMix[2].getNextValue(), waveformMix[3].getNextValue(),
                waveformMix[4].getNextValue()
            };
        }
        const auto smoothedPulseWidth = static_cast<double> (pulseWidth.getNextValue());

        if (phaseIncrement <= 0.0)
            return 0.0f;

        auto wrapCorrection = 0.0;
        if (phase < phaseIncrement)
        {
            const auto x = phase * inversePhaseIncrement;
            wrapCorrection = x + x - x * x - 1.0;
        }
        else if (phase > oneMinusPhaseIncrement)
        {
            const auto x = (phase - 1.0) * inversePhaseIncrement;
            wrapCorrection = x * x + x + x + 1.0;
        }

        const auto halfCycleDelta = phase - 0.5;
        auto halfCycleCorrection = 0.0;
        if (std::abs (halfCycleDelta) < phaseIncrement)
        {
            const auto x = halfCycleDelta * inversePhaseIncrement;
            halfCycleCorrection = x >= 0.0 ? x + x - x * x - 1.0
                                           : x * x + x + x + 1.0;
        }
        const auto square = (halfCycleDelta < 0.0 ? 1.0 : -1.0)
                            + wrapCorrection - halfCycleCorrection;
        triangleState = phaseIncrement * square
                        + oneMinusPhaseIncrement * triangleState;
        const auto triangleMixIndex = static_cast<std::size_t> (
            DeepPluckOscillatorWaveform::triangle);
        const auto triangleIsAudible = waveform == DeepPluckOscillatorWaveform::triangle
                                        || (crossfading && mixValues[triangleMixIndex] != 0.0f);
        constexpr auto exponentMask = std::uint64_t { 0x7ff0000000000000ULL };
        if (triangleIsAudible
            && (std::bit_cast<std::uint64_t> (triangleState) & exponentMask)
                   == exponentMask)
            triangleState = triangleInitialState();

        const auto saw = (phase * 2.0 - 1.0) - wrapCorrection;
        const auto triangle = 4.0 * triangleState;
        const auto pulseValue = [this, square, wrapCorrection, smoothedPulseWidth]
        {
            const auto guard = juce::jmin (0.49, juce::jmax (0.05, phaseIncrement));
            const auto effectiveWidth = juce::jlimit (
                guard, 1.0 - guard, smoothedPulseWidth);
            return 0.60
                   * (effectiveWidth == 0.5
                          ? square
                          : pulseSample (phase, phaseIncrement, effectiveWidth,
                                         wrapCorrection));
        };

        auto sample = 0.0;
        if (! crossfading)
        {
            switch (waveform)
            {
                case DeepPluckOscillatorWaveform::saw:      sample = saw; break;
                case DeepPluckOscillatorWaveform::triangle: sample = triangle; break;
                case DeepPluckOscillatorWaveform::square:   sample = 0.60 * square; break;
                case DeepPluckOscillatorWaveform::sine:
                    sample = 0.82 * std::sin (
                        juce::MathConstants<double>::twoPi * phase);
                    break;
                case DeepPluckOscillatorWaveform::pulse: sample = pulseValue(); break;
            }
        }
        else
        {
            --waveformRampSamplesRemaining;
            sample = static_cast<double> (mixValues[0]) * saw
                   + static_cast<double> (mixValues[1]) * triangle
                   + static_cast<double> (mixValues[2]) * 0.60 * square;
            if (mixValues[3] != 0.0f)
                sample += static_cast<double> (mixValues[3]) * 0.82
                          * std::sin (juce::MathConstants<double>::twoPi * phase);
            if (mixValues[4] != 0.0f)
                sample += static_cast<double> (mixValues[4]) * pulseValue();
        }

        phase += phaseIncrement;
        if (phase >= 1.0)
            phase -= 1.0;

        return static_cast<float> (sample);
    }

    [[nodiscard]] double getPhase() const noexcept { return phase; }
    [[nodiscard]] double getFrequency() const noexcept { return frequencyHz; }
    [[nodiscard]] DeepPluckOscillatorWaveform getWaveform() const noexcept
    {
        return waveform;
    }
    [[nodiscard]] float getCurrentPulseWidth() const noexcept
    {
        return pulseWidth.getCurrentValue();
    }
    [[nodiscard]] float getWaveformMix (DeepPluckOscillatorWaveform selected) const noexcept;

    [[nodiscard]] static DeepPluckOscillatorWaveform waveformFromParameter (
        float value) noexcept;

private:
    [[nodiscard]] static double polyBlep (double phase, double phaseIncrement) noexcept;
    [[nodiscard]] static double wrapPhase (double value) noexcept;
    [[nodiscard]] static double pulseSample (double phase, double phaseIncrement,
                                             double pulseWidth,
                                             double wrapCorrection) noexcept;
    [[nodiscard]] double triangleInitialState() const noexcept;
    void initialiseWaveformMix (DeepPluckOscillatorWaveform newWaveform) noexcept;

    double sampleRate = 44100.0;
    double phase = 0.0;
    double frequencyHz = 0.0;
    double phaseIncrement = 0.0;
    double oneMinusPhaseIncrement = 1.0;
    double inversePhaseIncrement = 0.0;
    double triangleState = -0.25;
    DeepPluckOscillatorWaveform waveform = DeepPluckOscillatorWaveform::saw;
    std::array<juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear>, 5>
        waveformMix;
    juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> pulseWidth;
    float pulseWidthTarget = 0.5f;
    int waveformRampLengthSamples = 1;
    int waveformRampSamplesRemaining = 0;
};
}
