package solutions.tretter.mindmachine.session

import kotlin.math.ln
import kotlin.math.pow

/**
 * Parameter definitions + scaling helpers.
 *
 * Internally, the editor stores point values as normalized [0,1].
 * We map to real parameter values using a logarithmic scale where required.
 */
object TimelineParameters {

    enum class ParameterId {
        BRIGHTNESS,
        BLINK_RATE,
        CARRIER_FREQUENCY,
        BINAURAL_BEAT,
    }

    data class Range(
        val min: Float,
        val max: Float,
        val logScale: Boolean,
        val units: String,
    ) {
        init {
            require(min > 0f) { "min must be > 0 for log scaling math" }
            require(max > min) { "max must be > min" }
        }
    }

    val ranges: Map<ParameterId, Range> = mapOf(
        // Brightness is special-cased to allow true 0 while still using a log-ish curve above 0.
        ParameterId.BRIGHTNESS to Range(min = 0.0001f, max = 1.0f, logScale = true, units = "%"),
        ParameterId.BLINK_RATE to Range(min = 0.1f, max = 30f, logScale = true, units = "Hz"),
        ParameterId.CARRIER_FREQUENCY to Range(min = 200f, max = 1200f, logScale = true, units = "Hz"),
        ParameterId.BINAURAL_BEAT to Range(min = 0.5f, max = 20f, logScale = true, units = "Hz"),
    )

    fun parameterForCurve(curve: TimelineEditorState.ActiveCurve): ParameterId {
        return when (curve) {
            TimelineEditorState.ActiveCurve.VISUAL_LEFT -> ParameterId.BRIGHTNESS
            TimelineEditorState.ActiveCurve.VISUAL_RIGHT -> ParameterId.BLINK_RATE
            TimelineEditorState.ActiveCurve.AUDIO_LEFT -> ParameterId.CARRIER_FREQUENCY
            TimelineEditorState.ActiveCurve.AUDIO_RIGHT -> ParameterId.BINAURAL_BEAT
        }
    }

    /**
     * normalized [0,1] -> actual value in parameter units.
     */
    fun denormalize(curve: TimelineEditorState.ActiveCurve, value01: Float): Float {
        val p = parameterForCurve(curve)
        val r = ranges.getValue(p)
        val t = value01.coerceIn(0f, 1f)

        // Brightness: allow exact 0.
        if (p == ParameterId.BRIGHTNESS && t <= 0f) return 0f

        return if (r.logScale) logDenormalize(t, r.min, r.max) else lerp(r.min, r.max, t)
    }

    /**
     * actual value -> normalized [0,1].
     */
    fun normalize(curve: TimelineEditorState.ActiveCurve, value: Float): Float {
        val p = parameterForCurve(curve)
        val r = ranges.getValue(p)

        // Brightness: allow exact 0.
        if (p == ParameterId.BRIGHTNESS && value <= 0f) return 0f

        val clamped = value.coerceIn(r.min, r.max)
        return if (r.logScale) logNormalize(clamped, r.min, r.max) else invLerp(r.min, r.max, clamped)
    }

    private fun lerp(a: Float, b: Float, t: Float): Float = a + (b - a) * t
    private fun invLerp(a: Float, b: Float, v: Float): Float = ((v - a) / (b - a)).coerceIn(0f, 1f)

    private fun logNormalize(v: Float, min: Float, max: Float): Float {
        // Map multiplicative range to linear [0,1].
        val lnMin = ln(min)
        val lnMax = ln(max)
        return ((ln(v) - lnMin) / (lnMax - lnMin)).toFloat().coerceIn(0f, 1f)
    }

    private fun logDenormalize(t: Float, min: Float, max: Float): Float {
        val lnMin = ln(min)
        val lnMax = ln(max)
        val lnV = lnMin + (lnMax - lnMin) * t
        return kotlin.math.exp(lnV).toFloat().coerceIn(min, max)
    }

    /** Brightness is stored in 0..1 but user-facing is 0..100%. */
    fun brightnessPercentFromNormalized(value01: Float): Int {
        val t = value01.coerceIn(0f, 1f)
        if (t <= 0f) return 0
        val v = logDenormalize(t, ranges.getValue(ParameterId.BRIGHTNESS).min, 1.0f)
        return (v * 100f).toInt().coerceIn(0, 100)
    }
}
