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

package space.rcmd.android.sizzle.ui.toolbox

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import space.rcmd.android.sizzle.model.ToolboxSlot
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.theme.LocalRetro
import kotlin.math.roundToInt

/**
 * Editor for the [space.rcmd.android.sizzle.model.ToolboxType.GRANULAR] effect: a
 * dotted oscilloscope of the sound passing through with the granular read head drawn
 * as a jumping playhead, and one horizontal Intensity slider below. The scope only
 * shows data while the effect is placed on a mixer channel that is receiving signal.
 */
@Composable
fun GranularEditor(vm: AppViewModel, slot: ToolboxSlot) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision
    val mono = FontFamily.Monospace

    // Polled once per frame from the audio engine: the scope samples, the playhead
    // position, and how many samples are live (-1 = not routed / no signal).
    val scope = remember { FloatArray(SCOPE_CAP) }
    var count by remember { mutableIntStateOf(-1) }
    var playhead by remember { mutableFloatStateOf(-1f) }
    var tick by remember { mutableIntStateOf(0) }
    LaunchedEffect(slot.index) {
        while (true) {
            withFrameNanos { }
            count = vm.effectScope(slot.index, scope)
            playhead = vm.effectPlayhead(slot.index)
            tick++
        }
    }

    Column(
        Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Box(
            Modifier.fillMaxWidth().height(160.dp).background(c.background).border(1.dp, c.grid, RectangleShape),
        ) {
            Canvas(Modifier.fillMaxWidth().height(160.dp)) {
                @Suppress("UNUSED_EXPRESSION") tick // subscribe the draw to the per-frame updates
                val w = size.width
                val h = size.height
                val cy = h * 0.5f
                val amp = h * 0.42f
                drawLine(c.grid, Offset(0f, cy), Offset(w, cy), strokeWidth = 1f) // baseline
                val n = count
                if (n > 1) {
                    // Dotted waveform of the signal passing through.
                    val step = w / (n - 1)
                    for (k in 0 until n) {
                        val y = cy - scope[k].coerceIn(-1f, 1f) * amp
                        drawCircle(c.textDim, radius = 2f, center = Offset(k * step, y))
                    }
                    // Jumping playhead over the scope.
                    val ph = playhead
                    if (ph in 0f..1f) {
                        val px = ph * w
                        drawLine(c.accent, Offset(px, 0f), Offset(px, h), strokeWidth = 2f)
                        drawCircle(c.accent, radius = 4f, center = Offset(px, cy))
                    }
                }
            }
            if (count < 0) {
                Text(
                    "Add to a mixer channel's FX and play to see the scope.",
                    color = c.textDim, fontFamily = mono, fontSize = 11.sp,
                    textAlign = TextAlign.Center,
                    modifier = Modifier.align(Alignment.Center).fillMaxWidth(),
                )
            }
        }

        val intensity = slot.float("intensity", 0.3f).coerceIn(0f, 1f)
        Text("Intensity  ${(intensity * 100).roundToInt()}%",
            color = c.accent, fontFamily = mono, fontSize = 12.sp)
        HorizontalSlider(
            value = intensity,
            onValueChange = { slot.set("intensity", it.coerceIn(0f, 1f)); vm.bumpForToolbar() },
            modifier = Modifier.fillMaxWidth().height(40.dp),
        )
        Text(
            "Low = occasional stutters · High = constant glitch & octave jumps.",
            color = c.textDim, fontFamily = mono, fontSize = 10.sp,
            textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(),
        )
    }
}

/** A horizontal fill-from-left slider matching the app's retro faders. */
@Composable
private fun HorizontalSlider(value: Float, onValueChange: (Float) -> Unit, modifier: Modifier) {
    val c = LocalRetro.current
    Box(
        modifier
            .border(1.dp, c.grid, RectangleShape)
            .background(c.background)
            .pointerInput(Unit) {
                detectTapGestures { off -> onValueChange(off.x / size.width) }
            }
            .pointerInput(Unit) {
                detectDragGestures { change, _ -> change.consume(); onValueChange(change.position.x / size.width) }
            },
        contentAlignment = Alignment.CenterStart,
    ) {
        Box(
            Modifier
                .fillMaxHeight()
                .fillMaxWidth(value.coerceIn(0.001f, 1f))
                .background(c.accent.copy(alpha = 0.30f)),
            contentAlignment = Alignment.CenterEnd,
        ) {
            Box(Modifier.fillMaxHeight().width(3.dp).background(c.accent)) // thumb
        }
    }
}

private const val SCOPE_CAP = 128 // UI buffer; the engine fills up to its own scope size
