// 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.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Arrangement
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.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.rememberUpdatedState
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.ToolboxSlot
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.components.PianoKey
import space.rcmd.android.sizzle.ui.components.PianoOctave
import space.rcmd.android.sizzle.ui.components.RepeatButton
import space.rcmd.android.sizzle.ui.components.RetroButton
import space.rcmd.android.sizzle.ui.theme.LocalRetro

/** Upper bound (ms) for the SoundFont loop crossfade stepper. */
private const val MAX_XFADE_MS = 500

/**
 * The SoundFont device editor: load an `.sf2` / `.xi` (or `.wav`) file from local
 * storage, preview its waveform, set output volume, and audition it pitched across
 * a two-octave keyboard. The extracted sample plays across the whole keyboard from
 * the file's root note (see [space.rcmd.android.sizzle.audio.SoundFontLoader]).
 */
@Composable
fun SoundFontEditor(vm: AppViewModel, slot: ToolboxSlot) {
    val c = LocalRetro.current
    val context = LocalContext.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe: waveform/info refresh

    val sfId = slot.string("sfSample")
    val sample = SampleStore.get(sfId)
    val root = slot.float("sfRoot", 60f).toInt()

    var importError by remember(slot.index) { mutableStateOf<String?>(null) }
    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.loadSoundFont(slot, uri.toString(), bytes) -> importError =
                "Couldn't load this instrument. Supported formats are .sf2, .xi and " +
                    "uncompressed .wav."
        }
    }

    val loopOn = slot.string("loop", "Off") == "On"
    val xfadeMs = slot.float("loopXfade", 0f).toInt()

    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
        RetroButton("LOAD SF2 / XI") {
            // .sf2/.xi have no reliable MIME; accept any file and detect by content.
            picker.launch(arrayOf("*/*"))
        }
        // Sustain loop (seamless while a note is held; releases on note-off) plus a
        // crossfade-time stepper in ms that smooths the loop seam (hold +/- to spin).
        Row(
            Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(6.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            RetroButton("LOOP", active = loopOn) {
                slot.set("loop", if (loopOn) "Off" else "On")
                vm.bumpForToolbar()
            }
            Text("XFADE", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp)
            RepeatButton("-") {
                slot.set("loopXfade", (xfadeMs - 1).coerceAtLeast(0).toFloat()); vm.bumpForToolbar()
            }
            Text("${xfadeMs}ms", color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 12.sp)
            RepeatButton("+") {
                slot.set("loopXfade", (xfadeMs + 1).coerceAtMost(MAX_XFADE_MS).toFloat()); vm.bumpForToolbar()
            }
        }
        Text(
            if (sample == null) {
                "No instrument loaded — load a .sf2 or .xi file."
            } else {
                "Loaded: ${sample.data.size} frames @ ${sample.sampleRate} Hz · root ${Pitch.name(root)}"
            },
            color = if (sample == null) c.textDim else c.text,
            fontFamily = FontFamily.Monospace, fontSize = 10.sp,
        )

        if (sample != null) {
            val peaks = remember(sfId, sample.data.size) { computePeaks(sample.data, 400) }
            Canvas(Modifier.fillMaxWidth().height(96.dp).background(c.surface)) {
                val (mins, maxs) = peaks
                val bw = size.width / mins.size
                val mid = size.height / 2f
                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)
                }
            }
        }

        // Volume + ADSR as compact vertical faders (one row). The audition keyboard
        // is pinned separately at the bottom of the editor (see [SlotAuditionKeyboard]).
        Text("Volume / envelope:", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp)
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
            VerticalSlider(vm, slot, rev, "volume", "VOL", 0f, 1f, 0.8f, Modifier.weight(1f))
            VerticalSlider(vm, slot, rev, "attack", "ATK", 0f, 2f, 0.005f, Modifier.weight(1f))
            VerticalSlider(vm, slot, rev, "decay", "DEC", 0f, 2f, 0.10f, Modifier.weight(1f))
            VerticalSlider(vm, slot, rev, "sustain", "SUS", 0f, 1f, 1f, Modifier.weight(1f))
            VerticalSlider(vm, slot, rev, "release", "REL", 0f, 3f, 0.20f, Modifier.weight(1f))
        }
    }

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

/** A compact vertical fader bound to a numeric slot param ([key], range [min]..[max]).
 *  Drag or tap the track to set the value; the current value shows above and the
 *  short label below. [rev] is passed only to defeat Compose strong-skipping so the
 *  fader refreshes when a preset load changes the value. */
