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

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

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.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.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import space.rcmd.android.sizzle.model.SynthDefs
import space.rcmd.android.sizzle.model.TimeDivision
import space.rcmd.android.sizzle.model.ToolboxSlot
import space.rcmd.android.sizzle.ui.AppViewModel
import space.rcmd.android.sizzle.ui.components.Knob
import space.rcmd.android.sizzle.ui.components.RetroDropdown
import space.rcmd.android.sizzle.ui.theme.LocalRetro
import kotlinx.coroutines.launch
import kotlin.math.roundToInt

/**
 * The bespoke editor for the subtractive synth ([space.rcmd.android.sizzle.model.ToolboxType.NES_SYNTH]).
 * Controls are split across swipeable cards by function — Oscillators, Filter,
 * Amp/LFO, and the Mod Matrix — with tappable dot pagination. Continuous params use
 * rotary [Knob]s; enumerated ones use [RetroDropdown]. The audition keyboard is
 * pinned at the bottom of the editor by the ParamEditor host (see [ToolboxScreen]).
 */
@Composable
fun NesSynthEditor(vm: AppViewModel, slot: ToolboxSlot) {
    val c = LocalRetro.current
    val rev = vm.revision
    val pager = rememberPagerState(pageCount = { 4 })
    val scope = rememberCoroutineScope()

    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
        HorizontalPager(
            state = pager,
            modifier = Modifier.fillMaxWidth().height(300.dp),
            pageSpacing = 12.dp,
        ) { page ->
            when (page) {
                0 -> OscCard(vm, slot, rev)
                1 -> FilterCard(vm, slot, rev)
                2 -> AmpLfoCard(vm, slot, rev)
                else -> ModCard(vm, slot, rev)
            }
        }
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            for (i in 0 until 4) {
                val active = i == pager.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 { pager.animateScrollToPage(i) } },
                )
            }
        }
    }
}

// ---------------------------------------------------------------- cards

@Composable
private fun OscCard(vm: AppViewModel, slot: ToolboxSlot, rev: Int) {
    Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
        CardTitle("OSCILLATORS")
        for (i in 0 until SynthDefs.OSC_COUNT) {
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp),
                verticalAlignment = Alignment.CenterVertically) {
                EnumParam(vm, slot, rev, "osc${i}wave", "OSC${i + 1}", SynthDefs.WAVES,
                    SynthDefs.DEF_OSC_WAVE[i], Modifier.weight(1.3f))
                ParamKnob(vm, slot, rev, "osc${i}oct", "OCT", -2f, 2f, 0f, Modifier.weight(1f), isInt = true)
                ParamKnob(vm, slot, rev, "osc${i}semi", "SEMI", -12f, 12f, 0f, Modifier.weight(1f), isInt = true)
                ParamKnob(vm, slot, rev, "osc${i}fine", "FINE", -50f, 50f, 0f, Modifier.weight(1f), isInt = true)
                ParamKnob(vm, slot, rev, "osc${i}level", "LVL", 0f, 1f, SynthDefs.DEF_OSC_LEVEL[i], Modifier.weight(1f))
            }
        }
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
            ParamKnob(vm, slot, rev, "noise", "NOISE", 0f, 1f, 0f, Modifier.weight(1f))
            Spacer(Modifier.weight(4f))
        }
    }
}

@Composable
private fun FilterCard(vm: AppViewModel, slot: ToolboxSlot, rev: Int) {
    Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
        CardTitle("FILTER")
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
            ParamKnob(vm, slot, rev, "cutoff", "CUTOFF", 0f, 1f, 0.55f, Modifier.weight(1f))
            ParamKnob(vm, slot, rev, "reso", "RESO", 0f, 1f, 0.15f, Modifier.weight(1f))
            ParamKnob(vm, slot, rev, "keytrack", "KEYTRK", 0f, 1f, 0.3f, Modifier.weight(1f))
            ParamKnob(vm, slot, rev, "fenvamt", "ENV AMT", -1f, 1f, 0.4f, Modifier.weight(1f))
        }
        CardTitle("FILTER ENVELOPE")
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
            ParamKnob(vm, slot, rev, "fatk", "ATK", 0f, 2f, 0.005f, Modifier.weight(1f), unit = "s")
            ParamKnob(vm, slot, rev, "fdec", "DEC", 0f, 2f, 0.25f, Modifier.weight(1f), unit = "s")
            ParamKnob(vm, slot, rev, "fsus", "SUS", 0f, 1f, 0.4f, Modifier.weight(1f))
            ParamKnob(vm, slot, rev, "frel", "REL", 0f, 3f, 0.2f, Modifier.weight(1f), unit = "s")
        }
    }
}

