#include "DeepPluckSynthesiser.h"

#include "DeepPluckSound.h"
#include "DeepPluckVoice.h"

#include <cmath>

namespace deeppluck
{
void DeepPluckSynthesiser::setCurrentPlaybackSampleRate (double newRate)
{
    const auto validatedRate = std::isfinite (newRate) && newRate > 0.0
                                 ? newRate : 44100.0;
    juce::Synthesiser::setCurrentPlaybackSampleRate (validatedRate);
    lfo.prepare (validatedRate);
    updateLfoParameters();
}

void DeepPluckSynthesiser::setParameterSource (
    const DeepPluckParameterSnapshot& parametersToRead) noexcept
{
    parameterSource = &parametersToRead;
    updateLfoParameters();
}

void DeepPluckSynthesiser::setHostTempoBpm (double newTempoBpm) noexcept
{
    if (std::isfinite (newTempoBpm) && newTempoBpm >= 20.0 && newTempoBpm <= 400.0)
        hostTempoBpm = newTempoBpm;
}

void DeepPluckSynthesiser::renderNextBlock (juce::AudioBuffer<float>& outputAudio,
                                   const juce::MidiBuffer& midiData,
                                   int startSample, int numSamples)
{
    updateLfoParameters();
    auto midiIterator = midiData.findNextSamplePosition (startSample);

    for (; numSamples > 0; ++midiIterator)
    {
        if (midiIterator == midiData.cend())
        {
            renderVoiceSegment (outputAudio, startSample, numSamples);
            return;
        }

        const auto metadata = *midiIterator;
        const auto samplesToEvent = metadata.samplePosition - startSample;

        if (samplesToEvent >= numSamples)
        {
            renderVoiceSegment (outputAudio, startSample, numSamples);
            return;
        }

        if (samplesToEvent > 0)
            renderVoiceSegment (outputAudio, startSample, samplesToEvent);

        handleMidiEvent (metadata.getMessage());
        startSample += samplesToEvent;
        numSamples -= samplesToEvent;
    }
}

void DeepPluckSynthesiser::noteOn (int midiChannel, int midiNoteNumber, float velocity)
{
    if (! juce::isPositiveAndBelow (midiNoteNumber, 128)
        || ! juce::isPositiveAndBelow (midiChannel - 1, 16))
        return;

    for (auto* sound : sounds)
    {
        if (! sound->appliesToNote (midiNoteNumber) || ! sound->appliesToChannel (midiChannel))
            continue;

        updateLfoParameters();
        if (! hasActiveVoice()
            && lfo.getRestartMode() == DeepPluckLfoRestartMode::retriggerFirstNote)
            lfo.restart();

        for (auto* voice : voices)
            if (voice->getCurrentlyPlayingNote() == midiNoteNumber
                && voice->isPlayingChannel (midiChannel))
                stopVoice (voice, 1.0f, true);

        startVoice (findFreeVoice (sound, midiChannel, midiNoteNumber, isNoteStealingEnabled()),
                    sound, midiChannel, midiNoteNumber, juce::jlimit (0.0f, 1.0f, velocity));
    }
}

void DeepPluckSynthesiser::updateLfoParameters() noexcept
{
    if (parameterSource == nullptr)
        return;

    lfo.setParameters ({ parameterSource->lfoRateHz,
                         parameterSource->lfoSync,
                         parameterSource->lfoSyncDivision,
                         parameterSource->lfoWaveform,
                         parameterSource->lfoRestartMode,
                         hostTempoBpm });
}

void DeepPluckSynthesiser::renderVoiceSegment (juce::AudioBuffer<float>& outputAudio,
                                               int startSample, int numSamples)
{
    while (numSamples > 0)
    {
        const auto chunkSamples = juce::jmin (numSamples, modulationChunkSize);

        for (auto sample = 0; sample < chunkSamples; ++sample)
            modulationBuffer[static_cast<std::size_t> (sample)] = lfo.processSample();

        if (outputAudio.getNumChannels() > 0)
        {
            for (auto* voice : voices)
                static_cast<DeepPluckVoice*> (voice)->setModulationBlock (
                    modulationBuffer.data(), startSample, chunkSamples);

            renderVoices (outputAudio, startSample, chunkSamples);
        }

        startSample += chunkSamples;
        numSamples -= chunkSamples;
    }
}

bool DeepPluckSynthesiser::hasActiveVoice() const noexcept
{
    for (const auto* voice : voices)
        if (voice->isVoiceActive())
            return true;

    return false;
}

void DeepPluckSynthesiser::noteOff (int midiChannel, int midiNoteNumber, float velocity,
                           bool allowTailOff)
{
    for (auto* voice : voices)
    {
        if (voice->getCurrentlyPlayingNote() != midiNoteNumber
            || ! voice->isPlayingChannel (midiChannel))
            continue;

        if (const auto sound = voice->getCurrentlyPlayingSound();
            sound != nullptr && sound->appliesToNote (midiNoteNumber)
                && sound->appliesToChannel (midiChannel))
        {
            voice->setKeyDown (false);
            stopVoice (voice, velocity, allowTailOff);
        }
    }
}

void DeepPluckSynthesiser::allNotesOff (int midiChannel, bool allowTailOff)
{
    for (auto* voice : voices)
        if (midiChannel <= 0 || voice->isPlayingChannel (midiChannel))
        {
            stopVoice (voice, 1.0f, allowTailOff);

            if (! allowTailOff)
                static_cast<DeepPluckVoice*> (voice)->discardResidualTail();
        }
}

void DeepPluckSynthesiser::handleMidiEvent (const juce::MidiMessage& message)
{
    if (message.isNoteOn())
        noteOn (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity());
    else if (message.isNoteOff())
        noteOff (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity(), true);
    else if (message.isAllSoundOff())
        allNotesOff (message.getChannel(), false);
    else if (message.isAllNotesOff())
        allNotesOff (message.getChannel(), true);
    else if (message.isPitchWheel())
    {
        for (auto* voice : voices)
            if (voice->isPlayingChannel (message.getChannel()))
                voice->pitchWheelMoved (message.getPitchWheelValue());
    }
    else if (message.isController())
    {
        for (auto* voice : voices)
            if (voice->isPlayingChannel (message.getChannel()))
                voice->controllerMoved (message.getControllerNumber(), message.getControllerValue());
    }
}

juce::SynthesiserVoice* DeepPluckSynthesiser::findFreeVoice (juce::SynthesiserSound* soundToPlay,
                                                    int midiChannel, int midiNoteNumber,
                                                    bool stealIfNoneAvailable) const
{
    for (auto* voice : voices)
        if (! voice->isVoiceActive() && voice->canPlaySound (soundToPlay))
            return voice;

    return stealIfNoneAvailable ? findVoiceToSteal (soundToPlay, midiChannel, midiNoteNumber)
                                : nullptr;
}

juce::SynthesiserVoice* DeepPluckSynthesiser::findVoiceToSteal (juce::SynthesiserSound* soundToPlay,
                                                       int midiChannel,
                                                       int midiNoteNumber) const
{
    juce::ignoreUnused (midiChannel, midiNoteNumber);
    DeepPluckVoice* quietestReleased = nullptr;
    DeepPluckVoice* quietest = nullptr;

    const auto preferQuieter = [] (DeepPluckVoice*& candidate,
                                   DeepPluckVoice* voiceToCompare) noexcept
    {
        constexpr float equalLevelTolerance = 1.0e-7f;
        if (candidate == nullptr
            || voiceToCompare->getCurrentAudibleLevel()
                   < candidate->getCurrentAudibleLevel() - equalLevelTolerance
            || (std::abs (voiceToCompare->getCurrentAudibleLevel()
                          - candidate->getCurrentAudibleLevel()) <= equalLevelTolerance
                && voiceToCompare->wasStartedBefore (*candidate)))
            candidate = voiceToCompare;
    };

    for (auto* voice : voices)
    {
        if (! voice->canPlaySound (soundToPlay))
            continue;

        auto* deepPluckVoice = static_cast<DeepPluckVoice*> (voice);
        preferQuieter (quietest, deepPluckVoice);

        if (deepPluckVoice->isReleasing())
            preferQuieter (quietestReleased, deepPluckVoice);
    }

    return quietestReleased != nullptr ? quietestReleased : quietest;
}

void configureDeepPluckSynthesiser (DeepPluckSynthesiser& synthesiser,
                                    const DeepPluckParameterSnapshot& parameters)
{
    synthesiser.clearVoices();
    synthesiser.clearSounds();

    synthesiser.setParameterSource (parameters);

    for (auto index = 0; index < voiceCount; ++index)
        synthesiser.addVoice (new DeepPluckVoice (
            parameters, static_cast<std::uint32_t> (index)));

    synthesiser.addSound (new DeepPluckSound());
    synthesiser.setNoteStealingEnabled (true);
    synthesiser.setMinimumRenderingSubdivisionSize (1, false);
}
}
