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

package space.rcmd.android.sizzle.ui.mixer

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableFloatState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
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.Mixer
import space.rcmd.android.sizzle.model.MixerChannel
import space.rcmd.android.sizzle.model.Pattern
import space.rcmd.android.sizzle.model.ToolboxKind
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.isLandscape
import space.rcmd.android.sizzle.ui.components.RepeatButton
import space.rcmd.android.sizzle.ui.components.RetroButton
import space.rcmd.android.sizzle.ui.components.RetroDropdown
import space.rcmd.android.sizzle.ui.components.SectionLabel
import space.rcmd.android.sizzle.ui.theme.LocalRetro

/**
 * The second tab: a four-channel mixer, one strip per tracker track. Each strip
 * chooses an instrument + up to four effects (all drawn from the toolbox), sets a
 * MIDI channel, and has a volume fader plus mute/solo. Everything writes straight
 * into the shared [Mixer] model that the audio engine reads.
 */
@Composable
fun MixerScreen(vm: AppViewModel) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val redraw = vm.revision

    // Live per-channel VU state, polled from the audio engine once per frame. Held in
    // stable state objects so only the fader's draw phase (which reads them) repaints —
    // the strips themselves never recompose from meter movement. Writing an unchanged
    // value is a no-op, so an idle (silent) mixer costs nothing but the frame tick.
    // The whole poll loop is skipped when VU meters are turned off in Settings.
    val vuOn = vm.vuMeters
    val levels = remember { List(Pattern.TRACK_COUNT) { mutableFloatStateOf(0f) } }
    val clips = remember { List(Pattern.TRACK_COUNT) { mutableStateOf(false) } }
    LaunchedEffect(vuOn) {
        if (!vuOn) {
            // Clear any lingering values so nothing shows if re-enabled while silent.
            for (ch in 0 until Pattern.TRACK_COUNT) { levels[ch].floatValue = 0f; clips[ch].value = false }
            return@LaunchedEffect
        }
        while (true) {
            withFrameNanos { }
            for (ch in 0 until Pattern.TRACK_COUNT) {
                levels[ch].floatValue = vm.channelMeter(ch)
                clips[ch].value = vm.channelClipped(ch)
            }
        }
    }

    Column(Modifier.fillMaxSize().background(c.background)) {
        RecordToolbar(vm)
        Row(
            Modifier.weight(1f).fillMaxWidth().padding(6.dp),
            horizontalArrangement = Arrangement.spacedBy(6.dp),
        ) {
            vm.project.mixer.channels.forEach { channel ->
                ChannelStrip(vm, channel, levels[channel.index], clips[channel.index], vuOn,
                    Modifier.weight(1f).fillMaxHeight())
            }
        }
    }
}

/**
 * Master-bus recorder controls at the top of the mixer: arm, FX-tail length, and
 * the output folder. Recording begins when playback starts and stops automatically
 * a tail (in bars) after the sequencer stops — a pause/stop, or the arrangement end
 * with looping off. WAV files are written to the chosen folder with a date-time name.
 */
