package dev.adriankuta.visualizer.domain.processors

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import dev.adriankuta.visualizer.data.FftFrame
import dev.adriankuta.visualizer.data.VisualizerFrame
import dev.adriankuta.visualizer.data.WaveformFrame
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.sqrt
import kotlin.reflect.KClass

/**
 * Shared utilities for brightness mapping.
 */
class BrightnessMapper(
    private val hue: Float = 210f,            // blue-ish
    private val saturation: Float = 0.9f,     // vivid color
    private val gain: Float = 8f,             // log mapping strength (higher -> more sensitive)
    private val attack: Float = 0.35f,        // 0..1, higher = faster rise
    private val release: Float = 0.08f,       // 0..1, lower = slower fall
    private val peakDecay: Float = 0.995f     // 0..1, how fast auto-gain reference decays
) {
    private var smoothed = 0f
    private var peak = 0.1f

    /**
     * @param rawEnergy input in [0, +inf) (e.g., RMS or avg magnitude)
     * @return Color with brightness (V) in [0,1]
     */
    fun toColor(rawEnergy: Float): Color {
        // 1) Log loudness mapping: map [0, +inf) → [0, 1]
        // normalizedLog = ln(1 + gain * x) / ln(1 + gain)
        val loud = if (rawEnergy <= 0f) 0f
        else (ln(1f + gain * rawEnergy) / ln(1f + gain))

        // 2) Peak/auto-gain tracking
        peak = max(loud, peak * peakDecay)
        val norm = if (peak > 1e-6f) loud / peak else loud
        val clamped = norm.coerceIn(0f, 1f)

        // 3) Attack/Release smoothing
        val a = if (clamped > smoothed) attack else release
        smoothed = smoothed + a * (clamped - smoothed)

        // 4) HSV → Color (fixed hue & saturation; dynamic value)
        return Color.hsv(
            hue = hue.coerceIn(0f, 360f),
            saturation = saturation.coerceIn(0f, 1f),
            value = smoothed.coerceIn(0f, 1f),
            colorSpace = ColorSpaces.Srgb
        )
    }
}

/**
 * Visualizer waveform → brightness.
 * Visualizer's waveform bytes are unsigned 8-bit: 0..255 with 128 ≈ 0.
 */
class WaveformToColorProcessor @Inject constructor(
) : VisualizerProcessor {
    private val brightness: BrightnessMapper = BrightnessMapper(
        hue = 210f,
        saturation = 0.9f,
        gain = 8f,
        attack = 0.35f,
        release = 0.08f,
        peakDecay = 0.995f
    )

    override suspend fun invoke(frame: VisualizerFrame): Color = withContext(Dispatchers.Default) {
        val bytes = frame.data
        if (bytes.isEmpty()) {
            return@withContext Color.Black
        }

        // Convert to [-1, 1] floats, compute RMS.
        var sumSq = 0.0
        val inv = 1f / 128f
        for (b in bytes) {
            // unsigned -> center at 128 -> normalize
            val s = ((b.toInt() and 0xFF) - 128) * inv
            sumSq += (s * s)
        }
        val rms = sqrt(sumSq / bytes.size).toFloat() // 0..~1

        brightness.toColor(rms)
    }
}

/**
 * Visualizer FFT → brightness.
 * Visualizer's FFT bytes are 8-bit signed fixed-point: interleaved (re, im).
 * We compute mean magnitude over bins. You can bias ranges by adding weights.
 */
class FftToColorProcessor @Inject constructor(
) : VisualizerProcessor {
    private val brightness: BrightnessMapper = BrightnessMapper(
        hue = 210f,
        saturation = 0.9f,
        gain = 10f,
        attack = 0.30f,
        release = 0.10f,
        peakDecay = 0.996f
    )
    private val binWeighting: ((binIndex: Int, totalBins: Int) -> Float)? = null

    override suspend fun invoke(frame: VisualizerFrame): Color = withContext(Dispatchers.Default) {
        val bytes = frame.data
        if (bytes.size < 2) {
            return@withContext Color.Black
        }

        // Each bin: (re, im) as signed bytes. Skip DC/nyquist if you like; here we include all valid pairs.
        val pairCount = bytes.size / 2
        var sumMag = 0.0
        var weightSum = 0.0

        var bin = 0
        var i = 0
        while (i + 1 < bytes.size) {
            val re = bytes[i].toInt().toByte().toInt() // ensure sign
            val im = bytes[i + 1].toInt().toByte().toInt()
            val mag = sqrt((re * re + im * im).toDouble()) // magnitude in byte units
            val w = binWeighting?.invoke(bin, pairCount)?.coerceAtLeast(0f)?.toDouble() ?: 1.0
            sumMag += mag * w
            weightSum += w
            bin += 1
            i += 2
        }

        // Average magnitude → normalize to something close to [0,1].
        // 8-bit range ≈ [-128,127], so typical magnitude can be up to ~181; average is far lower.
        val avgMag = if (weightSum > 0.0) sumMag / weightSum else 0.0
        val norm = (avgMag / 128.0).toFloat().coerceAtLeast(0f) // rough normalization

        brightness.toColor(norm)
    }
}
