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

package space.rcmd.android.sizzle.ui.tracker

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.horizontalScroll
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import space.rcmd.android.sizzle.model.Arrangement as ArrModel
import space.rcmd.android.sizzle.model.LoopRegion
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.components.GlyphCache
import space.rcmd.android.sizzle.ui.components.RepeatButton
import space.rcmd.android.sizzle.ui.components.RetroButton
import space.rcmd.android.sizzle.ui.theme.LocalRetro

/**
 * The lower-half arrangement. Layout (spreadsheet-style, like the sizzletracker
 * CLI): a fixed left column of lane numbers, a bar ruler across the top, and the
 * scrollable roll body. The ruler and body share one horizontal scroll so they
 * stay aligned; the roll body is drawn on a single [Canvas] for performance.
 *
 * Each lane is tied to its own tracker block (lane i ⟷ block i). Cells are NOT
 * toggled by tapping; instead you SELECT cells (tap a single cell, or long-press
 * and drag to sweep a rectangle) and then use the FILL / CLEAR toggle in the loop
 * toolbar to enable or disable the whole selection at once. When the playhead
 * crosses an enabled cell, the sequencer plays that block. Tap a lane number to
 * edit that block in the tracker above.
 *
 * The ruler also shows the A and B loop regions as coloured bands; the A/B
 * buttons in the toolbar assign the current selection to a region, and tapping a
 * button that is already highlighted removes that region (and its band).
 */
