package com.peterino.blastoise

import android.Manifest
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.InputType
import android.text.TextUtils
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.SeekBar
import android.widget.TextView
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import java.util.concurrent.Executor
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

private class CenteredIconButton(context: Context) : View(context) {
    private var icon: Drawable? = null
    private var iconSizePx = 0

    init {
        isClickable = true
        isFocusable = true
    }

    fun setIcon(drawable: Drawable?, color: Int, sizePx: Int) {
        icon = drawable?.mutate()?.apply { setTint(color) }
        iconSizePx = sizePx
        invalidate()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val drawable = icon ?: return
        val availableWidth = width - paddingLeft - paddingRight
        val availableHeight = height - paddingTop - paddingBottom
        val size = min(iconSizePx, min(availableWidth, availableHeight)).coerceAtLeast(0)
        val left = paddingLeft + (availableWidth - size) / 2
        val top = paddingTop + (availableHeight - size) / 2
        drawable.setBounds(left, top, left + size, top + size)
        drawable.draw(canvas)
    }
}

private class PixelCatView(context: Context) : View(context) {
    private val paint = Paint().apply {
        style = Paint.Style.FILL
        isAntiAlias = false
    }
    private val animator = Handler(Looper.getMainLooper())
    private var frame = 0
    private var progressFraction = 0.5f
    private var bodyColor = 0xFF050607.toInt()
    private var edgeColor = 0xFFA8E76C.toInt()
    private var eyeColor = 0xFFC8FF83.toInt()
    private var accentColor = 0xFFFF7AA8.toInt()
    private var shadowColor = 0x66374134

    private val tick = object : Runnable {
        override fun run() {
            frame = (frame + 1) % 4
            invalidate()
            animator.postDelayed(this, 520)
        }
    }

    init {
        setWillNotDraw(false)
        isClickable = false
        isFocusable = false
        importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
    }

    fun setProgressFraction(value: Float) {
        val next = value.coerceIn(0.04f, 0.96f)
        if (abs(progressFraction - next) > 0.002f) {
            progressFraction = next
            invalidate()
        }
    }

    fun setPalette(body: Int, edge: Int, eye: Int, accent: Int, shadow: Int) {
        bodyColor = body
        edgeColor = edge
        eyeColor = eye
        accentColor = accent
        shadowColor = shadow
        invalidate()
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        animator.removeCallbacks(tick)
        animator.post(tick)
    }

    override fun onDetachedFromWindow() {
        animator.removeCallbacks(tick)
        super.onDetachedFromWindow()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width <= 0 || height <= 0) return

        val gridWidth = 43f
        val gridHeight = 25f
        val lineClearance = resources.displayMetrics.density * 31f
        val availableHeight = (height - paddingTop - paddingBottom).toFloat() - lineClearance
        val unit = min(
            max(2.4f, resources.displayMetrics.density * 2.9f),
            max(2.0f, availableHeight / gridHeight),
        )
        val catWidth = gridWidth * unit
        val minX = paddingLeft.toFloat() + unit
        val maxX = (width - paddingRight).toFloat() - catWidth - unit
        val x = if (maxX > minX) minX + (maxX - minX) * progressFraction else minX
        val breathe = if (frame == 1) -unit * 0.35f else 0f
        val y = (
            height - paddingBottom - lineClearance - gridHeight * unit + breathe
        ).coerceAtLeast(paddingTop + unit * 0.5f)
        val shadowY = y + gridHeight * unit - unit * 0.8f

        rect(canvas, x + unit * 7f, shadowY, unit * 28f, unit, shadowColor)
        drawTail(canvas, x, y, unit)
        drawBody(canvas, x, y, unit)
        drawHead(canvas, x, y, unit)
        drawPurr(canvas, x, y, unit)
    }

    private fun drawTail(canvas: Canvas, x: Float, y: Float, unit: Float) {
        if (frame % 2 == 0) {
            px(canvas, x, y, 6, 16, 4, 1, edgeColor, unit)
            px(canvas, x, y, 5, 14, 1, 2, edgeColor, unit)
            px(canvas, x, y, 6, 12, 1, 2, edgeColor, unit)
            px(canvas, x, y, 7, 11, 2, 1, edgeColor, unit)
            px(canvas, x, y, 9, 12, 1, 1, edgeColor, unit)
            px(canvas, x, y, 10, 13, 1, 4, edgeColor, unit)
            px(canvas, x, y, 11, 17, 4, 1, edgeColor, unit)
            px(canvas, x, y, 15, 18, 2, 1, edgeColor, unit)
            px(canvas, x, y, 6, 14, 2, 2, bodyColor, unit)
            px(canvas, x, y, 7, 12, 2, 1, bodyColor, unit)
            px(canvas, x, y, 11, 14, 1, 3, bodyColor, unit)
        } else {
            px(canvas, x, y, 6, 17, 4, 1, edgeColor, unit)
            px(canvas, x, y, 5, 15, 1, 2, edgeColor, unit)
            px(canvas, x, y, 5, 13, 1, 2, edgeColor, unit)
            px(canvas, x, y, 6, 12, 2, 1, edgeColor, unit)
            px(canvas, x, y, 8, 13, 1, 1, edgeColor, unit)
            px(canvas, x, y, 9, 14, 1, 4, edgeColor, unit)
            px(canvas, x, y, 10, 18, 5, 1, edgeColor, unit)
            px(canvas, x, y, 15, 19, 2, 1, edgeColor, unit)
            px(canvas, x, y, 6, 15, 2, 2, bodyColor, unit)
            px(canvas, x, y, 6, 13, 2, 1, bodyColor, unit)
            px(canvas, x, y, 10, 15, 1, 3, bodyColor, unit)
        }
    }

    private fun drawBody(canvas: Canvas, x: Float, y: Float, unit: Float) {
        px(canvas, x, y, 13, 17, 15, 6, bodyColor, unit)
        px(canvas, x, y, 15, 15, 8, 2, bodyColor, unit)
        px(canvas, x, y, 25, 14, 7, 5, accentColor, unit)
        px(canvas, x, y, 28, 19, 4, 3, accentColor, unit)

        px(canvas, x, y, 13, 17, 1, 6, edgeColor, unit)
        px(canvas, x, y, 14, 16, 1, 1, edgeColor, unit)
        px(canvas, x, y, 15, 15, 8, 1, edgeColor, unit)
        px(canvas, x, y, 23, 16, 2, 1, edgeColor, unit)
        px(canvas, x, y, 25, 14, 7, 1, edgeColor, unit)
        px(canvas, x, y, 32, 15, 1, 8, edgeColor, unit)
        px(canvas, x, y, 14, 23, 18, 1, edgeColor, unit)

        px(canvas, x, y, 19, 21, 3, 3, accentColor, unit)
        px(canvas, x, y, 34, 21, 3, 3, accentColor, unit)
    }

    private fun drawHead(canvas: Canvas, x: Float, y: Float, unit: Float) {
        px(canvas, x, y, 21, 6, 16, 11, bodyColor, unit)
        px(canvas, x, y, 19, 10, 2, 4, bodyColor, unit)
        px(canvas, x, y, 23, 3, 4, 3, bodyColor, unit)
        px(canvas, x, y, 34, 3, 3, 3, bodyColor, unit)

        px(canvas, x, y, 21, 6, 16, 1, edgeColor, unit)
        px(canvas, x, y, 21, 16, 16, 1, edgeColor, unit)
        px(canvas, x, y, 21, 7, 1, 9, edgeColor, unit)
        px(canvas, x, y, 36, 7, 1, 9, edgeColor, unit)
        px(canvas, x, y, 19, 10, 2, 1, edgeColor, unit)
        px(canvas, x, y, 19, 13, 2, 1, edgeColor, unit)
        px(canvas, x, y, 23, 3, 4, 1, edgeColor, unit)
        px(canvas, x, y, 23, 4, 1, 2, edgeColor, unit)
        px(canvas, x, y, 26, 4, 1, 2, edgeColor, unit)
        px(canvas, x, y, 34, 3, 3, 1, edgeColor, unit)
        px(canvas, x, y, 34, 4, 1, 2, edgeColor, unit)
        px(canvas, x, y, 36, 4, 1, 2, edgeColor, unit)

        px(canvas, x, y, 24, 8, 3, 3, eyeColor, unit)
        px(canvas, x, y, 32, 8, 3, 3, eyeColor, unit)
        px(canvas, x, y, 38, 9, 2, 1, edgeColor, unit)
        px(canvas, x, y, 40, 7, 2, 1, edgeColor, unit)
        px(canvas, x, y, 38, 12, 2, 1, edgeColor, unit)
        px(canvas, x, y, 40, 14, 2, 1, edgeColor, unit)
    }

    private fun drawPurr(canvas: Canvas, x: Float, y: Float, unit: Float) {
        if (frame <= 1) {
            px(canvas, x, y, 28, 1, 1, 1, edgeColor, unit)
            px(canvas, x, y, 30, 2, 1, 1, edgeColor, unit)
            px(canvas, x, y, 42, 9, 1, 1, edgeColor, unit)
        }
    }

    private fun px(
        canvas: Canvas,
        originX: Float,
        originY: Float,
        x: Int,
        y: Int,
        width: Int,
        height: Int,
        color: Int,
        unit: Float,
    ) {
        rect(canvas, originX + x * unit, originY + y * unit, width * unit, height * unit, color)
    }

    private fun rect(canvas: Canvas, left: Float, top: Float, width: Float, height: Float, color: Int) {
        paint.color = color
        canvas.drawRect(left, top, left + width, top + height, paint)
    }
}

private class PixelBoxDrawable(
    private val fillColor: Int,
    private val strokeColor: Int?,
    private val unitPx: Float,
    private val strokePx: Float,
) : Drawable() {
    private val path = Path()
    private val fillPaint = Paint().apply {
        style = Paint.Style.FILL
        isAntiAlias = false
    }
    private val strokePaint = Paint().apply {
        style = Paint.Style.STROKE
        strokeJoin = Paint.Join.MITER
        strokeCap = Paint.Cap.SQUARE
        isAntiAlias = false
    }
    private val chipPaint = Paint().apply {
        style = Paint.Style.FILL
        isAntiAlias = false
    }

    override fun draw(canvas: Canvas) {
        val b = bounds
        if (b.width() <= 0 || b.height() <= 0) return

        val u = unitPx.coerceAtMost(min(b.width(), b.height()) / 4f).coerceAtLeast(1f)
        val s = strokePx.coerceAtLeast(1f)
        val l = b.left.toFloat() + s / 2f
        val t = b.top.toFloat() + s / 2f
        val r = b.right.toFloat() - s / 2f
        val bottom = b.bottom.toFloat() - s / 2f

        path.reset()
        path.moveTo(l + u, t)
        path.lineTo(r - (u * 2f), t)
        path.lineTo(r - (u * 2f), t + u)
        path.lineTo(r, t + u)
        path.lineTo(r, bottom - (u * 2f))
        path.lineTo(r - u, bottom - (u * 2f))
        path.lineTo(r - u, bottom)
        path.lineTo(l + (u * 2f), bottom)
        path.lineTo(l + (u * 2f), bottom - u)
        path.lineTo(l, bottom - u)
        path.lineTo(l, t + (u * 2f))
        path.lineTo(l + u, t + (u * 2f))
        path.close()

        fillPaint.color = fillColor
        canvas.drawPath(path, fillPaint)

        val edge = strokeColor ?: return
        strokePaint.color = edge
        strokePaint.strokeWidth = s
        canvas.drawPath(path, strokePaint)

        chipPaint.color = edge
        canvas.drawRect(l, t + (u * 3f), l + s, t + (u * 5f), chipPaint)
        canvas.drawRect(r - s, t + (u * 4f), r, t + (u * 7f), chipPaint)
        canvas.drawRect(l + (u * 4f), bottom - s, l + (u * 7f), bottom, chipPaint)
        canvas.drawRect(r - (u * 7f), t, r - (u * 4f), t + s, chipPaint)
    }

    override fun setAlpha(alpha: Int) {
        fillPaint.alpha = alpha
        strokePaint.alpha = alpha
        chipPaint.alpha = alpha
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        fillPaint.colorFilter = colorFilter
        strokePaint.colorFilter = colorFilter
        chipPaint.colorFilter = colorFilter
    }

    @Deprecated("Deprecated in Java")
    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}

