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

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

import android.content.Intent
import androidx.activity.compose.BackHandler
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.clickable
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.Spacer
import androidx.compose.foundation.layout.aspectRatio
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import space.rcmd.android.sizzle.model.ParamSpec
import space.rcmd.android.sizzle.model.ToolboxSlot
import space.rcmd.android.sizzle.model.ToolboxType
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.isLandscape
import space.rcmd.android.sizzle.ui.components.RetroButton
import space.rcmd.android.sizzle.ui.components.RetroDropdown
import space.rcmd.android.sizzle.ui.theme.LocalRetro
import kotlinx.coroutines.launch

/**
 * The third tab. Top half: a 4x4 grid of 16 slots holding instruments or effects.
 *  - SINGLE tap -> pick a device (empty tile) or open its settings (filled tile).
 *  - LONG press -> clear the tile (asks to confirm first, since it's destructive).
 *
 * Bottom half: a stacked two-octave keyboard (one octave per row) for quick testing.
 *
 * The parameter editor is generated automatically from the device's [ToolboxType]
 * parameter list, so new devices need no bespoke UI.
 */
@Composable
fun ToolboxScreen(vm: AppViewModel) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val redraw = vm.revision

    var editing by remember { mutableStateOf<Int?>(null) }
    var picking by remember { mutableStateOf<Int?>(null) }
    var clearing by remember { mutableStateOf<Int?>(null) }

    // Back closes an open device editor / picker / confirm before the app-level handler
    // switches tabs.
    BackHandler(enabled = editing != null || picking != null || clearing != null) {
        when {
            clearing != null -> clearing = null
            editing != null -> editing = null
            else -> picking = null
        }
    }

    // Shared tile gestures (index-based) so both layouts drive the same state.
    // Single tap: pick a device for an empty tile, else open the filled tile's settings.
    val onTapTile: (Int) -> Unit = { i -> if (vm.project.toolbox[i].isEmpty) picking = i else editing = i }
    // Long press: clear a filled tile — but confirm first (it discards the device and
    // its settings, and there's no undo). Long-pressing an empty tile does nothing.
    val onLongPressTile: (Int) -> Unit = { i -> if (!vm.project.toolbox[i].isEmpty) clearing = i }

    if (isLandscape()) {
        // Landscape: a fixed 4x4 grid squashed to fit the height (no scroll) BESIDE
        // the audition keyboard, which gets the larger width so its CH/OCT/VEL
        // toolbar fits without horizontal scrolling.
        Row(Modifier.fillMaxSize().background(c.background)) {
            ToolboxTileGrid(vm, onTapTile, onLongPressTile,
                Modifier.weight(1f).fillMaxHeight())
            Box(Modifier.width(2.dp).fillMaxHeight().background(c.accent))
            space.rcmd.android.sizzle.ui.StackedKeyboard(vm, punchIn = false, Modifier.weight(1.3f))
        }
    } else {
        // Portrait: a scrolling grid of square tiles above the keyboard (1.7 : 1).
        Column(Modifier.fillMaxSize().background(c.background)) {
            LazyVerticalGrid(
                columns = GridCells.Fixed(4),
                modifier = Modifier.weight(1.7f).fillMaxWidth().padding(6.dp),
                horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalArrangement = Arrangement.spacedBy(6.dp),
            ) {
                items(vm.project.toolbox) { slot ->
                    SlotTile(
                        rev = vm.revision, slot = slot,
                        onTap = { onTapTile(slot.index) },
                        onLongPress = { onLongPressTile(slot.index) },
                        modifier = Modifier.fillMaxWidth().aspectRatio(1f),
                    )
                }
            }
            Box(Modifier.fillMaxWidth().height(2.dp).background(c.accent)) // fixed divider
            space.rcmd.android.sizzle.ui.StackedKeyboard(vm, punchIn = false, Modifier.weight(1f))
        }
    }

    picking?.let { index ->
        DevicePicker(
            onDismiss = { picking = null },
            onPick = { type ->
                vm.project.toolbox[index].fill(type)
                vm.bumpForToolbar(); picking = null; editing = index
            },
        )
    }
    editing?.let { index ->
        ParamEditor(vm, vm.project.toolbox[index], onClose = { editing = null })
    }
    clearing?.let { index ->
        val slot = vm.project.toolbox[index]
        val label = slot.name.ifBlank { slot.type?.displayName ?: "" }
        AlertDialog(
            onDismissRequest = { clearing = null },
            title = { Text("Clear tile") },
            text = { Text("Remove \"$label\" from tile ${index + 1}? This clears the device and its settings and can't be undone.") },
            confirmButton = {
                TextButton(onClick = {
                    vm.project.toolbox[index].clearSlot()
                    vm.bumpForToolbar()
                    clearing = null
                }) { Text("Clear") }
            },
            dismissButton = { TextButton(onClick = { clearing = null }) { Text("Cancel") } },
        )
    }
}

