#include "DeepPluckEnvelope.h"

#include <cmath>

namespace deeppluck
{
namespace
{
constexpr float attackTargetRatio = 0.30f;
// Refine amplitude only: changing filter curvature also moves resonant peaks.
constexpr float amplitudeTargetRatio = 0.0002f;
constexpr float filterTargetRatio = 0.0001f;
constexpr float sustainTargetRatio = 0.0001f;
constexpr float sustainSmoothingSeconds = 0.015f;

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

DeepPluckEnvelope::DeepPluckEnvelope (Contour contour) noexcept
    : decayReleaseTargetRatio (contour == Contour::amplitude
                                  ? amplitudeTargetRatio : filterTargetRatio)
{
}

void DeepPluckEnvelope::prepare (double newSampleRate) noexcept
{
    sampleRate = std::isfinite (newSampleRate) && newSampleRate > 0.0 ? newSampleRate : 44100.0;
    setParameters (parameters);
    reset();
}

void DeepPluckEnvelope::reset() noexcept
{
    currentValue = 0.0f;
    recurrenceTarget = 0.0f;
    coefficient = 0.0f;
    stageMinimum = 0.0f;
    stageMaximum = 1.0f;
    samplesRemaining = 0;
    stage = Stage::idle;
}

void DeepPluckEnvelope::setParameters (DeepPluckEnvelopeParameters newParameters) noexcept
{
    const auto previous = parameters;
    parameters.attackSeconds = sanitise (newParameters.attackSeconds, 0.003f, 0.001f, 5.0f);
    parameters.decaySeconds = sanitise (newParameters.decaySeconds, 0.320f, 0.001f, 5.0f);
    parameters.sustainLevel = sanitise (newParameters.sustainLevel, 0.0f, 0.0f, 1.0f);
    parameters.releaseSeconds = sanitise (newParameters.releaseSeconds, 0.180f, 0.001f, 10.0f);

    if (stage == Stage::attack && ! juce::approximatelyEqual (previous.attackSeconds,
                                                               parameters.attackSeconds))
        enterStage (Stage::attack, 1.0f, parameters.attackSeconds, attackTargetRatio);
    else if (stage == Stage::decay
             && (! juce::approximatelyEqual (previous.decaySeconds, parameters.decaySeconds)
                 || ! juce::approximatelyEqual (previous.sustainLevel, parameters.sustainLevel)))
        enterStage (Stage::decay, parameters.sustainLevel, parameters.decaySeconds,
                    decayReleaseTargetRatio);
    else if (stage == Stage::sustain
             && ! juce::approximatelyEqual (previous.sustainLevel, parameters.sustainLevel))
        enterStage (Stage::sustain, parameters.sustainLevel, sustainSmoothingSeconds,
                    sustainTargetRatio);
    else if (stage == Stage::release
             && ! juce::approximatelyEqual (previous.releaseSeconds, parameters.releaseSeconds))
        enterStage (Stage::release, 0.0f, parameters.releaseSeconds,
                    decayReleaseTargetRatio);
}

void DeepPluckEnvelope::noteOn() noexcept
{
    currentValue = 0.0f;
    enterStage (Stage::attack, 1.0f, parameters.attackSeconds, attackTargetRatio);
}

void DeepPluckEnvelope::noteOff() noexcept
{
    if (stage == Stage::idle || currentValue <= 1.0e-8f)
    {
        reset();
        return;
    }

    enterStage (Stage::release, 0.0f, parameters.releaseSeconds,
                decayReleaseTargetRatio);
}

float DeepPluckEnvelope::getNextSample() noexcept
{
    if (stage == Stage::idle)
        return 0.0f;

    if (samplesRemaining > 0)
    {
        currentValue = recurrenceTarget + coefficient * (currentValue - recurrenceTarget);
        --samplesRemaining;
    }

    if (! std::isfinite (currentValue))
    {
        reset();
        return 0.0f;
    }

    // Floating-point recurrence may cross its destination slightly before the
    // scheduled endpoint. Bound to this stage's original interval, including
    // upward live Sustain changes, without advancing the stage early.
    currentValue = juce::jlimit (stageMinimum, stageMaximum, currentValue);

    if (samplesRemaining == 0)
        completeCurrentStage();

    return currentValue;
}

bool DeepPluckEnvelope::isActive() const noexcept
{
    return stage != Stage::idle;
}

void DeepPluckEnvelope::enterStage (Stage newStage, float destination,
                                    float durationSeconds, float targetRatio) noexcept
{
    stage = newStage;
    destination = juce::jlimit (0.0f, 1.0f, destination);
    stageMinimum = juce::jmin (currentValue, destination);
    stageMaximum = juce::jmax (currentValue, destination);

    const auto distance = std::abs (destination - currentValue);
    if (distance <= 1.0e-8f)
    {
        currentValue = destination;
        samplesRemaining = 0;
        completeCurrentStage();
        return;
    }

    samplesRemaining = durationInSamples (durationSeconds);
    const auto safeRatio = juce::jmax (targetRatio, 1.0e-6f);
    coefficient = std::exp (-std::log ((1.0f + safeRatio) / safeRatio)
                            / static_cast<float> (samplesRemaining));
    recurrenceTarget = destination + (destination > currentValue ? safeRatio * distance
                                                                  : -safeRatio * distance);
}

void DeepPluckEnvelope::completeCurrentStage() noexcept
{
    switch (stage)
    {
        case Stage::attack:
            currentValue = 1.0f;
            enterStage (Stage::decay, parameters.sustainLevel, parameters.decaySeconds,
                        decayReleaseTargetRatio);
            break;
        case Stage::decay:
        case Stage::sustain:
            currentValue = parameters.sustainLevel;
            stage = Stage::sustain;
            samplesRemaining = 0;
            break;
        case Stage::release:
            reset();
            break;
        case Stage::idle:
            reset();
            break;
    }
}

int DeepPluckEnvelope::durationInSamples (float durationSeconds) const noexcept
{
    const auto samples = std::round (static_cast<double> (durationSeconds) * sampleRate);
    return juce::jmax (1, static_cast<int> (samples));
}
}
