package com.ponzischeme89.memby.ui.player

import android.content.Context
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.annotation.MainThread
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import com.ponzischeme89.memby.R

/**
 * Keeps Memby's short local preroll prepared between playback sessions.
 *
 * Application start only registers an idle callback. Player construction and local-file
 * preparation therefore begin after the launcher's queued start-up work has drained, and
 * never block [android.app.Application.onCreate] or a home request. The same player is
 * sought back to the beginning and prepared again after use rather than reconstructed for
 * every title.
 */
@UnstableApi
internal object PrerollPreloader {
    private val mainHandler = Handler(Looper.getMainLooper())
    private var applicationContext: Context? = null
    private var preloadScheduled = false
    private var cachedPlayer: ExoPlayer? = null

    fun start(context: Context) {
        applicationContext = context.applicationContext
        mainHandler.post(::scheduleAtMainQueueIdle)
    }

    private fun scheduleAtMainQueueIdle() {
        if (preloadScheduled || cachedPlayer?.playbackState == ExoPlayer.STATE_READY) return
        preloadScheduled = true
        Looper.myQueue().addIdleHandler {
            preloadScheduled = false
            val context = applicationContext
            if (context != null) {
                val cached = cachedPlayer
                if (cached == null) {
                    // This runs directly from the main looper rather than from a
                    // coroutine, so an exception here is otherwise an uncaught process
                    // crash. The opening clip is optional: a broken vendor audio stack or
                    // unavailable extension must skip it and leave the launcher running.
                    cachedPlayer = runCatching { createPreparedPlayer(context) }.getOrNull()
                } else if (cached.playbackState == ExoPlayer.STATE_IDLE) {
                    runCatching { cached.prepare() }
                }
            }
            false
        }
    }

    /** Takes ownership of the prepared player until [recycle] or [discard] is called. */
    @MainThread
    fun acquire(context: Context): ExoPlayer {
        checkMainThread()
        applicationContext = context.applicationContext
        val prepared = cachedPlayer
        cachedPlayer = null
        if (prepared != null && prepared.playerError == null) return prepared.normalised()
        prepared?.release()
        return createPreparedPlayer(context.applicationContext)
    }

    /**
     * Hands the instance over in a known state.
     *
     * One player is passed between two callers that want different things of it — the
     * launcher's cold-start screen mutes it and loops it, the player's pre-roll needs it
     * audible and needs it to *end*, since `STATE_ENDED` is what hands over to the
     * programme. A borrower left one of those set and the next playback would sit behind a
     * silent clip that never finished. Normalising here rather than trusting each caller to
     * put it back is the only version of that which cannot be forgotten.
     */
    private fun ExoPlayer.normalised(): ExoPlayer = apply {
        volume = 1f
        repeatMode = ExoPlayer.REPEAT_MODE_OFF
    }

    /**
     * Returns a healthy player to the process cache without holding a second decoder while
     * the requested programme is playing. [start] prepares it again when Home next opens.
     */
    @MainThread
    fun recycle(player: ExoPlayer) {
        checkMainThread()
        if (player.playerError != null) {
            discard(player)
            return
        }
        player.playWhenReady = false
        player.pause()
        player.stop()
        player.seekTo(0L)
        player.normalised()
        cachedPlayer?.release()
        cachedPlayer = player
        // Keep it idle while the programme owns the decoder. Home calls [start] when it is
        // visible again, which prepares this cached instance without competing with video.
    }

    /** Drops a failed or otherwise unusable instance; the next Home/start call replaces it. */
    @MainThread
    fun discard(player: ExoPlayer) {
        checkMainThread()
        if (cachedPlayer === player) cachedPlayer = null
        player.release()
    }

    private fun createPreparedPlayer(context: Context): ExoPlayer =
        PlayerEngine.create(context).apply {
            setMediaItem(
                MediaItem.fromUri(
                    Uri.parse("android.resource://${context.packageName}/${R.raw.emby_preroll}"),
                ),
            )
            playWhenReady = false
            prepare()
        }

    private fun checkMainThread() {
        check(Looper.myLooper() == Looper.getMainLooper()) {
            "The preroll player must be transferred on the main thread"
        }
    }
}
