// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
// SPDX-License-Identifier: GPL-3.0-or-later

package space.rcmd.android.sizzle.audio

import kotlin.math.abs
import kotlin.math.sin
import kotlin.math.tan

/**
 * One playing note of the subtractive (Minimoog-style) synth. Per voice:
 *
 *   3 oscillators (+ white noise) → mixer → resonant 4-pole low-pass filter
 *   (with its own ADSR contour + key tracking) → amplifier (its own ADSR)
 *
 * A single tempo-synced LFO and a small modulation matrix ([SynthPatch]) route
 * modulation across pitch / detune / cutoff / pulse-width / amplitude.
 *
 * All parameters come from an immutable [SynthPatch] captured at [noteOn], so
 * [render] — which runs on the audio thread — never allocates. Oscillators use
 * PolyBLEP band-limiting on their discontinuities to keep saw/pulse tones from
 * aliasing; the filter cutoff is recomputed at control rate to keep the per-sample
 * cost down.
 */
class SynthVoice(private val sampleRate: Int) {

    private var patch: SynthPatch = SynthPatch.DEFAULT
    var pitch = -1; private set
    private var velocity = 0f
    private var baseFreq = 440.0
    private var keyNorm = 0f // note position relative to C-4 (60), -1..1

    // Oscillator phases (0..1) + per-note precomputed tuning.
    private val phase = DoubleArray(SynthPatch.OSC_COUNT)
    private val staticSemis = FloatArray(SynthPatch.OSC_COUNT) // oct+semi+fine, no modulation
    private val staticInc = DoubleArray(SynthPatch.OSC_COUNT)  // phase increment when un-modulated
    private val modInc = DoubleArray(SynthPatch.OSC_COUNT)     // modulated increment, refreshed at control rate
    private var pitchModActive = false                         // any mod slot routes to pitch?

    // Topology-preserving (Cytomic) 2-pole state-variable low-pass. Unconditionally
    // stable across cutoff modulation and self-oscillation, unlike the old cheap
    // ladder which went unstable (NaN → dead voice) at high cutoff. Coefficients are
    // refreshed at control rate; the two integrators run per sample.
    private var icB = 0f; private var icL = 0f
    private var ca1 = 0f; private var ca2 = 0f; private var ca3 = 0f
    private var ctrl = 0

    // LFO + sample-and-hold.
    private var lfoPhase = 0.0
    private var lfoInc = 0.0
    private var shValue = 0f
    private var rng = 0x2545F491

    private val ampEnv = Adsr()
    private val filtEnv = Adsr()

    val isActive: Boolean get() = ampEnv.active

    /** Start (or steal-and-restart) this voice with the given [patch]. */
    fun noteOn(midi: Int, velocity: Int, patch: SynthPatch) {
        this.patch = patch
        pitch = midi
        this.velocity = (velocity / 127f).coerceIn(0f, 1f)
        baseFreq = midiToHz(midi)
        keyNorm = ((midi - 60) / 60f).coerceIn(-1f, 1f)
        ampEnv.gateOn(patch.aA, patch.aD, patch.aS, patch.aR)
        filtEnv.gateOn(patch.fA, patch.fD, patch.fS, patch.fR)
        lfoInc = patch.lfoHz.toDouble() / sampleRate
        lfoPhase = 0.0 // retrigger the LFO per note for predictable modulation
        ctrl = 0       // force a cutoff recompute on the first sample
        // Precompute oscillator tuning so the per-sample loop skips Math.pow unless
        // pitch is actually being modulated (the common case).
        computeTuning()
    }

    fun noteOff() { ampEnv.gateOff(); filtEnv.gateOff() }

    /** Hard stop with no release tail (transport stop / voice steal). */
    fun kill() {
        ampEnv.kill(); filtEnv.kill()
        pitch = -1
        icB = 0f; icL = 0f
    }

    /** Swap in updated params for a still-sounding voice (live editing / playback
     *  block refresh). Envelopes keep their in-flight state; tuning + LFO rate are
     *  recomputed only when the patch actually changes, so held notes reflect knob
     *  turns (cutoff, levels, mod…) without clicking or retriggering. */
    fun setPatch(p: SynthPatch) {
        if (p === patch) return
        patch = p
        lfoInc = p.lfoHz.toDouble() / sampleRate
        computeTuning()
    }

    private fun computeTuning() {
        for (i in 0 until SynthPatch.OSC_COUNT) {
            val semis = patch.oscOct[i] * 12f + patch.oscSemi[i] + patch.oscFine[i] / 100f
            staticSemis[i] = semis
            staticInc[i] = (baseFreq * fastPow2(semis / 12.0) / sampleRate).coerceIn(0.0, 0.49)
            modInc[i] = staticInc[i] // valid until the first control-rate refresh
        }
        pitchModActive = (0 until SynthPatch.MOD_SLOTS).any { patch.modSrc[it] != 0 && patch.modDst[it] in 0..2 }
    }

