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

package space.rcmd.android.sizzle.audio

import space.rcmd.android.sizzle.model.DelayDivisions
import space.rcmd.android.sizzle.model.ToolboxSlot
import space.rcmd.android.sizzle.model.ToolboxType
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.tanh

/**
 * Real-time audio effect processors used by the mixer's per-channel insert
 * chains. Each effect reads its settings from the owning [ToolboxSlot] once per
 * audio block (via [update]) and then processes the signal one sample at a time
 * (via [process]) on the audio thread — so [process] must never allocate.
 *
 * The MIDI effects (arpeggiator / transposer / LFO) are NOT here: they alter note
 * generation rather than the audio signal, and hook into the sequencer instead.
 * Only the signal-processing effect types return a processor from [create].
 */
interface AudioEffect {
    /** Refresh cached parameters from the slot. Called once per block. */
    fun update(slot: ToolboxSlot, tempoBpm: Float)
    /** Process one mono sample. */
    fun process(x: Float): Float

    companion object {
        fun create(type: ToolboxType, sampleRate: Int): AudioEffect? = when (type) {
            ToolboxType.DELAY -> TapeDelay(sampleRate)
            ToolboxType.AMBIENCE -> AmbienceReverb(sampleRate)
            ToolboxType.FILTER -> BiquadFilterEffect(sampleRate)
            ToolboxType.BITCRUSHER -> Bitcrusher()
            ToolboxType.EQ10 -> GraphicEq(sampleRate)
            else -> null // arpeggiator / transposer / LFO are handled by the sequencer
        }
    }
}

// ---------------------------------------------------------------------------
// Multi-tap tape delay: one shared delay line read by up to four independent
// "tape heads", each with its own tempo-synced division and on/off switch. A tape
// character control adds saturation, per-repeat darkening and wow/flutter.
//
// The wet signal is the AVERAGE of the active heads and feedback is hard-capped
// below 1, so the round-trip loop gain can never exceed the feedback amount — a
// runaway positive-feedback loop is impossible regardless of how many heads are on.
// ---------------------------------------------------------------------------
private class TapeDelay(private val sampleRate: Int) : AudioEffect {
    private val buffer = FloatArray(sampleRate * 4)              // up to 4 s of delay line
    private var writeIndex = 0
    private val delaySamples = IntArray(DelayDivisions.HEADS) { sampleRate / 2 }
    private val headOn = BooleanArray(DelayDivisions.HEADS)
    private var feedback = 0.4f
    private var dryWet = 0.35f
    // Tape character:
    private var satBlend = 0f          // 0 = clean, 1 = full soft saturation on peaks
    private var lpAlpha = 1f           // feedback-path one-pole LP: 1 = bright, →0 = dark
    private var lpState = 0f
    private var flutterInc = 0.0
    private var flutterDepth = 0f
    private var flutterPhase = 0.0

    override fun update(slot: ToolboxSlot, tempoBpm: Float) {
        val samplesPerBeat = (60.0 / tempoBpm) * sampleRate
        for (h in 0 until DelayDivisions.HEADS) {
            // Precomputed key strings — building "head${h}On" here would allocate on
            // the audio thread every block (this runs from fillBlock).
            headOn[h] = slot.float(HEAD_ON_KEYS[h], if (h == 0) 1f else 0f) >= 0.5f
            val idx = slot.float(HEAD_DIV_KEYS[h], DelayDivisions.DEFAULT.toFloat()).toInt()
            delaySamples[h] = (DelayDivisions.beatFraction(idx) * samplesPerBeat)
                .toInt().coerceIn(1, buffer.size - 2)
        }
        feedback = slot.float("feedback", 0.4f).coerceIn(0f, DelayDivisions.MAX_FEEDBACK)
        dryWet = slot.float("drywet", 0.35f).coerceIn(0f, 1f)
        val tape = slot.float("tape", 0.3f).coerceIn(0f, 1f)
        satBlend = tape                              // more tape → more saturation
        lpAlpha = 1f - tape * 0.85f                  // more tape → darker repeats
        flutterInc = 2.0 * PI * FLUTTER_HZ / sampleRate
        flutterDepth = tape * sampleRate * 0.0015f   // up to ~1.5 ms wow/flutter
    }

    override fun process(x: Float): Float {
        flutterPhase += flutterInc
        if (flutterPhase > 2 * PI) flutterPhase -= 2 * PI
        val flutter = sin(flutterPhase).toFloat() * flutterDepth

        // Read each active head (fractional read + linear interpolation for flutter).
        // The wet signal is the AVERAGE of the active heads, so the summed level — and
        // the feedback loop gain — stays independent of how many heads are on.
        var wet = 0f
        var active = 0
        for (h in 0 until DelayDivisions.HEADS) {
            if (!headOn[h]) continue
            active++
            var pos = writeIndex - delaySamples[h] - flutter
            while (pos < 0f) pos += buffer.size
            val i0 = pos.toInt() % buffer.size
            val i1 = (i0 + 1) % buffer.size
            val frac = pos - pos.toInt()
            wet += buffer[i0] + (buffer[i1] - buffer[i0]) * frac
        }
        if (active > 0) wet /= active

        var fb = wet * feedback
        fb += satBlend * (tanh(fb) - fb)
        lpState += lpAlpha * (fb - lpState)
        fb = lpState

        buffer[writeIndex] = x + fb
        writeIndex = (writeIndex + 1) % buffer.size
        return x * (1f - dryWet) + wet * dryWet
    }

