package dev.adriankuta.visualizer.domain

import android.graphics.Color
import kotlin.math.*

// Keep this across frames (e.g., one per audio stream/visualizer)
data class VisualizerState(
    var prevMag: FloatArray? = null,
    var maxEnergy: Double = 1e-6,  // running max of energy
    var maxFlux: Double = 1e-6     // running max of flux
)

/**
 * Convert one FFT magnitude spectrum to a color.
 *
 * @param mags        Magnitude of the *half* spectrum (bins 0..Nyquist), non-negative.
 * @param sampleRate  Audio sample rate in Hz (e.g., 44100).
 * @param state       Persistent state for normalization & flux (keep between calls).
 * @return            ARGB color Int (Android).
 */
fun fftToColor(mags: FloatArray, sampleRate: Int, state: VisualizerState): Int {
    require(mags.isNotEmpty()) { "mags must not be empty" }
    val nBins = mags.size
    val nyquist = sampleRate * 0.5

    // --- Basic spectrum stats ---
    var sumMag = 0.0
    var weightedFreqSum = 0.0
    var energy = 0.0

    // Frequency step per bin (covers 0..Nyquist across nBins bins)
    val binHz = if (nBins > 1) nyquist / (nBins - 1) else 0.0

    for (k in 0 until nBins) {
        val m = mags[k].toDouble().coerceAtLeast(0.0)
        sumMag += m
        energy += m * m
        val fk = k * binHz
        weightedFreqSum += fk * m
    }

    // Spectral centroid in Hz (fallback to 0 when silent)
    val centroidHz = if (sumMag > 0.0) weightedFreqSum / sumMag else 0.0

    // Log-scaled centroid -> [0,1] (more perceptual)
    val hue01 = run {
        val num = ln(1.0 + centroidHz)
        val den = ln(1.0 + nyquist)
        if (den > 0.0) (num / den).coerceIn(0.0, 1.0) else 0.0
    }

    // --- Spectral flux (vs. previous frame) for saturation ---
    val prev = state.prevMag
    var flux = 0.0
    if (prev != null && prev.size == nBins) {
        for (k in 0 until nBins) {
            val d = mags[k].toDouble() - prev[k].toDouble()
            if (d > 0.0) flux += d
        }
    }
    // Update prev spectrum (copy to avoid aliasing)
    state.prevMag = mags.copyOf()

    // --- Adaptive normalization (with slow decay) ---
    // Update running maxima with slight decay so the visual doesn't "stick"
    fun decay(x: Double, factor: Double) = x * factor

    state.maxEnergy = max(decay(state.maxEnergy, 0.995), energy)
    state.maxFlux   = max(decay(state.maxFlux,   0.995), flux)

    // --- Map to HSV ---
    // V: log-compress energy; normalize by running max
    val v = run {
        val eNorm = if (state.maxEnergy > 1e-12) energy / state.maxEnergy else 0.0
        val logCompressed = ln(1.0 + 9.0 * eNorm) / ln(10.0) // [0,1], gentle curve
        logCompressed.coerceIn(0.0, 1.0)
    }

    // S: flux normalized by running max, then soft knee
    val s = run {
        val fNorm = if (state.maxFlux > 1e-12) flux / state.maxFlux else 0.0
        val soft = sqrt(fNorm.coerceIn(0.0, 1.0)) // emphasizes small changes less
        (0.15 + 0.85 * soft).coerceIn(0.0, 1.0)   // keep some color even when still
    }

    // H: map [0,1] over a 300° range so it wraps nicely (0=red, 0.5≈cyan, 1≈purple)
    val h = (hue01 * 300.0).toFloat()

    val hsv = floatArrayOf(h, s.toFloat(), v.toFloat())
    return Color.HSVToColor(hsv) // ARGB Int
}