    fun render(): Float {
        if (!ampEnv.active) return 0f
        val dt = 1f / sampleRate
        val ampE = ampEnv.advance(dt)
        val filtE = filtEnv.advance(dt)

        // ---- LFO ----
        lfoPhase += lfoInc
        if (lfoPhase >= 1.0) { lfoPhase -= 1.0; shValue = nextRand() } // clock S&H on wrap
        val lfo = lfoValue()

        // ---- modulation matrix ----
        var mPitch = 0f; var mOsc2 = 0f; var mOsc3 = 0f
        var mCut = 0f; var mPwm = 0f; var mAmp = 0f
        val src = patch.modSrc; val dst = patch.modDst; val amt = patch.modAmt
        for (n in 0 until SynthPatch.MOD_SLOTS) {
            val s = when (src[n]) {
                1 -> lfo            // bipolar
                2 -> filtE          // 0..1
                3 -> ampE           // 0..1
                4 -> velocity       // 0..1
                5 -> keyNorm        // bipolar
                else -> continue    // 0 == Off
            }
            val v = amt[n] * s
            when (dst[n]) {
                0 -> mPitch += v
                1 -> mOsc2 += v
                2 -> mOsc3 += v
                3 -> mCut += v
                4 -> mPwm += v
                5 -> mAmp += v
            }
        }

        // ---- control-rate update (every CTRL_INTERVAL samples): the expensive
        // Math.pow for cutoff + pitch runs ~1/16 as often. LFO/envelopes move slowly
        // enough that stepping the coefficients this often is inaudible. ----
        if (ctrl <= 0) {
            ctrl = CTRL_INTERVAL
            val exp = (patch.cutoff + patch.filterEnvAmt * filtE + patch.keytrack * keyNorm + mCut)
                .coerceIn(0f, 1.3f)
            val hz = (20.0 * fastPow(1000.0, exp.toDouble())).coerceIn(20.0, sampleRate * 0.45)
            val g = tan(Math.PI * hz / sampleRate).toFloat()
            val k = (2f - 1.96f * patch.resonance).coerceIn(0.05f, 2f) // lower k = more resonance
            ca1 = 1f / (1f + g * (g + k)); ca2 = g * ca1; ca3 = g * ca2
            if (pitchModActive) {
                for (i in 0 until SynthPatch.OSC_COUNT) {
                    var semis = staticSemis[i] + mPitch * 12f
                    if (i == 1) semis += mOsc2 * 12f
                    if (i == 2) semis += mOsc3 * 12f
                    modInc[i] = (baseFreq * fastPow2(semis / 12.0) / sampleRate).coerceIn(0.0, 0.49)
                }
            }
        }
        ctrl--

        // ---- oscillators + noise ----
        var mix = 0f
        for (i in 0 until SynthPatch.OSC_COUNT) {
            val level = patch.oscLevel[i]
            if (level <= 0.0001f) continue
            val inc = if (pitchModActive) modInc[i] else staticInc[i]
            var ph = phase[i] + inc
            if (ph >= 1.0) ph -= 1.0
            phase[i] = ph
            mix += waveform(patch.oscWave[i], ph, inc, mPwm) * level
        }
        if (patch.noiseLevel > 0.0001f) mix += nextRand() * patch.noiseLevel

        // ---- filter (coefficients computed at control rate, above) ----
        val filtered = svf(mix)

        // ---- amplifier ----
        var a = ampE * velocity * (1f + mAmp)
        if (a < 0f) a = 0f
        val y = filtered * a * OUT_GAIN
        return if (y.isFinite()) y else 0f
    }

    // ---- oscillator waveforms (PolyBLEP band-limited saw/pulse; naive triangle) ----
    private fun waveform(wave: Int, t: Double, dt: Double, pwm: Float): Float = when (wave) {
        1 -> (2.0 * t - 1.0 - polyBlep(t, dt)).toFloat()                       // Saw
        2 -> pulse(t, dt, (0.5f + pwm * 0.45f).coerceIn(0.02f, 0.98f))         // Square
        3 -> pulse(t, dt, (0.25f + pwm * 0.45f).coerceIn(0.02f, 0.98f))        // Pulse 25%
        4 -> pulse(t, dt, (0.125f + pwm * 0.45f).coerceIn(0.02f, 0.98f))       // Pulse 12.5%
        else -> (1.0 - 4.0 * abs(t - 0.5)).toFloat()                           // Triangle
    }

