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

package space.rcmd.android.sizzle.audio

import space.rcmd.android.sizzle.model.AmbiencePresets
import space.rcmd.android.sizzle.model.ToolboxSlot
import kotlin.math.abs
import kotlin.math.exp
import kotlin.math.floor
import kotlin.math.log10
import kotlin.math.tanh

/**
 * A mono port of OTODESK's "Ambience 1.0.1" algorithmic reverb — a 16-channel
 * Feedback Delay Network (FDN). Faithful to the original's architecture within the
 * limits of this app's mono, per-sample [AudioEffect] chain:
 *
 *  - 16 delay lines at mutually-prime, log-distributed lengths scaled by Room Size.
 *  - A lossless feedback matrix: Fast Walsh-Hadamard Transform + the original's
 *    fixed sign-flip pattern (orthonormal → the only loss is the damping stage,
 *    so RT60 is set purely by decay/damping and the loop can never run away).
 *  - Per-channel decay gain derived from the Decay (RT60) target, plus a one-pole
 *    HF-damping low-pass and an LF-absorption tap (with per-algorithm intrinsic
 *    tone baked from the plugin's measured RT60 curves).
 *  - A 4-stage input diffuser and a nested allpass per channel (Diffusion).
 *  - Bandlimited-noise LFO modulation of the delay reads (Mod Amount / Mod Rate).
 *  - ISM early-reflection tap patterns per algorithm (Room/Hall only), fed to the
 *    wet mix and blended 15% into the FDN input, as in the original.
 *  - Wet saturation, the original's decay-compensated make-up gain, Wet/Dry mix,
 *    and an input-driven ducking envelope.
 *
 * Deliberately omitted (per the port request): the graphic/output EQ, lo/hi cut
 * filters, and all visualizers; stereo width and Pro-mode per-band controls don't
 * apply to a mono chain.
 */
class AmbienceReverb(sampleRate: Int) : AudioEffect {

    private val fs = sampleRate.toFloat()

    // ---- delay lines ----
    private val preDelay = DelayLine((fs * 0.55f).toInt())
    private val erLine = DelayLine((fs * 0.6f).toInt())
    private val diffusers = Array(4) { DelayLine((fs * 0.03f).toInt()) }
    private val fdn = Array(N) { DelayLine((fs * 0.3f).toInt()) }
    private val apf = Array(N) { DelayLine((fs * 0.03f).toInt()) }

    // ---- FDN feedback state ----
    private val fbVec = FloatArray(N)
    private val fbScratch = FloatArray(N)
    private val nextFb = FloatArray(N)
    private val baseDelay = FloatArray(N)
    private val decayGain = FloatArray(N)
    private val hfState = FloatArray(N)
    private val lfState = FloatArray(N)
    private val freqMod = FloatArray(N) { 0.5f + (1f - it / (N - 1f)) }
    private val apfBase = FloatArray(N) { (2.3f + it * 0.37f) * 0.001f * fs }
    private val diffuserSmp = FloatArray(4) { (3f + it * 2f) * 0.001f * fs }

    // ---- per-channel noise LFO ----
    private val lfoState = IntArray(N) { 12345 + it * 9876 }
    private val lfoSmooth = FloatArray(N)
    private val lfoCoeff = FloatArray(N)
    private val lfoRateMul = FloatArray(N) { val a = it * PHI; 0.8f + (a - floor(a)) * 0.4f }

    // ---- resolved routing (recomputed on structural change) ----
    private var hfCoeff = 1f
    private var lfCoeff = 0f
    private var lfAmt = 0f
    private var apfGain = 0.5f
    private var diffusionSens = 1f
    private var bypassER = false
    private var makeup = 1f
    private var satMul = 1f
    private var modDepthScale = 1f
    private var erCount = 0
    private val erDelaySmp = FloatArray(MAX_ER)
    private val erGain = FloatArray(MAX_ER)

    // ---- per-block scalars ----
    private var wetLin = 0.63f
    private var dryLin = 1f
    private var preDelaySmp = 480f
    private var modDepth = 0f
    private var diffGain = 0.63f
    private var effApfGain = 0.4f
    private var satAmt = 0f
    private var erLevel = 0.6f
    // ducking
    private var duckAmtDb = 0f
    private var duckThreshDb = -20f
    private var duckThreshLin = 0.1f
    private var duckAtt = 0.01f
    private var duckRel = 0.001f
    private var duckEnv = 0f

