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

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

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
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.RectangleShape
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import space.rcmd.android.sizzle.audio.SampleStore
import space.rcmd.android.sizzle.model.Pitch
import space.rcmd.android.sizzle.model.SamplerPads
import space.rcmd.android.sizzle.model.ToolboxSlot
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.components.RetroButton
import space.rcmd.android.sizzle.ui.theme.LocalRetro
import kotlin.math.abs

/**
 * The Sampler's custom editor: a 16-pad drum-style bank plus a per-pad detail area.
 *
 * The bottom half is a 4x4 grid of pads mapped to consecutive notes from C2
 * ([SamplerPads]). Pressing a pad selects it AND auditions its sample the instant
 * it's touched (so pads are for both testing and picking which one to edit). The top
 * half acts on the selected pad: Import a WAV or Record to assign a sample, position
 * the two waveform markers to set the play slice, CLIP to destructively trim the
 * sample down to that slice, and CLEAR to empty just that pad (all no undo). Per-pad
 * volume lives in the fader bank. When the sequencer plays a pad's note the sample
 * fires at its natural pitch.
 */
@Composable
fun SamplerEditor(vm: AppViewModel, slot: ToolboxSlot) {
    val c = LocalRetro.current
    val context = LocalContext.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe: pads/waveform refresh

    var pad by remember(slot.index) { mutableIntStateOf(0) }
    var importError by remember(slot.index) { mutableStateOf<String?>(null) }
    val sampleId = slot.string(SamplerPads.key(pad))
    val sliceStart = slot.float(SamplerPads.sliceStartKey(pad), 0f)
    val sliceEnd = slot.float(SamplerPads.sliceEndKey(pad), 1f)

    // System file picker: assigns the chosen WAV to the currently-selected pad.
    // A read/decode failure reports a popup rather than silently doing nothing.
    val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
        if (uri == null) return@rememberLauncherForActivityResult
        val bytes = runCatching {
            context.contentResolver.openInputStream(uri)?.use { readCapped(it) }
        }.getOrNull()
        when {
            bytes == null -> importError =
                "Couldn't read the file, or it's larger than ${MAX_IMPORT_BYTES / (1024 * 1024)} MB."
            !vm.loadSampleInto(slot, pad, uri.toString(), bytes) -> importError =
                "Couldn't load this sample. Only uncompressed WAV files (8/16/24-bit " +
                    "PCM or 32-bit float) are supported."
        }
    }

    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        // ----- Selected pad + source buttons (act on the selected pad) -----
        Text(
            "Pad ${pad + 1} · ${Pitch.name(SamplerPads.noteFor(pad))}",
            color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 13.sp,
        )
        Row(
            Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            RetroButton("IMPORT WAV") { picker.launch(arrayOf("audio/*")) }
            if (vm.isRecording) {
                RetroButton("■ STOP REC", active = true) { vm.stopRecordingInto(slot, pad) }
            } else {
                RetroButton("● RECORD") { vm.startRecording() }
            }
            if (sampleId.isNotBlank()) {
                // Destructively trim the sample to the current slice markers (no undo).
                RetroButton("✂ CLIP") { vm.trimSamplerPad(slot, pad) }
                // Peak-normalize the sample to full scale (no undo).
                RetroButton("NORMALIZE") { vm.normalizeSamplerPad(slot, pad) }
                // Clear just the selected pad's sample (replaces the old long-press).
                RetroButton("CLEAR", danger = true) { vm.clearSamplerPad(slot, pad) }
            }
        }

        // ----- Waveform + draggable slice markers for the selected pad -----
        val sample = SampleStore.get(sampleId)
        val peaks = remember(sampleId, sample?.data?.size) { sample?.let { computePeaks(it.data, 400) } }
        var dragging by remember { mutableIntStateOf(-1) }
        Canvas(
            Modifier
                .fillMaxWidth()
                .height(110.dp)
                .background(c.surface)
                .pointerInput(sampleId, pad) {
                    detectDragGestures(
                        onDragStart = { off ->
                            val frac = (off.x / size.width).coerceIn(0f, 1f)
                            dragging = if (abs(frac - sliceStart) <= abs(frac - sliceEnd)) 0 else 1
                        },
                        onDrag = { change, _ ->
                            change.consume()
                            val frac = (change.position.x / size.width).coerceIn(0f, 1f)
                            if (dragging == 0) slot.set(SamplerPads.sliceStartKey(pad), frac)
                            else slot.set(SamplerPads.sliceEndKey(pad), frac)
                            vm.bumpForToolbar()
                        },
                    )
                },
        ) {
            val w = size.width
            val h = size.height
            val mid = h / 2f
            if (peaks != null) {
                val (mins, maxs) = peaks
                val bw = w / mins.size
                for (i in mins.indices) {
                    val x = i * bw
                    drawLine(c.text, Offset(x, mid - maxs[i] * mid), Offset(x, mid - mins[i] * mid), strokeWidth = 1f)
                }
            }
            val sx = sliceStart * w
            val ex = sliceEnd * w
            drawRect(c.accent.copy(alpha = 0.14f), Offset(sx, 0f), Size(ex - sx, h))
            drawLine(c.accent, Offset(sx, 0f), Offset(sx, h), strokeWidth = 3f)
            drawLine(c.playhead, Offset(ex, 0f), Offset(ex, h), strokeWidth = 3f)
        }
        Text(
            if (sample == null) "Pad empty — Import a WAV or Record to assign it."
            else "${sample.data.size} frames @ ${sample.sampleRate} Hz · drag the markers, then CLIP to trim",
            color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 10.sp,
        )

        // ----- Per-pad output volume: 16 vertical sliders laid out 8 x 2 -----
        Text(
            "Pad volumes (drag a slider; tap selects that pad):",
            color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp,
        )
        PadVolumeBank(vm, slot, pad, rev, onSelect = { pad = it })

        // ----- 16-pad bank (bottom section) -----
        Text(
            "Pads (from C2 — press to play & select; CLEAR empties the selected pad):",
            color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp,
        )
        // Pass rev so assign/clear (which only bump revision) redraw the pads: a
        // changed Int param defeats Compose strong-skipping on these leaves.
        PadBank(vm, slot, pad, rev, onSelect = { pad = it })
    }

    importError?.let { msg ->
        AlertDialog(
            onDismissRequest = { importError = null },
            title = { Text("Import failed") },
            text = { Text(msg) },
            confirmButton = { TextButton(onClick = { importError = null }) { Text("OK") } },
        )
    }
}

