package ca.ksamad.encore.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
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.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.automirrored.filled.QueueMusic
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.Cast
import androidx.compose.material.icons.filled.LibraryMusic
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ca.ksamad.encore.mpd.model.MpdSong
import ca.ksamad.encore.mpd.model.MpdStatus
import ca.ksamad.encore.mpd.model.PlayerState
import ca.ksamad.encore.playback.SongArt
import kotlinx.coroutines.delay

/** Largest the flexible album art is allowed to grow, so it doesn't sprawl on tablets/foldables. */
private val ART_MAX_SIZE = 480.dp

/**
 * Minimum usable height at which we trust the whole screen fits without scrolling (and switch to the
 * flexible, art-fills-the-slack layout). Below this — landscape, small phones — we scroll instead so
 * the lower controls never get clipped.
 */
private val FLEX_MIN_HEIGHT = 740.dp

/**
 * The now-playing screen: current track, a live seek bar, transport controls, volume, and the
 * repeat/random toggles. Everything reads from the [PlayerViewModel] flows, so it updates whenever
 * the server pushes a change — including changes made from other clients.
 */
@Composable
fun NowPlayingScreen(
    vm: PlayerViewModel,
    onOpenSettings: () -> Unit,
    onOpenLibrary: () -> Unit,
    onOpenQueue: () -> Unit,
) {
    val status by vm.status.collectAsStateWithLifecycle()
    val song by vm.currentSong.collectAsStateWithLifecycle()
    var showDisconnectConfirm by remember { mutableStateOf(false) }

    // On a tall enough screen, lay everything out without scrolling and let the album art
    // flex into the leftover vertical space (capped at ART_MAX_SIZE) — bigger displays show
    // bigger art, smaller ones just shrink it. On a short viewport (landscape, small phones)
    // fall back to a scrolling column with a fixed art size so the lower controls stay reachable.
    val scrollState = rememberScrollState()
    BoxWithConstraints(modifier = Modifier.fillMaxSize().systemBarsPadding()) {
        val flexible = maxHeight >= FLEX_MIN_HEIGHT
        // top = 0: the icon buttons carry their own internal padding, and systemBarsPadding
        // already clears the status bar — any extra top padding just pushes them down.
        val columnModifier =
            if (flexible) {
                Modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, bottom = 12.dp)
            } else {
                Modifier.fillMaxSize()
                    .verticalScroll(scrollState)
                    .padding(start = 24.dp, end = 24.dp, bottom = 24.dp)
            }

        Column(modifier = columnModifier, horizontalAlignment = Alignment.CenterHorizontally) {
            // --- Top bar: disconnect (left) + queue/library/settings (right) ----
            Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
                IconButton(onClick = { showDisconnectConfirm = true }) {
                    Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = "Disconnect")
                }
                Spacer(Modifier.weight(1f))
                IconButton(onClick = onOpenQueue) {
                    Icon(Icons.AutoMirrored.Filled.QueueMusic, contentDescription = "Queue")
                }
                IconButton(onClick = onOpenLibrary) {
                    Icon(Icons.Filled.LibraryMusic, contentDescription = "Albums")
                }
                IconButton(onClick = onOpenSettings) {
                    Icon(Icons.Filled.Settings, contentDescription = "Settings")
                }
            }

            // --- Album art ---------------------------------------------------
            val artModel = song?.let { SongArt(it.uri, it.lastModified) }
            if (flexible) {
                // Fill the leftover height, kept square and capped by width/height/max.
                BoxWithConstraints(
                    modifier = Modifier.weight(1f).fillMaxWidth().padding(vertical = 12.dp),
                    contentAlignment = Alignment.Center,
                ) {
                    val side = minOf(maxWidth, maxHeight, ART_MAX_SIZE)
                    ArtImage(
                        model = artModel,
                        iconSize = 96.dp,
                        modifier = Modifier.size(side).clip(RoundedCornerShape(16.dp)),
                    )
                }
            } else {
                ArtImage(
                    model = artModel,
                    iconSize = 96.dp,
                    modifier =
                        Modifier.padding(vertical = 16.dp)
                            .fillMaxWidth(0.9f)
                            .aspectRatio(1f)
                            .clip(RoundedCornerShape(16.dp)),
                )
            }

            PlaybackDetails(vm = vm, status = status, song = song)
        }
    }

    if (showDisconnectConfirm) {
        AlertDialog(
            onDismissRequest = { showDisconnectConfirm = false },
            title = { Text("Disconnect?") },
            text = { Text("Disconnect from the server and return to the connect screen.") },
            confirmButton = {
                TextButton(
                    onClick = {
                        showDisconnectConfirm = false
                        vm.disconnect()
                    }
                ) {
                    Text("Disconnect")
                }
            },
            dismissButton = {
                TextButton(onClick = { showDisconnectConfirm = false }) { Text("Cancel") }
            },
        )
    }
}

