#include "DeepPluckVoice.h"

#include <cmath>

namespace deeppluck
{
namespace
{
constexpr double pitchSmoothingSeconds = 0.020;
constexpr double oscillator2LevelSmoothingSeconds = 0.020;
constexpr double oscillator2BypassSmoothingSeconds = 0.005;
constexpr double filterSmoothingSeconds = 0.020;
constexpr double filterBypassSmoothingSeconds = 0.010;
constexpr double modulationAmountSmoothingSeconds = 0.020;
constexpr double stealTailSeconds = 0.002;
constexpr float voiceGain = 0.08f;
constexpr int spreadControlIntervalSamples = 16;

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

float cutoffOctaves (float cutoffHz) noexcept
{
    return std::log2 (sanitise (cutoffHz, 1800.0f, 20.0f, 20000.0f));
}
}

DeepPluckVoice::DeepPluckVoice (const DeepPluckParameterSnapshot& parametersToRead,
                                std::uint32_t voiceIndex) noexcept
    : parameters (parametersToRead), unison (voiceIndex), transientNoise (voiceIndex)
{
}

bool DeepPluckVoice::canPlaySound (juce::SynthesiserSound* sound)
{
    return dynamic_cast<DeepPluckSound*> (sound) != nullptr;
}

void DeepPluckVoice::setCurrentPlaybackSampleRate (double newRate)
{
    juce::SynthesiserVoice::setCurrentPlaybackSampleRate (newRate);
    unison.prepare (newRate);
    envelope.prepare (newRate);
    filterEnvelope.prepare (newRate);
    transientNoise.prepare (newRate);
    transientNoise.setParameters ({ parameters.transientEnabled,
                                    parameters.transientLevelDb,
                                    parameters.transientDecaySeconds,
                                    parameters.transientTone,
                                    parameters.noiseEnabled,
                                    parameters.noiseLevelDb,
                                    parameters.noiseDecaySeconds,
                                    parameters.noiseTone });
    for (auto& filter : filters)
        filter.prepare (newRate);
    smoothedFrequency.reset (newRate, pitchSmoothingSeconds);
    smoothedSecondFrequency.reset (newRate, pitchSmoothingSeconds);
    secondOscillatorLevel.reset (newRate, oscillator2LevelSmoothingSeconds);
    secondOscillatorMix.reset (newRate, oscillator2BypassSmoothingSeconds);
    smoothedCutoffOctaves.reset (newRate, filterSmoothingSeconds);
    smoothedResonance.reset (newRate, filterSmoothingSeconds);
    smoothedKeyTracking.reset (newRate, filterSmoothingSeconds);
    smoothedEnvAmount.reset (newRate, filterSmoothingSeconds);
    filterMix.reset (newRate, filterBypassSmoothingSeconds);
    lfoFilterAmount.reset (newRate, modulationAmountSmoothingSeconds);
    lfoPitchAmount.reset (newRate, modulationAmountSmoothingSeconds);
    lfoSpreadAmount.reset (newRate, modulationAmountSmoothingSeconds);
    residualTailLength = juce::jmax (1, static_cast<int> (std::round (newRate * stealTailSeconds)));
}

void DeepPluckVoice::startNote (int midiNoteNumber, float velocity,
                       juce::SynthesiserSound* sound, int currentPitchWheelPosition)
{
    juce::ignoreUnused (sound, currentPitchWheelPosition);

    noteNumber = juce::jlimit (0, 127, midiNoteNumber);
    velocityGain = juce::jlimit (0.0f, 1.0f, velocity) * voiceGain;

    const auto frequency = noteFrequency (noteNumber);
    smoothedFrequency.setCurrentAndTargetValue (frequency);

    const auto secondFrequency = secondNoteFrequency (noteNumber);
    smoothedSecondFrequency.setCurrentAndTargetValue (secondFrequency);
    secondOscillatorLevel.setCurrentAndTargetValue (secondOscillatorTargetGain());
    secondOscillatorMix.setCurrentAndTargetValue (
        sanitise (parameters.osc2Enabled, 1.0f, 0.0f, 1.0f) >= 0.5f ? 1.0f : 0.0f);
    unison.noteOn (noteNumber,
                   static_cast<int> (std::round (
                       sanitise (parameters.unisonVoices, 2.0f, 1.0f, 4.0f))),
                   sanitise (parameters.unisonDetuneCents, 4.0f, 0.0f, 30.0f),
                   sanitise (parameters.stereoSpread, 0.35f, 0.0f, 1.0f),
                   oscillatorParameters());

    transientNoise.setParameters ({ parameters.transientEnabled,
                                    parameters.transientLevelDb,
                                    parameters.transientDecaySeconds,
                                    parameters.transientTone,
                                    parameters.noiseEnabled,
                                    parameters.noiseLevelDb,
                                    parameters.noiseDecaySeconds,
                                    parameters.noiseTone });
    transientNoise.noteOn (noteNumber);

    for (auto& filter : filters)
    {
        filter.setCutoffFrequencyHz (
            sanitise (parameters.filterCutoffHz, 1800.0f, 20.0f, 20000.0f));
        filter.setResonance (sanitise (parameters.filterResonance, 0.18f, 0.0f, 1.0f));
        filter.setDriveDb (parameters.filterDriveDb);
        filter.reset();
    }
    smoothedCutoffOctaves.setCurrentAndTargetValue (cutoffOctaves (parameters.filterCutoffHz));
    smoothedResonance.setCurrentAndTargetValue (
        sanitise (parameters.filterResonance, 0.18f, 0.0f, 1.0f));
    smoothedKeyTracking.setCurrentAndTargetValue (
        sanitise (parameters.filterKeyTracking, 0.35f, 0.0f, 1.0f));
    smoothedEnvAmount.setCurrentAndTargetValue (
        sanitise (parameters.filterEnvAmountOctaves, 2.5f, -5.0f, 5.0f));
    filterMix.setCurrentAndTargetValue (
        sanitise (parameters.filterEnabled, 1.0f, 0.0f, 1.0f) >= 0.5f ? 1.0f : 0.0f);
    lfoFilterAmount.setCurrentAndTargetValue (
        sanitise (parameters.lfoFilterAmountOctaves, 0.0f, -2.0f, 2.0f));
    lfoPitchAmount.setCurrentAndTargetValue (
        sanitise (parameters.lfoPitchAmountCents, 0.0f, -50.0f, 50.0f));
    lfoSpreadAmount.setCurrentAndTargetValue (
        sanitise (parameters.lfoSpreadAmount, 0.0f, -1.0f, 1.0f));
    spreadControlSamplesRemaining = 0;

    envelope.reset();
    envelope.setParameters ({ parameters.ampAttackSeconds,
                              parameters.ampDecaySeconds,
                              parameters.ampSustainLevel,
                              parameters.ampReleaseSeconds });
    envelope.noteOn();

    filterEnvelope.reset();
    filterEnvelope.setParameters ({ parameters.filterAttackSeconds,
                                    parameters.filterDecaySeconds,
                                    parameters.filterSustainLevel,
                                    parameters.filterReleaseSeconds });
    filterEnvelope.noteOn();
}

void DeepPluckVoice::setModulationBlock (const float* samples, int absoluteStartSample,
                                         int numberOfSamples) noexcept
{
    modulationSamples = samples;
    modulationStartSample = absoluteStartSample;
    modulationSampleCount = juce::jmax (0, numberOfSamples);
}

void DeepPluckVoice::stopNote (float velocity, bool allowTailOff)
{
    juce::ignoreUnused (velocity);

    if (allowTailOff && envelope.isActive())
    {
        envelope.noteOff();
        filterEnvelope.noteOff();
        return;
    }

    residualTailStart = lastRenderedSamples;
    residualTailSamplesRemaining = residualTailLength;
    resetVoice (true);
    clearCurrentNote();
}

void DeepPluckVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
                             int startSample, int numSamples)
{
    if (! isVoiceActive() || numSamples <= 0)
        return;

    smoothedFrequency.setTargetValue (noteFrequency (noteNumber));
    smoothedSecondFrequency.setTargetValue (secondNoteFrequency (noteNumber));
    secondOscillatorLevel.setTargetValue (secondOscillatorTargetGain());
    secondOscillatorMix.setTargetValue (
        sanitise (parameters.osc2Enabled, 1.0f, 0.0f, 1.0f) >= 0.5f ? 1.0f : 0.0f);
    envelope.setParameters ({ parameters.ampAttackSeconds,
                              parameters.ampDecaySeconds,
                              parameters.ampSustainLevel,
                              parameters.ampReleaseSeconds });
    filterEnvelope.setParameters ({ parameters.filterAttackSeconds,
                                    parameters.filterDecaySeconds,
                                    parameters.filterSustainLevel,
                                    parameters.filterReleaseSeconds });
    transientNoise.setParameters ({ parameters.transientEnabled,
                                    parameters.transientLevelDb,
                                    parameters.transientDecaySeconds,
                                    parameters.transientTone,
                                    parameters.noiseEnabled,
                                    parameters.noiseLevelDb,
                                    parameters.noiseDecaySeconds,
                                    parameters.noiseTone });
    smoothedCutoffOctaves.setTargetValue (cutoffOctaves (parameters.filterCutoffHz));
    smoothedResonance.setTargetValue (
        sanitise (parameters.filterResonance, 0.18f, 0.0f, 1.0f));
    smoothedKeyTracking.setTargetValue (
        sanitise (parameters.filterKeyTracking, 0.35f, 0.0f, 1.0f));
    smoothedEnvAmount.setTargetValue (
        sanitise (parameters.filterEnvAmountOctaves, 2.5f, -5.0f, 5.0f));
    filterMix.setTargetValue (
        sanitise (parameters.filterEnabled, 1.0f, 0.0f, 1.0f) >= 0.5f ? 1.0f : 0.0f);
    lfoFilterAmount.setTargetValue (
        sanitise (parameters.lfoFilterAmountOctaves, 0.0f, -2.0f, 2.0f));
    lfoPitchAmount.setTargetValue (
        sanitise (parameters.lfoPitchAmountCents, 0.0f, -50.0f, 50.0f));
    lfoSpreadAmount.setTargetValue (
        sanitise (parameters.lfoSpreadAmount, 0.0f, -1.0f, 1.0f));
    for (auto& filter : filters)
        filter.setDriveDb (parameters.filterDriveDb);

    const auto baseStereoSpread = sanitise (parameters.stereoSpread, 0.35f, 0.0f, 1.0f);
    unison.setDetuneTarget (sanitise (parameters.unisonDetuneCents, 4.0f, 0.0f, 30.0f));
    unison.setOscillatorTargets (oscillatorParameters());
    if (lfoSpreadAmount.getCurrentValue() == 0.0f
        && lfoSpreadAmount.getTargetValue() == 0.0f)
        unison.setStereoSpreadTarget (baseStereoSpread);

    const auto endSample = startSample + numSamples;
    const auto numChannels = outputBuffer.getNumChannels();

    for (auto sampleIndex = startSample; sampleIndex < endSample; ++sampleIndex)
    {
        const auto modulation = modulationValueForSample (sampleIndex);
        const auto filterAmount = lfoFilterAmount.getNextValue();
        const auto pitchAmount = lfoPitchAmount.getNextValue();
        const auto spreadAmount = lfoSpreadAmount.getNextValue();
        lastModulationValue = modulation;

        const auto basePrimaryFrequency = smoothedFrequency.getNextValue();
        const auto baseSecondFrequency = smoothedSecondFrequency.getNextValue();
        auto effectivePrimaryFrequency = basePrimaryFrequency;
        auto effectiveSecondFrequency = baseSecondFrequency;
        if (pitchAmount != 0.0f)
        {
            const auto pitchRatio = std::exp2 (
                static_cast<double> (modulation * pitchAmount) / 1200.0);
            effectivePrimaryFrequency *= pitchRatio;
            effectiveSecondFrequency *= pitchRatio;
        }
        lastEffectivePrimaryFrequencyHz = effectivePrimaryFrequency;

        if (spreadControlSamplesRemaining <= 0)
        {
            auto effectiveSpread = baseStereoSpread;
            if (spreadAmount != 0.0f)
            {
                const auto signedMovement = juce::jlimit (
                    -1.0f, 1.0f, modulation * spreadAmount);
                const auto availableRoom = signedMovement >= 0.0f
                                               ? 1.0f - baseStereoSpread
                                               : baseStereoSpread;
                effectiveSpread = juce::jlimit (0.0f, 1.0f,
                                                baseStereoSpread
                                                    + signedMovement * availableRoom);
            }
            unison.setStereoSpreadTarget (effectiveSpread);
            lastEffectiveStereoSpread = effectiveSpread;
            spreadControlSamplesRemaining = spreadControlIntervalSamples;
        }
        --spreadControlSamplesRemaining;

        const auto tonal = unison.processSample (
            effectivePrimaryFrequency, effectiveSecondFrequency,
            secondOscillatorLevel.getNextValue(), secondOscillatorMix.getNextValue());
        const auto transientNoiseSample = transientNoise.processSample();
        const std::array drySamples { tonal.left + transientNoiseSample,
                                      tonal.right + transientNoiseSample };
        const auto filterEnvelopeValue = filterEnvelope.getNextSample();
        const auto noteOffsetOctaves = (static_cast<float> (noteNumber) - 60.0f) / 12.0f
                                       * smoothedKeyTracking.getNextValue();
        auto effectiveCutoffOctaves = smoothedCutoffOctaves.getNextValue()
                                      + noteOffsetOctaves
                                      + smoothedEnvAmount.getNextValue() * filterEnvelopeValue;
        if (filterAmount != 0.0f)
            effectiveCutoffOctaves += modulation * filterAmount;
        const auto cutoffHz = std::exp2 (effectiveCutoffOctaves);
        lastEffectiveFilterCutoffHz = std::isfinite (cutoffHz)
                                          ? juce::jlimit (20.0f, 20000.0f, cutoffHz)
                                          : 1800.0f;
        const auto resonance = smoothedResonance.getNextValue();
        std::array<float, 2> wetSamples;
        for (auto channel = 0; channel < 2; ++channel)
        {
            auto& filter = filters[static_cast<std::size_t> (channel)];
            filter.setCutoffFrequencyHz (cutoffHz);
            filter.setResonance (resonance);
            wetSamples[static_cast<std::size_t> (channel)] = filter.processSample (
                drySamples[static_cast<std::size_t> (channel)]);
        }
        const auto mix = filterMix.getNextValue();
        const auto ampGain = envelope.getNextSample() * velocityGain;
        std::array voiceSamples {
            (drySamples[0] + mix * (wetSamples[0] - drySamples[0])) * ampGain,
            (drySamples[1] + mix * (wetSamples[1] - drySamples[1])) * ampGain
        };

        if (residualTailSamplesRemaining > 0)
        {
            const auto tailGain = static_cast<float> (residualTailSamplesRemaining)
                                  / static_cast<float> (residualTailLength);
            voiceSamples[0] += residualTailStart[0] * tailGain;
            voiceSamples[1] += residualTailStart[1] * tailGain;
            --residualTailSamplesRemaining;
        }

        lastRenderedSamples = voiceSamples;

        if (numChannels == 1)
            outputBuffer.addSample (0, sampleIndex, 0.5f * (voiceSamples[0] + voiceSamples[1]));
        else if (numChannels >= 2)
        {
            outputBuffer.addSample (0, sampleIndex, voiceSamples[0]);
            outputBuffer.addSample (1, sampleIndex, voiceSamples[1]);
        }

        if (! envelope.isActive())
        {
            resetVoice();
            clearCurrentNote();
            break;
        }
    }
}