@Composable
private fun VerticalSlider(
    vm: AppViewModel,
    slot: ToolboxSlot,
    rev: Int,
    key: String,
    label: String,
    min: Float,
    max: Float,
    default: Float,
    modifier: Modifier,
) {
    @Suppress("UNUSED_PARAMETER") val ignored = rev // param only used to bust skipping
    val c = LocalRetro.current
    val value = slot.float(key, default).coerceIn(min, max)
    val frac = if (max > min) (value - min) / (max - min) else 0f

    fun setFromY(y: Float, h: Float) {
        val f = (1f - y / h).coerceIn(0f, 1f)
        slot.set(key, min + f * (max - min))
        vm.bumpForToolbar()
    }

    Column(modifier, horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(3.dp)) {
        Text(formatSliderValue(value), color = c.text,
            fontFamily = FontFamily.Monospace, fontSize = 9.sp, maxLines = 1)
        Canvas(
            Modifier
                .fillMaxWidth()
                .height(120.dp)
                .background(c.surface)
                .border(1.dp, c.grid, RectangleShape)
                .pointerInput(key, slot.index) {
                    detectTapGestures { off -> setFromY(off.y, size.height.toFloat()) }
                }
                .pointerInput(key, slot.index) {
                    detectDragGestures { change, _ -> change.consume(); setFromY(change.position.y, size.height.toFloat()) }
                },
        ) {
            val fillH = frac.coerceIn(0f, 1f) * size.height
            drawRect(c.accent.copy(alpha = 0.5f), Offset(0f, size.height - fillH), Size(size.width, fillH))
        }
        Text(label, color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 9.sp, maxLines = 1)
    }
}

private fun formatSliderValue(v: Float): String =
    if (v == v.toInt().toFloat()) v.toInt().toString() else String.format("%.2f", v)

/** A slot-auditioning keyboard, laid out and sized like the shared audition /
 *  punch-in keyboard ([space.rcmd.android.sizzle.ui.StackedKeyboard]): a small octave
 *  selector above two weighted octaves. It is pinned at the bottom of the editor (by
 *  ToolboxScreen's ParamEditor) rather than scrolling with the controls. Holding a key
 *  sounds THIS slot's instrument — used by the SoundFont and synth editors. */
@Composable
fun SlotAuditionKeyboard(vm: AppViewModel, slot: ToolboxSlot, modifier: Modifier) {
    val c = LocalRetro.current
    var octave by remember(slot.index) { mutableIntStateOf(3) }
    Column(
        modifier.fillMaxWidth().background(c.background).padding(6.dp),
        verticalArrangement = Arrangement.spacedBy(4.dp),
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.spacedBy(6.dp),
        ) {
            Text("OCT", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 11.sp)
            RepeatButton("-") { octave = (octave - 1).coerceAtLeast(0) }
            Text("$octave–${octave + 1}", color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 13.sp)
            RepeatButton("+") { octave = (octave + 1).coerceAtMost(8) }
        }
        AuditionOctave(vm, slot.index, (octave + 2) * 12, Modifier.weight(1f)) // upper octave on top
        AuditionOctave(vm, slot.index, (octave + 1) * 12, Modifier.weight(1f)) // lower octave below
    }
}

/** One octave of the SoundFont audition keyboard — a real piano octave (like the
 *  toolbox piano and the cell-editor keyboard); hold a key to sound the slot's
 *  instrument. [modifier] carries the weight so the two octaves share the pinned
 *  keyboard's height (PianoOctave needs a bounded height to lay its keys out). */
@Composable
private fun AuditionOctave(vm: AppViewModel, slotIndex: Int, startMidi: Int, modifier: Modifier) {
    PianoOctave(modifier.fillMaxWidth()) { pc, isBlack, keyMod ->
        val midi = (startMidi + pc).coerceIn(Pitch.LOWEST, Pitch.HIGHEST)
        val liveMidi = rememberUpdatedState(midi)
        PianoKey(
            label = Pitch.name(midi).replace("-", ""),
            isBlack = isBlack,
            enabled = true,
            selected = false,
            pressed = vm.isNoteHeld(midi),
            modifier = keyMod.pointerInput(Unit) {
                awaitEachGesture {
                    awaitFirstDown()
                    val n = liveMidi.value
                    vm.auditionOn(slotIndex, n)
                    waitForUpOrCancellation()
                    vm.auditionOff(n)
                }
            },
        )
    }
}
