package com.dtourolle.jellytau

import android.app.Activity
import android.os.Handler
import android.os.Looper
import android.view.WindowManager
import java.lang.ref.WeakReference

/**
 * Which playback paths currently want the screen kept awake.
 *
 * Pure state, deliberately free of any Android type so it can be unit-tested —
 * see ScreenWakeStateTest. Two independent holders, because video can be
 * rendered by either renderer and only one of them is active at a time:
 *
 * - **native** — ExoPlayer drawing into the TextureView (DR-192)
 * - **html5** — a `<video>` inside the WebView, reported by the frontend
 *
 * Audio is deliberately *not* a holder. Playing music with the screen off is the
 * point of the audio path; only video needs the display alive.
 *
 * TRACES: UR-003 | DR-202 | UT-199
 */
class ScreenWakeState {
    private var nativeVideoPlaying = false
    private var html5VideoPlaying = false

    /** True while any video renderer is actively playing. */
    val keepScreenOn: Boolean
        get() = nativeVideoPlaying || html5VideoPlaying

    /**
     * @param playing whether ExoPlayer is playing right now
     * @param isVideo whether what it is playing is video rather than audio
     */
    fun updateNative(playing: Boolean, isVideo: Boolean) {
        nativeVideoPlaying = playing && isVideo
    }

    /**
     * @param active whether a webview `<video>` is the current playback surface
     * @param playing whether that element is playing right now
     */
    fun updateHtml5(active: Boolean, playing: Boolean) {
        html5VideoPlaying = active && playing
    }

    /** Drop every hold (teardown, or a page that can no longer be trusted). */
    fun reset() {
        nativeVideoPlaying = false
        html5VideoPlaying = false
    }
}

/**
 * Keeps the display awake while video is playing.
 *
 * TRACES: UR-003 | DR-202
 *
 * ## Why this is needed at all
 *
 * Android turns the screen off on its own display timeout, counted from the last
 * *user input*. Watching a film is precisely the case where there is none, so
 * without an explicit hold the screen dimmed and slept mid-playback and the user
 * had to keep tapping it. Nothing in the app held it: `FLAG_KEEP_SCREEN_ON`
 * appeared nowhere, and neither renderer supplies one for free — ExoPlayer's
 * `setWakeMode` is a *CPU/wifi* wake lock and says nothing about the display,
 * and it draws into a `TextureView` we own rather than a `PlayerView`, which is
 * the media3 widget that would otherwise set `keepScreenOn` itself. The WebView
 * `<video>` path does not either: the display wake lock Chrome takes for video
 * lives in the browser layer, not in an embedded WebView.
 *
 * ## Approach
 *
 * `FLAG_KEEP_SCREEN_ON` on the Activity window rather than a
 * `PowerManager.WakeLock`: the flag is scoped to the window, so it stops
 * applying the moment the app is not visible and cannot survive a crash or a
 * missed release the way an explicitly acquired wake lock can. It needs no
 * permission. (The manifest's `WAKE_LOCK` is the media service's, unrelated.)
 *
 * The two renderers report independently and are OR-ed together in
 * [ScreenWakeState]:
 *
 * - `JellyTauPlayer.onIsPlayingChanged` and its surface teardown drive the
 *   native path — ExoPlayer is the authoritative source of playback state, so
 *   the hold follows what it reports rather than what the UI intends.
 * - `MainActivity`'s `AndroidPictureInPicture.setHtml5VideoState` bridge drives
 *   the webview path. The frontend already reports that state on every
 *   play/pause and on player teardown for PiP, so no new bridge is needed.
 *
 * The Activity reference is weak and re-set on every `onCreate`, so a
 * recreation (rotation) re-applies the current hold to the new window.
 */
object ScreenWakeManager {

    private const val TAG = "ScreenWakeManager"

    private val mainHandler = Handler(Looper.getMainLooper())
    private val state = ScreenWakeState()
    private var activityRef: WeakReference<Activity>? = null

    /**
     * Adopt the Activity whose window carries the flag, and re-apply the current
     * hold to it. Called from `MainActivity.onCreate`, so a rotation-recreated
     * Activity keeps the screen awake without waiting for the next state report.
     */
    @Synchronized
    fun setActivity(activity: Activity) {
        activityRef = WeakReference(activity)
        apply()
    }

    /** Drop the Activity on destroy, unless a newer one has already replaced it. */
    @Synchronized
    fun clearActivity(activity: Activity) {
        if (activityRef?.get() === activity) {
            activityRef = null
        }
    }

    /** ExoPlayer's playback state changed. */
    @Synchronized
    fun onNativePlaybackChanged(playing: Boolean, isVideo: Boolean) {
        state.updateNative(playing, isVideo)
        apply()
    }

    /**
     * The frontend reported the webview `<video>` state. Arrives on a WebView
     * binder thread, hence the synchronization and the post to the main thread.
     */
    @Synchronized
    fun onHtml5VideoState(active: Boolean, playing: Boolean) {
        state.updateHtml5(active, playing)
        apply()
    }

    /**
     * Drop every hold. Used when a new WebView/page load invalidates whatever the
     * previous page last reported — a page that goes away without a final
     * `setHtml5VideoState(false, …)` would otherwise leave the screen pinned on
     * for the life of the process.
     */
    @Synchronized
    fun releaseAll() {
        state.reset()
        apply()
    }

    private fun apply() {
        val desired = state.keepScreenOn
        val activity = activityRef?.get() ?: return
        mainHandler.post {
            try {
                if (activity.isFinishing || activity.isDestroyed) return@post
                if (desired) {
                    activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
                } else {
                    activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
                }
                android.util.Log.d(TAG, "keepScreenOn = $desired")
            } catch (e: Exception) {
                android.util.Log.w(TAG, "Failed to apply keep-screen-on flag", e)
            }
        }
    }
}
