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

package space.rcmd.android.sizzle.audio

import space.rcmd.android.sizzle.model.SynthDefs
import space.rcmd.android.sizzle.model.TimeDivision
import space.rcmd.android.sizzle.model.ToolboxSlot

/**
 * An immutable snapshot of every parameter of the subtractive (Minimoog-style)
 * synth voice, parsed once from a [ToolboxSlot] at note-trigger time. Holding it on
 * the voice keeps the audio thread allocation-free during [SynthVoice.render].
 *
 * Signal path per voice: 3 oscillators (+ noise) → mixer → resonant low-pass filter
 * (with its own ADSR contour + key tracking) → amplifier (its own ADSR). A single
 * tempo-synced LFO and a small [MOD_SRC]→[MOD_DST] modulation matrix route
 * modulation across the path.
 */
class SynthPatch(
    // ---- oscillators (index 0..2) ----
    val oscWave: IntArray,     // index into WAVES
    val oscOct: IntArray,      // octave transpose, -2..+2
    val oscSemi: IntArray,     // semitone detune, -12..+12
    val oscFine: FloatArray,   // fine detune in cents, -50..+50
    val oscLevel: FloatArray,  // mixer level, 0..1
    val noiseLevel: Float,     // white-noise mixer level, 0..1
    // ---- filter ----
    val cutoff: Float,         // 0..1 (mapped to Hz, exponential)
    val resonance: Float,      // 0..1 (self-oscillates near 1)
    val keytrack: Float,       // 0..1 how far the played note opens the cutoff
    val filterEnvAmt: Float,   // -1..1 filter-envelope (contour) amount
    val fA: Float, val fD: Float, val fS: Float, val fR: Float, // filter ADSR (s / 0..1)
    // ---- amplifier ----
    val aA: Float, val aD: Float, val aS: Float, val aR: Float, // amp ADSR (s / 0..1)
    // ---- LFO ----
    val lfoWave: Int,          // index into LFO_WAVES
    val lfoHz: Float,          // tempo-synced rate, precomputed from the division + tempo
    // ---- modulation matrix (index 0..MOD_SLOTS-1) ----
    val modSrc: IntArray,      // index into MOD_SRC (0 == Off)
    val modDst: IntArray,      // index into MOD_DST
    val modAmt: FloatArray,    // -1..1
) {
    companion object {
        const val OSC_COUNT = SynthDefs.OSC_COUNT
        const val MOD_SLOTS = SynthDefs.MOD_SLOTS

        private fun waveIndex(s: String) = SynthDefs.WAVES.indexOf(s).let { if (it < 0) 0 else it }
        private fun lfoWaveIndex(s: String) = SynthDefs.LFO_WAVES.indexOf(s).let { if (it < 0) 0 else it }
        private fun srcIndex(s: String) = SynthDefs.MOD_SRC.indexOf(s).let { if (it < 0) 0 else it }
        private fun dstIndex(s: String) = SynthDefs.MOD_DST.indexOf(s).let { if (it < 0) 0 else it }

        /** Parse [slot]'s value map into a patch. [tempoBpm] fixes the LFO rate from
         *  its tempo-synced division. A null/empty slot yields a plain init patch. */
        fun from(slot: ToolboxSlot?, tempoBpm: Float): SynthPatch {
            val wave = IntArray(OSC_COUNT) { i ->
                waveIndex(slot?.string("osc${i}wave", SynthDefs.DEF_OSC_WAVE[i]) ?: SynthDefs.DEF_OSC_WAVE[i])
            }
            val oct = IntArray(OSC_COUNT) { i -> (slot?.float("osc${i}oct", 0f) ?: 0f).toInt().coerceIn(-2, 2) }
            val semi = IntArray(OSC_COUNT) { i -> (slot?.float("osc${i}semi", 0f) ?: 0f).toInt().coerceIn(-12, 12) }
            val fine = FloatArray(OSC_COUNT) { i -> (slot?.float("osc${i}fine", 0f) ?: 0f).coerceIn(-50f, 50f) }
            val level = FloatArray(OSC_COUNT) { i ->
                (slot?.float("osc${i}level", SynthDefs.DEF_OSC_LEVEL[i]) ?: SynthDefs.DEF_OSC_LEVEL[i]).coerceIn(0f, 1f)
            }

            val divIdx = (slot?.float("lfodiv", 3f) ?: 3f).toInt()
            val beatFraction = TimeDivision.fromIndex(divIdx).beatFraction.toFloat()
            val lfoHz = if (beatFraction > 0f) (tempoBpm / 60f) / beatFraction else 2f

            val src = IntArray(MOD_SLOTS) { n -> srcIndex(slot?.string("m${n}src", "Off") ?: "Off") }
            val dst = IntArray(MOD_SLOTS) { n -> dstIndex(slot?.string("m${n}dst", "Cutoff") ?: "Cutoff") }
            val amt = FloatArray(MOD_SLOTS) { n -> (slot?.float("m${n}amt", 0f) ?: 0f).coerceIn(-1f, 1f) }

            return SynthPatch(
                oscWave = wave, oscOct = oct, oscSemi = semi, oscFine = fine, oscLevel = level,
                noiseLevel = (slot?.float("noise", 0f) ?: 0f).coerceIn(0f, 1f),
                cutoff = (slot?.float("cutoff", 0.55f) ?: 0.55f).coerceIn(0f, 1f),
                resonance = (slot?.float("reso", 0.15f) ?: 0.15f).coerceIn(0f, 1f),
                keytrack = (slot?.float("keytrack", 0.3f) ?: 0.3f).coerceIn(0f, 1f),
                filterEnvAmt = (slot?.float("fenvamt", 0.4f) ?: 0.4f).coerceIn(-1f, 1f),
                fA = (slot?.float("fatk", 0.005f) ?: 0.005f), fD = (slot?.float("fdec", 0.25f) ?: 0.25f),
                fS = (slot?.float("fsus", 0.4f) ?: 0.4f), fR = (slot?.float("frel", 0.2f) ?: 0.2f),
                aA = (slot?.float("attack", 0.005f) ?: 0.005f), aD = (slot?.float("decay", 0.2f) ?: 0.2f),
                aS = (slot?.float("sustain", 0.7f) ?: 0.7f), aR = (slot?.float("release", 0.2f) ?: 0.2f),
                lfoWave = lfoWaveIndex(slot?.string("lfowave", "Tri") ?: "Tri"),
                lfoHz = lfoHz,
                modSrc = src, modDst = dst, modAmt = amt,
            )
        }

        /** A plain default patch (for generic live audition not tied to a synth slot). */
        val DEFAULT: SynthPatch = from(null, 120f)
    }
}