/** A fixed 4x4 grid of the 16 slot tiles that fills [modifier]'s bounds — used in
 *  landscape so every tile fits the short height without scrolling (tiles become
 *  rectangular rather than square). */
@Composable
private fun ToolboxTileGrid(
    vm: AppViewModel,
    onTap: (Int) -> Unit,
    onLongPress: (Int) -> Unit,
    modifier: Modifier,
) {
    Column(modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
        for (row in 0 until 4) {
            Row(Modifier.fillMaxWidth().weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
                for (col in 0 until 4) {
                    val idx = row * 4 + col
                    SlotTile(
                        rev = vm.revision,
                        slot = vm.project.toolbox[idx],
                        onTap = { onTap(idx) },
                        onLongPress = { onLongPress(idx) },
                        modifier = Modifier.weight(1f).fillMaxHeight(),
                    )
                }
            }
        }
    }
}

/** One toolbox slot tile. [modifier] carries the sizing: a square (portrait grid)
 *  or a weighted cell that fills the height (the landscape fixed grid). */
@Composable
private fun SlotTile(
    rev: Int,
    slot: ToolboxSlot,
    onTap: () -> Unit,
    onLongPress: () -> Unit,
    modifier: Modifier = Modifier,
) {
    @Suppress("UNUSED_PARAMETER") val ignored = rev // param only used to bust skipping
    val c = LocalRetro.current
    val borderColor = if (!slot.isEmpty) c.accent else c.grid
    val fill = if (!slot.isEmpty) c.accent.copy(alpha = 0.12f) else c.surface
    Box(
        modifier
            .border(1.dp, borderColor, RectangleShape)
            .background(fill)
            .pointerInput(slot.index, slot.isEmpty) {
                // Single tap acts immediately (no double-tap wait); long press clears.
                detectTapGestures(
                    onTap = { onTap() },
                    onLongPress = { onLongPress() },
                )
            },
        contentAlignment = Alignment.Center,
    ) {
        // Inner padding keeps labels off the tile border; the name clips with an
        // ellipsis so a long preset name can't spill into (or past) the edges.
        Column(
            Modifier.fillMaxWidth().padding(horizontal = 5.dp, vertical = 3.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text("${slot.index + 1}".padStart(2, '0'),
                color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 10.sp)
            Text(
                if (slot.isEmpty) "＋" else slot.name,
                color = if (slot.isEmpty) c.textDim else c.text,
                fontFamily = FontFamily.Monospace, fontSize = 11.sp,
                textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis,
            )
            slot.type?.let {
                Text(it.kind.name.take(3), color = c.textDim,
                    fontFamily = FontFamily.Monospace, fontSize = 9.sp)
            }
        }
    }
}

/** A simple overlay list of all available instruments and effects to load. */
@Composable
private fun DevicePicker(onDismiss: () -> Unit, onPick: (ToolboxType) -> Unit) {
    val c = LocalRetro.current
    Box(
        Modifier.fillMaxSize().background(c.background.copy(alpha = 0.92f))
            .pointerInput(Unit) { detectTapGestures { onDismiss() } },
        contentAlignment = Alignment.Center,
    ) {
        Column(Modifier.padding(16.dp)) {
            Text("SELECT DEVICE", color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 14.sp)
            ToolboxType.entries.forEach { type ->
                Text(
                    "[${type.kind.name.take(3)}] ${type.displayName}",
                    color = c.text, fontFamily = FontFamily.Monospace, fontSize = 13.sp,
                    modifier = Modifier
                        .fillMaxWidth()
                        .pointerInput(type) { detectTapGestures { onPick(type) } }
                        .padding(vertical = 6.dp),
                )
            }
        }
    }
}

/** Auto-generated parameter editor. Presets are managed by the [PresetBar]. */
@Composable
private fun ParamEditor(vm: AppViewModel, slot: ToolboxSlot, onClose: () -> Unit) {
    val c = LocalRetro.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision // subscribe so title/params refresh
    val type = slot.type ?: return

    // The SoundFont and synth editors pin an audition keyboard at the bottom (same as
    // the toolbox / tracker keyboards), so their controls scroll in a weighted region
    // above the keyboard rather than the whole editor scrolling as one.
    val pinnedKeyboard = type == ToolboxType.SOUNDFONT || type == ToolboxType.NES_SYNTH

    Box(Modifier.fillMaxSize().background(c.background.copy(alpha = 0.96f))) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier
                    .weight(if (pinnedKeyboard) 1.7f else 1f)
                    .fillMaxWidth()
                    .padding(12.dp)
                    .verticalScroll(rememberScrollState()),
                verticalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
                    Text(slot.name, color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 15.sp)
                    Text("CLOSE ✕", color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 13.sp,
                        modifier = Modifier.pointerInput(Unit) { detectTapGestures { onClose() } })
                }

                // Preset browser: load / save / import / share for this device.
                PresetBar(vm, slot)

                // The Sampler / SoundFont / Delay get bespoke editors; the reverb has so
                // many controls it's paged (swipeable sections with dot pagination); the
                // LFO adds a modulation-target picker; everything else uses the
                // auto-generated parameter list, laid out two-per-row to fit more.
                when (type) {
                    ToolboxType.NES_SYNTH -> NesSynthEditor(vm, slot)
                    ToolboxType.SAMPLER -> SamplerEditor(vm, slot)
                    ToolboxType.SOUNDFONT -> SoundFontEditor(vm, slot)
                    ToolboxType.DELAY -> DelayEditor(vm, slot)
                    ToolboxType.AMBIENCE -> AmbiencePager(vm, slot)
                    ToolboxType.LFO -> {
                        ParamGrid(vm, slot, type.params)
                        LfoTargetPicker(vm, slot)
                    }
                    else -> ParamGrid(vm, slot, type.params)
                }
            }

            if (pinnedKeyboard) {
                Box(Modifier.fillMaxWidth().height(2.dp).background(c.accent)) // fixed divider
                SlotAuditionKeyboard(vm, slot, Modifier.weight(1f))
            }
        }
    }
}