/**
 * Everything below the album art: track metadata, the seek bar, transport, volume, and the option
 * chips. Emitted straight into the caller's centered [Column] so it's shared by both the flexible
 * (no-scroll) and scrolling layouts.
 */
@Composable
private fun PlaybackDetails(
    vm: PlayerViewModel,
    status: MpdStatus?,
    song: MpdSong?,
) {
    // --- Track metadata --------------------------------------------------
    Text(
        text = song?.title ?: song?.uri ?: "Nothing playing",
        style = MaterialTheme.typography.headlineSmall,
        textAlign = TextAlign.Center,
        maxLines = 2,
        overflow = TextOverflow.Ellipsis,
    )
    Text(
        text = song?.artist ?: "—",
        style = MaterialTheme.typography.titleMedium,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
        modifier = Modifier.padding(top = 4.dp),
        maxLines = 1,
        overflow = TextOverflow.Ellipsis,
    )
    Text(
        text = song?.album ?: "",
        style = MaterialTheme.typography.bodyMedium,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
        maxLines = 1,
        overflow = TextOverflow.Ellipsis,
    )

    // Descriptive metadata (release year · genre), when the tags are present.
    val descriptors =
        listOfNotNull(
            song?.date?.let(::releaseYear),
            song?.genre?.takeIf { it.isNotBlank() },
        )
    if (descriptors.isNotEmpty()) {
        Text(
            text = descriptors.joinToString(" · "),
            style = MaterialTheme.typography.bodySmall,
            color = MaterialTheme.colorScheme.onSurfaceVariant,
            modifier = Modifier.padding(top = 2.dp),
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
        )
    }

    AudioPropertyPills(status = status)

    Spacer(Modifier.height(24.dp))

    SeekBar(status = status, onSeek = { vm.seekTo(it) })

    Spacer(Modifier.height(16.dp))

    // --- Transport (repeat · prev · play/pause · next · shuffle) ---------
    // Repeat and shuffle are icon-only toggles — their icons are well-known, so no
    // label is needed. They tint to the primary colour when on, muted when off.
    val playing = status?.state == PlayerState.PLAY
    val repeatOn = status?.repeat == true
    val shuffleOn = status?.random == true
    Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        IconButton(onClick = { vm.setRepeat(!repeatOn) }, modifier = Modifier.size(48.dp)) {
            Icon(
                Icons.Filled.Repeat,
                contentDescription = if (repeatOn) "Repeat on" else "Repeat off",
                tint =
                    if (repeatOn) MaterialTheme.colorScheme.primary
                    else MaterialTheme.colorScheme.onSurfaceVariant,
                modifier = Modifier.size(26.dp),
            )
        }
        IconButton(onClick = vm::previous, modifier = Modifier.size(56.dp)) {
            Icon(
                Icons.Filled.SkipPrevious,
                contentDescription = "Previous",
                modifier = Modifier.size(36.dp),
            )
        }
        FilledIconButton(onClick = vm::togglePlayPause, modifier = Modifier.size(72.dp)) {
            Icon(
                if (playing) Icons.Filled.Pause else Icons.Filled.PlayArrow,
                contentDescription = if (playing) "Pause" else "Play",
                modifier = Modifier.size(40.dp),
            )
        }
        IconButton(onClick = vm::next, modifier = Modifier.size(56.dp)) {
            Icon(
                Icons.Filled.SkipNext,
                contentDescription = "Next",
                modifier = Modifier.size(36.dp),
            )
        }
        IconButton(onClick = { vm.setRandom(!shuffleOn) }, modifier = Modifier.size(48.dp)) {
            Icon(
                Icons.Filled.Shuffle,
                contentDescription = if (shuffleOn) "Shuffle on" else "Shuffle off",
                tint =
                    if (shuffleOn) MaterialTheme.colorScheme.primary
                    else MaterialTheme.colorScheme.onSurfaceVariant,
                modifier = Modifier.size(26.dp),
            )
        }
    }

    Spacer(Modifier.height(20.dp))

    VolumeControl(volume = status?.volume, onSetVolume = { vm.setVolume(it) })
}