double DeepPluckVoice::noteFrequency (int midiNoteNumber) const noexcept
{
    const auto tune = std::isfinite (parameters.masterTuneHz)
                        ? juce::jlimit (420.0, 460.0, static_cast<double> (parameters.masterTuneHz))
                        : 440.0;
    return tune * std::exp2 ((static_cast<double> (midiNoteNumber) - 69.0) / 12.0);
}

double DeepPluckVoice::secondNoteFrequency (int midiNoteNumber) const noexcept
{
    const auto octave = std::round (sanitise (parameters.osc2Octave, 0.0f, -1.0f, 1.0f));
    const auto semitone = std::round (
        sanitise (parameters.osc2Semitone, 0.0f, -12.0f, 12.0f));
    const auto fineTune = sanitise (parameters.osc2FineTuneCents, -7.0f, -50.0f, 50.0f);
    const auto offsetSemitones = 12.0 * static_cast<double> (octave)
                                 + static_cast<double> (semitone)
                                 + static_cast<double> (fineTune) * 0.01;
    return noteFrequency (midiNoteNumber) * std::exp2 (offsetSemitones / 12.0);
}

float DeepPluckVoice::secondOscillatorTargetGain() const noexcept
{
    const auto levelDb = sanitise (parameters.osc2LevelDb, -15.0f, -60.0f, 0.0f);
    return juce::Decibels::decibelsToGain (levelDb);
}

