package com.ponzischeme89.memby.ui.player

import android.content.Context
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.extractor.DefaultExtractorsFactory
import com.ponzischeme89.memby.data.playback.AudioPassthroughPreference
import com.ponzischeme89.memby.data.remote.HttpStack
import java.util.concurrent.TimeUnit

/**
 * Builds the one ExoPlayer this app plays video with.
 *
 * It lives apart from [PlayerActivity] because construction is on the critical path of
 * every launch and wants to happen before any of the activity's decoration: the activity
 * builds this first, hands it the stream, and only then wires up its overlays.
 *
 * The configuration here is aimed squarely at *time to first frame on a resume*, which is
 * the slowest thing the player does — a resume is a seek, and a seek over HTTP is several
 * more requests before a single frame is decoded. Two of the choices are borrowed from
 * Wholphin (https://github.com/damontecres/Wholphin), an Android TV Jellyfin client under
 * the same GPL-2.0 licence as this app: constant-bitrate seeking, and pulling media bytes
 * through the app's own OkHttp client rather than media3's default.
 */
@UnstableApi
internal object PlayerEngine {

    /**
     * [audio] decides whether a surround track is bitstreamed to the receiver or decoded
     * here into PCM. It defaults to automatic, which is also the right answer for the
     * pre-roll: that clip is stereo AAC, and it borrows this same builder.
     */
    fun create(
        context: Context,
        audio: AudioPassthroughPreference = AudioPassthroughPreference.AUTOMATIC,
    ): ExoPlayer = try {
        buildPlayer(context, audio, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
    } catch (_: RuntimeException) {
        // Extension construction happens while ExoPlayer is built. A binary mismatch or
        // broken vendor audio implementation must degrade to Android's renderers rather
        // than make Memby close during its process-wide pre-roll warm-up.
        buildPlayer(context, audio, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
    } catch (_: LinkageError) {
        buildPlayer(context, audio, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
    }

    private fun buildPlayer(
        context: Context,
        audio: AudioPassthroughPreference,
        extensionRendererMode: Int,
    ): ExoPlayer = ExoPlayer.Builder(context)
        .setMediaSourceFactory(mediaSourceFactory(context))
        .setRenderersFactory(
            MembyRenderersFactory(context, audio)
                // Some Android TV firmwares advertise a preferred hardware decoder which
                // fails only after initialization. Let Media3 try another installed decoder
                // before declaring the file unsupported.
                .setEnableDecoderFallback(true)
                // The bundled FFmpeg audio renderer sits after platform decoders. It is
                // reached only when Android cannot decode a surround format itself; its
                // output is PCM, so passthrough-capable tracks still bypass it untouched.
                .setExtensionRendererMode(extensionRendererMode),
        )
        .setTrackSelector(DefaultTrackSelector(context))
        .setLoadControl(loadControl())
        .build()
        .apply {
            setAudioAttributes(
                AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(),
                /* handleAudioFocus = */ false,
            )
        }

    private fun mediaSourceFactory(context: Context) = DefaultMediaSourceFactory(
        // DefaultDataSource still handles the non-HTTP schemes (file:, asset:, content:);
        // only the HTTP half is swapped for ours.
        DefaultDataSource.Factory(context, OkHttpDataSource.Factory(streamClient)),
        extractorsFactory(),
    )

    /**
     * Derived from the shared stack, so the stream, the artwork and the API calls share one
     * connection pool. That is what a resume is really paying for: opening a file part-way
     * through means a request for the container header, usually another for the seek index
     * (in a Matroska file the cues commonly sit at the *end*), and then a third at the
     * offset the viewer actually stopped at. On the default client each of those is a fresh
     * connection — and over HTTPS, a fresh TLS handshake — before any bytes arrive.
     *
     * The read timeout is generous rather than absent: a stalled stream must eventually
     * fail so [PlaybackRecovery] can re-negotiate it, but it must not fail on a slow seek.
     * No call timeout is set, and none should be — that would cap the length of the film.
     */
    private val streamClient by lazy {
        HttpStack.base.newBuilder()
            .connectTimeout(STREAM_CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .readTimeout(STREAM_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .build()
    }

    /**
     * Constant-bitrate seeking is the difference between a resume that is a byte-offset
     * calculation and one that is a scan through the file. Media3 only uses it for
     * containers with no seek table of their own, so switching it on cannot make an indexed
     * MP4 or MKV any less accurate; it only rescues the formats that would otherwise have
     * to read their way to the position. `AlwaysEnabled` extends that to streams that
     * declare a seek table but cannot use it.
     */
    private fun extractorsFactory() = DefaultExtractorsFactory()
        .setConstantBitrateSeekingEnabled(true)
        .setConstantBitrateSeekingAlwaysEnabled(true)

    private fun loadControl() = DefaultLoadControl.Builder()
        .setBufferDurationsMs(
            MIN_BUFFER_MS,
            MAX_BUFFER_MS,
            BUFFER_FOR_PLAYBACK_MS,
            BUFFER_AFTER_REBUFFER_MS,
        )
        .setPrioritizeTimeOverSizeThresholds(true)
        .build()

    private const val STREAM_CONNECT_TIMEOUT_SECONDS = 15L
    private const val STREAM_READ_TIMEOUT_SECONDS = 30L
    private const val MIN_BUFFER_MS = 10_000
    private const val MAX_BUFFER_MS = 30_000
    private const val BUFFER_FOR_PLAYBACK_MS = 750
    private const val BUFFER_AFTER_REBUFFER_MS = 1_500
}