    private companion object {
        const val FLUTTER_HZ = 5.0
        val HEAD_ON_KEYS = Array(DelayDivisions.HEADS) { "head${it}On" }
        val HEAD_DIV_KEYS = Array(DelayDivisions.HEADS) { "head${it}Div" }
    }
}

// ---------------------------------------------------------------------------
// RBJ biquad filter with selectable LP / HP / BP response, an ADSR cutoff envelope
// (triggered by signal activity on the channel) and analog-style warmth saturation.
// ---------------------------------------------------------------------------
private class BiquadFilterEffect(private val sampleRate: Int) : AudioEffect {
    private var b0 = 1.0; private var b1 = 0.0; private var b2 = 0.0
    private var a1 = 0.0; private var a2 = 0.0
    private var z1 = 0.0; private var z2 = 0.0 // transposed direct form II state

    private val sr = sampleRate.toFloat()
    private var type = "LPF"
    private var baseCutoff = 0.7f
    private var q = 1.0
    private var warmth = 0f
    private var envAmount = 0f

    // Cutoff ADSR (0..1), retriggered by an input-level gate.
    private enum class Phase { IDLE, ATTACK, DECAY, SUSTAIN, RELEASE }
    private var phase = Phase.IDLE
    private var env = 0f
    private var attackInc = 1f
    private var decayInc = 1f
    private var sustainLevel = 1f
    private var releaseSeconds = 0.2f
    private var releaseInc = 1f
    private var levelEnv = 0f
    private var levelCoeff = 0f
    private var gateOpen = false
    private var ctrl = 0

    override fun update(slot: ToolboxSlot, tempoBpm: Float) {
        type = slot.string("type", "LPF")
        baseCutoff = slot.float("cutoff", 0.7f).coerceIn(0f, 1f)
        q = (0.5 + slot.float("resonance", 0.2f) * 8f).toDouble()
        warmth = slot.float("warmth", 0f).coerceIn(0f, 1f)
        envAmount = slot.float("envamt", 0f).coerceIn(0f, 1f)
        sustainLevel = slot.float("sustain", 1f).coerceIn(0f, 1f)
        attackInc = 1f / (max(slot.float("attack", 0.005f), 0.0005f) * sr)
        val decay = slot.float("decay", 0.10f)
        decayInc = if (decay <= 0f) 1f else (1f - sustainLevel) / (decay * sr)
        releaseSeconds = slot.float("release", 0.20f)
        levelCoeff = 1f - exp(-1f / (sr * 0.005f)) // ~5 ms input-level follower
        ctrl = 0 // force a coefficient refresh on the next sample
    }

    private fun computeCoefficients(type: String, freq: Double, q: Double) {
        val w0 = 2.0 * PI * freq / sampleRate
        val cosW = cos(w0)
        val alpha = sin(w0) / (2.0 * q)
        val a0: Double
        when (type) {
            "HPF" -> {
                b0 = (1 + cosW) / 2; b1 = -(1 + cosW); b2 = (1 + cosW) / 2
                a0 = 1 + alpha; a1 = -2 * cosW; a2 = 1 - alpha
            }
            "BPF" -> {
                b0 = alpha; b1 = 0.0; b2 = -alpha
                a0 = 1 + alpha; a1 = -2 * cosW; a2 = 1 - alpha
            }
            else -> { // LPF
                b0 = (1 - cosW) / 2; b1 = 1 - cosW; b2 = (1 - cosW) / 2
                a0 = 1 + alpha; a1 = -2 * cosW; a2 = 1 - alpha
            }
        }
        // Normalise by a0.
        b0 /= a0; b1 /= a0; b2 /= a0; a1 /= a0; a2 /= a0
    }

