package com.ponzischeme89.memby.ui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import com.ponzischeme89.memby.ui.player.PrerollPreloader
import kotlinx.coroutines.delay

/** How long the last frame is held after the clip ends, before the launcher is uncovered. */
internal const val LAUNCH_INTRO_HOLD_MS = 2_000L

/**
 * The longest the launcher may ever be held back by the opening clip.
 *
 * The clip is four seconds and the hold is two, so this is that plus room for a slow
 * decoder on a weak box. It is the outer guarantee: whatever happens to the player, the
 * viewer reaches their rows.
 */
internal const val LAUNCH_INTRO_MAX_MS = 9_000L

/** How long a borrowed player has to put a frame on screen before it is given up on. */
private const val LAUNCH_INTRO_FIRST_FRAME_MS = 3_000L

/**
 * Whether this process has already played the opening clip.
 *
 * Process-scoped rather than persisted: the intro belongs to opening the app, and a
 * television that has been sitting on the launcher all evening has already had it. It is
 * also what stops the clip gating any *later* appearance of the loading screen — a profile
 * switch is not an app launch.
 */
internal object LaunchIntro {
    var played: Boolean = false
}

/**
 * Memby's own short clip, played over the cold-start screen while the app opens.
 *
 * The clip used to be decoration: it looped quietly behind the "opening" text and the
 * launcher uncovered it whenever it happened to be ready, which on a warm start was a
 * fraction of a second — so the one thing Memby owns was, in practice, never seen. It now
 * **gates the launcher**: it plays once from the beginning, its last frame is held for
 * [LAUNCH_INTRO_HOLD_MS], and only then is the home screen composed.
 *
 * Things worth preserving:
 *
 * - **It gates, but it can never trap.** Every failure — no player, a decoder error, a set
 *   that renders no frame within [LAUNCH_INTRO_FIRST_FRAME_MS] — reports itself finished
 *   immediately, and [LAUNCH_INTRO_MAX_MS] in `AppRoot` is the outer bound on top of that.
 *   A viewer must never be held on a black screen by a branding clip.
 * - **[onVisible] is the first *rendered* frame**, not the play call, so the fade never
 *   uncovers a black rectangle.
 * - **It does not loop any more.** Looping existed so a slow cold start never froze on the
 *   last frame; the hold and the fade-out do that job now, and a clip that never ends
 *   cannot gate anything.
 * - **It is muted.** The launcher's own clip plays on every single app open, and a set that
 *   chimes each time it is switched on wears out fast. The pre-roll before a programme
 *   keeps its audio — [PrerollPreloader.acquire] puts the volume back.
 * - **The player is returned, not released.** It goes back to the process cache on dispose,
 *   which is what lets the very next playback still open on a prepared instance.
 */
@UnstableApi
@Composable
internal fun LaunchPrerollVideo(
    modifier: Modifier = Modifier,
    onVisible: () -> Unit,
    onFinished: () -> Unit,
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val currentOnVisible by rememberUpdatedState(onVisible)
    val currentOnFinished by rememberUpdatedState(onFinished)

    // Deliberately *not* borrowed during composition. On a cold start there is nothing
    // cached yet, so acquiring here would construct an ExoPlayer and open the local
    // resource on the main thread inside the first composition of the one screen whose
    // whole job is to appear immediately.
    var player by remember { mutableStateOf<ExoPlayer?>(null) }
    LaunchedEffect(Unit) {
        withFrameNanos { }
        val acquired = runCatching { PrerollPreloader.acquire(context) }.getOrNull()
        if (acquired == null) currentOnFinished() else player = acquired
    }
    // A null is an ordinary outcome — the caller has already been told to carry on.
    val active = player ?: return

    var rendered by remember(active) { mutableStateOf(false) }
    var completed by remember(active) { mutableStateOf(false) }
    var failed by remember(active) { mutableStateOf(false) }

    DisposableEffect(active, lifecycleOwner) {
        val listener = object : Player.Listener {
            override fun onRenderedFirstFrame() {
                rendered = true
                currentOnVisible()
            }

            override fun onPlaybackStateChanged(state: Int) {
                if (state == Player.STATE_ENDED) completed = true
            }

            override fun onPlayerError(error: PlaybackException) {
                // Nothing to say and nowhere to say it. The screen underneath is complete,
                // and the launcher must not wait on a clip that has stopped.
                active.playWhenReady = false
                failed = true
            }
        }
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_STOP -> active.playWhenReady = false
                Lifecycle.Event.ON_START -> if (!completed) active.playWhenReady = true
                else -> Unit
            }
        }
        active.addListener(listener)
        lifecycleOwner.lifecycle.addObserver(observer)
        active.volume = 0f
        active.repeatMode = Player.REPEAT_MODE_OFF
        active.seekTo(0L)
        if (active.playbackState == Player.STATE_IDLE) active.prepare()
        active.playWhenReady = lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
            active.removeListener(listener)
            PrerollPreloader.recycle(active)
        }
    }

    // A set that cannot draw the clip at all is indistinguishable, from here, from one that
    // is simply slow — so give it a bounded chance and then get out of the way.
    LaunchedEffect(active) {
        delay(LAUNCH_INTRO_FIRST_FRAME_MS)
        if (!rendered) failed = true
    }

    LaunchedEffect(completed, failed) {
        when {
            failed -> currentOnFinished()
            completed -> {
                // The pause the whole feature is for: the mark is on screen, still, long
                // enough to have been looked at rather than glimpsed.
                delay(LAUNCH_INTRO_HOLD_MS)
                currentOnFinished()
            }
        }
    }

    AndroidView(
        modifier = modifier,
        factory = { viewContext ->
            PlayerView(viewContext).apply {
                useController = false
                // Fill rather than fit: this sits behind text as a wash, and letterbox bars
                // on a screen whose whole job is to look like Memby would read as a fault.
                resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
                setShutterBackgroundColor(android.graphics.Color.TRANSPARENT)
                this.player = active
            }
        },
        onRelease = { it.player = null },
    )
}
