package com.radiola.service

import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.audio.TeeAudioProcessor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.cos
import kotlin.math.ln
import kotlin.math.sqrt

/** Итеративный radix-2 FFT (in-place). Размер — степень двойки. */
internal object Fft {
    fun transform(re: FloatArray, im: FloatArray) {
        val n = re.size
        var j = 0
        for (i in 1 until n) {
            var bit = n shr 1
            while (j and bit != 0) {
                j = j xor bit
                bit = bit shr 1
            }
            j = j or bit
            if (i < j) {
                var t = re[i]; re[i] = re[j]; re[j] = t
                t = im[i]; im[i] = im[j]; im[j] = t
            }
        }
        var len = 2
        while (len <= n) {
            val ang = -2.0 * Math.PI / len
            val wr = cos(ang).toFloat()
            val wi = kotlin.math.sin(ang).toFloat()
            var i = 0
            while (i < n) {
                var curR = 1f
                var curI = 0f
                val half = len / 2
                for (k in 0 until half) {
                    val reK = re[i + k + half]
                    val imK = im[i + k + half]
                    val bR = reK * curR - imK * curI
                    val bI = reK * curI + imK * curR
                    val aR = re[i + k]
                    val aI = im[i + k]
                    re[i + k] = aR + bR
                    im[i + k] = aI + bI
                    re[i + k + half] = aR - bR
                    im[i + k + half] = aI - bI
                    val nCurR = curR * wr - curI * wi
                    curI = curR * wi + curI * wr
                    curR = nCurR
                }
                i += len
            }
            len = len shl 1
        }
    }
}

/**
 * Подключается к декодированному PCM в аудио-конвейере ExoPlayer (через
 * TeeAudioProcessor — без изменения звука и без разрешений) и считает спектр
 * (FFT → лог-полосы) для «живого» эквалайзера, реагирующего на реальный звук.
 */
@OptIn(UnstableApi::class)
class AudioSpectrumAnalyzer(
    private val bands: Int = 32,
) : TeeAudioProcessor.AudioBufferSink {

    private val _spectrum = MutableStateFlow(FloatArray(bands))
    val spectrum: StateFlow<FloatArray> = _spectrum

    // FFT считаем ТОЛЬКО когда есть наблюдатель (открыт плеер). Иначе ~86 FFT/с
    // молотят впустую при фоновом проигрывании (экран выключен) — главный
    // пожиратель батареи. Ставится из UI плеера (VisualizerHost).
    @Volatile
    var active: Boolean = false

    // Меньше окно = меньше задержка реакции на удар (групповая задержка Hann ~окно/2)
    // и чаще обновления. Лайвность держит автогейн, а не размер окна.
    private val fftSize = 1024
    private val sample = FloatArray(fftSize)
    private val re = FloatArray(fftSize)
    private val im = FloatArray(fftSize)
    private val window = FloatArray(fftSize) { 0.5f * (1f - cos(2.0 * Math.PI * it / (fftSize - 1)).toFloat()) }
    private val smoothed = FloatArray(bands)
    private var filled = 0
    private var channelCount = 2
    private var pcm16 = true
    private var sampleRate = 44100
    private var lastEmit = 0L
    // Автогейн: бегущий пик амплитуды — чтобы столбики всегда использовали всю
    // высоту независимо от громкости трека.
    private var agcPeak = 1e-4f

    override fun flush(sampleRateHz: Int, channelCount: Int, encoding: Int) {
        this.sampleRate = if (sampleRateHz > 0) sampleRateHz else 44100
        this.channelCount = channelCount.coerceAtLeast(1)
        this.pcm16 = encoding == C.ENCODING_PCM_16BIT
        filled = 0
    }

    override fun handleBuffer(buffer: ByteBuffer) {
        if (!pcm16) return
        // Нет наблюдателя — не тратим CPU на FFT (батарея при фоновом проигрывании).
        if (!active) {
            filled = 0
            return
        }
        val b = buffer.duplicate().order(ByteOrder.LITTLE_ENDIAN)
        val ch = channelCount
        val hop = fftSize / 2
        while (b.remaining() >= 2 * ch) {
            var sum = 0f
            for (c in 0 until ch) sum += b.short.toFloat()
            sample[filled++] = (sum / ch) / 32768f
            if (filled >= fftSize) {
                compute()
                // Перекрытие 50%: оставляем вторую половину — чаще обновляем (~43к/с),
                // спектр идёт «впритык» к биту, без рывков.
                System.arraycopy(sample, hop, sample, 0, fftSize - hop)
                filled = fftSize - hop
            }
        }
    }

    private fun compute() {
        for (i in 0 until fftSize) {
            re[i] = sample[i] * window[i]
            im[i] = 0f
        }
        Fft.transform(re, im)

        val half = fftSize / 2
        val binHz = sampleRate.toFloat() / fftSize
        val fMin = 40f
        val fMax = 16000f
        val ratio = fMax / fMin

        val raw = FloatArray(bands)
        var frameMax = 0f
        for (band in 0 until bands) {
            // Лог-частотные полосы 40Гц..16кГц, среднее по бинам полосы.
            val fLo = fMin * Math.pow(ratio.toDouble(), band.toDouble() / bands).toFloat()
            val fHi = fMin * Math.pow(ratio.toDouble(), (band + 1.0) / bands).toFloat()
            val binLo = (fLo / binHz).toInt().coerceIn(1, half - 1)
            val binHi = (fHi / binHz).toInt().coerceIn(binLo + 1, half)
            var sum = 0f
            for (bin in binLo until binHi) {
                sum += sqrt(re[bin] * re[bin] + im[bin] * im[bin])
            }
            // Лёгкий подъём верхов (у них меньше энергии) — чтобы спектр был ровнее.
            val tilt = 1f + 1.5f * (band.toFloat() / bands)
            val mag = sum / (binHi - binLo) * tilt
            raw[band] = mag
            if (mag > frameMax) frameMax = mag
        }

        // Автогейн: мгновенный рост пика, плавный спад → всегда полная высота.
        agcPeak = maxOf(agcPeak * 0.94f, frameMax, 1e-4f)
        val out = FloatArray(bands)
        for (band in 0 until bands) {
            // Нормируем по пику + перцептивный лифт (sqrt), чтобы тихое было видно.
            val v = sqrt((raw[band] / agcPeak).coerceIn(0f, 1f))
            val prev = smoothed[band]
            // Мгновенный рост на удар, быстрый спад — чтобы попадать в ритм, не «висеть».
            smoothed[band] = if (v > prev) v else prev * 0.55f + v * 0.45f
            out[band] = smoothed[band]
        }
        // Считаем сглаживание каждый хоп (плавность), но эмитим в UI ~45/с, чтобы
        // не перегружать перерисовку плеера.
        val now = System.nanoTime()
        if (now - lastEmit >= 22_000_000L) {
            lastEmit = now
            _spectrum.value = out
        }
    }
}