/** The 4x4 pad grid. Pads read ascending left-to-right, top-to-bottom (pad 0 = C2). */
@Composable
private fun PadBank(vm: AppViewModel, slot: ToolboxSlot, selected: Int, rev: Int, onSelect: (Int) -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        for (row in 0 until 4) {
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                for (col in 0 until 4) {
                    PadCell(vm, slot, row * 4 + col, selected, rev, Modifier.weight(1f), onSelect)
                }
            }
        }
    }
}

@Composable
private fun PadCell(
    vm: AppViewModel,
    slot: ToolboxSlot,
    pad: Int,
    selected: Int,
    rev: Int,
    modifier: Modifier,
    onSelect: (Int) -> Unit,
) {
    @Suppress("UNUSED_PARAMETER") val ignored = rev // param only used to bust skipping
    val c = LocalRetro.current
    val isSelected = selected == pad
    val assigned = slot.string(SamplerPads.key(pad)).isNotBlank()
    val note = SamplerPads.noteFor(pad)
    val border = when { isSelected -> c.playhead; assigned -> c.accent; else -> c.grid }
    val fill = when {
        isSelected -> c.accent.copy(alpha = 0.28f)
        assigned -> c.accent.copy(alpha = 0.12f)
        else -> c.surface
    }
    Box(
        modifier
            .height(46.dp)
            .border(if (isSelected) 2.dp else 1.dp, border, RectangleShape)
            .background(fill)
            .pointerInput(pad, slot.index) {
                detectTapGestures(
                    // Fire on touch-DOWN so a pad sounds the instant it's pressed
                    // (like a drum pad), not on release — no "select then tap".
                    // Clearing is done with the CLEAR button, so there is no
                    // long-press action here.
                    onPress = { onSelect(pad); vm.auditionSample(slot, note) },
                )
            },
        contentAlignment = Alignment.Center,
    ) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Text(Pitch.name(note), color = if (assigned) c.text else c.textDim,
                fontFamily = FontFamily.Monospace, fontSize = 10.sp)
            Text(if (assigned) "●" else "＋", color = if (assigned) c.accent else c.textDim,
                fontFamily = FontFamily.Monospace, fontSize = 10.sp)
        }
    }
}