    private var dc1 = 0f
    private var dcY = 0f

    // Last structural params seen, for a no-allocation dirty check (a String key here
    // would allocate on the audio thread every block).
    private var lastAlgo = -1
    private var lastSize = Float.NaN
    private var lastDecay = Float.NaN
    private var lastHf = Float.NaN
    private var lastLf = Float.NaN
    private var lastDiffusion = Float.NaN

    override fun update(slot: ToolboxSlot, tempoBpm: Float) {
        val algo = AmbiencePresets.ALGORITHMS.indexOf(slot.string("algo", "Room 1")).coerceIn(0, 6)
        val size = slot.float("roomsize", 1f).coerceIn(0.3f, 2f)
        val decay = slot.float("decay", 1.5f).coerceIn(0.1f, 20f)
        val hf = slot.float("hfdamp", 0f).coerceIn(0f, 1f)
        val lf = slot.float("lfabsorb", 0f).coerceIn(0f, 1f)
        val diffusion = slot.float("diffusion", 0.7f).coerceIn(0f, 1f)

        // Cheap per-block scalars.
        wetLin = db2lin(slot.float("wet", -4f))
        dryLin = db2lin(slot.float("dry", 0f))
        preDelaySmp = slot.float("predelay", 10f).coerceIn(0f, 500f) * 0.001f * fs
        erLevel = slot.float("erlevel", 0.6f).coerceIn(0f, 1f)
        satAmt = slot.float("saturation", 0f).coerceIn(0f, 1f)
        val modAmt = slot.float("modamt", 0.25f).coerceIn(0f, 1f)
        val modRate = slot.float("modrate", 0.5f).coerceIn(0.05f, 2f)
        modDepth = modAmt * modAmt * 0.001f * fs * modDepthScale
        for (i in 0 until N) {
            lfoCoeff[i] = (1f - exp(-TWO_PI * modRate * lfoRateMul[i] / fs)).coerceIn(0.0001f, 0.9999f)
        }
        // Ducking.
        duckAmtDb = slot.float("duckamt", 0f).coerceIn(0f, 20f)
        duckThreshDb = slot.float("duckthresh", -20f)
        duckThreshLin = db2lin(duckThreshDb)
        duckAtt = 1f - exp(-1f / (fs * slot.float("duckattack", 10f).coerceAtLeast(0.1f) * 0.001f))
        duckRel = 1f - exp(-1f / (fs * slot.float("duckrelease", 200f).coerceAtLeast(0.1f) * 0.001f))
        val effDiff = diffusion * diffusionSens
        diffGain = 0.25f + effDiff * 0.55f
        effApfGain = apfGain * (0.6f + effDiff * 0.4f) * 0.78f

        // Structural recompute only when a delay/tone-affecting param changes
        // (primitive compare — no per-block allocation).
        if (algo != lastAlgo || size != lastSize || decay != lastDecay ||
            hf != lastHf || lf != lastLf || diffusion != lastDiffusion) {
            lastAlgo = algo; lastSize = size; lastDecay = decay
            lastHf = hf; lastLf = lf; lastDiffusion = diffusion
            recompute(algo, size, decay, hf, lf, diffusion)
            // effApfGain/diffGain depend on diffusionSens which recompute() may change.
            val d2 = diffusion * diffusionSens
            diffGain = 0.25f + d2 * 0.55f
            effApfGain = apfGain * (0.6f + d2 * 0.4f) * 0.78f
        }
    }