private class SeraphBoxDrawable(
    private val fillColor: Int,
    private val strokeColor: Int?,
    private val unitPx: Float,
    private val strokePx: Float,
) : Drawable() {
    private val path = Path()
    private val fillPaint = Paint().apply {
        style = Paint.Style.FILL
        isAntiAlias = true
    }
    private val strokePaint = Paint().apply {
        style = Paint.Style.STROKE
        strokeJoin = Paint.Join.MITER
        strokeCap = Paint.Cap.SQUARE
        isAntiAlias = true
    }
    private val accentPaint = Paint().apply {
        style = Paint.Style.STROKE
        strokeCap = Paint.Cap.SQUARE
        isAntiAlias = true
    }

    override fun draw(canvas: Canvas) {
        val b = bounds
        if (b.width() <= 0 || b.height() <= 0) return

        val u = unitPx.coerceAtMost(min(b.width(), b.height()) / 5f).coerceAtLeast(1f)
        val s = strokePx.coerceAtLeast(1f)
        val l = b.left.toFloat() + s / 2f
        val t = b.top.toFloat() + s / 2f
        val r = b.right.toFloat() - s / 2f
        val bottom = b.bottom.toFloat() - s / 2f

        path.reset()
        path.moveTo(l + u, t)
        path.lineTo(r - u, t)
        path.lineTo(r, t + u)
        path.lineTo(r, bottom - u)
        path.lineTo(r - u, bottom)
        path.lineTo(l + u, bottom)
        path.lineTo(l, bottom - u)
        path.lineTo(l, t + u)
        path.close()

        fillPaint.color = fillColor
        canvas.drawPath(path, fillPaint)

        val edge = strokeColor ?: return
        strokePaint.color = edge
        strokePaint.strokeWidth = s
        canvas.drawPath(path, strokePaint)

        accentPaint.color = edge
        accentPaint.alpha = 150
        accentPaint.strokeWidth = max(1f, s / 1.4f)
        canvas.drawLine(l + u * 2f, t + u * 1.35f, r - u * 2f, t + u * 1.35f, accentPaint)
        canvas.drawLine(l + u * 2f, bottom - u * 1.35f, r - u * 2f, bottom - u * 1.35f, accentPaint)
        accentPaint.alpha = 255
    }

    override fun setAlpha(alpha: Int) {
        fillPaint.alpha = alpha
        strokePaint.alpha = alpha
        accentPaint.alpha = alpha
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        fillPaint.colorFilter = colorFilter
        strokePaint.colorFilter = colorFilter
        accentPaint.colorFilter = colorFilter
    }

    @Deprecated("Deprecated in Java")
    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}

private enum class FrameStyle { PIXEL, SERAPH }

private data class ThemeSpec(
    val key: String,
    val label: String,
    val bg: Int,
    val panel: Int,
    val panel2: Int,
    val stroke: Int,
    val text: Int,
    val muted: Int,
    val accent: Int,
    val green: Int,
    val amber: Int,
    val red: Int,
    val purple: Int,
    val displayFont: Int,
    val bodyFont: Int,
    val bodyStrongFont: Int,
    val monoFont: Int,
    val frameStyle: FrameStyle,
    val lightSystemBars: Boolean,
)

class MainActivity : Activity(), PlaybackSnapshotListener {
    private enum class ScreenMode { ROOMS, QUEUE, PEOPLE, LIBRARY, PLAYLISTS }

    private val mainHandler = Handler(Looper.getMainLooper())
    private var controllerFuture: com.google.common.util.concurrent.ListenableFuture<MediaController>? = null
    private var controller: MediaController? = null

    private lateinit var displayFont: Typeface
    private lateinit var bodyFont: Typeface
    private lateinit var bodyStrongFont: Typeface
    private lateinit var monoFont: Typeface

    private lateinit var serverInput: EditText
    private lateinit var authPanel: LinearLayout
    private lateinit var appShell: LinearLayout
    private lateinit var usernameInput: EditText
    private lateinit var passwordInput: EditText
    private lateinit var authMessage: TextView
    private lateinit var statusBadge: TextView
    private lateinit var radioTab: TextView
    private lateinit var queueTab: TextView
    private lateinit var peopleTab: TextView
    private lateinit var libraryTab: TextView
    private lateinit var playlistTab: TextView
    private lateinit var themeButton: TextView
    private lateinit var angelBanner: ImageView
    private lateinit var modeLabel: TextView
    private lateinit var sourceLabel: TextView
    private lateinit var titleText: TextView
    private lateinit var metaText: TextView
    private lateinit var elapsedText: TextView
    private lateinit var durationText: TextView
    private lateinit var progressSeek: SeekBar
    private lateinit var pixelCat: PixelCatView
    private lateinit var prevButton: CenteredIconButton
    private lateinit var rewindButton: CenteredIconButton
    private lateinit var playButton: CenteredIconButton
    private lateinit var forwardButton: CenteredIconButton
    private lateinit var nextButton: CenteredIconButton
    private lateinit var playbackModeButton: TextView
    private lateinit var queueCurrentButton: TextView
    private lateinit var playNextCurrentButton: TextView
    private lateinit var exitButton: TextView
    private lateinit var syncBadge: TextView
    private lateinit var driftBadge: TextView
    private lateinit var sessionBadge: TextView
    private lateinit var radioContent: LinearLayout
    private lateinit var queueContent: LinearLayout
    private lateinit var peopleContent: LinearLayout
    private lateinit var libraryContent: LinearLayout
    private lateinit var playlistContent: LinearLayout
    private lateinit var debugText: TextView

    private var screenMode = ScreenMode.ROOMS
    private var authState = AuthState.CHECKING
    private var currentUser: UserSession? = null
    private var currentRoomListeners: List<String> = emptyList()
    private var libraryQuery = ""
    private var currentChannelId: String? = null
    private var currentTrackId: String? = null
    private var localLibraryIndex = -1
    private var lastPaused = true
    private var lastStatus = "Starting"
    private var lastChannels: List<ChannelInfo> = emptyList()
    private var libraryTracks: List<Track> = emptyList()
    private var libraryLoaded = false
    private var myPlaylists: List<Playlist> = emptyList()
    private var sharedPlaylists: List<Playlist> = emptyList()
    private var playlistsLoaded = false
    private var selectedPlaylistId: String? = null
    private var selectedPlaylist: Playlist? = null
    private var lastQueue: List<Track> = emptyList()
    private var lastQueueLoaded = false
    private var lastCurrentIndex = 0
    private var playbackMode = "repeat-all"
    private var lastChannelName = "No channel"
    private var lastTrackTitle = "No track"
    private var lastTrackDuration = 0.0
    private var lastServerTimestampMs = 0L
    private var lastStateRealtimeMs = 0L
    private var lastExpectedPositionMs = 0L
    private var lastPlayerPositionMs = 0L
    private var lastDriftMs = 0L
    private var lastPlaybackState = "none"
    private var lastPlayerIsPlaying = false
    private var userSeeking = false
    private val debugEvents = mutableListOf<String>()

