#include "DeepPluckUnison.h"

#include <cmath>

namespace deeppluck
{
namespace
{
constexpr double smoothingSeconds = 0.020;
constexpr float oneThird = 1.0f / 3.0f;
constexpr float fourVoiceInnerDetune = 0.60f;
constexpr float normalisationExponent = -0.90f;

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

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

DeepPluckUnison::DeepPluckUnison (std::uint32_t voiceIndexToUse) noexcept
    : voiceIndex (voiceIndexToUse)
{
}

void DeepPluckUnison::prepare (double newSampleRate) noexcept
{
    sampleRate = std::isfinite (newSampleRate) && newSampleRate > 0.0
                   ? newSampleRate : 44100.0;

    for (auto index = 0; index < maximumUnisonVoices; ++index)
    {
        primaryOscillators[static_cast<std::size_t> (index)].prepare (sampleRate);
        secondOscillators[static_cast<std::size_t> (index)].prepare (sampleRate);
        detuneRatios[static_cast<std::size_t> (index)].reset (sampleRate, smoothingSeconds);
        leftPanGains[static_cast<std::size_t> (index)].reset (sampleRate, smoothingSeconds);
        rightPanGains[static_cast<std::size_t> (index)].reset (sampleRate, smoothingSeconds);
        detuneRatios[static_cast<std::size_t> (index)].setCurrentAndTargetValue (1.0);
        leftPanGains[static_cast<std::size_t> (index)].setCurrentAndTargetValue (1.0f);
        rightPanGains[static_cast<std::size_t> (index)].setCurrentAndTargetValue (1.0f);
    }

    reset();
}

void DeepPluckUnison::noteOn (int midiNoteNumber, int newVoiceCount,
                              float detuneCents, float stereoSpread,
                              const DeepPluckUnisonOscillatorParameters&
                                  oscillatorParameters) noexcept
{
    midiNote = juce::jlimit (0, 127, midiNoteNumber);
    activeVoiceCount = juce::jlimit (1, maximumUnisonVoices, newVoiceCount);
    orientation = ((static_cast<int> (voiceIndex) + midiNote) & 1) == 0 ? 1.0f : -1.0f;
    targetDetuneCents = sanitise (detuneCents, 0.0f, 0.0f, 30.0f);
    targetStereoSpread = sanitise (stereoSpread, 0.0f, 0.0f, 1.0f);
    normalisationGain = normalisationFor (activeVoiceCount);
    oscillatorTargets = oscillatorParameters;

    for (auto index = 0; index < maximumUnisonVoices; ++index)
    {
        const auto offset = index < activeVoiceCount
                              ? phaseOffset (activeVoiceCount, index) : 0.0;
        primaryOscillators[static_cast<std::size_t> (index)].reset (
            wrapPhase (offset), oscillatorTargets.primaryWaveform,
            oscillatorTargets.primaryPulseWidth);
        secondOscillators[static_cast<std::size_t> (index)].reset (
            wrapPhase (0.25 + offset), oscillatorTargets.secondWaveform,
            oscillatorTargets.secondPulseWidth);
    }

    applyDetuneTarget (true);
    applyStereoSpreadTarget (true);
}

void DeepPluckUnison::setTargets (float detuneCents, float stereoSpread) noexcept
{
    setDetuneTarget (detuneCents);
    setStereoSpreadTarget (stereoSpread);
}

void DeepPluckUnison::setDetuneTarget (float detuneCents) noexcept
{
    const auto boundedDetune = sanitise (detuneCents, 0.0f, 0.0f, 30.0f);

    if (boundedDetune == targetDetuneCents)
        return;

    targetDetuneCents = boundedDetune;
    applyDetuneTarget (false);
}

void DeepPluckUnison::setStereoSpreadTarget (float stereoSpread) noexcept
{
    const auto boundedSpread = sanitise (stereoSpread, 0.0f, 0.0f, 1.0f);

    if (boundedSpread == targetStereoSpread)
        return;

    targetStereoSpread = boundedSpread;
    applyStereoSpreadTarget (false);
}

void DeepPluckUnison::setOscillatorTargets (
    const DeepPluckUnisonOscillatorParameters& oscillatorParameters) noexcept
{
    oscillatorTargets = oscillatorParameters;
    for (auto index = 0; index < activeVoiceCount; ++index)
    {
        const auto arrayIndex = static_cast<std::size_t> (index);
        primaryOscillators[arrayIndex].setWaveform (oscillatorTargets.primaryWaveform);
        primaryOscillators[arrayIndex].setPulseWidth (oscillatorTargets.primaryPulseWidth);
        secondOscillators[arrayIndex].setWaveform (oscillatorTargets.secondWaveform);
        secondOscillators[arrayIndex].setPulseWidth (oscillatorTargets.secondPulseWidth);
    }
}

void DeepPluckUnison::reset() noexcept
{
    activeVoiceCount = 1;
    normalisationGain = 1.0f;
    targetDetuneCents = 0.0f;
    targetStereoSpread = 0.0f;

    for (auto index = 0; index < maximumUnisonVoices; ++index)
    {
        primaryOscillators[static_cast<std::size_t> (index)].reset();
        secondOscillators[static_cast<std::size_t> (index)].reset (0.25);
        detuneRatios[static_cast<std::size_t> (index)].setCurrentAndTargetValue (1.0);
        leftPanGains[static_cast<std::size_t> (index)].setCurrentAndTargetValue (1.0f);
        rightPanGains[static_cast<std::size_t> (index)].setCurrentAndTargetValue (1.0f);
    }
}

DeepPluckStereoSample DeepPluckUnison::processSample (
    double primaryFrequencyHz, double secondFrequencyHz,
    float secondOscillatorLevel, float secondOscillatorMix) noexcept
{
    const auto boundedSecondLevel = sanitise (secondOscillatorLevel, 0.0f, 0.0f, 1.0f);
    const auto boundedSecondMix = sanitise (secondOscillatorMix, 0.0f, 0.0f, 1.0f);
    DeepPluckStereoSample result;

    for (auto index = 0; index < activeVoiceCount; ++index)
    {
        const auto arrayIndex = static_cast<std::size_t> (index);
        const auto ratio = detuneRatios[arrayIndex].getNextValue();
        primaryOscillators[arrayIndex].setFrequency (primaryFrequencyHz * ratio);
        secondOscillators[arrayIndex].setFrequency (secondFrequencyHz * ratio);
        const auto tonal = (primaryOscillators[arrayIndex].processSample()
                            + secondOscillators[arrayIndex].processSample()
                                  * boundedSecondLevel * boundedSecondMix)
                           * normalisationGain;
        result.left += tonal * leftPanGains[arrayIndex].getNextValue();
        result.right += tonal * rightPanGains[arrayIndex].getNextValue();
    }

    return result;
}

double DeepPluckUnison::getPrimaryPhase (int index) const noexcept
{
    return primaryOscillators[static_cast<std::size_t> (boundedIndex (index))].getPhase();
}

double DeepPluckUnison::getSecondPhase (int index) const noexcept
{
    return secondOscillators[static_cast<std::size_t> (boundedIndex (index))].getPhase();
}

double DeepPluckUnison::getCurrentDetuneRatio (int index) const noexcept
{
    return detuneRatios[static_cast<std::size_t> (boundedIndex (index))].getCurrentValue();
}

float DeepPluckUnison::getCurrentLeftGain (int index) const noexcept
{
    return leftPanGains[static_cast<std::size_t> (boundedIndex (index))].getCurrentValue();
}

float DeepPluckUnison::getCurrentRightGain (int index) const noexcept
{
    return rightPanGains[static_cast<std::size_t> (boundedIndex (index))].getCurrentValue();
}

DeepPluckOscillatorWaveform DeepPluckUnison::getPrimaryWaveform (int index) const noexcept
{
    return primaryOscillators[static_cast<std::size_t> (boundedIndex (index))].getWaveform();
}

DeepPluckOscillatorWaveform DeepPluckUnison::getSecondWaveform (int index) const noexcept
{
    return secondOscillators[static_cast<std::size_t> (boundedIndex (index))].getWaveform();
}

float DeepPluckUnison::getPrimaryPulseWidth (int index) const noexcept
{
    return primaryOscillators[static_cast<std::size_t> (boundedIndex (index))]
        .getCurrentPulseWidth();
}

float DeepPluckUnison::getSecondPulseWidth (int index) const noexcept
{
    return secondOscillators[static_cast<std::size_t> (boundedIndex (index))]
        .getCurrentPulseWidth();
}

float DeepPluckUnison::detuneFactor (int voiceCount, int index) noexcept
{
    const auto count = juce::jlimit (1, maximumUnisonVoices, voiceCount);
    const auto slot = juce::jlimit (0, count - 1, index);

    switch (count)
    {
        case 2:
            return slot == 0 ? -0.5f : 0.5f;
        case 3:
            return std::array { -1.0f, 0.0f, 1.0f }[static_cast<std::size_t> (slot)];
        case 4:
            return std::array { -1.0f, -fourVoiceInnerDetune,
                                fourVoiceInnerDetune, 1.0f }
                [static_cast<std::size_t> (slot)];
        default:
            return 0.0f;
    }
}

float DeepPluckUnison::panPosition (int voiceCount, int index) noexcept
{
    const auto count = juce::jlimit (1, maximumUnisonVoices, voiceCount);
    const auto slot = juce::jlimit (0, count - 1, index);

    switch (count)
    {
        case 2:
            return slot == 0 ? -1.0f : 1.0f;
        case 3:
            return std::array { -1.0f, 0.0f, 1.0f }[static_cast<std::size_t> (slot)];
        case 4:
            return std::array { -1.0f, -oneThird, oneThird, 1.0f }
                [static_cast<std::size_t> (slot)];
        default:
            return 0.0f;
    }
}

double DeepPluckUnison::phaseOffset (int voiceCount, int index) noexcept
{
    const auto count = juce::jlimit (1, maximumUnisonVoices, voiceCount);
    const auto slot = juce::jlimit (0, count - 1, index);

    switch (count)
    {
        case 2:
            return std::array { -1.0 / 32.0, 1.0 / 32.0 }
                [static_cast<std::size_t> (slot)];
        case 3:
            return std::array { -1.0 / 12.0, 0.0, 1.0 / 12.0 }
                [static_cast<std::size_t> (slot)];
        case 4:
            return std::array { -1.0 / 8.0, -1.0 / 24.0,
                                 1.0 / 24.0, 1.0 / 8.0 }
                [static_cast<std::size_t> (slot)];
        default:
            return 0.0;
    }
}

float DeepPluckUnison::normalisationFor (int voiceCount) noexcept
{
    const auto count = static_cast<float> (
        juce::jlimit (1, maximumUnisonVoices, voiceCount));
    return std::pow (count, normalisationExponent);
}

void DeepPluckUnison::applyDetuneTarget (bool initialise) noexcept
{
    for (auto index = 0; index < activeVoiceCount; ++index)
    {
        const auto arrayIndex = static_cast<std::size_t> (index);
        const auto cents = static_cast<double> (targetDetuneCents
                                                 * detuneFactor (activeVoiceCount, index));
        const auto ratio = std::exp2 (cents / 1200.0);

        if (initialise)
            detuneRatios[arrayIndex].setCurrentAndTargetValue (ratio);
        else
            detuneRatios[arrayIndex].setTargetValue (ratio);
    }
}

void DeepPluckUnison::applyStereoSpreadTarget (bool initialise) noexcept
{
    for (auto index = 0; index < activeVoiceCount; ++index)
    {
        const auto arrayIndex = static_cast<std::size_t> (index);
        const auto pan = juce::jlimit (-1.0f, 1.0f,
                                      panPosition (activeVoiceCount, index)
                                          * targetStereoSpread * orientation);
        const auto leftGain = std::sqrt (juce::jmax (0.0f, 1.0f - pan));
        const auto rightGain = std::sqrt (juce::jmax (0.0f, 1.0f + pan));

        if (initialise)
        {
            leftPanGains[arrayIndex].setCurrentAndTargetValue (leftGain);
            rightPanGains[arrayIndex].setCurrentAndTargetValue (rightGain);
        }
        else
        {
            leftPanGains[arrayIndex].setTargetValue (leftGain);
            rightPanGains[arrayIndex].setTargetValue (rightGain);
        }
    }
}

int DeepPluckUnison::boundedIndex (int index) noexcept
{
    return juce::jlimit (0, maximumUnisonVoices - 1, index);
}
}