@Composable
private fun AmpLfoCard(vm: AppViewModel, slot: ToolboxSlot, rev: Int) {
    Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
        CardTitle("AMPLIFIER ENVELOPE")
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
            ParamKnob(vm, slot, rev, "attack", "ATK", 0f, 2f, 0.005f, Modifier.weight(1f), unit = "s")
            ParamKnob(vm, slot, rev, "decay", "DEC", 0f, 2f, 0.2f, Modifier.weight(1f), unit = "s")
            ParamKnob(vm, slot, rev, "sustain", "SUS", 0f, 1f, 0.7f, Modifier.weight(1f))
            ParamKnob(vm, slot, rev, "release", "REL", 0f, 3f, 0.2f, Modifier.weight(1f), unit = "s")
        }
        CardTitle("LFO (tempo-synced)")
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically) {
            EnumParam(vm, slot, rev, "lfowave", "WAVE", SynthDefs.LFO_WAVES, "Tri", Modifier.weight(1f))
            LfoRateDropdown(vm, slot, rev, Modifier.weight(1f))
        }
    }
}

@Composable
private fun ModCard(vm: AppViewModel, slot: ToolboxSlot, rev: Int) {
    Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
        CardTitle("MOD MATRIX")
        for (n in 0 until SynthDefs.MOD_SLOTS) {
            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp),
                verticalAlignment = Alignment.CenterVertically) {
                EnumParam(vm, slot, rev, "m${n}src", "SRC", SynthDefs.MOD_SRC, "Off", Modifier.weight(1f))
                EnumParam(vm, slot, rev, "m${n}dst", "DST", SynthDefs.MOD_DST, "Cutoff", Modifier.weight(1f))
                ParamKnob(vm, slot, rev, "m${n}amt", "AMT", -1f, 1f, 0f, Modifier.weight(0.9f))
            }
        }
    }
}

// ---------------------------------------------------------------- controls

@Composable
private fun CardTitle(text: String) {
    val c = LocalRetro.current
    Text(text, color = c.accent, fontFamily = FontFamily.Monospace, fontSize = 12.sp)
}

/** A rotary [Knob] bound to a numeric slot param. [rev] busts Compose strong-skipping
 *  so the knob refreshes when a preset load changes the value. */
@Composable
private fun ParamKnob(
    vm: AppViewModel,
    slot: ToolboxSlot,
    rev: Int,
    key: String,
    label: String,
    min: Float,
    max: Float,
    default: Float,
    modifier: Modifier,
    isInt: Boolean = false,
    unit: String = "",
) {
    @Suppress("UNUSED_PARAMETER") val ignored = rev
    val value = slot.float(key, default)
    val text = if (isInt) "${value.roundToInt()}$unit" else String.format("%.2f", value) + unit
    Knob(label, value, min, max, text, modifier, default) { nv ->
        slot.set(key, if (isInt) nv.roundToInt().toFloat() else nv)
        vm.bumpForToolbar()
    }
}

/** A [RetroDropdown] bound to a string (enumerated) slot param. */
@Composable
private fun EnumParam(
    vm: AppViewModel,
    slot: ToolboxSlot,
    rev: Int,
    key: String,
    label: String,
    options: List<String>,
    default: String,
    modifier: Modifier,
) {
    @Suppress("UNUSED_PARAMETER") val ignored = rev
    RetroDropdown(
        label = label,
        options = options,
        selected = slot.string(key, default).let { if (it in options) it else default },
        modifier = modifier,
        onSelect = { slot.set(key, it); vm.bumpForToolbar() },
    )
}

/** The tempo-synced LFO rate, stored as an index into [TimeDivision]. */
@Composable
private fun LfoRateDropdown(vm: AppViewModel, slot: ToolboxSlot, rev: Int, modifier: Modifier) {
    @Suppress("UNUSED_PARAMETER") val ignored = rev
    val current = TimeDivision.fromIndex(slot.float("lfodiv", 3f).toInt())
    RetroDropdown(
        label = "RATE",
        options = TimeDivision.entries,
        selected = current,
        modifier = modifier,
        optionLabel = { it.label },
        onSelect = { slot.set("lfodiv", it.ordinal.toFloat()); vm.bumpForToolbar() },
    )
}