    private fun recompute(algo: Int, size: Float, decay: Float, hf: Float, lf: Float, diffusion: Float) {
        // Topology routing (per the original updateTopologyAndRouting()). Room 1/2
        // use 0.3; halls 0.618; the non-room algos their own values.
        when (algo) {
            0, 1 -> { apfGain = 0.3f; diffusionSens = 1f; bypassER = false }
            2, 3 -> { apfGain = 0.618f; diffusionSens = 1f; bypassER = false }
            4 -> { apfGain = 0.7f; diffusionSens = 0.7f; bypassER = true }
            5 -> { apfGain = 0.5f; diffusionSens = 0.5f; bypassER = true }
            else -> { apfGain = 0.75f; diffusionSens = 0.8f; bypassER = true }
        }

        // Prime delay lengths: log-distributed between size-dependent bounds.
        val sizeCoeff = (size + 0.5f).coerceIn(0.5f, 2f)
        val minMs = 15f + sizeCoeff * 7.5f
        val maxMs = 50f + sizeCoeff * 75f
        val minS = kotlin.math.max(11, (minMs * 0.001f * fs).toInt())
        val maxS = (maxMs * 0.001f * fs).toInt().coerceAtLeast(minS + 1)
        val logMin = kotlin.math.ln(minS.toFloat())
        val logMax = kotlin.math.ln(maxS.toFloat())
        val used = IntArray(N)
        for (i in 0 until N) {
            val t = i / (N - 1f)
            val target = exp(logMin + t * (logMax - logMin)).toInt()
            val p = nearestUniquePrime(target, used, i)
            used[i] = p
            baseDelay[i] = p.toFloat().coerceAtMost(fdn[i].maxDelay())
        }

        // Decay → per-channel broadband feedback gain: g = 10^(-3 T / RT60).
        for (i in 0 until N) {
            val tSec = baseDelay[i] / fs
            decayGain[i] = pow10(-3f * tSec / decay).coerceIn(0f, 0.9995f)
        }

        // HF damping = user + algorithm-intrinsic; maps to a one-pole LP cutoff.
        val totalHf = (hf + ALGO_HF[algo]).coerceIn(0f, 0.95f)
        val hfCut = 16000f * (1f - totalHf) + 700f * totalHf
        hfCoeff = (1f - exp(-TWO_PI * hfCut / fs)).coerceIn(0.02f, 1f)
        lfCoeff = (1f - exp(-TWO_PI * 200f / fs)).coerceIn(0f, 1f)
        lfAmt = lf * 0.9f

        // Modulation deepens with longer decay (anti-metallic), as in the original.
        modDepthScale = 1f + ((decay - 1f) * 0.5f).coerceIn(0f, 2f)

        // Make-up gain: base 16 dB + decay compensation + per-algorithm offset.
        val decayCompDb = 7f * log10(decay.coerceAtLeast(0.1f))
        makeup = db2lin(16f + decayCompDb + ALGO_OFFSET_DB[algo])
        satMul = ALGO_SAT_MUL[algo]

        // Early reflections (Room/Hall only), scaled by room size.
        val pattern = ER_PATTERNS[algo]
        erCount = if (bypassER) 0 else minOf(pattern.size / 2, MAX_ER_ACTIVE)
        val erSizeScale = 0.5f + size
        for (t in 0 until erCount) {
            erDelaySmp[t] = (pattern[t * 2] * 0.001f * fs * erSizeScale).coerceIn(1f, erLine.maxDelay())
            erGain[t] = pattern[t * 2 + 1]
        }
    }