/** Lay a list of [params] out two-per-row so the editor fits more controls than a
 *  single stacked column. Each [ParamControl] fills its half-width cell (an odd last
 *  param gets an empty cell beside it). */
@Composable
private fun ParamGrid(vm: AppViewModel, slot: ToolboxSlot, params: List<ParamSpec>) {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
        var i = 0
        while (i < params.size) {
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                Box(Modifier.weight(1f)) { ParamControl(vm, slot, params[i]) }
                if (i + 1 < params.size) {
                    Box(Modifier.weight(1f)) { ParamControl(vm, slot, params[i + 1]) }
                } else {
                    Spacer(Modifier.weight(1f))
                }
            }
            i += 2
        }
    }
}

/** The reverb has ~17 controls — too many to stack. They're grouped into labelled
 *  sections shown one page at a time in a swipeable [HorizontalPager] with tappable
 *  dot pagination below. Each page uses the same two-column [ParamGrid]. Works the
 *  same in portrait and landscape (the editor overlay is full-width in both). */
@Composable
private fun AmbiencePager(vm: AppViewModel, slot: ToolboxSlot) {
    val c = LocalRetro.current
    val byKey = (slot.type ?: return).params.associateBy { it.key }
    val pagerState = rememberPagerState(pageCount = { AMBIENCE_SECTIONS.size })
    val scope = rememberCoroutineScope()

    Column(Modifier.fillMaxWidth()) {
        HorizontalPager(
            state = pagerState,
            modifier = Modifier.fillMaxWidth().height(210.dp),
            pageSpacing = 12.dp,
        ) { page ->
            val section = AMBIENCE_SECTIONS[page]
            Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
                Text(
                    section.title, color = c.accent,
                    fontFamily = FontFamily.Monospace, fontSize = 12.sp,
                )
                ParamGrid(vm, slot, section.keys.mapNotNull { byKey[it] })
            }
        }
        // Dot pagination: tap a dot (or swipe the page) to jump to that section.
        Row(
            Modifier.fillMaxWidth().padding(top = 8.dp),
            horizontalArrangement = Arrangement.Center,
        ) {
            for (i in AMBIENCE_SECTIONS.indices) {
                val active = i == pagerState.currentPage
                Box(
                    Modifier
                        .padding(horizontal = 4.dp)
                        .size(if (active) 10.dp else 7.dp)
                        .background(if (active) c.accent else c.grid, CircleShape)
                        .clickable { scope.launch { pagerState.animateScrollToPage(i) } },
                )
            }
        }
    }
}

/** Grouping of the reverb's parameters into swipeable sections. */
private class AmbienceSection(val title: String, val keys: List<String>)

private val AMBIENCE_SECTIONS = listOf(
    AmbienceSection("Space", listOf("algo", "predelay", "roomsize", "decay")),
    AmbienceSection("Tone", listOf("hfdamp", "lfabsorb", "diffusion")),
    AmbienceSection("Modulation", listOf("modamt", "modrate")),
    AmbienceSection("Character", listOf("erlevel", "saturation")),
    AmbienceSection("Mix", listOf("wet", "dry")),
    AmbienceSection("Ducking", listOf("duckamt", "duckthresh", "duckattack", "duckrelease")),
)

/**
 * The preset browser bar shown at the top of every device editor: a dropdown of
 * the presets saved for this device type, plus Save (to the app's on-disk preset
 * library), Import (from a picked text preset or — for the Sampler — a `.zip`
 * bundle), and Share (via the system share sheet). Presets are the plain-text
 * [space.rcmd.android.sizzle.io.PresetIo] format, stored one folder
 * per instrument/effect.
 */
