package org.openrndr.extra.timeoperators

import org.openrndr.extra.parameters.BooleanParameter
import org.openrndr.extra.parameters.DoubleParameter
import org.openrndr.math.clamp
import org.openrndr.math.mix
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow


enum class EnvelopePhase {
    Rest, Attack, Decay
}

// Exponential Ease-In and Ease-Out by Golan Levin
// http://www.flong.com/texts/code/shapers_exp/
private fun exponentialEasing(x: Double, a: Double): Double {
    var a = a
    val epsilon = 0.00001
    val minParamA = (0.0 + epsilon)
    val maxParamA = (1.0 - epsilon)
    a = max(minParamA, min(maxParamA, a))

    return if (a < 0.5) {
        // emphasis
        a = (2.0 * a)
        x.pow(a)
    } else {
        // de-emphasis
        a = (2.0 * (a - 0.5))
        x.pow(1.0 / (1 - a))
    }
}

class Envelope(
        var restValue: Double = 0.0,
        var targetValue: Double = 1.0,
        @DoubleParameter("Attack Duration", 0.0, 5.0, 3, 0)
        var attack: Double = 0.3,
        @DoubleParameter("Decay Duration", 0.0, 5.0, 3, 1)
        var decay: Double = 0.5,
        @DoubleParameter("Easing Factor", 0.0, 1.0, 3, 2)
        var easingFactor: Double = 0.3,
        @BooleanParameter("Re-trigger", 3)
        var reTrigger: Boolean = false
) : TimeTools
{
    var phase = EnvelopePhase.Rest
        set(value) {
            if (value == EnvelopePhase.Rest) {
                initialTime = Double.NEGATIVE_INFINITY
                current = initialRestValue
            }

            field = value
        }

    val value: Double
        get() {
            return current
        }

    private var initialTime = Double.NEGATIVE_INFINITY
    private var current = restValue
    private var initialRestValue = restValue
    private val cycleDuration: Double
        get() {
            return attack + decay
        }

    override fun tick(seconds: Double, deltaTime: Double, frameCount: Int) {
        if (phase == EnvelopePhase.Rest) return

        // TODO: what happens if deltaTime < cycleDuration?

        if (initialTime == Double.NEGATIVE_INFINITY) {
            initialTime = seconds
        }

        val cycleTime = seconds - initialTime

        if (cycleTime < 0) {
            phase = EnvelopePhase.Rest
            return
        }

        if (cycleTime <= attack) {
            phase = EnvelopePhase.Attack
        } else if (cycleTime > attack && cycleTime < cycleDuration) {
            phase = EnvelopePhase.Decay
        } else {
            phase = EnvelopePhase.Rest
            return
        }

        if (phase == EnvelopePhase.Attack) {
            val denom = if (attack == 0.0) 1.0 else attack

            val t = clamp(cycleTime / denom, 0.0, 1.0)

            current = mix(restValue, targetValue, exponentialEasing(t, easingFactor))
        }

        if (phase == EnvelopePhase.Decay) {
            val denom = if (decay == 0.0) 1.0 else decay

            val t = clamp((cycleTime - attack) / denom, 0.0, 1.0)

            current = mix(targetValue, initialRestValue, exponentialEasing(t, easingFactor))
        }
    }

    fun trigger(value: Double = targetValue) {
        restValue = if (initialTime != Double.NEGATIVE_INFINITY) {
            current
        } else {
            initialRestValue
        }

        if (reTrigger) {
            restValue = initialRestValue
        }

        initialTime = Double.NEGATIVE_INFINITY
        phase = EnvelopePhase.Attack

        targetValue = value
    }
}