    override fun process(x: Float): Float {
        // Pre-delay feeds both ER and FDN paths.
        preDelay.write(x)
        val din = if (preDelaySmp > 0.5f) preDelay.read(preDelaySmp) else x

        // Ducking envelope from the dry input.
        val peak = abs(x)
        duckEnv += (peak - duckEnv) * (if (peak > duckEnv) duckAtt else duckRel)
        var duckGain = 1f
        if (duckAmtDb > 0.001f && duckEnv > duckThreshLin) {
            val overDb = 20f * log10(kotlin.math.max(duckEnv, 1e-6f)) - duckThreshDb
            duckGain = db2lin(-kotlin.math.min(overDb, duckAmtDb))
        }

        // Input diffuser (4 series allpasses).
        var fdnIn = din
        for (i in 0 until 4) {
            val d = diffusers[i].read(diffuserSmp[i])
            val w = fdnIn + diffGain * d
            diffusers[i].write(w)
            fdnIn = d - diffGain * w
        }

        // Early reflections.
        var er = 0f
        if (!bypassER) {
            erLine.write(din)
            for (t in 0 until erCount) er += erLine.read(erDelaySmp[t]) * erGain[t] * 0.5f
            fdnIn += er * 0.15f
        }

        // Lossless feedback matrix on the previous frame's output.
        System.arraycopy(fbVec, 0, fbScratch, 0, N)
        fwht(fbScratch)
        for (i in 0 until N) fbScratch[i] *= SIGN_FLIP[i]

        val inPerCh = fdnIn * 0.25f
        var out = 0f
        for (i in 0 until N) {
            val lfo = nextLfo(i)
            var d = fdn[i].read(baseDelay[i] + lfo * modDepth * freqMod[i])
            // Broadband decay + HF damping (DC-unity LP) + LF absorption.
            d *= decayGain[i]
            hfState[i] += hfCoeff * (d - hfState[i]); d = hfState[i]
            lfState[i] += lfCoeff * (d - lfState[i]); d -= lfAmt * lfState[i]
            // Nested allpass (late-field density).
            val ad = apf[i].read(apfBase[i] + lfo * modDepth * 0.1f * freqMod[i])
            val aw = d + effApfGain * ad
            apf[i].write(aw)
            val apfOut = ad - effApfGain * aw

            nextFb[i] = apfOut
            fdn[i].write(inPerCh + fbScratch[i])
            out += apfOut
        }
        System.arraycopy(nextFb, 0, fbVec, 0, N)

        // Late field: sum → make-up gain → saturation. LATE_SCALE keeps the summed
        // level consistent regardless of the FDN size N (so halving N doesn't halve
        // the wet signal).
        var late = out * LATE_SCALE * makeup
        val amt = (satAmt * satMul).coerceIn(0f, 1f)
        if (amt > 0.0001f) late += amt * (tanh(late * 2f) * 0.5f - late)

        // DC blocker on the wet output.
        val wetRaw = (if (bypassER) 0f else er * erLevel) + late
        val dcOut = wetRaw - dc1 + 0.999f * dcY
        dc1 = wetRaw; dcY = dcOut

        val wet = softClip(dcOut * wetLin * duckGain)
        return x * dryLin + wet
    }

    // xorshift white noise → one-pole lowpass, per channel.
    private fun nextLfo(i: Int): Float {
        var s = lfoState[i]
        s = s xor (s shl 13); s = s xor (s ushr 17); s = s xor (s shl 5)
        lfoState[i] = s
        val n = (s.toLong() and 0xFFFFFFFFL).toFloat() * 2.3283064e-10f * 2f - 1f
        lfoSmooth[i] += (n - lfoSmooth[i]) * lfoCoeff[i]
        return lfoSmooth[i]
    }

    /** A power-of-two circular buffer with fractional (linear-interpolated) reads. */
    private class DelayLine(size: Int) {
        private val buf: FloatArray
        private val mask: Int
        private var w = 0
        init {
            var p = 1
            while (p < size) p *= 2
            buf = FloatArray(p)
            mask = p - 1
        }
        fun maxDelay(): Float = (mask - 2).toFloat()
        fun write(x: Float) { buf[w] = x; w = (w + 1) and mask }
        fun read(delay: Float): Float {
            val d = delay.coerceIn(1f, mask.toFloat() - 1f)
            val rp = w - d
            val i0f = floor(rp)
            val frac = rp - i0f
            val i0 = i0f.toInt() and mask
            val i1 = (i0 + 1) and mask
            return buf[i0] * (1f - frac) + buf[i1] * frac
        }
    }

