// 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.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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
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.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
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.TimeDivision
import space.rcmd.android.sizzle.model.ToolboxSlot
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.components.Knob
import space.rcmd.android.sizzle.ui.theme.LocalRetro
import kotlin.math.cos
import kotlin.math.hypot
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sin

/**
 * Editor for the [space.rcmd.android.sizzle.model.ToolboxType.TAPE_ENGINE] effect,
 * drawn as a reel-to-reel tape transport: two NAB reels (metal flange with three
 * rounded-triangular bobbin holes) feeding tape down to a playhead block between them
 * under tension. **Press and hold a reel to brake it** — the tape (and its pitch)
 * dives, then springs back when released. Below is a row of vintage round knobs: wow,
 * flutter, hiss, and the tempo-sync division for the wow wobble.
 */
@Composable
fun TapeEngineEditor(vm: AppViewModel, slot: ToolboxSlot) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision
    val mono = FontFamily.Monospace

    // Rotation + a display speed eased toward the slot's target, so the reels dive and
    // recover in step with the audio's own glide. Advanced once per frame.
    var angle by remember { mutableFloatStateOf(0f) }
    var displaySpeed by remember { mutableFloatStateOf(1f) }
    LaunchedEffect(slot.index) {
        while (true) {
            withFrameNanos { }
            val target = slot.float("speed", 1f).coerceIn(0.25f, 1f)
            displaySpeed += (target - displaySpeed) * 0.10f
            angle = (angle + displaySpeed * DEG_PER_FRAME) % 360f
        }
    }

    fun setSpeed(v: Float) { slot.set("speed", v.coerceIn(0.25f, 1f)); vm.bumpForToolbar() }

    Column(
        Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Box(
            Modifier
                .fillMaxWidth()
                .height(180.dp)
                .background(c.surface)
                // Press a reel to brake the tape; release to let it spin back up.
                .pointerInput(slot.index) {
                    detectTapGestures(
                        onPress = { off ->
                            if (overReel(off, size.width.toFloat(), size.height.toFloat())) {
                                setSpeed(BRAKE_SPEED)
                                tryAwaitRelease()
                                setSpeed(1f)
                            }
                        },
                    )
                },
        ) {
            Canvas(Modifier.fillMaxWidth().height(180.dp)) {
                val cx0 = size.width * REEL_L_X
                val cx1 = size.width * REEL_R_X
                val cy = size.height * REEL_CY
                val r = min(size.height * 0.30f, size.width * 0.20f)

                // Playhead block between + below the reels.
                val hw = (cx1 - cx0) * 0.46f
                val hh = size.height * 0.13f
                val hx = size.width * 0.5f
                val hy = cy + r + size.height * 0.14f
                val tl = Offset(hx - hw / 2f, hy - hh / 2f)
                val tr = Offset(hx + hw / 2f, hy - hh / 2f)

                // Tensioned tape: from each reel's bottom straight down to the playhead
                // and across its top. No line over the top of the reels.
                val p0 = Offset(cx0, cy + r)
                val p1 = Offset(cx1, cy + r)
                val tape = c.textDim
                drawLine(tape, p0, tl, strokeWidth = 2.5f)
                drawLine(tape, tl, tr, strokeWidth = 2.5f)
                drawLine(tape, tr, p1, strokeWidth = 2.5f)

                // Playhead (rounded rectangle) drawn over the tape it tensions.
                drawRoundRect(
                    color = c.grid, topLeft = Offset(hx - hw / 2f, hy - hh / 2f),
                    size = Size(hw, hh), cornerRadius = CornerRadius(hh * 0.45f, hh * 0.45f),
                )
                drawRoundRect(
                    color = c.accent, topLeft = Offset(hx - hw / 2f, hy - hh / 2f),
                    size = Size(hw, hh), cornerRadius = CornerRadius(hh * 0.45f, hh * 0.45f),
                    style = Stroke(width = 2f),
                )
                // Head gap slit in the middle of the playhead.
                drawLine(c.accent, Offset(hx, hy - hh / 2f + 3f), Offset(hx, hy + hh / 2f - 3f), strokeWidth = 2f)

                drawReel(cx0, cy, r, angle, c.accent, c.grid, c.surface, c.textDim)
                drawReel(cx1, cy, r, angle, c.accent, c.grid, c.surface, c.textDim)
            }
        }
        Text(
            "Hold a reel to brake the tape — pitch dives, then springs back.",
            color = c.textDim, fontFamily = mono, fontSize = 11.sp,
            textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(),
        )

        // Vintage round knobs, all in a row (each weighted so all four fit).
        val divIdx = slot.float("division", 2f).toInt().coerceIn(0, TimeDivision.entries.lastIndex)
        Row(
            Modifier.fillMaxWidth().padding(top = 2.dp),
            horizontalArrangement = Arrangement.spacedBy(6.dp),
            verticalAlignment = Alignment.Top,
        ) {
            PercentKnob(vm, slot, "wow", "WOW", 0.15f, Modifier.weight(1f))
            PercentKnob(vm, slot, "flutter", "FLUTTER", 0.15f, Modifier.weight(1f))
            PercentKnob(vm, slot, "hiss", "HISS", 0.10f, Modifier.weight(1f))
            Knob(
                label = "WOW SYNC",
                value = divIdx.toFloat(), min = 0f, max = TimeDivision.entries.lastIndex.toFloat(),
                valueText = TimeDivision.fromIndex(divIdx).label, default = 2f,
                modifier = Modifier.weight(1f), knobSize = KNOB_SIZE,
                onChange = { v ->
                    slot.set("division", v.roundToInt().coerceIn(0, TimeDivision.entries.lastIndex).toFloat())
                    vm.bumpForToolbar()
                },
            )
        }
    }
}

