package io.unom.punktfunk

import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeSource
import io.unom.punktfunk.kit.deviceBodyVibrator

// The gamepad-driven settings screen — the Android mirror of the Apple client's GamepadSettingsView:
// the couch-relevant subset of the touch settings restyled as a console page and fully navigable with
// a controller: up/down moves the focus bar, left/right steps the focused value, A cycles/toggles it,
// B closes. Both write the same SharedPreferences, so values round-trip with the touch settings.

private class GpRow(
    val id: String,
    val header: String?,
    val label: String,
    val value: String,
    val detail: String,
    val adjust: (Int) -> Boolean, // left/right; returns whether the value actually changed
    val activate: () -> Unit,     // A → cycle forward (wrapping) / flip
    val toggled: Boolean? = null, // non-null = a toggle row, drawn as a ConsoleSwitch (not text)
)

@Composable
fun GamepadSettingsScreen(
    initial: Settings,
    onChange: (Settings) -> Unit,
    onBack: () -> Unit,
    navActive: Boolean = true, // false while this screen is cross-fading out, so it drops the pad
) {
    var s by remember { mutableStateOf(initial) }
    fun update(next: Settings) { s = next; onChange(next) }

    val context = LocalContext.current
    // Gates the "Rumble on this phone" row — a TV box has no body vibrator to mirror onto.
    val hasBodyVibrator = remember { deviceBodyVibrator(context) != null }
    val rows = buildSettingsRows(s, hasBodyVibrator, ::update)
    var focus by remember { mutableIntStateOf(0) }
    if (focus > rows.lastIndex) focus = rows.lastIndex
    // The direction the focused value last stepped (+1 forward / -1 back) — drives which way the
    // value text slides in its AnimatedContent, so the motion matches the button press.
    var adjustDir by remember { mutableIntStateOf(1) }
    val listState = rememberLazyListState()

    val landscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE

    BackHandler(onBack = onBack)
    GamepadNavEffect2D(
        active = navActive,
        onDirection = { dir ->
            when (dir) {
                NavDir.UP -> if (focus > 0) focus--
                NavDir.DOWN -> if (focus < rows.lastIndex) focus++
                NavDir.LEFT -> { adjustDir = -1; rows.getOrNull(focus)?.adjust(-1) }
                NavDir.RIGHT -> { adjustDir = 1; rows.getOrNull(focus)?.adjust(1) }
            }
        },
        onActivate = { adjustDir = 1; rows.getOrNull(focus)?.activate() },
    )
    // Keep the focused row on screen, but only SCROLL when it's actually off-screen — so entering the
    // screen (focus on the first row) leaves the "Settings" heading visible instead of jumping past it.
    // +1 accounts for the heading being item 0.
    LaunchedEffect(focus) {
        runCatching {
            val itemIndex = focus + 1
            val info = listState.layoutInfo
            val item = info.visibleItemsInfo.firstOrNull { it.index == itemIndex }
            val offScreen = item == null ||
                item.offset < info.viewportStartOffset ||
                item.offset + item.size > info.viewportEndOffset - 96 // keep clear of the floating legend
            if (offScreen) listState.animateScrollToItem(itemIndex)
        }
    }

    val hazeState = remember { HazeState() }

    Box(Modifier.fillMaxSize()) {
        // Everything scrolls — including the heading — so nothing is pinned. Vital in landscape,
        // where a fixed title + a fixed detail/legend strip ate most of the (short) height.
        Box(Modifier.fillMaxSize().hazeSource(hazeState)) {
            GamepadFormBackground(Modifier.fillMaxSize())
            LazyColumn(
                state = listState,
                modifier = Modifier.fillMaxSize().systemBarsPadding(),
                contentPadding = PaddingValues(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 104.dp),
                verticalArrangement = Arrangement.spacedBy(6.dp),
            ) {
            item(key = "__title") {
                ConsoleHeader("Settings", horizontalInset = false)
            }
            itemsIndexed(rows, key = { _, r -> r.id }) { index, row ->
                SettingRowView(row, focused = index == focus, adjustDir = adjustDir, onClick = {
                    if (focus == index) { adjustDir = 1; row.activate() } else focus = index
                })
            }
            }
        }

        // Floating frosted legend — a real backdrop blur of the rows scrolling behind it (no dedicated
        // strip). In landscape it ignores the safe area so it hugs the corner instead of the nav-bar inset.
        Box(
            Modifier
                .align(Alignment.BottomStart)
                .then(if (landscape) Modifier else Modifier.systemBarsPadding())
                .padding(ConsoleLegendInset),
        ) {
            GamepadHintBar(
                listOf(
                    GamepadHint('↔', Color(0xFF9A93C7), "Adjust"),
                    // Tappable too (touch escape hatch): Change cycles the focused row, Done leaves.
                    PadGlyph.hint('A', "Change") { rows.getOrNull(focus)?.activate() },
                    PadGlyph.hint('B', "Done", onClick = onBack),
                ),
                hazeState = hazeState,
            )
        }
    }
}