    private companion object {
        // Feedback-delay-network size. The original port used 16 lines, but on a
        // mid-range phone that render cost (16 interpolated delay reads + nested
        // allpass + one-poles + LFO per sample, thrashing >1 MB of delay buffers)
        // overran the real-time budget on its own. 8 lines halve the dominant loop
        // while still giving a dense, smooth tail. FWHT stays orthonormal via
        // FWHT_NORM = 1/sqrt(N), so the loop remains lossless.
        const val N = 8
        val FWHT_NORM = 1f / kotlin.math.sqrt(N.toFloat())
        // Late-sum scale, level-matched to the original 16-line version (16→0.125).
        val LATE_SCALE = 2f / N
        const val MAX_ER = 12
        // Cap on early-reflection taps evaluated per sample (Room/Hall). The patterns
        // list up to 12; the later, quietest taps cost more than they add.
        const val MAX_ER_ACTIVE = 6
        const val PHI = 1.6180339887f
        const val TWO_PI = 6.28318530718f

        // Per-algorithm intrinsic HF damping, derived from the plugin's measured
        // RT60 curves (rt60[8kHz]/rt60[500Hz]): darker spaces damp highs more.
        val ALGO_HF = floatArrayOf(0.00f, 0.26f, 0.26f, 0.22f, 0.36f, 0.14f, 0.30f)
        // Make-up gain offsets and saturation multipliers, per the original.
        val ALGO_OFFSET_DB = floatArrayOf(0.8f, 0.9f, 0.5f, 0.5f, 1.5f, 0.6f, 0.6f)
        val ALGO_SAT_MUL = floatArrayOf(0.90f, 0.90f, 0.93f, 0.93f, 1.00f, 1.05f, 1.02f)

        // Fixed sign-flip pattern for the FWHT feedback matrix (one per FDN line).
        val SIGN_FLIP = floatArrayOf(
            1f, -1f, 1f, -1f, -1f, 1f, -1f, 1f,
        )

        // ISM early-reflection tap patterns (delayMs, gain)… per algorithm. Plate,
        // Spring and Goldfoil are non-rooms → no ER (empty).
        val ER_PATTERNS: Array<FloatArray> = arrayOf(
            // Room 1
            floatArrayOf(
                5.2f, 0.65f, 8.7f, 0.58f, 12.4f, 0.52f, 15.8f, 0.46f, 19.3f, 0.42f, 24.1f, 0.36f,
                28.6f, 0.32f, 33.5f, 0.28f, 38.9f, 0.24f, 45.2f, 0.20f, 52.8f, 0.16f, 62.4f, 0.13f,
            ),
            // Room 2
            floatArrayOf(
                7.5f, 0.62f, 12.3f, 0.55f, 17.1f, 0.49f, 22.8f, 0.43f, 28.5f, 0.38f, 34.2f, 0.33f,
                41.6f, 0.28f, 49.3f, 0.24f, 57.8f, 0.20f, 67.5f, 0.17f, 78.2f, 0.14f, 90.6f, 0.11f,
            ),
            // Hall 1
            floatArrayOf(
                12.0f, 0.58f, 18.5f, 0.52f, 25.7f, 0.47f, 33.4f, 0.42f, 42.1f, 0.38f, 51.6f, 0.33f,
                62.3f, 0.29f, 73.9f, 0.25f, 86.5f, 0.21f, 99.8f, 0.18f, 113.4f, 0.15f, 128.7f, 0.12f,
            ),
            // Hall 2
            floatArrayOf(
                16.5f, 0.55f, 24.8f, 0.50f, 33.7f, 0.45f, 43.5f, 0.40f, 54.2f, 0.36f, 65.8f, 0.32f,
                78.4f, 0.28f, 92.1f, 0.24f, 107.3f, 0.21f, 123.6f, 0.18f, 141.2f, 0.15f, 160.5f, 0.12f,
            ),
            floatArrayOf(), // Plate
            floatArrayOf(), // Spring
            floatArrayOf(), // Goldfoil
        )

        fun db2lin(db: Float): Float = pow10(db / 20f)
        fun pow10(x: Float): Float = exp(x * 2.302585093f)

        /** In-place size-N Fast Walsh-Hadamard transform, normalised (÷√N) → orthonormal. */
        fun fwht(v: FloatArray) {
            var h = 1
            while (h < N) {
                var i = 0
                while (i < N) {
                    var j = i
                    while (j < i + h) {
                        val a = v[j]; val b = v[j + h]
                        v[j] = a + b; v[j + h] = a - b
                        j++
                    }
                    i += h * 2
                }
                h *= 2
            }
            for (k in 0 until N) v[k] *= FWHT_NORM
        }

        fun isPrime(n: Int): Boolean {
            if (n < 2) return false
            if (n == 2) return true
            if (n % 2 == 0) return false
            var i = 3
            while (i * i <= n) { if (n % i == 0) return false; i += 2 }
            return true
        }

        fun nearestUniquePrime(target: Int, used: IntArray, count: Int): Int {
            val t = kotlin.math.max(target, 2)
            var offset = 0
            while (offset < 100000) {
                val hi = t + offset
                if (isPrime(hi) && (0 until count).none { used[it] == hi }) return hi
                val lo = t - offset
                if (offset > 0 && lo >= 2 && isPrime(lo) && (0 until count).none { used[it] == lo }) return lo
                offset++
            }
            return t
        }

        fun softClip(x: Float): Float = when {
            x > 1f -> 1f - 1f / (x + 1f)
            x < -1f -> -1f + 1f / (-x + 1f)
            else -> x
        }
    }
}