    override fun process(x: Float): Float {
        // Gate the ADSR from the input level (hysteresis avoids chatter). A rising
        // edge above GATE_HI (re)triggers attack; falling below GATE_LO releases.
        levelEnv += (abs(x) - levelEnv) * levelCoeff
        if (!gateOpen && levelEnv > GATE_HI) {
            gateOpen = true; phase = Phase.ATTACK
        } else if (gateOpen && levelEnv < GATE_LO) {
            gateOpen = false
            releaseInc = if (releaseSeconds <= 0f) 1f else env / (releaseSeconds * sr)
            if (releaseInc <= 0f) releaseInc = 1f
            phase = Phase.RELEASE
        }
        when (phase) {
            Phase.ATTACK -> { env += attackInc; if (env >= 1f) { env = 1f; phase = Phase.DECAY } }
            Phase.DECAY -> { env -= decayInc; if (env <= sustainLevel) { env = sustainLevel; phase = Phase.SUSTAIN } }
            Phase.SUSTAIN -> {}
            Phase.RELEASE -> { env -= releaseInc; if (env <= 0f) { env = 0f; phase = Phase.IDLE } }
            Phase.IDLE -> {}
        }

        // Recompute the biquad at control rate as the envelope opens the cutoff (the
        // envelope lifts the cutoff from its base toward fully open by Env Amount).
        if (ctrl <= 0) {
            ctrl = CTRL_INTERVAL
            val eff = (baseCutoff + envAmount * env * (1f - baseCutoff)).coerceIn(0f, 1f)
            val freq = 20.0 * 900.0.pow(eff.toDouble())
            computeCoefficients(type, freq.coerceIn(20.0, sampleRate * 0.45), q)
        }
        ctrl--

        val input = x.toDouble()
        val out = b0 * input + z1
        z1 = b1 * input - a1 * out + z2
        z2 = b2 * input - a2 * out
        var y = out.toFloat()

        // Warmth: soft saturation blended in so the small-signal gain (and level)
        // stays put — it only rounds off louder peaks, adding gentle harmonics.
        if (warmth > 0.0001f) y += warmth * (tanh(y * 1.8f) / 1.8f - y)
        return y
    }

    private companion object {
        const val CTRL_INTERVAL = 32   // samples between cutoff-coefficient refreshes
        const val GATE_HI = 0.010f     // input level that opens the envelope gate
        const val GATE_LO = 0.004f     // input level that releases it (hysteresis)
    }
}

// ---------------------------------------------------------------------------
// Bitcrusher: bit-depth reduction + sample-rate reduction + drive.
// ---------------------------------------------------------------------------
private class Bitcrusher : AudioEffect {
    private var levels = 256f
    private var downsample = 1
    private var drive = 1f
    private var counter = 0
    private var held = 0f

    override fun update(slot: ToolboxSlot, tempoBpm: Float) {
        val bits = slot.float("bits", 8f).coerceIn(1f, 16f)
        levels = 2f.pow(bits)
        downsample = slot.float("downsample", 1f).toInt().coerceAtLeast(1)
        drive = 1f + slot.float("drive", 0.2f) * 4f
    }

    override fun process(x: Float): Float {
        if (counter <= 0) {
            counter = downsample
            // Quantise to the reduced bit depth after applying drive.
            val driven = tanh(x * drive)
            held = (driven * levels).roundToInt() / levels
        }
        counter--
        return held
    }
}

// ---------------------------------------------------------------------------
// 10-band graphic EQ: ten peaking biquads at octave-spaced centre frequencies.
// ---------------------------------------------------------------------------
private class GraphicEq(private val sampleRate: Int) : AudioEffect {
    private val centers = floatArrayOf(31f, 63f, 125f, 250f, 500f, 1000f, 2000f, 4000f, 8000f, 16000f)
    private val bands = Array(10) { PeakBiquad() }
    // Precomputed keys — "band$i" would allocate on the audio thread every block.
    private val bandKeys = Array(10) { "band$it" }

    override fun update(slot: ToolboxSlot, tempoBpm: Float) {
        for (i in bands.indices) {
            val gainDb = slot.float(bandKeys[i], 0f).coerceIn(-1f, 1f) * 12f // -12..+12 dB
            bands[i].set(sampleRate, centers[i], gainDb)
        }
    }

    override fun process(x: Float): Float {
        var y = x
        for (b in bands) y = b.process(y)
        return y
    }

    private class PeakBiquad {
        private var b0 = 1.0; private var b1 = 0.0; private var b2 = 0.0
        private var a1 = 0.0; private var a2 = 0.0
        private var z1 = 0.0; private var z2 = 0.0

        fun set(sampleRate: Int, freq: Float, gainDb: Float) {
            if (freq >= sampleRate * 0.45f) return
            val a = 10.0.pow(gainDb / 40.0)
            val w0 = 2.0 * PI * freq / sampleRate
            val cosW = cos(w0)
            val alpha = sin(w0) / (2.0 * 1.0) // Q ~ 1 per band
            val a0 = 1 + alpha / a
            b0 = (1 + alpha * a) / a0
            b1 = (-2 * cosW) / a0
            b2 = (1 - alpha * a) / a0
            a1 = (-2 * cosW) / a0
            a2 = (1 - alpha / a) / a0
        }

        fun process(x: Float): Float {
            val input = x.toDouble()
            val out = b0 * input + z1
            z1 = b1 * input - a1 * out + z2
            z2 = b2 * input - a2 * out
            return out.toFloat()
        }
    }
}