@Composable
private fun RecordToolbar(vm: AppViewModel) {
    val c = LocalRetro.current
    val dirPicker = rememberLauncherForActivityResult(
        ActivityResultContracts.OpenDocumentTree(),
    ) { uri -> uri?.let { vm.setRecordDir(it) } }

    Row(
        Modifier
            .fillMaxWidth()
            .background(c.surface)
            .padding(6.dp)
            .horizontalScroll(rememberScrollState()),
        horizontalArrangement = Arrangement.spacedBy(6.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        val recording = vm.masterRecording
        // Arm/record. With no folder yet, the first tap opens the picker instead.
        RetroButton(
            if (recording) "● REC" else "● ARM",
            active = vm.recordArmed && !recording,
            danger = recording,
            onClick = { if (vm.recordDirUri == null) dirPicker.launch(null) else vm.toggleRecordArm() },
        )
        RetroDropdown(
            label = "TAIL",
            options = listOf(0, 1, 2, 3, 4),
            selected = vm.recordTailBars,
            optionLabel = { "$it bar" + if (it == 1) "" else "s" },
            onSelect = { vm.updateRecordTailBars(it) },
        )
        RetroButton(
            "DIR:" + (vm.recordDirName ?: "—"),
            onClick = { dirPicker.launch(null) },
        )
        val status = when {
            recording -> "recording…"
            vm.recordDirUri == null -> "pick a folder to record"
            vm.recordArmed -> "armed — starts on play"
            vm.lastRecordingName != null -> "saved ${vm.lastRecordingName}"
            else -> "master → WAV"
        }
        Text(
            status, color = c.textDim, fontFamily = FontFamily.Monospace,
            fontSize = 11.sp, maxLines = 1,
        )
    }
}

@Composable
private fun ChannelStrip(
    vm: AppViewModel,
    channel: MixerChannel,
    level: MutableFloatState,
    clipped: State<Boolean>,
    showVu: Boolean,
    modifier: Modifier,
) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so faders/mute/solo/routing refresh

    Column(
        modifier.background(c.surface).padding(6.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Text("CH ${channel.index + 1}", color = c.accent,
            fontFamily = FontFamily.Monospace, fontSize = 13.sp)

        if (isLandscape()) {
            // Landscape: two columns per bus — routing on the left, level controls on
            // the right — so the fader + LIM/mute/solo are visible without scrolling
            // in the short height. The routing column scrolls if a device is extra
            // short; the level column never does.
            Row(Modifier.weight(1f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
                Column(
                    Modifier.weight(1f).fillMaxHeight().verticalScroll(rememberScrollState()),
                    verticalArrangement = Arrangement.spacedBy(4.dp),
                ) { ChannelRouting(vm, channel) }
                Column(
                    Modifier.weight(1f).fillMaxHeight(),
                    verticalArrangement = Arrangement.spacedBy(4.dp),
                ) { ChannelLevels(vm, channel, level, clipped, showVu) }
            }
        } else {
            // Portrait: one column, the fader filling the leftover height.
            ChannelRouting(vm, channel)
            ChannelLevels(vm, channel, level, clipped, showVu)
        }
    }
}

/** Instrument + FX + MIDI-channel selectors for a mixer [channel]. */
@Composable
private fun ColumnScope.ChannelRouting(vm: AppViewModel, channel: MixerChannel) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision // defeat strong-skipping so routing refreshes
    val toolbox = vm.project.toolbox
    val instrumentSlots = toolbox.filter { it.type?.kind == ToolboxKind.INSTRUMENT }
    val effectSlots = toolbox.filter { it.type?.kind == ToolboxKind.EFFECT }

    SectionLabel("Instrument")
    RetroDropdown(
        label = "",
        options = listOf(-1) + instrumentSlots.map { it.index },
        selected = channel.instrumentSlot,
        optionLabel = { idx -> if (idx < 0) "—" else "${idx + 1}:${toolbox[idx].name}" },
        onSelect = { channel.instrumentSlot = it; vm.bumpForToolbar() },
        modifier = Modifier.fillMaxWidth(),
    )

    SectionLabel("FX 1-4")
    for (fx in 0 until Mixer.FX_SLOTS) {
        RetroDropdown(
            label = "",
            options = listOf(-1) + effectSlots.map { it.index },
            selected = channel.fxSlots[fx],
            optionLabel = { idx -> if (idx < 0) "—" else "${idx + 1}:${toolbox[idx].name}" },
            onSelect = { channel.fxSlots[fx] = it; vm.rebuildAudioRouting() },
            modifier = Modifier.fillMaxWidth(),
        )
    }

    SectionLabel("MIDI Ch")
    Row(verticalAlignment = Alignment.CenterVertically) {
        RepeatButton("-") { channel.midiChannel = (channel.midiChannel - 1).coerceAtLeast(0); vm.bumpForToolbar() }
        Text(
            "${channel.midiChannel}", color = c.text,
            fontFamily = FontFamily.Monospace, fontSize = 12.sp,
            textAlign = TextAlign.Center, maxLines = 1,
            modifier = Modifier.width(28.dp),
        )
        RepeatButton("+") { channel.midiChannel = (channel.midiChannel + 1).coerceAtMost(16); vm.bumpForToolbar() }
    }
}

/** Volume fader + limiter + mute/solo for a mixer [channel]. The fader fills the
 *  leftover height of its column. */
@Composable
private fun ColumnScope.ChannelLevels(
    vm: AppViewModel,
    channel: MixerChannel,
    level: MutableFloatState,
    clipped: State<Boolean>,
    showVu: Boolean,
) {
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision // defeat strong-skipping so LIM/mute/solo refresh
    SectionLabel("Volume")
    VerticalFader(
        value = channel.volume,
        default = Mixer.DEFAULT_VOLUME,
        onValueChange = { channel.volume = it; vm.bumpForToolbar() },
        level = level,
        clipped = clipped,
        showVu = showVu,
        modifier = Modifier.weight(1f).fillMaxWidth(),
    )
    // Per-bus soft-clip limiter, sitting above mute/solo.
    RetroButton("LIM", active = channel.limiter, modifier = Modifier.fillMaxWidth()) {
        channel.limiter = !channel.limiter; vm.bumpForToolbar()
    }
    Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
        RetroButton("M", active = channel.mute, modifier = Modifier.weight(1f)) {
            channel.mute = !channel.mute; vm.bumpForToolbar()
        }
        RetroButton("S", active = channel.solo, modifier = Modifier.weight(1f)) {
            channel.solo = !channel.solo; vm.bumpForToolbar()
        }
    }
}

