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

package space.rcmd.android.sizzle.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import space.rcmd.android.sizzle.ui.theme.LocalRetro

/**
 * One octave laid out as a real piano keyboard: seven white keys across the bottom
 * with the five black keys overlaid on top, each centred on the gap between the two
 * white keys it sits between (C# D# _ F# G# A#).
 *
 * Input is handled at the octave level so a finger can **slide across keys** — a
 * glissando: the note under each finger retriggers as it crosses a key boundary
 * (black keys win in their upper region, exactly like a physical keyboard), and
 * multiple fingers play independently (chords still work). For each pitch class the
 * caller supplies its [label], whether it's [enabled] (out-of-scale keys are dimmed
 * and skipped), whether it's currently [pressed] (visual), and [onNoteOn] /
 * [onNoteOff] to sound it — [onNoteOn]'s `slid` flag is false on the initial press
 * and true when a finger slides onto the key, so callers can treat the two
 * differently (e.g. only punch-record the initial press).
 */
@Composable
fun PianoOctave(
    label: (pitchClass: Int) -> String,
    enabled: (pitchClass: Int) -> Boolean,
    pressed: (pitchClass: Int) -> Boolean,
    onNoteOn: (pitchClass: Int, slid: Boolean) -> Unit,
    onNoteOff: (pitchClass: Int) -> Unit,
    modifier: Modifier = Modifier,
    blackWidthFraction: Float = 0.62f,
    blackHeightFraction: Float = 0.62f,
) {
    // Latest callbacks, so the long-lived gesture always calls the current ones
    // (octave / scale changes swap the lambdas without restarting the gesture).
    val onOn by rememberUpdatedState(onNoteOn)
    val onOff by rememberUpdatedState(onNoteOff)
    val isEnabled by rememberUpdatedState(enabled)

    BoxWithConstraints(
        modifier.pointerInput(Unit) {
            awaitEachGesture {
                // Per-pointer current pitch class (-1 = on a disabled key or off the
                // keyboard). Independent pointers → chords; a moving pointer → glissando.
                val active = HashMap<PointerId, Int>()
                do {
                    val event = awaitPointerEvent()
                    for (ch in event.changes) {
                        if (ch.pressed) {
                            val raw = pcAt(ch.position, size, blackWidthFraction, blackHeightFraction)
                            val pc = if (raw in 0..11 && isEnabled(raw)) raw else -1
                            val prev = active[ch.id]
                            when {
                                prev == null -> { active[ch.id] = pc; if (pc >= 0) onOn(pc, false) }
                                pc != prev -> {
                                    if (prev >= 0) onOff(prev)
                                    if (pc >= 0) onOn(pc, true)
                                    active[ch.id] = pc
                                }
                            }
                            ch.consume()
                        } else {
                            val prev = active.remove(ch.id)
                            if (prev != null && prev >= 0) onOff(prev)
                            ch.consume()
                        }
                    }
                } while (active.isNotEmpty())
            }
        },
    ) {
        val whiteW = maxWidth / 7
        val blackW = whiteW * blackWidthFraction
        val blackH = maxHeight * blackHeightFraction
        // White keys: C D E F G A B, filling the width evenly.
        Row(Modifier.fillMaxSize()) {
            for (pc in WHITE_PCS) {
                PianoKey(label(pc), isBlack = false, enabled = enabled(pc), selected = false,
                    modifier = Modifier.weight(1f).fillMaxHeight(), pressed = pressed(pc))
            }
        }
        // Black keys, each centred on the boundary after a given white-key index,
        // drawn last so they sit on top of the white keys they overlap.
        for ((pc, afterWhite) in BLACK_PCS) {
            PianoKey(
                label(pc), isBlack = true, enabled = enabled(pc), selected = false,
                modifier = Modifier
                    .offset(x = whiteW * (afterWhite + 1) - blackW / 2)
                    .width(blackW)
                    .height(blackH),
                pressed = pressed(pc),
            )
        }
    }
}

/** Hit-test a touch position to a pitch class (0..11), or -1 if off the keyboard.
 *  Black keys occupy their narrower upper region; below that (and between them) the
 *  white key at that x wins — matching how the keys are drawn. */
private fun pcAt(pos: Offset, size: IntSize, blackWidthFraction: Float, blackHeightFraction: Float): Int {
    val w = size.width.toFloat()
    val h = size.height.toFloat()
    if (w <= 0f || h <= 0f || pos.x < 0f || pos.x >= w || pos.y < 0f || pos.y >= h) return -1
    val whiteW = w / 7f
    if (pos.y <= h * blackHeightFraction) {
        val blackW = whiteW * blackWidthFraction
        for ((pc, afterWhite) in BLACK_PCS) {
            val bx = whiteW * (afterWhite + 1) - blackW / 2f
            if (pos.x >= bx && pos.x <= bx + blackW) return pc
        }
    }
    val wi = (pos.x / whiteW).toInt().coerceIn(0, 6)
    return WHITE_PCS[wi]
}

/**
 * A single piano key face: light for white keys, dark for black, dimmed when
 * [enabled] is false and accent-filled when [selected]. [pressed] lights the key
 * in the accent colour while it is held down (from any input source) — the visual
 * feedback shared by every keyboard widget. The gesture (if any) is supplied by the
 * caller through [modifier].
 */
@Composable
fun PianoKey(
    label: String,
    isBlack: Boolean,
    enabled: Boolean,
    selected: Boolean,
    modifier: Modifier,
    pressed: Boolean = false,
) {
    val c = LocalRetro.current
    val lit = selected || pressed
    val bg = when {
        lit -> c.accent
        isBlack -> if (enabled) BLACK_KEY else BLACK_KEY_OFF
        else -> if (enabled) WHITE_KEY else WHITE_KEY_OFF
    }
    val fg = when {
        lit -> c.background
        isBlack -> if (enabled) Color(0xFFDDDDDD) else Color(0xFF555555)
        else -> if (enabled) Color(0xFF141414) else Color(0xFF6A6A6A)
    }
    Box(
        modifier
            .background(bg)
            .border(1.dp, if (lit) c.accent else KEY_BORDER, RectangleShape),
        contentAlignment = Alignment.BottomCenter,
    ) {
        Text(
            label, color = fg, fontFamily = FontFamily.Monospace, fontSize = 12.sp,
            maxLines = 1, softWrap = false,
            modifier = Modifier.padding(bottom = 3.dp),
        )
    }
}

private val WHITE_PCS = intArrayOf(0, 2, 4, 5, 7, 9, 11)          // C D E F G A B
// pitch class -> index of the white key it sits immediately after (0=C .. 6=B).
private val BLACK_PCS = listOf(1 to 0, 3 to 1, 6 to 3, 8 to 4, 10 to 5)
private val WHITE_KEY = Color(0xFFE8E8E8)
private val WHITE_KEY_OFF = Color(0xFF5A5A5A)
private val BLACK_KEY = Color(0xFF181820)
private val BLACK_KEY_OFF = Color(0xFF0C0C10)
private val KEY_BORDER = Color(0xFF000000)