@Composable
private fun PresetBar(vm: AppViewModel, slot: ToolboxSlot) {
    val c = LocalRetro.current
    val context = LocalContext.current
    @Suppress("UNUSED_VARIABLE") val rev = vm.revision // refresh the list after save/import
    val type = slot.type ?: return

    val names = vm.presetNames(type)
    var showSave by remember { mutableStateOf(false) }
    var confirmDelete by remember { mutableStateOf<String?>(null) }
    var importError by remember { mutableStateOf<String?>(null) }
    // The preset currently chosen in the dropdown (what Delete acts on). Falls back
    // to the slot's name, then the first available preset.
    var selected by remember(slot.index) { mutableStateOf<String?>(null) }
    val current = selected?.takeIf { it in names } ?: slot.name.takeIf { it in names } ?: names.firstOrNull()

    // System file picker for importing a preset (text preset or sampler .zip bundle).
    // Any read/parse failure surfaces as a popup instead of silently doing nothing.
    val importer = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
        if (uri == null) return@rememberLauncherForActivityResult
        val bytes = runCatching {
            context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
        }.getOrNull()
        when {
            bytes == null -> importError = "Couldn't read the selected file."
            !vm.importPreset(slot, bytes) -> importError =
                "Couldn't import this preset. The file may be corrupt, or not a valid " +
                    "${type.displayName} preset or bundle."
        }
    }

    Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
        // Row 1: the preset browser dropdown, above the action buttons.
        Row(
            Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            if (names.isNotEmpty() && current != null) {
                RetroDropdown(
                    label = "PRESET",
                    options = names,
                    selected = current,
                    onSelect = { selected = it; vm.loadPreset(slot, it) },
                )
            } else {
                Text("no presets", color = c.textDim,
                    fontFamily = FontFamily.Monospace, fontSize = 11.sp)
            }
        }
        // Row 2: the action buttons.
        Row(
            Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
            horizontalArrangement = Arrangement.spacedBy(6.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            // DELETE kept at the far LEFT, isolated by a gap, so reaching for the
            // other actions can't accidentally hit it.
            if (current != null) {
                RetroButton("DELETE", danger = true) { confirmDelete = current }
                Spacer(Modifier.width(20.dp))
            }
            RetroButton("SAVE") { showSave = true }
            RetroButton("IMPORT") {
                // Accept text presets and (for the Sampler) zip bundles.
                importer.launch(arrayOf("application/zip", "text/*", "application/octet-stream", "*/*"))
            }
            RetroButton("SHARE") {
                val name = slot.name.ifBlank { type.displayName }
                vm.savePreset(slot, name)                    // ensure a file exists to share
                vm.presetShareFile(type, name)?.let { sharePresetFile(context, it) }
            }
        }
        Text(
            "Presets are plain text, saved per device under the app's storage. " +
                "Sampler presets carry the sample (shared as a .zip bundle).",
            color = c.textDim, fontFamily = FontFamily.Monospace, fontSize = 10.sp,
        )
    }

    if (showSave) {
        var name by remember { mutableStateOf(slot.name.ifBlank { type.displayName }) }
        AlertDialog(
            onDismissRequest = { showSave = false },
            title = { Text("Save preset") },
            text = {
                OutlinedTextField(
                    value = name,
                    onValueChange = { name = it },
                    singleLine = true,
                    label = { Text("Name") },
                )
            },
            confirmButton = {
                TextButton(onClick = {
                    if (name.isNotBlank()) vm.savePreset(slot, name.trim())
                    showSave = false
                }) { Text("Save") }
            },
            dismissButton = { TextButton(onClick = { showSave = false }) { Text("Cancel") } },
        )
    }

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

    confirmDelete?.let { name ->
        AlertDialog(
            onDismissRequest = { confirmDelete = null },
            title = { Text("Delete preset") },
            text = { Text("Delete \"$name\"? This cannot be undone.") },
            confirmButton = {
                TextButton(onClick = {
                    vm.deletePreset(type, name)
                    selected = null
                    confirmDelete = null
                }) { Text("Delete") }
            },
            dismissButton = { TextButton(onClick = { confirmDelete = null }) { Text("Cancel") } },
        )
    }
}

/** Launch the system share sheet for a saved preset file (text) or bundle (zip). */
private fun sharePresetFile(context: android.content.Context, file: java.io.File) {
    val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
    val mime = if (file.name.endsWith(".zip")) "application/zip" else "text/plain"
    val send = Intent(Intent.ACTION_SEND).apply {
        type = mime
        putExtra(Intent.EXTRA_STREAM, uri)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }
    context.startActivity(Intent.createChooser(send, "Share preset"))
}