@Composable
private fun SettingRowView(row: GpRow, focused: Boolean, adjustDir: Int, onClick: () -> Unit) {
    val visuals = animateConsoleFocus(active = focused)
    val shape = RoundedCornerShape(14.dp)
    // The chevrons keep their layout slot and only fade, so the value never jumps sideways when
    // focus arrives; the value colour cross-fades with them.
    val chevronAlpha by animateFloatAsState(if (focused) 0.6f else 0f, tween(160), label = "chevrons")
    val valueColor by animateColorAsState(
        Color.White.copy(alpha = if (focused) 1f else 0.6f),
        tween(160),
        label = "valueColor",
    )
    Column {
        if (row.header != null) {
            Text(
                row.header.uppercase(),
                style = MaterialTheme.typography.labelMedium,
                color = Color.White.copy(alpha = 0.45f),
                letterSpacing = 1.4.sp,
                modifier = Modifier.padding(start = 16.dp, top = 14.dp, bottom = 4.dp),
            )
        }
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .graphicsLayer { scaleX = visuals.scale; scaleY = visuals.scale }
                .clip(shape)
                .background(visuals.background)
                .border(1.dp, visuals.border, shape)
                .clickable(
                    interactionSource = remember { MutableInteractionSource() },
                    indication = null,
                    onClick = onClick,
                )
                .padding(horizontal = 16.dp, vertical = 13.dp),
        ) {
            Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
                Text(
                    row.label,
                    style = MaterialTheme.typography.bodyLarge,
                    fontWeight = FontWeight.SemiBold,
                    color = Color.White,
                    maxLines = 1,
                )
                Spacer(Modifier.weight(1f))
                if (row.toggled != null) {
                    // A toggle is a switch, not text — the sliding knob + tinting track IS the value.
                    ConsoleSwitch(on = row.toggled, focused = focused)
                } else {
                    Text("‹ ", color = Color.White, modifier = Modifier.graphicsLayer { alpha = chevronAlpha })
                    // The value slides in the direction it was stepped and its width animates, so
                    // cycling a choice reads as motion through a list rather than a text swap.
                    AnimatedContent(
                        targetState = row.value,
                        transitionSpec = {
                            val dir = adjustDir
                            (slideInHorizontally(tween(180)) { w -> w / 2 * dir } + fadeIn(tween(180))) togetherWith
                                (slideOutHorizontally(tween(140)) { w -> -w / 2 * dir } + fadeOut(tween(100))) using
                                SizeTransform(clip = false)
                        },
                        label = "value",
                    ) { value ->
                        Text(
                            value,
                            style = MaterialTheme.typography.bodyMedium,
                            color = valueColor,
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis,
                        )
                    }
                    Text(" ›", color = Color.White, modifier = Modifier.graphicsLayer { alpha = chevronAlpha })
                }
            }
            // The focused row carries its own one-line description — no dedicated (space-eating)
            // detail strip. It unfolds right where you're looking, and the row grows to fit.
            AnimatedVisibility(
                visible = focused && row.detail.isNotBlank(),
                enter = fadeIn(tween(180, delayMillis = 60)) + expandVertically(tween(180)),
                exit = fadeOut(tween(90)) + shrinkVertically(tween(150)),
            ) {
                Text(
                    row.detail,
                    style = MaterialTheme.typography.bodySmall,
                    color = Color.White.copy(alpha = 0.6f),
                    maxLines = 2,
                    modifier = Modifier.padding(top = 6.dp),
                )
            }
        }
    }
}

/** Build the console settings rows from the current [Settings], writing through [update].
 * [hasBodyVibrator] gates the "Rumble on this phone" row (absent on TVs). */