    private val ticker = object : Runnable {
        override fun run() {
            updateDeck()
            mainHandler.postDelayed(this, 500)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        SessionStore.load(this)
        ACTIVE_THEME = themeForKey(SessionStore.themeKey)
        applySystemBars()
        loadThemeFonts()
        maybeRequestNotificationPermission()
        buildUi()
        PlaybackBridge.register(this)
        connectMediaController()
        mainHandler.post(ticker)
    }

    override fun onDestroy() {
        mainHandler.removeCallbacks(ticker)
        PlaybackBridge.unregister(this)
        controllerFuture?.let { MediaController.releaseFuture(it) }
        controllerFuture = null
        controller = null
        super.onDestroy()
    }

    override fun onPlaybackSnapshot(snapshot: PlaybackSnapshot) = runOnUiThread {
        val authChanged = authState != snapshot.authState || currentUser != snapshot.currentUser
        val stationsChanged = lastChannels != snapshot.channels ||
            lastQueue != snapshot.queue ||
            currentChannelId != snapshot.currentChannelId ||
            lastCurrentIndex != snapshot.currentIndex ||
            lastQueueLoaded != snapshot.queueLoaded
        val peopleChanged = currentRoomListeners != snapshot.currentRoomListeners
        val libraryChanged = libraryTracks !== snapshot.libraryTracks ||
            libraryLoaded != snapshot.libraryLoaded ||
            localLibraryIndex != snapshot.localLibraryIndex
        val playlistsChanged = myPlaylists != snapshot.myPlaylists ||
            sharedPlaylists != snapshot.sharedPlaylists ||
            playlistsLoaded != snapshot.playlistsLoaded ||
            selectedPlaylistId != snapshot.selectedPlaylistId ||
            selectedPlaylist != snapshot.selectedPlaylist

        authState = snapshot.authState
        currentUser = snapshot.currentUser
        lastStatus = snapshot.status
        lastChannels = snapshot.channels
        libraryTracks = snapshot.libraryTracks
        libraryLoaded = snapshot.libraryLoaded
        myPlaylists = snapshot.myPlaylists
        sharedPlaylists = snapshot.sharedPlaylists
        playlistsLoaded = snapshot.playlistsLoaded
        selectedPlaylistId = snapshot.selectedPlaylistId
        selectedPlaylist = snapshot.selectedPlaylist
        currentChannelId = snapshot.currentChannelId
        currentTrackId = snapshot.currentTrackId
        localLibraryIndex = snapshot.localLibraryIndex
        currentRoomListeners = snapshot.currentRoomListeners
        lastPaused = snapshot.paused
        lastQueue = snapshot.queue
        lastQueueLoaded = snapshot.queueLoaded
        lastCurrentIndex = snapshot.currentIndex
        playbackMode = snapshot.playbackMode
        lastChannelName = snapshot.channelName
        lastTrackTitle = snapshot.trackTitle
        lastTrackDuration = snapshot.trackDuration
        lastServerTimestampMs = snapshot.serverTimestampMs
        lastStateRealtimeMs = snapshot.stateRealtimeMs
        lastExpectedPositionMs = snapshot.expectedPositionMs
        lastPlayerPositionMs = snapshot.playerPositionMs
        lastDriftMs = snapshot.driftMs
        lastPlaybackState = snapshot.playbackState
        lastPlayerIsPlaying = snapshot.isPlaying
        debugEvents.clear()
        debugEvents.addAll(snapshot.debugEvents)

        if (authChanged) renderAuthState()
        if (stationsChanged) {
            renderStations()
            renderQueue()
        }
        if (peopleChanged || stationsChanged) renderPeople()
        if (libraryChanged) renderLibrary()
        if (playlistsChanged || libraryChanged) renderPlaylists()
        updateDeck()
    }

    private fun buildUi() {
        val baseLeftPadding = dp(18)
        val baseTopPadding = dp(16)
        val baseRightPadding = dp(18)
        val baseBottomPadding = dp(24)
        val root = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(baseLeftPadding, baseTopPadding, baseRightPadding, baseBottomPadding)
            setBackgroundColor(Color.TRANSPARENT)
        }
        root.setOnApplyWindowInsetsListener { view, insets ->
            view.setPadding(
                baseLeftPadding + insets.systemWindowInsetLeft,
                baseTopPadding + insets.systemWindowInsetTop,
                baseRightPadding + insets.systemWindowInsetRight,
                baseBottomPadding + insets.systemWindowInsetBottom,
            )
            insets
        }

        val bodyShell = FrameLayout(this).apply {
            setBackgroundColor(BG)
        }
        val showAngelBody = ACTIVE_THEME.key == "seraph"
        val bodyArt = ImageView(this).apply {
            setImageResource(R.drawable.anime_angel_banner)
            scaleType = ImageView.ScaleType.CENTER_CROP
            alpha = if (showAngelBody) 0.82f else 0f
            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
        }
        bodyShell.addView(bodyArt, FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT,
        ))
        bodyShell.addView(View(this).apply {
            setBackgroundColor(if (showAngelBody) withAlpha(BG, 174) else BG)
            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
        }, FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT,
        ))

        val scroll = ScrollView(this).apply {
            isFillViewport = true
            setBackgroundColor(Color.TRANSPARENT)
            addView(root)
        }
        bodyShell.addView(scroll, FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT,
        ))
        scroll.post { root.requestApplyInsets() }

        val header = row().apply { gravity = Gravity.CENTER_VERTICAL }
        header.addView(text("BLASTOISE", 22f, TEXT, Typeface.BOLD).apply {
            maxLines = 1
            ellipsize = TextUtils.TruncateAt.END
        }, weightWrap(1f))
        statusBadge = tag("Starting", AMBER, BG).apply {
            maxLines = 1
            ellipsize = TextUtils.TruncateAt.END
        }
        header.addView(statusBadge, LinearLayout.LayoutParams(dp(94), dp(42)).apply {
            leftMargin = dp(6)
        })
        themeButton = button(
            "",
            PANEL2,
            ACCENT,
            Typeface.BOLD,
            if (ACTIVE_THEME.key == "cat") R.drawable.ic_cat else R.drawable.ic_halo,
            18,
        ).apply {
            contentDescription = "Switch theme"
            setOnClickListener { cycleTheme() }
        }
        header.addView(themeButton, LinearLayout.LayoutParams(dp(46), dp(42)).apply {
            leftMargin = dp(6)
        })
        exitButton = button("EXIT", RED, TEXT, Typeface.BOLD, R.drawable.ic_power, 17).apply {
            contentDescription = "Stop and exit"
            setOnClickListener { stopAndExit() }
            setOnTouchListener { _, event ->
                if (event.action == MotionEvent.ACTION_UP) stopAndExit()
                true
            }
        }
        header.addView(exitButton, LinearLayout.LayoutParams(dp(92), dp(42)).apply {
            leftMargin = dp(6)
        })
        root.addView(header, matchWrap())

        authPanel = panel().apply {
            setPadding(dp(14), dp(14), dp(14), dp(14))
        }
        authPanel.addView(text("ROOM ACCESS", 20f, TEXT, Typeface.BOLD), matchWrap())
        authMessage = text("Sign in to join rooms, see listeners, and change room queues.", 14f, MUTED).apply {
            setPadding(0, dp(3), 0, dp(10))
        }
        authPanel.addView(authMessage, matchWrap())

        val serverRow = row().apply {
            gravity = Gravity.CENTER_VERTICAL
            setPadding(0, 0, 0, dp(10))
        }
        serverInput = EditText(this).apply {
            setSingleLine(true)
            textSize = 14f
            typeface = bodyFont
            setText(SessionStore.serverBaseUrl)
            setTextColor(TEXT)
            setHintTextColor(MUTED)
            hint = "Server URL"
            setPadding(dp(12), 0, dp(12), 0)
            background = box(PANEL, STROKE)
        }
        serverRow.addView(serverInput, LinearLayout.LayoutParams(0, dp(48), 1f))
        serverRow.addView(button("CHECK", PANEL2, TEXT, iconRes = R.drawable.ic_connect).apply {
            setOnClickListener { connectToServer() }
        }, LinearLayout.LayoutParams(dp(132), dp(48)).apply { leftMargin = dp(8) })
        authPanel.addView(serverRow, matchWrap())

        usernameInput = EditText(this).apply {
            setSingleLine(true)
            textSize = 15f
            typeface = bodyFont
            setTextColor(TEXT)
            setHintTextColor(MUTED)
            hint = "Username"
            setPadding(dp(12), 0, dp(12), 0)
            background = box(PANEL2, STROKE)
        }
        authPanel.addView(usernameInput, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48)))

        passwordInput = EditText(this).apply {
            setSingleLine(true)
            inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
            textSize = 15f
            typeface = bodyFont
            setTextColor(TEXT)
            setHintTextColor(MUTED)
            hint = "Password"
            setPadding(dp(12), 0, dp(12), 0)
            background = box(PANEL2, STROKE)
        }
        authPanel.addView(passwordInput, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48)).apply {
            topMargin = dp(8)
        })

        val authButtons = row().apply {
            gravity = Gravity.CENTER_VERTICAL
            setPadding(0, dp(10), 0, 0)
        }
        authButtons.addView(button("SIGN IN", GREEN, BG, Typeface.BOLD, R.drawable.ic_connect).apply {
            setOnClickListener { signIn() }
        }, LinearLayout.LayoutParams(0, dp(50), 1f).apply { rightMargin = dp(5) })
        authButtons.addView(button("SIGN UP", ACCENT, BG, Typeface.BOLD, R.drawable.ic_queue_add).apply {
            setOnClickListener { signUp() }
        }, LinearLayout.LayoutParams(0, dp(50), 1f).apply { leftMargin = dp(5) })
        authPanel.addView(authButtons, matchWrap())
        root.addView(authPanel, matchWrapWithTop(dp(12)))

        appShell = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
        }
        root.addView(appShell, matchWrap())

        val tabs = row().apply {
            gravity = Gravity.CENTER_VERTICAL
            background = box(PANEL, STROKE)
            setPadding(dp(4), dp(4), dp(4), dp(4))
        }
        radioTab = tab("ROOMS", true, R.drawable.ic_radio).apply { setOnClickListener { switchMode(ScreenMode.ROOMS) } }
        queueTab = tab("QUEUE", false, R.drawable.ic_queue).apply { setOnClickListener { switchMode(ScreenMode.QUEUE) } }
        peopleTab = tab("PEOPLE", false, R.drawable.ic_people).apply { setOnClickListener { switchMode(ScreenMode.PEOPLE) } }
        libraryTab = tab("LIB", false, R.drawable.ic_library).apply { setOnClickListener { switchMode(ScreenMode.LIBRARY) } }
        playlistTab = tab("LISTS", false, R.drawable.ic_playlist).apply { setOnClickListener { switchMode(ScreenMode.PLAYLISTS) } }
        tabs.addView(radioTab, LinearLayout.LayoutParams(0, dp(44), 1f))
        tabs.addView(queueTab, LinearLayout.LayoutParams(0, dp(44), 1f))
        tabs.addView(peopleTab, LinearLayout.LayoutParams(0, dp(44), 1f))
        tabs.addView(libraryTab, LinearLayout.LayoutParams(0, dp(44), 1f))
        tabs.addView(playlistTab, LinearLayout.LayoutParams(0, dp(44), 1f))
        appShell.addView(tabs, matchWrapWithTop(dp(12)))

        val deck = panel().apply {
            setPadding(dp(16), dp(16), dp(16), dp(14))
        }
        val deckTop = row().apply { gravity = Gravity.CENTER_VERTICAL }
        val badge = TextView(this).apply {
            text = "TUNE"
            textSize = 22f
            typeface = displayFont
            letterSpacing = 0f
            gravity = Gravity.CENTER
            setTextColor(BG)
            background = box(ACCENT)
        }
        deckTop.addView(badge, LinearLayout.LayoutParams(dp(70), dp(70)))

        val deckMeta = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            setPadding(dp(14), 0, 0, 0)
        }
        modeLabel = text("RADIO", 12f, GREEN, Typeface.BOLD)
        sourceLabel = text("No channel", 24f, TEXT, Typeface.BOLD).apply {
            maxLines = 1
            ellipsize = TextUtils.TruncateAt.END
        }
        metaText = text("JOIN A STATION", 13f, MUTED)
        deckMeta.addView(modeLabel, matchWrap())
        deckMeta.addView(sourceLabel, matchWrap())
        deckMeta.addView(metaText, matchWrap())
        deckTop.addView(deckMeta, weightWrap(1f))
        deck.addView(deckTop, matchWrap())

        angelBanner = ImageView(this).apply {
            setImageResource(R.drawable.anime_angel_banner)
            scaleType = ImageView.ScaleType.CENTER_CROP
            cropToPadding = true
            setPadding(dp(3), dp(3), dp(3), dp(3))
            background = box(PANEL2, STROKE)
            alpha = 0.96f
            importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
            visibility = if (ACTIVE_THEME.key == "seraph") View.VISIBLE else View.GONE
        }
        deck.addView(angelBanner, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(116)).apply {
            topMargin = dp(14)
        })

        titleText = text("No track", 28f, TEXT, Typeface.BOLD).apply {
            setPadding(0, dp(18), 0, dp(10))
            maxLines = 3
            ellipsize = TextUtils.TruncateAt.END
        }
        deck.addView(titleText, matchWrap())

        val timeRow = row().apply { gravity = Gravity.CENTER_VERTICAL }
        elapsedText = text("0:00", 13f, MUTED, Typeface.BOLD)
        durationText = text("--:--", 13f, MUTED, Typeface.BOLD).apply { gravity = Gravity.END }
        timeRow.addView(elapsedText, weightWrap(1f))
        timeRow.addView(durationText, weightWrap(1f))
        deck.addView(timeRow, matchWrap())

        progressSeek = SeekBar(this).apply {
            max = 10000
            progressTintList = ColorStateList.valueOf(GREEN)
            progressBackgroundTintList = ColorStateList.valueOf(PANEL2)
            thumbTintList = ColorStateList.valueOf(TEXT)
            setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
                override fun onStartTrackingTouch(seekBar: SeekBar) {
                    userSeeking = true
                }

                override fun onStopTrackingTouch(seekBar: SeekBar) {
                    val duration = lastTrackDuration
                    if (duration > 0.0) {
                        val target = duration * seekBar.progress.toDouble() / seekBar.max.toDouble()
                        if (isRadioBacked()) {
                            playbackService()?.seekTo(target)
                        } else {
                            playbackService()?.seekTo(target)
                        }
                    }
                    userSeeking = false
                }

                override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit
            })
        }
        val progressStack = FrameLayout(this)
        progressStack.addView(progressSeek, FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            dp(42),
        ).apply {
            gravity = Gravity.BOTTOM
        })
        pixelCat = PixelCatView(this).apply {
            setPadding(dp(8), 0, dp(8), 0)
            visibility = if (ACTIVE_THEME.key == "cat") View.VISIBLE else View.GONE
        }
        progressStack.addView(pixelCat, FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            dp(96),
        ).apply {
            gravity = Gravity.BOTTOM
        })
        deck.addView(progressStack, LinearLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            dp(104),
        ))

        val deckFace = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            background = box(PANEL2, STROKE)
            setPadding(dp(10), dp(10), dp(10), dp(10))
        }

        val transportLabel = row().apply { gravity = Gravity.CENTER_VERTICAL }
        transportLabel.addView(text("TRANSPORT", 12f, MUTED, Typeface.BOLD).apply {
            letterSpacing = 0f
        }, weightWrap(1f))
        transportLabel.addView(text("TRACK / SEEK", 12f, MUTED, Typeface.BOLD).apply {
            gravity = Gravity.END
            letterSpacing = 0f
        }, weightWrap(1f))
        deckFace.addView(transportLabel, matchWrap())

        val controls = row().apply {
            gravity = Gravity.CENTER
            setPadding(0, dp(8), 0, 0)
        }
        prevButton = iconButton(R.drawable.ic_skip_previous, PANEL, TEXT, 26).apply { setOnClickListener { previous() } }
        rewindButton = iconButton(R.drawable.ic_seek_back, PANEL, TEXT, 28).apply { setOnClickListener { seekBy(-15) } }
        playButton = iconButton(R.drawable.ic_play, GREEN, BG, 30, stroke = false).apply { setOnClickListener { togglePlay() } }
        forwardButton = iconButton(R.drawable.ic_seek_forward, PANEL, TEXT, 28).apply { setOnClickListener { seekBy(15) } }
        nextButton = iconButton(R.drawable.ic_skip_next, PANEL, TEXT, 26).apply { setOnClickListener { next() } }
        controls.addView(prevButton, smallControlParams())
        controls.addView(rewindButton, smallControlParams())
        controls.addView(playButton, mainControlParams())
        controls.addView(forwardButton, smallControlParams())
        controls.addView(nextButton, smallControlParams())
        deckFace.addView(controls, matchWrap())

        val queueControls = row().apply {
            gravity = Gravity.CENTER
            setPadding(0, dp(8), 0, 0)
        }
        playbackModeButton = transportButton("LOOP ALL", PANEL, TEXT, iconRes = R.drawable.ic_repeat).apply { setOnClickListener { cyclePlaybackMode() } }
        queueCurrentButton = transportButton("QUEUE", PANEL, TEXT, iconRes = R.drawable.ic_queue_add).apply { setOnClickListener { queueCurrentTrack(false) } }
        playNextCurrentButton = transportButton("NEXT", PANEL, TEXT, iconRes = R.drawable.ic_play_next).apply { setOnClickListener { queueCurrentTrack(true) } }
        queueControls.addView(playbackModeButton, wideControlParams())
        queueControls.addView(queueCurrentButton, wideControlParams())
        queueControls.addView(playNextCurrentButton, wideControlParams())
        deckFace.addView(queueControls, matchWrap())

        deck.addView(deckFace, matchWrapWithTop(dp(10)))

        val meters = row().apply { gravity = Gravity.CENTER_VERTICAL }
        syncBadge = tag("LIVE", GREEN, BG)
        driftBadge = tag("DRIFT", PANEL2, TEXT)
        sessionBadge = tag("SESSION", PURPLE, BG)
        meters.addView(syncBadge)
        meters.addView(driftBadge, leftGap())
        meters.addView(sessionBadge, leftGap())
        sessionBadge.setOnClickListener { signOut() }
        deck.addView(meters, matchWrap())
        appShell.addView(deck, matchWrapWithTop(dp(12)))

        radioContent = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        queueContent = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        peopleContent = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        libraryContent = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        playlistContent = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
        appShell.addView(radioContent, matchWrapWithTop(dp(16)))
        appShell.addView(queueContent, matchWrapWithTop(dp(16)))
        appShell.addView(peopleContent, matchWrapWithTop(dp(16)))
        appShell.addView(libraryContent, matchWrapWithTop(dp(16)))
        appShell.addView(playlistContent, matchWrapWithTop(dp(16)))

        val debugPanel = panel().apply {
            setPadding(dp(14), dp(12), dp(14), dp(12))
        }
        debugPanel.addView(sectionTitle("DIAGNOSTICS"), matchWrap())
        debugText = TextView(this).apply {
            textSize = 11f
            typeface = monoFont
            setTextColor(MUTED)
            setPadding(0, dp(8), 0, 0)
            text = "Starting..."
        }
        debugPanel.addView(debugText, matchWrap())
        appShell.addView(debugPanel, matchWrapWithTop(dp(16)))

        setContentView(bodyShell)
        renderMode()
        renderAuthState()
    }

    private fun connectToServer() {
        val service = playbackService() ?: return
        service.connectToServer(serverInput.text.toString())
        serverInput.setText(SessionStore.serverBaseUrl)
        if (screenMode == ScreenMode.LIBRARY) {
            service.loadLibrary()
        } else if (screenMode == ScreenMode.PLAYLISTS) {
            service.loadPlaylists()
        }
    }

    private fun signIn() {
        playbackService()?.signIn(
            serverInput.text.toString(),
            usernameInput.text.toString(),
            passwordInput.text.toString(),
        )
    }

    private fun signUp() {
        playbackService()?.signUp(
            serverInput.text.toString(),
            usernameInput.text.toString(),
            passwordInput.text.toString(),
        )
    }

    private fun signOut() {
        playbackService()?.signOut()
    }

    private fun stopAndExit() {
        playbackService()?.stopPlaybackAndExit()
        finishAndRemoveTask()
    }

    private fun loadThemeFonts() {
        displayFont = resources.getFont(ACTIVE_THEME.displayFont)
        bodyFont = resources.getFont(ACTIVE_THEME.bodyFont)
        bodyStrongFont = resources.getFont(ACTIVE_THEME.bodyStrongFont)
        monoFont = resources.getFont(ACTIVE_THEME.monoFont)
    }

    private fun applySystemBars() {
        window.statusBarColor = BG
        window.navigationBarColor = BG
        var flags = 0
        if (ACTIVE_THEME.lightSystemBars && Build.VERSION.SDK_INT >= 23) {
            flags = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
        }
        if (ACTIVE_THEME.lightSystemBars && Build.VERSION.SDK_INT >= 26) {
            flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
        }
        window.decorView.systemUiVisibility = flags
    }

    private fun cycleTheme() {
        val currentIndex = THEMES.indexOfFirst { it.key == ACTIVE_THEME.key }.coerceAtLeast(0)
        ACTIVE_THEME = THEMES[(currentIndex + 1) % THEMES.size]
        SessionStore.themeKey = ACTIVE_THEME.key
        SessionStore.save(this)
        recreate()
    }

    private fun switchMode(mode: ScreenMode) {
        if (screenMode == mode) return
        screenMode = mode
        renderMode()
        when (mode) {
            ScreenMode.LIBRARY -> {
                playbackService()?.enterLibraryMode()
            }
            ScreenMode.ROOMS -> {
                lastStatus = "Rooms"
                localLibraryIndex = -1
                val target = currentChannelId ?: lastChannels.firstOrNull { it.isDefault }?.id ?: lastChannels.firstOrNull()?.id
                if (target != null) {
                    joinChannel(target, ScreenMode.ROOMS)
                } else {
                    playbackService()?.ensureRadioMode()
                }
            }
            ScreenMode.QUEUE -> {
                lastStatus = "Room queue"
                localLibraryIndex = -1
                playbackService()?.ensureRadioMode()
            }
            ScreenMode.PEOPLE -> {
                lastStatus = "Room people"
                localLibraryIndex = -1
                playbackService()?.ensureRadioMode()
            }
            ScreenMode.PLAYLISTS -> {
                lastStatus = "Playlists"
                localLibraryIndex = -1
                playbackService()?.ensureRadioMode()
                playbackService()?.loadPlaylists()
            }
        }
        updateDeck()
    }

    private fun renderMode() {
        if (!::radioContent.isInitialized) return
        radioTab.background = box(if (screenMode == ScreenMode.ROOMS) ACCENT else PANEL)
        radioTab.setTextColor(if (screenMode == ScreenMode.ROOMS) BG else MUTED)
        setButtonIcon(radioTab, R.drawable.ic_radio, if (screenMode == ScreenMode.ROOMS) BG else MUTED)
        queueTab.background = box(if (screenMode == ScreenMode.QUEUE) ACCENT else PANEL)
        queueTab.setTextColor(if (screenMode == ScreenMode.QUEUE) BG else MUTED)
        setButtonIcon(queueTab, R.drawable.ic_queue, if (screenMode == ScreenMode.QUEUE) BG else MUTED)
        peopleTab.background = box(if (screenMode == ScreenMode.PEOPLE) ACCENT else PANEL)
        peopleTab.setTextColor(if (screenMode == ScreenMode.PEOPLE) BG else MUTED)
        setButtonIcon(peopleTab, R.drawable.ic_people, if (screenMode == ScreenMode.PEOPLE) BG else MUTED)
        libraryTab.background = box(if (screenMode == ScreenMode.LIBRARY) ACCENT else PANEL)
        libraryTab.setTextColor(if (screenMode == ScreenMode.LIBRARY) BG else MUTED)
        setButtonIcon(libraryTab, R.drawable.ic_library, if (screenMode == ScreenMode.LIBRARY) BG else MUTED)
        playlistTab.background = box(if (screenMode == ScreenMode.PLAYLISTS) ACCENT else PANEL)
        playlistTab.setTextColor(if (screenMode == ScreenMode.PLAYLISTS) BG else MUTED)
        setButtonIcon(playlistTab, R.drawable.ic_playlist, if (screenMode == ScreenMode.PLAYLISTS) BG else MUTED)
        radioContent.visibility = if (screenMode == ScreenMode.ROOMS) View.VISIBLE else View.GONE
        queueContent.visibility = if (screenMode == ScreenMode.QUEUE) View.VISIBLE else View.GONE
        peopleContent.visibility = if (screenMode == ScreenMode.PEOPLE) View.VISIBLE else View.GONE
        libraryContent.visibility = if (screenMode == ScreenMode.LIBRARY) View.VISIBLE else View.GONE
        playlistContent.visibility = if (screenMode == ScreenMode.PLAYLISTS) View.VISIBLE else View.GONE
        renderStations()
        renderQueue()
        renderPeople()
        renderLibrary()
        renderPlaylists()
    }

    private fun joinChannel(channelId: String, targetMode: ScreenMode = ScreenMode.ROOMS) {
        screenMode = targetMode
        renderMode()
        currentChannelId = channelId
        localLibraryIndex = -1
        lastStatus = "Joining"
        renderStations()
        updateDeck()
        playbackService()?.joinChannel(channelId)
    }

    private fun playLibraryTrack(index: Int) {
        val track = libraryTracks.getOrNull(index) ?: return
        playbackService()?.queueTrack(track, false)
    }

    private fun cyclePlaybackMode() {
        playbackService()?.cyclePlaybackMode()
    }

    private fun queueCurrentTrack(playNext: Boolean) {
        playbackService()?.queueCurrentTrack(playNext)
    }

    private fun queueTrack(track: Track, playNext: Boolean) {
        playbackService()?.queueTrack(track, playNext)
    }

    private fun queueTrackIds(trackIds: List<String>, playNext: Boolean) {
        playbackService()?.queueTrackIds(trackIds, playNext)
    }

    private fun addPlaylistToQueue(playlist: Playlist, playNext: Boolean) {
        playbackService()?.addPlaylistToQueue(playlist, playNext)
    }

    private fun selectPlaylist(playlistId: String) {
        selectedPlaylistId = playlistId
        selectedPlaylist = null
        playbackService()?.loadPlaylist(playlistId)
    }

    private fun togglePlay() {
        playbackService()?.togglePlay()
    }

    private fun previous() {
        playbackService()?.previous()
    }

    private fun next() {
        playbackService()?.next()
    }

    private fun seekBy(seconds: Int) {
        playbackService()?.seekBy(seconds)
    }

    private fun stopPlayback() {
        playbackService()?.stopPlayback()
    }

    private fun jumpRadio(delta: Int) {
        if (delta < 0) playbackService()?.previous() else playbackService()?.next()
    }

    private fun isRadioBacked(): Boolean {
        return true
    }

    private fun playbackService(): PlaybackService? {
        return PlaybackBridge.service
    }

    private fun renderAuthState() {
        if (!::authPanel.isInitialized || !::appShell.isInitialized) return
        val signedIn = authState == AuthState.SIGNED_IN && currentUser?.isSignedIn == true
        authPanel.visibility = if (signedIn) View.GONE else View.VISIBLE
        appShell.visibility = if (signedIn) View.VISIBLE else View.GONE
        authMessage.text = when (authState) {
            AuthState.CHECKING -> "Checking your saved Blastoise session..."
            AuthState.GUEST -> "This server recognized you as a guest. Sign in to join rooms and edit queues."
            AuthState.SIGNED_OUT -> "Sign in to join rooms, see listeners, and change room queues."
            AuthState.SIGNED_IN -> "Signed in as ${currentUser?.username ?: "user"}."
        }
    }

    private fun modeLabel(mode: String): String {
        return when (mode) {
            "once" -> "LOOP OFF"
            "repeat-all" -> "LOOP ALL"
            "repeat-one" -> "LOOP ONE"
            "shuffle" -> "SHUFFLE"
            else -> mode.uppercase()
        }
    }

    private fun allPlaylists(): List<Playlist> {
        return myPlaylists + sharedPlaylists
    }

    private fun trackById(trackId: String): Track {
        return libraryTracks.firstOrNull { it.id == trackId }
            ?: lastQueue.firstOrNull { it.id == trackId }
            ?: Track(trackId, trackId, trackId.take(24), 0.0)
    }

    private fun submitLibrarySearch(value: String) {
        libraryQuery = value.trim()
        renderLibrary()
        updateDeck()
    }

    private fun renderStations() {
        if (!::radioContent.isInitialized) return
        radioContent.removeAllViews()
        radioContent.addView(sectionTitle("ROOMS"), matchWrap())
        if (lastChannels.isEmpty()) {
            radioContent.addView(emptyLine("Loading rooms..."), matchWrapWithTop(dp(8)))
            return
        }

        for (channel in lastChannels) {
            val selected = channel.id == currentChannelId
            val card = row().apply {
                gravity = Gravity.CENTER_VERTICAL
                setPadding(0, 0, 0, 0)
                background = box(if (selected) PANEL2 else PANEL, if (selected) ACCENT else STROKE)
            }
            val rail = View(this).apply {
                setBackgroundColor(if (selected) ACCENT else STROKE)
            }
            card.addView(rail, LinearLayout.LayoutParams(dp(5), ViewGroup.LayoutParams.MATCH_PARENT))

            if (ACTIVE_THEME.key == "seraph") {
                val roomArt = ImageView(this).apply {
                    setImageResource(R.drawable.anime_angel_room_badge)
                    scaleType = ImageView.ScaleType.CENTER_CROP
                    cropToPadding = true
                    setPadding(dp(2), dp(2), dp(2), dp(2))
                    background = box(BG, if (selected) ACCENT else STROKE)
                    alpha = if (selected) 1f else 0.88f
                    importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
                }
                card.addView(roomArt, LinearLayout.LayoutParams(dp(56), dp(56)).apply {
                    leftMargin = dp(10)
                })
            }

            val info = LinearLayout(this).apply {
                orientation = LinearLayout.VERTICAL
                setPadding(dp(12), dp(10), dp(10), dp(10))
            }
            info.addView(text(channel.name, 21f, TEXT, Typeface.BOLD).apply {
                maxLines = 1
                ellipsize = TextUtils.TruncateAt.END
            }, matchWrap())
            val detail = when {
                channel.description.isNotBlank() -> channel.description
                channel.isDefault -> "Default library room"
                else -> "Shared room"
            }
            info.addView(text("$detail  |  ${channel.listenerCount} listening  |  ${channel.trackCount} queued", 13f, MUTED), matchWrap())
            card.addView(info, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))

            val join = button(
                if (selected) "LIVE" else "JOIN",
                if (selected) GREEN else ACCENT,
                BG,
                Typeface.BOLD,
                R.drawable.ic_radio,
            )
            join.setOnClickListener { joinChannel(channel.id) }
            card.setOnClickListener { joinChannel(channel.id) }
            card.addView(join, LinearLayout.LayoutParams(dp(96), dp(48)).apply { rightMargin = dp(10) })
            radioContent.addView(card, matchWrapWithTop(dp(8)))
        }

        radioContent.addView(sectionTitle("UP NEXT IN THIS ROOM"), matchWrapWithTop(dp(16)))
        val queuePanel = panel().apply {
            setPadding(dp(12), dp(10), dp(12), dp(10))
        }
        when {
            !lastQueueLoaded -> queuePanel.addView(emptyLine("Waiting for queue snapshot"), matchWrap())
            lastQueue.isEmpty() -> queuePanel.addView(emptyLine("Queue is empty"), matchWrap())
            lastQueue.size == 1 -> queuePanel.addView(emptyLine("Only this track is queued"), matchWrap())
            else -> {
                val count = min(4, lastQueue.size - 1)
                for (offset in 1..count) {
                    val index = (lastCurrentIndex + offset) % lastQueue.size
                    val track = lastQueue[index]
                    queuePanel.addView(trackLine("${offset}. ${track.title}", if (offset == 1) TEXT else MUTED), matchWrap())
                }
            }
        }
        radioContent.addView(queuePanel, matchWrapWithTop(dp(8)))
    }

    private fun renderLibrary() {
        if (!::libraryContent.isInitialized) return
        libraryContent.removeAllViews()
        libraryContent.addView(sectionTitle("LIBRARY BROWSER"), matchWrap())
        libraryContent.addView(text(
            if (libraryLoaded) "${libraryTracks.size} tracks available to add into ${lastChannelName.ifBlank { "the current room" }}" else "Loading room library...",
            14f,
            MUTED,
        ), matchWrapWithTop(dp(2)))

        val searchRow = row().apply {
            gravity = Gravity.CENTER_VERTICAL
        }
        val searchInput = EditText(this).apply {
            setSingleLine(true)
            imeOptions = EditorInfo.IME_ACTION_SEARCH
            textSize = 15f
            typeface = bodyFont
            setTextColor(TEXT)
            setHintTextColor(MUTED)
            hint = "Search songs"
            setText(libraryQuery)
            setSelection(text.length)
            setPadding(dp(12), 0, dp(12), 0)
            background = box(PANEL2, STROKE)
            setOnEditorActionListener { _, actionId, _ ->
                if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                    submitLibrarySearch(text.toString())
                    true
                } else {
                    false
                }
            }
        }
        searchRow.addView(searchInput, LinearLayout.LayoutParams(0, dp(48), 1f))
        searchRow.addView(button("SEARCH", ACCENT, BG, Typeface.BOLD, R.drawable.ic_search, 17).apply {
            setOnClickListener { submitLibrarySearch(searchInput.text.toString()) }
        }, LinearLayout.LayoutParams(dp(124), dp(48)).apply {
            leftMargin = dp(8)
        })
        libraryContent.addView(searchRow, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48)).apply {
            topMargin = dp(10)
        })

        if (!libraryLoaded) {
            libraryContent.addView(emptyLine("Library will appear here"), matchWrapWithTop(dp(12)))
            return
        }
        if (libraryTracks.isEmpty()) {
            libraryContent.addView(emptyLine("No tracks found"), matchWrapWithTop(dp(12)))
            return
        }

        val query = libraryQuery.trim()
        val indexedTracks = libraryTracks.withIndex().filter { (_, track) ->
            query.isBlank() ||
                track.title.contains(query, ignoreCase = true) ||
                track.filename.contains(query, ignoreCase = true)
        }
        if (indexedTracks.isEmpty()) {
            libraryContent.addView(emptyLine("No matches"), matchWrapWithTop(dp(12)))
            return
        }

        val maxRows = min(80, indexedTracks.size)
        for (rowIndex in 0 until maxRows) {
            val indexed = indexedTracks[rowIndex]
            val i = indexed.index
            val track = indexed.value
            val selected = i == localLibraryIndex
            val row = LinearLayout(this).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                setPadding(dp(12), dp(9), dp(10), dp(9))
                background = box(if (selected) PANEL2 else PANEL, if (selected) ACCENT else STROKE)
                setOnClickListener { playLibraryTrack(i) }
            }
            val index = text((i + 1).toString().padStart(2, '0'), 13f, if (selected) ACCENT else MUTED, Typeface.BOLD).apply {
                gravity = Gravity.CENTER
            }
            row.addView(index, LinearLayout.LayoutParams(dp(34), ViewGroup.LayoutParams.WRAP_CONTENT))

            val title = text(track.title, 17f, if (selected) TEXT else MUTED, if (selected) Typeface.BOLD else Typeface.NORMAL).apply {
                maxLines = 1
                ellipsize = TextUtils.TruncateAt.END
            }
            row.addView(title, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            row.addView(trackActionButton(R.drawable.ic_queue_add, if (selected) ACCENT else MUTED).apply {
                setOnClickListener { queueTrack(track, false) }
            }, trackActionParams())
            row.addView(trackActionButton(R.drawable.ic_play_next, if (selected) TEXT else MUTED).apply {
                setOnClickListener { queueTrack(track, true) }
            }, trackActionParams())
            row.addView(text(fmtTime(track.duration.toLong()), 13f, MUTED, Typeface.BOLD), LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
            ))
            libraryContent.addView(row, matchWrapWithTop(dp(6)))
        }
        if (indexedTracks.size > maxRows) {
            libraryContent.addView(emptyLine("Showing first $maxRows matches"), matchWrapWithTop(dp(8)))
        }
    }

    private fun renderPlaylists() {
        if (!::playlistContent.isInitialized) return
        playlistContent.removeAllViews()
        playlistContent.addView(sectionTitle("PLAYLISTS"), matchWrap())
        playlistContent.addView(text(
            if (playlistsLoaded) "${myPlaylists.size} mine | ${sharedPlaylists.size} shared | add into ${lastChannelName.ifBlank { "current room" }}" else "Loading playlists...",
            14f,
            MUTED,
        ), matchWrapWithTop(dp(2)))

        if (!playlistsLoaded) {
            playlistContent.addView(emptyLine("Playlists will appear here"), matchWrapWithTop(dp(12)))
            return
        }

        val playlists = allPlaylists()
        if (playlists.isEmpty()) {
            playlistContent.addView(emptyLine("No playlists found for this account"), matchWrapWithTop(dp(12)))
            return
        }

        val maxPlaylistRows = min(40, playlists.size)
        for (i in 0 until maxPlaylistRows) {
            val playlist = playlists[i]
            val selected = playlist.id == selectedPlaylistId
            val mine = myPlaylists.any { it.id == playlist.id }
            val actionPlaylist = selectedPlaylist?.takeIf { it.id == playlist.id } ?: playlist
            val row = LinearLayout(this).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                setPadding(dp(12), dp(10), dp(8), dp(10))
                background = box(if (selected) PANEL2 else PANEL, if (selected) ACCENT else STROKE)
                setOnClickListener { selectPlaylist(playlist.id) }
            }
            val info = LinearLayout(this).apply {
                orientation = LinearLayout.VERTICAL
            }
            info.addView(text(playlist.name, 19f, if (selected) TEXT else MUTED, if (selected) Typeface.BOLD else Typeface.NORMAL).apply {
                maxLines = 1
                ellipsize = TextUtils.TruncateAt.END
            }, matchWrap())
            val owner = when {
                mine -> "mine"
                playlist.ownerName.isNotBlank() -> "by ${playlist.ownerName}"
                else -> "shared"
            }
            val visibility = if (playlist.isPublic) "public" else "private"
            info.addView(text("${playlist.trackIds.size} tracks | $owner | $visibility", 12f, MUTED), matchWrap())
            row.addView(info, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            row.addView(trackActionButton(R.drawable.ic_queue_add, if (selected) ACCENT else MUTED).apply {
                setOnClickListener {
                    if (actionPlaylist.trackIds.isEmpty()) {
                        selectPlaylist(playlist.id)
                    } else {
                        addPlaylistToQueue(actionPlaylist, false)
                    }
                }
            }, trackActionParams())
            row.addView(trackActionButton(R.drawable.ic_play_next, if (selected) TEXT else MUTED).apply {
                setOnClickListener {
                    if (actionPlaylist.trackIds.isEmpty()) {
                        selectPlaylist(playlist.id)
                    } else {
                        addPlaylistToQueue(actionPlaylist, true)
                    }
                }
            }, trackActionParams())
            playlistContent.addView(row, matchWrapWithTop(dp(6)))
        }
        if (playlists.size > maxPlaylistRows) {
            playlistContent.addView(emptyLine("Showing first $maxPlaylistRows playlists"), matchWrapWithTop(dp(8)))
        }

        playlistContent.addView(sectionTitle("SELECTED LIST"), matchWrapWithTop(dp(16)))
        val selectedId = selectedPlaylistId
        val selected = selectedPlaylist
        if (selectedId != null && selected == null) {
            playlistContent.addView(emptyLine("Loading playlist tracks..."), matchWrapWithTop(dp(8)))
            return
        }
        if (selected == null) {
            playlistContent.addView(emptyLine("Select a playlist"), matchWrapWithTop(dp(8)))
            return
        }

        val header = row().apply {
            gravity = Gravity.CENTER_VERTICAL
            setPadding(dp(12), dp(10), dp(8), dp(10))
            background = box(PANEL2, ACCENT)
        }
        val selectedInfo = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
        }
        selectedInfo.addView(text(selected.name, 22f, TEXT, Typeface.BOLD).apply {
            maxLines = 1
            ellipsize = TextUtils.TruncateAt.END
        }, matchWrap())
        val description = selected.description.ifBlank {
            "${selected.trackIds.size} tracks ready for the room queue"
        }
        selectedInfo.addView(text(description, 13f, MUTED).apply {
            maxLines = 2
            ellipsize = TextUtils.TruncateAt.END
        }, matchWrap())
        header.addView(selectedInfo, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
        header.addView(trackActionButton(R.drawable.ic_queue_add, ACCENT, 22).apply {
            setOnClickListener { addPlaylistToQueue(selected, false) }
        }, trackActionParams())
        header.addView(trackActionButton(R.drawable.ic_play_next, TEXT, 22).apply {
            setOnClickListener { addPlaylistToQueue(selected, true) }
        }, trackActionParams())
        playlistContent.addView(header, matchWrapWithTop(dp(8)))

        if (selected.trackIds.isEmpty()) {
            playlistContent.addView(emptyLine("This playlist is empty"), matchWrapWithTop(dp(8)))
            return
        }

        val maxTrackRows = min(80, selected.trackIds.size)
        for (i in 0 until maxTrackRows) {
            val track = trackById(selected.trackIds[i])
            val row = LinearLayout(this).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                setPadding(dp(10), dp(8), dp(8), dp(8))
                background = box(PANEL, STROKE)
            }
            row.addView(text((i + 1).toString().padStart(2, '0'), 13f, MUTED, Typeface.BOLD).apply {
                gravity = Gravity.CENTER
            }, LinearLayout.LayoutParams(dp(36), ViewGroup.LayoutParams.WRAP_CONTENT))
            val info = LinearLayout(this).apply {
                orientation = LinearLayout.VERTICAL
            }
            info.addView(text(track.title, 17f, MUTED).apply {
                maxLines = 1
                ellipsize = TextUtils.TruncateAt.END
            }, matchWrap())
            info.addView(text(fmtTime(track.duration.toLong()), 12f, MUTED), matchWrap())
            row.addView(info, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            row.addView(trackActionButton(R.drawable.ic_queue_add, MUTED).apply {
                setOnClickListener { queueTrack(track, false) }
            }, trackActionParams())
            row.addView(trackActionButton(R.drawable.ic_play_next, MUTED).apply {
                setOnClickListener { queueTrack(track, true) }
            }, trackActionParams())
            playlistContent.addView(row, matchWrapWithTop(dp(6)))
        }
        if (selected.trackIds.size > maxTrackRows) {
            playlistContent.addView(emptyLine("Showing first $maxTrackRows tracks"), matchWrapWithTop(dp(8)))
        }
    }

    private fun renderQueue() {
        if (!::queueContent.isInitialized) return
        queueContent.removeAllViews()
        queueContent.addView(sectionTitle("ROOM QUEUE"), matchWrap())
        queueContent.addView(text(
            if (currentChannelId == null) "Join a room before editing its queue" else "${lastQueue.size} tracks in ${lastChannelName.ifBlank { "this room" }}",
            14f,
            MUTED,
        ), matchWrapWithTop(dp(2)))

        if (!lastQueueLoaded) {
            queueContent.addView(emptyLine("Waiting for the room queue snapshot"), matchWrapWithTop(dp(12)))
            return
        }
        if (lastQueue.isEmpty()) {
            queueContent.addView(emptyLine("This room queue is empty"), matchWrapWithTop(dp(12)))
            return
        }

        val maxRows = min(80, lastQueue.size)
        for (i in 0 until maxRows) {
            val track = lastQueue[i]
            val current = i == lastCurrentIndex
            val row = LinearLayout(this).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                setPadding(dp(10), dp(8), dp(8), dp(8))
                background = box(if (current) PANEL2 else PANEL, if (current) GREEN else STROKE)
                setOnClickListener { playbackService()?.jumpToQueueIndex(i) }
            }
            row.addView(text((i + 1).toString().padStart(2, '0'), 13f, if (current) GREEN else MUTED, Typeface.BOLD).apply {
                gravity = Gravity.CENTER
            }, LinearLayout.LayoutParams(dp(36), ViewGroup.LayoutParams.WRAP_CONTENT))
            val info = LinearLayout(this).apply {
                orientation = LinearLayout.VERTICAL
            }
            info.addView(text(track.title, 17f, if (current) TEXT else MUTED, if (current) Typeface.BOLD else Typeface.NORMAL).apply {
                maxLines = 1
                ellipsize = TextUtils.TruncateAt.END
            }, matchWrap())
            info.addView(text(if (current) "now playing" else fmtTime(track.duration.toLong()), 12f, MUTED), matchWrap())
            row.addView(info, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            row.addView(trackActionButton(R.drawable.ic_play, if (current) GREEN else MUTED, if (current) 23 else 21).apply {
                setOnClickListener { playbackService()?.jumpToQueueIndex(i) }
            }, trackActionParams())
            row.addView(trackActionButton(R.drawable.ic_delete, RED, 21).apply {
                setOnClickListener { playbackService()?.removeQueueIndex(i) }
            }, trackActionParams())
            queueContent.addView(row, matchWrapWithTop(dp(6)))
        }
        if (lastQueue.size > maxRows) {
            queueContent.addView(emptyLine("Showing first $maxRows queued tracks"), matchWrapWithTop(dp(8)))
        }
    }

    private fun renderPeople() {
        if (!::peopleContent.isInitialized) return
        peopleContent.removeAllViews()
        peopleContent.addView(sectionTitle("PEOPLE IN ROOM"), matchWrap())
        val roomName = lastChannelName.ifBlank { "No room" }
        peopleContent.addView(text(roomName, 22f, TEXT, Typeface.BOLD).apply {
            maxLines = 1
            ellipsize = TextUtils.TruncateAt.END
        }, matchWrapWithTop(dp(4)))

        val listeners = currentRoomListeners.ifEmpty {
            lastChannels.firstOrNull { it.id == currentChannelId }?.listeners.orEmpty()
        }
        if (currentChannelId == null) {
            peopleContent.addView(emptyLine("Join a room to see who is listening with you"), matchWrapWithTop(dp(12)))
            return
        }
        if (listeners.isEmpty()) {
            peopleContent.addView(emptyLine("No listener names reported yet"), matchWrapWithTop(dp(12)))
            return
        }

        for (name in listeners) {
            val mine = currentUser?.username == name
            val row = LinearLayout(this).apply {
                orientation = LinearLayout.HORIZONTAL
                gravity = Gravity.CENTER_VERTICAL
                setPadding(dp(12), dp(10), dp(12), dp(10))
                background = box(if (mine) PANEL2 else PANEL, if (mine) GREEN else STROKE)
            }
            row.addView(text(if (mine) "YOU" else "USER", 12f, if (mine) GREEN else MUTED, Typeface.BOLD), LinearLayout.LayoutParams(dp(58), ViewGroup.LayoutParams.WRAP_CONTENT))
            row.addView(text(name, 19f, if (mine) TEXT else MUTED, if (mine) Typeface.BOLD else Typeface.NORMAL).apply {
                maxLines = 1
                ellipsize = TextUtils.TruncateAt.END
            }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
            peopleContent.addView(row, matchWrapWithTop(dp(6)))
        }
    }

    private fun connectMediaController() {
        val token = SessionToken(this, ComponentName(this, PlaybackService::class.java))
        val future = MediaController.Builder(this, token).buildAsync()
        controllerFuture = future
        future.addListener(
            {
                controller = future.get()
                lastStatus = "Player ready"
                val service = playbackService()
                if (PlaybackBridge.snapshot.authState == AuthState.CHECKING) {
                    service?.validateSession()
                } else if (
                    PlaybackBridge.snapshot.authState == AuthState.SIGNED_IN &&
                    PlaybackBridge.snapshot.channels.isEmpty()
                ) {
                    service?.connectToServer(SessionStore.serverBaseUrl)
                }
                updateDeck()
            },
            Executor { command -> runOnUiThread(command) },
        )
    }

    private fun updateDeck() {
        if (!::titleText.isInitialized) return

        val expectedMs = lastExpectedPositionMs
        val playerMs = lastPlayerPositionMs
        val driftMs = lastDriftMs
        val durationMs = (lastTrackDuration * 1000.0).toLong()

        statusBadge.text = lastStatus.uppercase()
        if (screenMode == ScreenMode.LIBRARY && lastStatus == "Disconnected") {
            lastStatus = "Library search"
            statusBadge.text = "LIBRARY"
        }
        statusBadge.background = box(statusColor())
        modeLabel.text = when (screenMode) {
            ScreenMode.ROOMS -> "ROOM"
            ScreenMode.QUEUE -> "QUEUE"
            ScreenMode.PEOPLE -> "PEOPLE"
            ScreenMode.LIBRARY -> "LIBRARY"
            ScreenMode.PLAYLISTS -> "LISTS"
        }
        sourceLabel.text = lastChannelName.ifBlank { "No room" }
        metaText.text = when (screenMode) {
            ScreenMode.ROOMS -> "JOINED ROOM"
            ScreenMode.QUEUE -> "${lastQueue.size} TRACKS | ROOM QUEUE"
            ScreenMode.PEOPLE -> "${currentRoomListeners.size} LISTENING"
            ScreenMode.LIBRARY -> "${libraryTracks.size} TRACKS | ADD TO ROOM"
            ScreenMode.PLAYLISTS -> "${allPlaylists().size} PLAYLISTS | ADD TO ROOM"
        }
        titleText.text = lastTrackTitle
        setIconButtonIcon(playButton, if (lastPaused) R.drawable.ic_play else R.drawable.ic_pause, BG, 30)
        playButton.contentDescription = if (lastPaused) "Play" else "Pause"
        playbackModeButton.text = modeLabel(playbackMode)
        themeButton.text = ""
        themeButton.setTextColor(ACCENT)
        themeButton.background = box(PANEL2, STROKE)
        setButtonIcon(themeButton, if (ACTIVE_THEME.key == "cat") R.drawable.ic_cat else R.drawable.ic_halo, ACCENT, 18)
        angelBanner.visibility = if (ACTIVE_THEME.key == "seraph") View.VISIBLE else View.GONE
        angelBanner.background = box(PANEL2, STROKE)
        val modeForeground = if (playbackMode == "shuffle") BG else TEXT
        playbackModeButton.setTextColor(modeForeground)
        playbackModeButton.background = box(if (playbackMode == "shuffle") ACCENT else PANEL, STROKE)
        setButtonIcon(
            playbackModeButton,
            if (playbackMode == "shuffle") R.drawable.ic_shuffle else R.drawable.ic_repeat,
            modeForeground,
            20,
        )
        val currentActionColor = if (currentTrackId == null) MUTED else TEXT
        queueCurrentButton.setTextColor(currentActionColor)
        playNextCurrentButton.setTextColor(currentActionColor)
        setButtonIcon(queueCurrentButton, R.drawable.ic_queue_add, currentActionColor, 20)
        setButtonIcon(playNextCurrentButton, R.drawable.ic_play_next, currentActionColor, 20)
        syncBadge.text = if (isRadioBacked()) "LIVE SYNC" else "LOCAL"
        driftBadge.text = if (isRadioBacked()) "DRIFT ${driftMs}MS" else "DIRECT PLAY"
        driftBadge.background = box(if (isRadioBacked()) driftColor(driftMs) else AMBER)
        sessionBadge.text = currentUser?.username?.uppercase()?.let { "SIGNED IN: $it" } ?: "SIGN IN"

        val visualPositionMs = if (isRadioBacked()) expectedMs else playerMs
        val progressFraction = if (durationMs > 0) {
            (visualPositionMs.toFloat() / durationMs.toFloat()).coerceIn(0f, 1f)
        } else {
            0.5f
        }
        elapsedText.text = fmtTime(visualPositionMs / 1000)
        durationText.text = if (durationMs > 0) fmtTime(durationMs / 1000) else "--:--"
        if (!userSeeking && durationMs > 0) {
            val progress = (progressFraction * progressSeek.max)
                .toInt()
                .coerceIn(0, progressSeek.max)
            progressSeek.progress = progress
        } else if (!userSeeking && durationMs <= 0) {
            progressSeek.progress = 0
        }
        pixelCat.visibility = if (ACTIVE_THEME.key == "cat") View.VISIBLE else View.GONE
        pixelCat.setPalette(
            body = 0xFF020402.toInt(),
            edge = ACCENT,
            eye = AMBER,
            accent = TEXT,
            shadow = withAlpha(PURPLE, 72),
        )
        pixelCat.setProgressFraction(0.22f)

        val title = if (lastTrackTitle.length > 64) lastTrackTitle.take(61) + "..." else lastTrackTitle
        debugText.text = buildString {
            appendLine("mode: $screenMode")
            appendLine("server: ${SessionStore.serverBaseUrl}")
            appendLine("auth: $authState user=${currentUser?.username ?: "-"} cookie=${if (SessionStore.cookieHeader.isBlank()) "none" else "yes"}")
            appendLine("rooms: ${lastChannels.size} current=${currentChannelId ?: "-"} listeners=${currentRoomListeners.size}")
            appendLine("library: ${libraryTracks.size} query=${libraryQuery.ifBlank { "-" }}")
            appendLine("playlists: mine=${myPlaylists.size} shared=${sharedPlaylists.size} selected=${selectedPlaylistId ?: "-"}")
            appendLine("playbackMode: $playbackMode")
            appendLine("track: ${currentTrackId?.take(24) ?: "-"}")
            appendLine("title: $title")
            appendLine("expected=${expectedMs}ms player=${playerMs}ms drift=${driftMs}ms state=$lastPlaybackState playing=$lastPlayerIsPlaying")
            appendLine("events:")
            debugEvents.takeLast(5).forEach { appendLine(it) }
        }
    }

    private fun maybeRequestNotificationPermission() {
        if (Build.VERSION.SDK_INT >= 33 &&
            checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
        ) {
            requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 100)
        }
    }

    private fun fmtTime(totalSeconds: Long): String {
        val seconds = max(0L, totalSeconds)
        val minutes = seconds / 60
        val remainder = seconds % 60
        return "$minutes:${remainder.toString().padStart(2, '0')}"
    }

    private fun statusColor(): Int {
        return when {
            lastStatus.contains("error", ignoreCase = true) -> RED
            lastStatus.contains("disconnect", ignoreCase = true) -> RED
            lastStatus.contains("loading", ignoreCase = true) -> AMBER
            else -> GREEN
        }
    }

    private fun driftColor(driftMs: Long): Int {
        val drift = abs(driftMs)
        return when {
            drift < 1000 -> GREEN
            drift < 2000 -> AMBER
            else -> RED
        }
    }

    private fun row() = LinearLayout(this).apply {
        orientation = LinearLayout.HORIZONTAL
    }

    private fun panel() = LinearLayout(this).apply {
        orientation = LinearLayout.VERTICAL
        background = box(PANEL, STROKE)
        foreground = if (ACTIVE_THEME.frameStyle == FrameStyle.PIXEL) frameOverlay(STROKE) else null
    }

    private fun text(value: String, size: Float, color: Int, style: Int = Typeface.NORMAL) =
        TextView(this).apply {
            text = value
            textSize = size
            setTextColor(color)
            typeface = when (style) {
                Typeface.BOLD -> displayFont
                else -> bodyFont
            }
            includeFontPadding = true
        }

    private fun sectionTitle(value: String) = text(value, 17f, TEXT, Typeface.BOLD).apply {
        letterSpacing = 0f
    }

    private fun emptyLine(value: String) = text(value, 14f, MUTED).apply {
        setPadding(0, dp(4), 0, dp(4))
    }

    private fun trackLine(value: String, color: Int) = text(value, 15f, color).apply {
        maxLines = 1
        ellipsize = TextUtils.TruncateAt.END
        setPadding(0, dp(5), 0, dp(5))
    }

    private fun button(
        label: String,
        bg: Int,
        fg: Int,
        style: Int = Typeface.NORMAL,
        iconRes: Int? = null,
        iconSize: Int = 18,
    ) = TextView(this).apply {
        text = label
        textSize = 13f
        typeface = if (style == Typeface.BOLD) bodyStrongFont else bodyFont
        maxLines = 1
        ellipsize = TextUtils.TruncateAt.END
        gravity = Gravity.CENTER
        setTextColor(fg)
        letterSpacing = 0f
        minHeight = dp(44)
        setPadding(dp(12), 0, dp(12), 0)
        background = box(bg)
        setButtonIcon(this, iconRes, fg, iconSize)
        if (label.isBlank() && iconRes != null) contentDescription = iconDescription(iconRes)
        isClickable = true
        isFocusable = true
    }

    private fun transportButton(
        label: String,
        bg: Int,
        fg: Int,
        style: Int = Typeface.NORMAL,
        iconRes: Int? = null,
        iconSize: Int = 22,
    ) = TextView(this).apply {
        text = label
        textSize = if (style == Typeface.BOLD) 14f else 12f
        typeface = if (style == Typeface.BOLD) bodyStrongFont else monoFont
        maxLines = 1
        ellipsize = TextUtils.TruncateAt.END
        gravity = Gravity.CENTER
        setTextColor(fg)
        letterSpacing = 0f
        minHeight = dp(54)
        setPadding(dp(6), 0, dp(6), 0)
        background = box(bg, if (bg == GREEN) null else STROKE)
        setButtonIcon(this, iconRes, fg, iconSize)
        if (label.isBlank() && iconRes != null) contentDescription = iconDescription(iconRes)
        isClickable = true
        isFocusable = true
    }

    private fun iconButton(
        iconRes: Int,
        bg: Int,
        fg: Int,
        iconSize: Int = 24,
        stroke: Boolean = true,
    ) = CenteredIconButton(this).apply {
        background = box(bg, if (stroke) STROKE else null)
        setIconButtonIcon(this, iconRes, fg, iconSize)
        contentDescription = iconDescription(iconRes)
    }

    private fun trackActionButton(
        iconRes: Int,
        fg: Int,
        iconSize: Int = 21,
    ) = CenteredIconButton(this).apply {
        background = box(0x00000000)
        setIconButtonIcon(this, iconRes, fg, iconSize)
        contentDescription = iconDescription(iconRes)
    }

    private fun tab(label: String, active: Boolean, iconRes: Int) = button(
        label,
        if (active) ACCENT else PANEL,
        if (active) BG else MUTED,
        Typeface.BOLD,
        iconRes,
        16,
    ).apply {
        textSize = 12f
        letterSpacing = 0f
        setPadding(dp(4), 0, dp(4), 0)
        compoundDrawablePadding = dp(4)
    }

    private fun setButtonIcon(view: TextView, iconRes: Int?, color: Int, sizeDp: Int = 18) {
        if (iconRes == null) {
            view.setCompoundDrawables(null, null, null, null)
            return
        }
        val drawable = getDrawable(iconRes)?.mutate() ?: return
        val size = dp(sizeDp)
        drawable.setBounds(0, 0, size, size)
        drawable.setTint(color)
        if (view.text.isNullOrBlank()) {
            view.setCompoundDrawables(drawable, null, null, null)
            view.compoundDrawablePadding = 0
        } else {
            view.setCompoundDrawables(drawable, null, null, null)
            view.compoundDrawablePadding = dp(7)
        }
    }

    private fun setIconButtonIcon(view: CenteredIconButton, iconRes: Int, color: Int, sizeDp: Int = 24) {
        view.setIcon(getDrawable(iconRes), color, dp(sizeDp))
    }

    private fun iconDescription(iconRes: Int): String {
        return when (iconRes) {
            R.drawable.ic_connect -> "Connect"
            R.drawable.ic_cat -> "Switch theme"
            R.drawable.ic_delete -> "Remove"
            R.drawable.ic_halo -> "Switch theme"
            R.drawable.ic_library -> "Library"
            R.drawable.ic_pause -> "Pause"
            R.drawable.ic_people -> "People"
            R.drawable.ic_play -> "Play"
            R.drawable.ic_play_next -> "Play next"
            R.drawable.ic_playlist -> "Playlists"
            R.drawable.ic_power -> "Stop and exit"
            R.drawable.ic_queue -> "Queue"
            R.drawable.ic_queue_add -> "Add to queue"
            R.drawable.ic_radio -> "Radio"
            R.drawable.ic_repeat -> "Playback mode"
            R.drawable.ic_search -> "Search"
            R.drawable.ic_seek_back -> "Seek back 15 seconds"
            R.drawable.ic_seek_forward -> "Seek forward 15 seconds"
            R.drawable.ic_shuffle -> "Shuffle"
            R.drawable.ic_skip_next -> "Next track"
            R.drawable.ic_skip_previous -> "Previous track"
            else -> "Control"
        }
    }

    private fun tag(label: String, bg: Int, fg: Int) = TextView(this).apply {
        text = label
        textSize = 12f
        typeface = bodyStrongFont
        gravity = Gravity.CENTER
        setTextColor(fg)
        letterSpacing = 0f
        setPadding(dp(10), dp(6), dp(10), dp(6))
        background = box(bg)
    }

    private fun box(color: Int, stroke: Int? = null): Drawable {
        val fill = themedFill(color)
        val edge = stroke ?: when {
            color ushr 24 == 0 -> null
            color == PANEL || color == PANEL2 -> STROKE
            else -> BG
        }
        return when (ACTIVE_THEME.frameStyle) {
            FrameStyle.PIXEL -> PixelBoxDrawable(
                fillColor = fill,
                strokeColor = edge,
                unitPx = dp(4).toFloat(),
                strokePx = dp(1).toFloat(),
            )
            FrameStyle.SERAPH -> SeraphBoxDrawable(
                fillColor = fill,
                strokeColor = edge,
                unitPx = dp(9).toFloat(),
                strokePx = dp(1).toFloat(),
            )
        }
    }

    private fun themedFill(color: Int): Int {
        if (ACTIVE_THEME.key != "seraph") return color
        return when (color) {
            PANEL -> withAlpha(color, 240)
            PANEL2 -> withAlpha(color, 226)
            else -> color
        }
    }

    private fun withAlpha(color: Int, alpha: Int): Int {
        return (color and 0x00FFFFFF) or (alpha.coerceIn(0, 255) shl 24)
    }

    private fun frameOverlay(color: Int): Drawable? {
        val overlay = when (ACTIVE_THEME.frameStyle) {
            FrameStyle.PIXEL -> R.drawable.pixel_frame_overlay
            FrameStyle.SERAPH -> R.drawable.seraph_frame_overlay
        }
        return getDrawable(overlay)?.mutate()?.apply {
            setTint(color)
        }
    }

    private fun matchWrap() = LinearLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
    )

    private fun matchWrapWithTop(top: Int) = matchWrap().apply {
        topMargin = top
    }

    private fun weightWrap(weight: Float) = LinearLayout.LayoutParams(
        0,
        ViewGroup.LayoutParams.WRAP_CONTENT,
        weight,
    )

    private fun leftGap() = LinearLayout.LayoutParams(
        ViewGroup.LayoutParams.WRAP_CONTENT,
        ViewGroup.LayoutParams.WRAP_CONTENT,
    ).apply {
        leftMargin = dp(8)
    }

    private fun smallControlParams() = LinearLayout.LayoutParams(
        0,
        dp(56),
        1.25f,
    ).apply {
        leftMargin = dp(3)
        rightMargin = dp(3)
    }

    private fun mainControlParams() = LinearLayout.LayoutParams(
        0,
        dp(64),
        1.7f,
    ).apply {
        leftMargin = dp(4)
        rightMargin = dp(4)
    }

    private fun wideControlParams() = LinearLayout.LayoutParams(
        0,
        dp(52),
        1f,
    ).apply {
        leftMargin = dp(3)
        rightMargin = dp(3)
    }

    private fun trackActionParams() = LinearLayout.LayoutParams(
        dp(44),
        dp(44),
    ).apply {
        leftMargin = dp(2)
    }

    private fun dp(value: Int): Int {
        return (value * resources.displayMetrics.density + 0.5f).toInt()
    }

    companion object {
        private val THEMES = listOf(
            ThemeSpec(
                key = "arcade",
                label = "PIXEL",
                bg = 0xFF0D0B09.toInt(),
                panel = 0xFF191511.toInt(),
                panel2 = 0xFF262019.toInt(),
                stroke = 0xFF4A3A2B.toInt(),
                text = 0xFFFFF4E3.toInt(),
                muted = 0xFFB8A995.toInt(),
                accent = 0xFFFF6B35.toInt(),
                green = 0xFFA7E15D.toInt(),
                amber = 0xFFE0A94F.toInt(),
                red = 0xFFD63B2E.toInt(),
                purple = 0xFFC78944.toInt(),
                displayFont = R.font.pixelify_sans,
                bodyFont = R.font.pixelify_sans,
                bodyStrongFont = R.font.pixelify_sans,
                monoFont = R.font.pixelify_sans,
                frameStyle = FrameStyle.PIXEL,
                lightSystemBars = false,
            ),
            ThemeSpec(
                key = "seraph",
                label = "AURA",
                bg = 0xFFF7F2FF.toInt(),
                panel = 0xFFFFFCF6.toInt(),
                panel2 = 0xFFF0E8FF.toInt(),
                stroke = 0xFFD8B95A.toInt(),
                text = 0xFF30243E.toInt(),
                muted = 0xFF7F708D.toInt(),
                accent = 0xFF83C4FF.toInt(),
                green = 0xFF73D9C7.toInt(),
                amber = 0xFFF2C665.toInt(),
                red = 0xFFFF7188.toInt(),
                purple = 0xFFD5B8FF.toInt(),
                displayFont = R.font.rajdhani_bold,
                bodyFont = R.font.rajdhani_regular,
                bodyStrongFont = R.font.rajdhani_semibold,
                monoFont = R.font.jetbrains_mono_regular,
                frameStyle = FrameStyle.SERAPH,
                lightSystemBars = true,
            ),
            ThemeSpec(
                key = "cat",
                label = "BLACK CAT",
                bg = 0xFF070908.toInt(),
                panel = 0xFF101511.toInt(),
                panel2 = 0xFF182019.toInt(),
                stroke = 0xFF5D704F.toInt(),
                text = 0xFFF2F7E6.toInt(),
                muted = 0xFF9CAB91.toInt(),
                accent = 0xFFB7F06A.toInt(),
                green = 0xFFC9FF74.toInt(),
                amber = 0xFFE7D86A.toInt(),
                red = 0xFFFF7AA8.toInt(),
                purple = 0xFF9E7BFF.toInt(),
                displayFont = R.font.pixelify_sans,
                bodyFont = R.font.pixelify_sans,
                bodyStrongFont = R.font.pixelify_sans,
                monoFont = R.font.pixelify_sans,
                frameStyle = FrameStyle.PIXEL,
                lightSystemBars = false,
            ),
        )
        private var ACTIVE_THEME = THEMES.first()

        private fun themeForKey(key: String): ThemeSpec {
            return THEMES.firstOrNull { it.key == key } ?: THEMES.first()
        }

        private val BG get() = ACTIVE_THEME.bg
        private val PANEL get() = ACTIVE_THEME.panel
        private val PANEL2 get() = ACTIVE_THEME.panel2
        private val STROKE get() = ACTIVE_THEME.stroke
        private val TEXT get() = ACTIVE_THEME.text
        private val MUTED get() = ACTIVE_THEME.muted
        private val ACCENT get() = ACTIVE_THEME.accent
        private val GREEN get() = ACTIVE_THEME.green
        private val AMBER get() = ACTIVE_THEME.amber
        private val RED get() = ACTIVE_THEME.red
        private val PURPLE get() = ACTIVE_THEME.purple
    }
}