/** The per-pad volume bank: 16 vertical sliders in an 8-wide, 2-tall grid, so it
 *  reads left-to-right, top-to-bottom just like the [PadBank] pad grid above. */
@Composable
private fun PadVolumeBank(vm: AppViewModel, slot: ToolboxSlot, selected: Int, rev: Int, onSelect: (Int) -> Unit) {
    Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
        for (row in 0 until 2) {
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                for (col in 0 until 8) {
                    val p = row * 8 + col
                    VerticalPadSlider(vm, slot, p, selected == p, rev, Modifier.weight(1f), onSelect)
                }
            }
        }
    }
}

/** One vertical fader for a pad's output volume (0..1). Drag anywhere on the track
 *  to set the level; a tap also selects the pad so the detail area follows it. The
 *  fill height is the level; assigned pads read in the accent colour, empty ones dim. */
@Composable
private fun VerticalPadSlider(
    vm: AppViewModel,
    slot: ToolboxSlot,
    pad: Int,
    isSelected: Boolean,
    rev: Int,
    modifier: Modifier,
    onSelect: (Int) -> Unit,
) {
    @Suppress("UNUSED_PARAMETER") val ignored = rev // param only used to bust skipping
    val c = LocalRetro.current
    val vol = slot.float(SamplerPads.volumeKey(pad), SamplerPads.DEFAULT_VOLUME)
    val assigned = slot.string(SamplerPads.key(pad)).isNotBlank()
    val border = when { isSelected -> c.playhead; assigned -> c.accent; else -> c.grid }
    val fillColor = if (assigned) c.accent.copy(alpha = 0.55f) else c.grid.copy(alpha = 0.55f)
    Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) {
        Canvas(
            Modifier
                .fillMaxWidth()
                .height(96.dp)
                .background(c.surface)
                .border(if (isSelected) 2.dp else 1.dp, border, RectangleShape)
                .pointerInput(pad, slot.index) {
                    detectTapGestures { off ->
                        onSelect(pad)
                        slot.set(SamplerPads.volumeKey(pad), snapVol(1f - off.y / size.height))
                        vm.bumpForToolbar()
                    }
                }
                .pointerInput(pad, slot.index) {
                    detectDragGestures(
                        onDragStart = { onSelect(pad) },
                        onDrag = { change, _ ->
                            change.consume()
                            slot.set(SamplerPads.volumeKey(pad), snapVol(1f - change.position.y / size.height))
                            vm.bumpForToolbar()
                        },
                    )
                },
        ) {
            val fillH = vol.coerceIn(0f, 1f) * size.height
            drawRect(fillColor, Offset(0f, size.height - fillH), Size(size.width, fillH))
        }
        Text(
            Pitch.name(SamplerPads.noteFor(pad)),
            color = if (assigned) c.text else c.textDim,
            fontFamily = FontFamily.Monospace, fontSize = 8.sp,
        )
    }
}

/** Snap a raw pad-fader value to the default volume when it lands within a small
 *  band, so returning to the standard level is easy to hit (like the mixer fader). */
private fun snapVol(raw: Float): Float {
    val v = raw.coerceIn(0f, 1f)
    return if (kotlin.math.abs(v - SamplerPads.DEFAULT_VOLUME) < VOL_SNAP_BAND) SamplerPads.DEFAULT_VOLUME else v
}

private const val VOL_SNAP_BAND = 0.03f

/** Down-sample the waveform to [buckets] min/max pairs for cheap drawing.
 *  Shared with the SoundFont editor's preview. */
internal fun computePeaks(data: FloatArray, buckets: Int): Pair<FloatArray, FloatArray> {
    val mins = FloatArray(buckets)
    val maxs = FloatArray(buckets)
    if (data.isEmpty()) return mins to maxs
    val step = (data.size / buckets).coerceAtLeast(1)
    for (b in 0 until buckets) {
        var mn = 1f; var mx = -1f
        val start = b * step
        val end = minOf(start + step, data.size)
        if (start >= data.size) { mins[b] = 0f; maxs[b] = 0f; continue }
        for (i in start until end) {
            val v = data[i]
            if (v < mn) mn = v
            if (v > mx) mx = v
        }
        mins[b] = mn.coerceIn(-1f, 1f)
        maxs[b] = mx.coerceIn(-1f, 1f)
    }
    return mins to maxs
}