/**
 * Seek bar that ticks locally between server updates. MPD only pushes a change event on discrete
 * events (play/pause/seek/song change), not once per second, so we advance [MpdStatus.elapsed]
 * locally while playing to keep the bar moving, resyncing whenever a fresh status arrives.
 */
@Composable
private fun SeekBar(
    status: MpdStatus?,
    onSeek: (Double) -> Unit,
) {
    val duration = status?.duration ?: 0.0
    val playing = status?.state == PlayerState.PLAY

    // Local playback position, reseeded on every new status snapshot.
    var position by
        remember(status?.songId, status?.elapsed, status?.state) {
            mutableFloatStateOf((status?.elapsed ?: 0.0).toFloat())
        }
    var dragValue by remember { mutableStateOf<Float?>(null) }

    // Advance ~4x/sec while playing and not being dragged.
    LaunchedEffect(status?.songId, status?.elapsed, status?.state) {
        if (!playing) return@LaunchedEffect
        while (true) {
            delay(250)
            if (dragValue == null && position < duration) position += 0.25f
        }
    }

    val shown = dragValue ?: position
    Slider(
        value = shown.coerceIn(0f, duration.toFloat().coerceAtLeast(0f)),
        onValueChange = { dragValue = it },
        onValueChangeFinished = {
            dragValue?.let { onSeek(it.toDouble()) }
            dragValue = null
        },
        valueRange = 0f..duration.toFloat().coerceAtLeast(1f),
        enabled = status != null && duration > 0,
        modifier = Modifier.fillMaxWidth(),
    )
    Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
        Text(formatTime(shown.toDouble()), style = MaterialTheme.typography.labelMedium)
        Text(formatTime(duration), style = MaterialTheme.typography.labelMedium)
    }
}

/**
 * Little pills describing the current stream's audio format — sample rate, bit depth, channels, and
 * (live) bitrate — derived from MPD's `audio`/`bitrate` status fields. Renders nothing when there's
 * no format info (e.g. stopped).
 */
@Composable
private fun AudioPropertyPills(status: MpdStatus?) {
    val pills =
        remember(status?.audio, status?.bitrate) {
            buildList {
                addAll(audioFormatPills(status?.audio))
                status?.bitrate?.takeIf { it > 0 }?.let { add("$it kbps") }
            }
        }
    AudioFormatPills(pills = pills, modifier = Modifier.padding(top = 10.dp))
}

/** "Casting" affordance: signals that the volume below controls the server, not the device. */
@Composable
private fun CastIndicator(host: String?) {
    if (host == null) return
    Row(
        modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Center,
    ) {
        Icon(
            Icons.Filled.Cast,
            contentDescription = null,
            modifier = Modifier.size(16.dp),
            tint = MaterialTheme.colorScheme.primary,
        )
        Text(
            text = "Controlling $host",
            style = MaterialTheme.typography.labelMedium,
            color = MaterialTheme.colorScheme.primary,
            modifier = Modifier.padding(start = 6.dp),
        )
    }
}

@Composable
private fun VolumeControl(
    volume: Int?,
    onSetVolume: (Int) -> Unit,
) {
    // Local thumb position seeded from the server; committed on release.
    var dragValue by remember(volume) { mutableStateOf<Float?>(null) }
    val enabled = volume != null
    val shown = dragValue ?: (volume ?: 0).toFloat()

    Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
        Icon(
            Icons.AutoMirrored.Filled.VolumeUp,
            contentDescription = "Volume",
            modifier = Modifier.size(24.dp),
        )
        Slider(
            value = shown,
            onValueChange = { dragValue = it },
            onValueChangeFinished = {
                dragValue?.let { onSetVolume(it.toInt()) }
                dragValue = null
            },
            valueRange = 0f..100f,
            enabled = enabled,
            modifier = Modifier.weight(1f).padding(horizontal = 12.dp),
        )
        Text(
            text = if (enabled) "${shown.toInt()}" else "—",
            style = MaterialTheme.typography.labelMedium,
            modifier = Modifier.size(width = 32.dp, height = 20.dp),
            textAlign = TextAlign.End,
        )
    }
}

/** The 4-digit year from an MPD `Date` tag (`"2019"`, `"2019-05-03"`, …), or null. */
private fun releaseYear(date: String): String? = Regex("\\d{4}").find(date)?.value

private fun formatTime(seconds: Double): String {
    val total = seconds.toInt().coerceAtLeast(0)
    val m = total / 60
    val s = total % 60
    return "%d:%02d".format(m, s)
}