/** A 0..1 knob showing its value as a percentage, writing [key] on the slot. */
@Composable
private fun PercentKnob(vm: AppViewModel, slot: ToolboxSlot, key: String, label: String, default: Float, modifier: Modifier) {
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision
    val value = slot.float(key, default).coerceIn(0f, 1f)
    Knob(
        label = label, value = value, min = 0f, max = 1f,
        valueText = "${(value * 100).roundToInt()}%", default = default,
        modifier = modifier, knobSize = KNOB_SIZE,
        onChange = { slot.set(key, it.coerceIn(0f, 1f)); vm.bumpForToolbar() },
    )
}

/** True if [off] is within either reel's circle (used to gate the brake gesture). */
private fun overReel(off: Offset, w: Float, h: Float): Boolean {
    val cy = h * REEL_CY
    val r = min(h * 0.30f, w * 0.20f)
    return hypot(off.x - w * REEL_L_X, off.y - cy) <= r ||
        hypot(off.x - w * REEL_R_X, off.y - cy) <= r
}

/** Draw one NAB reel: metal flange, three rotating rounded-triangular bobbin holes,
 *  and the hub in the centre. */
private fun DrawScope.drawReel(
    cx: Float, cy: Float, r: Float, angle: Float,
    accent: Color, flange: Color, hole: Color, rim: Color,
) {
    val center = Offset(cx, cy)
    drawCircle(flange, radius = r, center = center)                              // metal flange
    for (k in 0 until 3) drawPath(bobbinHole(cx, cy, r, angle + k * 120f), hole) // trapezoidal cutouts
    drawCircle(hole, radius = r * 0.26f, center = center)                        // bobbin centre (open)
    drawCircle(accent, radius = r * 0.26f, center = center, style = Stroke(width = 3f))
    drawCircle(accent, radius = r * 0.14f, center = center, style = Stroke(width = 2f))
    drawCircle(rim, radius = r, center = center, style = Stroke(width = 2.5f))   // flange edge
}

/** One rounded-trapezoidal bobbin hole at [thetaDeg] around the reel: wide toward the
 *  hub, narrow toward the rim (the narrow side faces outward). */
private fun bobbinHole(cx: Float, cy: Float, r: Float, thetaDeg: Float): Path {
    val rIn = r * 0.40f; val rOut = r * 0.86f
    val inner = 44f     // angular half-spread at the hub (wide side)
    val outer = 13f     // angular half-spread at the rim (narrow side)
    val innerL = polar(cx, cy, rIn, thetaDeg - inner)
    val outerL = polar(cx, cy, rOut, thetaDeg - outer)
    val outerR = polar(cx, cy, rOut, thetaDeg + outer)
    val innerR = polar(cx, cy, rIn, thetaDeg + inner)
    return roundedPoly(arrayOf(innerL, outerL, outerR, innerR), r * 0.09f)
}

/** A closed polygon through [v] with every corner rounded to radius [corner]. */
private fun roundedPoly(v: Array<Offset>, corner: Float): Path {
    val n = v.size
    val path = Path()
    for (i in 0 until n) {
        val curr = v[i]
        val prev = v[(i + n - 1) % n]
        val next = v[(i + 1) % n]
        val a = curr + toward(curr, prev, corner) // approach point on the incoming edge
        val b = curr + toward(curr, next, corner) // leave point on the outgoing edge
        if (i == 0) path.moveTo(a.x, a.y) else path.lineTo(a.x, a.y)
        path.quadraticBezierTo(curr.x, curr.y, b.x, b.y)
    }
    path.close()
    return path
}

/** Vector of length [len] from [from] toward [to]. */
private fun toward(from: Offset, to: Offset, len: Float): Offset {
    val dx = to.x - from.x; val dy = to.y - from.y
    val d = hypot(dx, dy).coerceAtLeast(1e-3f)
    return Offset(dx / d * len, dy / d * len)
}

private fun polar(cx: Float, cy: Float, rad: Float, deg: Float): Offset {
    val a = Math.toRadians(deg.toDouble())
    return Offset(cx + rad * cos(a).toFloat(), cy + rad * sin(a).toFloat())
}

private val KNOB_SIZE = 58.dp         // a touch bigger than the default 44 dp knob
private const val DEG_PER_FRAME = 6f  // ~1 rev/sec at 60 fps and full speed
private const val BRAKE_SPEED = 0.5f  // target while a reel is held (≈ one octave down)
private const val REEL_L_X = 0.30f
private const val REEL_R_X = 0.70f
private const val REEL_CY = 0.38f     // reel centre as a fraction of the canvas height