/** A vertical volume fader with a live VU meter drawn inside its filled area: fill
 *  grows from the bottom to the set [value]; tap or drag to set. The VU ([level], a
 *  peak amplitude polled each frame) rises from the bottom within the fill, green →
 *  amber as it gets hotter, and the whole meter turns red while the bus is [clipped]
 *  (past 0 dBFS). Snaps to the [default] level within a small band so returning to the
 *  standard volume is easy to hit.
 *
 *  Everything is painted in a single [drawBehind]; [level]/[clipped] are read in the
 *  draw phase, so meter movement repaints only this fader — no recomposition. */
@Composable
private fun VerticalFader(
    value: Float,
    default: Float,
    onValueChange: (Float) -> Unit,
    level: MutableFloatState,
    clipped: State<Boolean>,
    showVu: Boolean,
    modifier: Modifier,
) {
    val c = LocalRetro.current
    val volFill = c.accent.copy(alpha = 0.30f)
    val vuLo = c.playhead          // green — nominal level
    val vuHi = c.accent            // amber — getting hot
    val vuClip = c.danger          // red — clipping
    Box(
        modifier
            .border(1.dp, c.grid, RectangleShape)
            .background(c.background)
            .drawBehind {
                val h = size.height
                val w = size.width
                val vol = value.coerceIn(0f, 1f)
                val volH = vol * h
                val volTop = h - volH
                // Volume fill (the set level), translucent.
                if (volH > 0f) drawRect(volFill, topLeft = Offset(0f, volTop), size = Size(w, volH))

                // VU meter, contained within the fill. Colour maps to ABSOLUTE level
                // over the full track height (bottom green → top red), so a taller bar
                // is visibly hotter; the gradient's red tip is what "hard clipping turns
                // to a red hue" looks like as the signal climbs. Skipped when the VU
                // meters are turned off in Settings.
                val lvl = if (showVu) level.floatValue.coerceIn(0f, 1f) else 0f
                val meterFrac = if (lvl < vol) lvl else vol // stay inside the filled area
                if (showVu && meterFrac > 0.001f) {
                    val mH = meterFrac * h
                    val top = h - mH
                    if (clipped.value) {
                        drawRect(vuClip, topLeft = Offset(0f, top), size = Size(w, mH))
                    } else {
                        val brush = Brush.verticalGradient(
                            0f to vuLo, 0.65f to vuLo, 0.85f to vuHi, 1f to vuClip,
                            startY = h, endY = 0f, // stop 0 = bottom, stop 1 = top of the track
                        )
                        drawRect(brush, topLeft = Offset(0f, top), size = Size(w, mH))
                    }
                }

                // Thumb line at the set level.
                val thumb = 3.dp.toPx()
                drawRect(c.accent, topLeft = Offset(0f, (volTop - thumb / 2f).coerceIn(0f, h - thumb)),
                    size = Size(w, thumb))
            }
            .pointerInput(default) {
                detectTapGestures { off -> onValueChange(snapDefault(1f - off.y / size.height, default)) }
            }
            .pointerInput(default) {
                detectDragGestures { change, _ ->
                    change.consume()
                    onValueChange(snapDefault(1f - change.position.y / size.height, default))
                }
            },
    )
}

/** Snap the fader to the [default] level when the raw value lands within a small
 *  band around it, so returning to the standard volume is easy. */
private fun snapDefault(raw: Float, default: Float): Float {
    val v = raw.coerceIn(0f, 1f)
    return if (kotlin.math.abs(v - default) < SNAP_BAND) default else v
}

private const val SNAP_BAND = 0.03f