@Composable
fun ArrangementRoll(vm: AppViewModel) {
    val c = LocalRetro.current
    // Composition read: structural edits bump revision (playback does not), so the
    // lane column / ruler recompose on edits but not per beat. The roll Canvas also
    // reads vm.revision in its draw phase (below) so cell edits redraw it directly.
    val redraw = vm.revision
    @Suppress("UNUSED_EXPRESSION") redraw
    // Held as State and read ONLY in the draw phase + the follow-scroll collector
    // below, never in composition, so a playhead advance invalidates the roll draw
    // alone — no recomposition.
    val transportState = vm.transport.collectAsState()
    val arr = vm.project.arrangement
    val sig = vm.project.timeSignature
    // Canvas width in beats for the active signature (the roll spans arr.lengthBars
    // bars). Recomputed on signature change; captured by the pointer handlers below,
    // which are keyed on (arr, sig) so they see a consistent value.
    val lengthBeats = arr.canvasBeats(sig)
    val scroll = rememberScrollState()
    // Width of the scrollable roll viewport (px), measured below; used to keep the
    // playhead centred once it reaches the middle of the view.
    var viewportWidthPx by remember { mutableIntStateOf(0) }
    val density = LocalDensity.current
    val measurer = rememberTextMeasurer()
    val glyphs = remember(measurer) { GlyphCache(measurer) }

    val beatWidth = 20.dp
    val muteColWidth = 34.dp // wide enough to hit comfortably from the screen edge
    val laneColWidth = 68.dp // mute toggle + block number
    val rulerHeight = 18.dp
    val beatWidthPx = with(density) { beatWidth.toPx() }
    val rulerHeightPx = with(density) { rulerHeight.toPx() }

    // Rectangular cell selection: anchor (…0) + live corner (…1). Beat < 0 = none.
    // Unlike before, selecting does NOT change any cell; the toolbar toggle acts
    // on the selection. Read in composition (for the toolbar's FILL/CLEAR label)
    // and captured by the Canvas draw below.
    var selLane0 by remember { mutableIntStateOf(-1) }
    var selBeat0 by remember { mutableIntStateOf(-1) }
    var selLane1 by remember { mutableIntStateOf(-1) }
    var selBeat1 by remember { mutableIntStateOf(-1) }

    val hasSelection = selBeat0 >= 0
    val laneRange = if (hasSelection) minOf(selLane0, selLane1)..maxOf(selLane0, selLane1) else IntRange.EMPTY
    val beatRange = if (hasSelection) minOf(selBeat0, selBeat1)..maxOf(selBeat0, selBeat1) else IntRange.EMPTY
    // Whether every cell in the selection is already filled (drives FILL vs CLEAR).
    val selectionAllFilled = hasSelection &&
        laneRange.all { lane -> beatRange.all { beat -> arr.patternAt(lane, beat) != ArrModel.EMPTY } }

    val rulerStyle = remember(c) { TextStyle(color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 9.sp) }
    val regionAStyle = remember(c) { TextStyle(color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 9.sp) }
    val regionBStyle = remember(c) { TextStyle(color = c.playhead, fontFamily = FontFamily.Monospace, fontSize = 9.sp) }

    // Follow the playhead by scrolling horizontally. Collects the transport flow
    // directly rather than keying a LaunchedEffect on a composition read of the beat,
    // so following the playhead never triggers recomposition. De-duped by beat so we
    // only animate when the beat actually changes.
    LaunchedEffect(scroll, beatWidthPx) {
        var lastBeat = -1
        vm.transport.collect { t ->
            if (t.isPlaying && t.currentBeat != lastBeat) {
                lastBeat = t.currentBeat
                // Keep the playhead at the middle of the view: with the (coerced) 0
                // floor this means the roll stays put until the playhead reaches the
                // centre (~the 3rd bar), then follows it centred. Falls back to a
                // fixed offset until the viewport width has been measured.
                val half = if (viewportWidthPx > 0) viewportWidthPx / 2f else 200f
                val target = (t.currentBeat * beatWidthPx - half).toInt()
                    .coerceIn(0, scroll.maxValue)
                scroll.animateScrollTo(target)
            }
        }
    }

    // Enable or disable every selected cell. A mixed/empty selection fills; a
    // fully-filled selection clears — so the toolbar button is a true toggle.
    fun toggleSelectedCells() {
        if (!hasSelection) return
        val clear = selectionAllFilled
        for (lane in laneRange) for (beat in beatRange) {
            arr.set(lane, beat, if (clear) ArrModel.EMPTY else lane)
        }
        vm.bumpForToolbar()
    }

    fun assignRegion(region: LoopRegion) {
        if (!hasSelection) return
        region.start = beatRange.first
        region.end = beatRange.last + 1
        region.enabled = true
        vm.bumpForToolbar()
    }

    fun clearRegion(region: LoopRegion) {
        region.enabled = false
        region.start = 0
        region.end = 0
        vm.bumpForToolbar()
    }

    Column(Modifier.fillMaxSize().background(c.background)) {
        Box(Modifier.weight(1f).fillMaxWidth()) {
            Row(Modifier.fillMaxSize()) {
                // ---- Fixed left column: corner + per-lane [mute | block number] ----
                Column(Modifier.width(laneColWidth).fillMaxHeight()) {
                    Box(Modifier.height(rulerHeight).fillMaxWidth().background(c.surface),
                        contentAlignment = Alignment.Center) {
                        Text("BLK", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 8.sp)
                    }
                    for (lane in 0 until ArrModel.LANE_COUNT) {
                        val active = vm.activeBlock == lane
                        val muted = arr.laneMuted[lane]
                        Row(Modifier.weight(1f).fillMaxWidth()) {
                            // Mute toggle (left of the block header): mutes this lane.
                            Box(
                                Modifier
                                    .width(muteColWidth)
                                    .fillMaxHeight()
                                    .background(if (muted) c.danger.copy(alpha = 0.30f) else c.surface)
                                    .clickable { vm.toggleArrangementLaneMute(lane) },
                                contentAlignment = Alignment.Center,
                            ) {
                                Text("M", color = if (muted) c.danger else c.textDim,
                                    fontFamily = FontFamily.Monospace, fontSize = 9.sp)
                            }
                            // Block number: tap to edit that block in the tracker above.
                            Box(
                                Modifier
                                    .weight(1f)
                                    .fillMaxHeight()
                                    .background(if (active) c.accent.copy(alpha = 0.25f) else c.surface)
                                    .clickable { vm.setActiveBlock(lane) },
                                contentAlignment = Alignment.Center,
                            ) {
                                Text("${lane + 1}", color = if (active) c.accent else c.textDim,
                                    fontFamily = FontFamily.Monospace, fontSize = 11.sp)
                            }
                        }
                    }
                }

                // ---- Scrollable ruler + roll (one Canvas) ----
                Box(
                    Modifier.weight(1f).fillMaxHeight()
                        .onSizeChanged { viewportWidthPx = it.width }
                        .horizontalScroll(scroll),
                ) {
                    Canvas(
                        Modifier
                            .width(beatWidth * lengthBeats)
                            .fillMaxHeight()
                            // Tap selects a single cell (no toggle). The ruler is
                            // display-only, so ruler taps are ignored.
                            .pointerInput(arr, sig) {
                                detectTapGestures { off ->
                                    if (off.y < rulerHeightPx) return@detectTapGestures
                                    val laneH = (size.height - rulerHeightPx) / ArrModel.LANE_COUNT
                                    val lane = ((off.y - rulerHeightPx) / laneH).toInt()
                                        .coerceIn(0, ArrModel.LANE_COUNT - 1)
                                    val beat = (off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
                                    selLane0 = lane; selLane1 = lane
                                    selBeat0 = beat; selBeat1 = beat
                                }
                            }
                            // Long-press then drag sweeps out a rectangular selection.
                            .pointerInput(arr, sig) {
                                fun laneAt(off: Offset): Int {
                                    val laneH = (size.height - rulerHeightPx) / ArrModel.LANE_COUNT
                                    return ((off.y - rulerHeightPx) / laneH).toInt()
                                        .coerceIn(0, ArrModel.LANE_COUNT - 1)
                                }
                                fun beatAt(off: Offset): Int =
                                    (off.x / beatWidthPx).toInt().coerceIn(0, lengthBeats - 1)
                                detectDragGesturesAfterLongPress(
                                    onDragStart = { off ->
                                        selLane0 = laneAt(off); selLane1 = selLane0
                                        selBeat0 = beatAt(off); selBeat1 = selBeat0
                                    },
                                    onDrag = { change, _ ->
                                        selLane1 = laneAt(change.position)
                                        selBeat1 = beatAt(change.position)
                                    },
                                )
                            },
                    ) {
                        // Subscribe the draw phase to edit revisions (live state read,
                        // not the captured `redraw` Int) so toggling a cell redraws the
                        // roll even when no other draw-read state changed.
                        @Suppress("UNUSED_EXPRESSION") vm.revision
                        // Draw-phase read: a playhead advance invalidates this draw
                        // only, never composition.
                        val transport = transportState.value
                        val w = size.width
                        val h = size.height
                        val laneH = (h - rulerHeightPx) / ArrModel.LANE_COUNT
                        val beatsPerBar = sig.beatsPerBar

                        // Viewport culling: with the canvas up to 256 bars wide, only
                        // iterate the beats currently scrolled into view (plus a small
                        // margin) instead of all of them. Reading scroll.value here ties
                        // the draw to scrolling, so the visible slice redraws as it moves.
                        val scrollPx = scroll.value.toFloat()
                        val visW = if (viewportWidthPx > 0) viewportWidthPx.toFloat() else w
                        val firstBeat = (scrollPx / beatWidthPx).toInt().coerceIn(0, (lengthBeats - 1).coerceAtLeast(0))
                        val lastBeat = (((scrollPx + visW) / beatWidthPx).toInt() + 2).coerceIn(1, lengthBeats)

                        // Ruler background + per-bar labels/ticks.
                        drawRect(c.surface, Offset(0f, 0f), Size(w, rulerHeightPx))
                        for (beat in firstBeat until lastBeat) {
                            val x = beat * beatWidthPx
                            if (beat % beatsPerBar == 0) {
                                drawLine(c.grid, Offset(x, 0f), Offset(x, h), strokeWidth = 1f)
                                val bar = beat / beatsPerBar + 1
                                val layout = glyphs.measure("$bar", rulerStyle)
                                drawText(layout, topLeft = Offset(x + 2f, (rulerHeightPx - layout.size.height) / 2f))
                            }
                        }

                        // A / B loop-region bands on the ruler (drawn over the ticks).
                        fun drawRegionBand(region: LoopRegion, color: androidx.compose.ui.graphics.Color, letter: String, style: TextStyle) {
                            if (!region.isValid) return
                            val x0 = region.start * beatWidthPx
                            val x1 = region.end * beatWidthPx
                            drawRect(color.copy(alpha = 0.25f), Offset(x0, 0f), Size(x1 - x0, rulerHeightPx))
                            drawLine(color, Offset(x0, 0f), Offset(x1, 0f), strokeWidth = 2f)
                            val layout = glyphs.measure(letter, style)
                            // If the region starts on a bar boundary a bar number sits
                            // at x0; nudge the A/B letter past its (1- or 2-digit) width
                            // plus a small gap so they never overlap.
                            val letterX = if (region.start % beatsPerBar == 0) {
                                val barNum = region.start / beatsPerBar + 1
                                x0 + 2f + glyphs.measure("$barNum", rulerStyle).size.width + 3f
                            } else {
                                x0 + 2f
                            }
                            drawText(layout, topLeft = Offset(letterX, (rulerHeightPx - layout.size.height) / 2f))
                        }
                        drawRegionBand(arr.regionA, c.accent, "A", regionAStyle)
                        drawRegionBand(arr.regionB, c.playhead, "B", regionBStyle)

                        // Lane cells.
                        for (lane in 0 until ArrModel.LANE_COUNT) {
                            val laneY = rulerHeightPx + lane * laneH
                            for (beat in firstBeat until lastBeat) {
                                val x = beat * beatWidthPx
                                val filled = arr.patternAt(lane, beat) != ArrModel.EMPTY
                                val onPlayhead = transport.isPlaying && beat == transport.currentBeat
                                val inSel = selBeat0 >= 0 && lane in laneRange && beat in beatRange
                                val fill = when {
                                    filled -> c.accent.copy(alpha = 0.85f)
                                    onPlayhead -> c.playhead.copy(alpha = 0.3f)
                                    inSel -> c.bar
                                    beat % beatsPerBar == 0 -> c.surface
                                    else -> c.grid.copy(alpha = 0.35f)
                                }
                                drawRect(fill, Offset(x + 0.5f, laneY + 0.5f), Size(beatWidthPx - 1f, laneH - 1f))
                            }
                            // Lane separator.
                            drawLine(c.background, Offset(0f, laneY), Offset(w, laneY), strokeWidth = 1f)
                        }

                        // Selection outline (rectangle across the selected lanes/beats).
                        if (selBeat0 >= 0) {
                            val bx0 = beatRange.first * beatWidthPx
                            val bx1 = (beatRange.last + 1) * beatWidthPx
                            val ly0 = rulerHeightPx + laneRange.first * laneH
                            val ly1 = rulerHeightPx + (laneRange.last + 1) * laneH
                            drawRect(
                                c.accent,
                                topLeft = Offset(bx0, ly0),
                                size = Size(bx1 - bx0, ly1 - ly0),
                                style = Stroke(width = 2f),
                            )
                        }

                        // Playhead marker across the whole roll.
                        if (transport.isPlaying) {
                            val px = transport.currentBeat * beatWidthPx
                            drawLine(c.playhead, Offset(px, 0f), Offset(px, h), strokeWidth = 2f)
                        }
                    }
                }
            }
        }

        LoopToolbar(
            vm = vm,
            arr = arr,
            hasSelection = hasSelection,
            selectionAllFilled = selectionAllFilled,
            onToggleCells = { toggleSelectedCells() },
            onTapA = { if (arr.regionA.isValid) clearRegion(arr.regionA) else assignRegion(arr.regionA) },
            onTapB = { if (arr.regionB.isValid) clearRegion(arr.regionB) else assignRegion(arr.regionB) },
        )
    }
}

@Composable
private fun LoopToolbar(
    vm: AppViewModel,
    arr: ArrModel,
    hasSelection: Boolean,
    selectionAllFilled: Boolean,
    onToggleCells: () -> Unit,
    onTapA: () -> Unit,
    onTapB: () -> Unit,
) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision
    Row(
        Modifier.fillMaxWidth().background(c.surface).horizontalScroll(rememberScrollState()).padding(6.dp),
        horizontalArrangement = Arrangement.spacedBy(6.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        RetroButton("LOOP", active = arr.loopEnabled) {
            arr.loopEnabled = !arr.loopEnabled; vm.bumpForToolbar()
        }
        // Fill/clear the current cell selection (replaces tap-to-toggle on cells).
        // Fixed width so the button doesn't resize as the label toggles FILL/CLEAR.
        RetroButton(
            if (selectionAllFilled) "CLEAR" else "FILL",
            active = hasSelection, width = 72.dp, onClick = onToggleCells,
        )
        RegionControl("A", arr.regionA, onTap = onTapA, vm = vm)
        RegionControl("B", arr.regionB, onTap = onTapB, vm = vm)
    }
}

@Composable
private fun RegionControl(name: String, region: LoopRegion, onTap: () -> Unit, vm: AppViewModel) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision
    Row(verticalAlignment = Alignment.CenterVertically) {
        // Highlighted when the region is set; tapping a highlighted button removes
        // the region, otherwise it assigns the current selection to it.
        RetroButton(name, active = region.isValid, onClick = onTap)
        // Repeat count updates live during playback (no transport reset).
        RepeatButton("-") { vm.changeRegionRepeats(region, -1) }
        Text(" x${region.repeats} ", color = c.text, fontFamily = FontFamily.Monospace, fontSize = 11.sp)
        RepeatButton("+") { vm.changeRegionRepeats(region, +1) }
    }
}