    private fun pulse(t: Double, dt: Double, w: Float): Float {
        var y = if (t < w) 1.0 else -1.0
        y += polyBlep(t, dt)
        var t2 = t + (1.0 - w)
        if (t2 >= 1.0) t2 -= 1.0
        y -= polyBlep(t2, dt)
        return y.toFloat()
    }

    /** PolyBLEP residual that rounds off a unit step at a phase discontinuity. */
    private fun polyBlep(t: Double, dt: Double): Double {
        if (dt <= 0.0) return 0.0
        return when {
            t < dt -> { val x = t / dt; x + x - x * x - 1.0 }
            t > 1.0 - dt -> { val x = (t - 1.0) / dt; x * x + x + x + 1.0 }
            else -> 0.0
        }
    }

    private fun lfoValue(): Float {
        val p = lfoPhase
        return when (patch.lfoWave) {
            1 -> (2.0 * p - 1.0).toFloat()                    // Saw
            2 -> if (p < 0.5) 1f else -1f                     // Square
            3 -> sin(2.0 * Math.PI * p).toFloat()             // Sine
            4 -> shValue                                      // Sample & Hold
            else -> (1.0 - 4.0 * abs(p - 0.5)).toFloat()      // Triangle
        }
    }

    /** Cytomic TPT state-variable low-pass (2-pole). Coefficients [ca1]/[ca2]/[ca3]
     *  are precomputed at control rate; this runs the two integrators per sample. It
     *  is unconditionally stable, so cutoff/resonance always track audibly and can't
     *  blow up. Recovers to silence if a non-finite input ever slips through. */
    private fun svf(input: Float): Float {
        val v3 = input - icL
        val v1 = ca1 * icB + ca2 * v3
        val v2 = icL + ca2 * icB + ca3 * v3
        icB = 2f * v1 - icB
        icL = 2f * v2 - icL
        if (!v2.isFinite()) { icB = 0f; icL = 0f; return 0f }
        return v2
    }

    private fun nextRand(): Float {
        var s = rng
        s = s xor (s shl 13); s = s xor (s ushr 17); s = s xor (s shl 5)
        rng = s
        return s * 4.656613e-10f // -1..1
    }

    /** Cheap 2^x for pitch (via Math.pow, only 1-3 oscillators per sample). */
    private fun fastPow2(x: Double): Double = Math.pow(2.0, x)
    private fun fastPow(base: Double, x: Double): Double = Math.pow(base, x)

    /** Linear ADSR envelope (levels 0..1). Level is not reset on gate-on so a
     *  stolen-then-restarted voice fades in continuously instead of clicking. */
    private class Adsr {
        private var stage = 0 // 0 idle, 1 attack, 2 decay, 3 sustain, 4 release
        private var level = 0f
        private var a = 0.01f; private var d = 0.1f; private var s = 0.7f; private var r = 0.2f
        val active: Boolean get() = stage != 0

        fun gateOn(aa: Float, dd: Float, ss: Float, rr: Float) {
            a = aa.coerceAtLeast(0.0005f); d = dd; s = ss.coerceIn(0f, 1f); r = rr
            stage = 1
        }

        fun gateOff() { if (stage != 0) stage = 4 }
        fun kill() { stage = 0; level = 0f }

        fun advance(dt: Float): Float {
            when (stage) {
                1 -> { level += dt / a; if (level >= 1f) { level = 1f; stage = 2 } }
                2 -> { level -= dt / d.coerceAtLeast(0.001f) * (1f - s); if (level <= s) { level = s; stage = 3 } }
                3 -> {}
                // Release ramps to zero at a rate set by the release TIME alone. The
                // decrement was previously scaled by the sustain level (`* s`), so a
                // percussive patch (sustain≈0) released ~1000× too slowly — the voice
                // effectively never freed, piling up stuck notes and CPU until the
                // audio callback missed its deadline (dropouts) and note-offs looked
                // ignored (panic needed). A constant 1/r rate guarantees the voice
                // reaches silence within r seconds from any level.
                4 -> { level -= dt / r.coerceAtLeast(0.001f); if (level <= 0f) { level = 0f; stage = 0 } }
            }
            return level
        }
    }

    companion object {
        private const val OUT_GAIN = 0.5f
        private const val CTRL_INTERVAL = 16 // samples between cutoff recomputes
        /** Equal-tempered MIDI note -> frequency in Hz (A4 = 69 = 440 Hz). */
        fun midiToHz(midi: Int): Double = 440.0 * Math.pow(2.0, (midi - 69) / 12.0)
    }
}