private fun buildSettingsRows(
    s: Settings,
    hasBodyVibrator: Boolean,
    update: (Settings) -> Unit,
): List<GpRow> {
    fun <T> choice(
        id: String, header: String?, label: String, detail: String,
        options: List<Pair<T, String>>, current: T, write: (T) -> Unit,
    ): GpRow {
        val idx = options.indexOfFirst { it.first == current }
        return GpRow(
            id, header, label,
            value = options.getOrNull(idx)?.second ?: "—",
            detail = detail,
            adjust = { delta ->
                if (idx < 0) {
                    options.firstOrNull()?.let { write(it.first) } != null
                } else {
                    val t = idx + delta
                    if (t in options.indices) { write(options[t].first); true } else false
                }
            },
            activate = {
                val i = if (idx < 0) 0 else (idx + 1) % options.size
                options.getOrNull(i)?.let { write(it.first) }
            },
        )
    }
    fun toggle(
        id: String, header: String?, label: String, detail: String,
        value: Boolean, write: (Boolean) -> Unit,
    ): GpRow = GpRow(
        id, header, label,
        value = if (value) "On" else "Off",
        detail = detail,
        adjust = { delta -> val target = delta > 0; if (value != target) { write(target); true } else false },
        activate = { write(!value) },
        toggled = value,
    )

    return listOf(
        choice(
            "resolution", "Stream", "Resolution",
            "The host creates a virtual display at exactly this size — no scaling. " +
                "Custom sizes are typed in the touch settings.",
            // A custom size (typed in the touch settings) leads the list so it stays visible and
            // selectable here instead of being silently snapped to Native — a pad can keep a
            // custom size, it just can't type one.
            (if (s.isCustomResolution()) {
                listOf((s.width to s.height) to "Custom · ${s.width} × ${s.height}")
            } else {
                emptyList()
            }) + RESOLUTION_OPTIONS.map { (w, h, lbl) -> (w to h) to lbl },
            s.width to s.height,
        ) { (w, h) -> update(s.copy(width = w, height = h)) },
        choice(
            "refresh", null, "Refresh rate", "Frame rate the host renders and streams at.",
            REFRESH_OPTIONS, s.hz,
        ) { update(s.copy(hz = it)) },
        choice(
            "bitrate", null, "Bitrate",
            "Automatic uses the host's default. Run a speed test from the touch UI for an informed value.",
            BITRATE_OPTIONS, s.bitrateKbps,
        ) { update(s.copy(bitrateKbps = it)) },
        choice(
            "compositor", null, "Compositor",
            "Which compositor drives the virtual output — honored only if available on the host.",
            COMPOSITOR_OPTIONS.mapIndexed { i, lbl -> i to lbl }, s.compositor,
        ) { update(s.copy(compositor = it)) },

        choice(
            "codec", "Video", "Video codec",
            "A preference — the host falls back if it can't encode this one.",
            CODEC_OPTIONS, s.codec,
        ) { update(s.copy(codec = it)) },
        toggle(
            "hdr", null, "10-bit HDR",
            "HDR10 — engages when the host sends HDR content and this display supports it.",
            s.hdrEnabled,
        ) { update(s.copy(hdrEnabled = it)) },
        toggle(
            "lowLatency", null, "Low-latency mode",
            "The fast pipeline (async decode + system tuning). On by default — turn off to fall back if the stream stutters or glitches.",
            s.lowLatencyMode,
        ) { update(s.copy(lowLatencyMode = it)) },

        choice(
            "audio", "Audio", "Audio channels", "The speaker layout requested from the host.",
            AUDIO_CHANNEL_OPTIONS, s.audioChannels,
        ) { update(s.copy(audioChannels = it)) },
        toggle(
            "mic", null, "Microphone", "Send this device's microphone to the host's virtual mic.",
            s.micEnabled,
        ) { update(s.copy(micEnabled = it)) },

        choice(
            "padType", "Controller", "Controller type",
            "The virtual pad the host creates — Automatic matches this controller.",
            GAMEPAD_OPTIONS.mapIndexed { i, lbl -> i to lbl }, s.gamepad,
        ) { update(s.copy(gamepad = it)) },
    ) + listOfNotNull(
        if (hasBodyVibrator) {
            toggle(
                "phoneRumble", null, "Rumble on this phone",
                "Also play controller 1's rumble on this phone's own vibration motor — " +
                    "for clip-on pads without rumble motors.",
                s.rumbleOnPhone,
            ) { update(s.copy(rumbleOnPhone = it)) }
        } else {
            null
        },
    ) + listOf(
        choice(
            "hud", "Interface", "Statistics overlay",
            "How much the overlay shows: Compact (one line) → Normal → Detailed (full HUD). " +
                "A 3-finger tap cycles the tiers live.",
            STATS_VERBOSITY_OPTIONS, s.statsVerbosity,
        ) { update(s.copy(statsVerbosity = it)) },
        toggle(
            "library", null, "Game library",
            "Browse a paired host's games with Y (experimental).",
            s.libraryEnabled,
        ) { update(s.copy(libraryEnabled = it)) },
        toggle(
            "autoWake", null, "Auto-wake on connect",
            "Wake a saved host with Wake-on-LAN when it isn't seen on the network, then connect.",
            s.autoWakeEnabled,
        ) { update(s.copy(autoWakeEnabled = it)) },
        toggle(
            "gamepadUI", null, "Controller-optimized UI",
            "Turn off to use the touch interface even with a controller connected.",
            s.gamepadUiEnabled,
        ) { update(s.copy(gamepadUiEnabled = it)) },
    )
}