DeepPluckUnisonOscillatorParameters DeepPluckVoice::oscillatorParameters() const noexcept
{
    return {
        DeepPluckOscillator::waveformFromParameter (parameters.osc1Waveform),
        sanitise (parameters.osc1PulseWidth, 0.50f, 0.05f, 0.95f),
        DeepPluckOscillator::waveformFromParameter (parameters.osc2Waveform),
        sanitise (parameters.osc2PulseWidth, 0.50f, 0.05f, 0.95f)
    };
}

float DeepPluckVoice::modulationValueForSample (int absoluteSample) const noexcept
{
    const auto relativeSample = absoluteSample - modulationStartSample;
    if (modulationSamples == nullptr
        || ! juce::isPositiveAndBelow (relativeSample, modulationSampleCount))
        return 0.0f;

    const auto value = modulationSamples[relativeSample];
    return std::isfinite (value) ? juce::jlimit (-1.0f, 1.0f, value) : 0.0f;
}

void DeepPluckVoice::resetVoice (bool preserveResidualTail) noexcept
{
    envelope.reset();
    filterEnvelope.reset();
    transientNoise.reset();
    for (auto& filter : filters)
        filter.reset();
    unison.reset();
    modulationSamples = nullptr;
    modulationSampleCount = 0;
    spreadControlSamplesRemaining = 0;
    velocityGain = 0.0f;
    lastRenderedSamples.fill (0.0f);

    if (! preserveResidualTail)
        discardResidualTail();
}
}
