package com.dtourolle.jellytau

import android.app.Activity
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Rational
import android.view.ViewGroup
import android.webkit.WebView
import androidx.annotation.RequiresApi
import com.dtourolle.jellytau.player.JellyTauPlayer

/**
 * Drives Android picture-in-picture for native (ExoPlayer) video playback.
 *
 * TRACES: UR-041 | IR-026 | DR-053
 *
 * PiP shrinks the whole Activity into a floating window, so the only thing that
 * should remain visible is the video SurfaceView that [VideoOverlayManager]
 * attached at the bottom of the z-order. The WebView carrying the Svelte UI is
 * hidden for the duration - it is opaque and sits *above* the surface, so
 * leaving it visible would occlude the video entirely.
 *
 * Playback itself is untouched: ExoPlayer keeps rendering into the same surface
 * across the transition, so entering and leaving PiP never interrupts the video.
 */
object PictureInPictureManager {

    private const val TAG = "PictureInPictureManager"

    /** Action for the play/pause RemoteAction shown inside the PiP window. */
    private const val ACTION_MEDIA_CONTROL = "com.dtourolle.jellytau.PIP_MEDIA_CONTROL"
    private const val EXTRA_CONTROL_TYPE = "control_type"
    private const val CONTROL_PLAY = 1
    private const val CONTROL_PAUSE = 2

    /** Request codes must differ per action or the PendingIntents collapse into one. */
    private const val REQUEST_PLAY = 101
    private const val REQUEST_PAUSE = 102

    private var receiver: BroadcastReceiver? = null
    private var hiddenWebView: WebView? = null

    /**
     * State of an HTML5 `<video>` playing inside the WebView, reported by the
     * frontend.
     *
     * PiP was written for the native ExoPlayer surface only — [canEnterPip]
     * required a SurfaceView to be attached and rendering. But native video is
     * behind `experimentalNativeVideo`, which defaults to **off**, so in the
     * shipping configuration video plays in the WebView's `<video>` element and
     * every one of those conditions is false. `enterPip` therefore always bailed
     * with "no local video playing": PiP could not work at all, however the
     * button was pressed.
     *
     * On this path the WebView *is* the video, which inverts two things: the
     * WebView must stay visible in PiP rather than be hidden, and play/pause has
     * to reach the element rather than ExoPlayer. Both are handled below.
     *
     * TRACES: UR-041 | DR-160
     */
    @Volatile
    private var html5VideoActive = false

    @Volatile
    private var html5VideoPlaying = false

    @Volatile
    private var html5AspectRatio: Rational? = null

    /**
     * Report the WebView `<video>` state from the frontend.
     *
     * @param active whether a video element is currently the playback surface
     * @param width intrinsic video width, for the PiP window's aspect ratio
     * @param height intrinsic video height
     * @param playing whether it is playing right now, for the PiP play/pause action
     */
    fun setHtml5VideoState(active: Boolean, width: Int, height: Int, playing: Boolean) {
        html5VideoActive = active
        html5VideoPlaying = playing
        html5AspectRatio = if (active && width > 0 && height > 0) {
            clampedRatio(width.toDouble() / height.toDouble())
        } else {
            null
        }
    }

    /** True when PiP would be showing the native surface rather than the WebView. */
    private fun isNativeVideoPath(): Boolean = try {
        val player = JellyTauPlayer.getInstance()
        player.isPlayingVideo() &&
            player.getSurfaceView() != null &&
            VideoOverlayManager.isVideoSurfaceAttached()
    } catch (e: Exception) {
        android.util.Log.w(TAG, "native video path check failed", e)
        false
    }

    /**
     * Whether this device/OS can do PiP at all. Android 8.0 introduced the API,
     * and the user (or device manufacturer) can disable the feature per-app.
     */
    fun isPipSupported(activity: Activity): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
        return activity.packageManager.hasSystemFeature(
            android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE
        )
    }

    /**
     * Whether entering PiP right now makes sense: a native video must actually
     * be playing locally. Audio-only playback and remote/cast sessions render
     * nothing on this device, so a PiP window would be an empty black box.
     */
    fun canEnterPip(activity: Activity): Boolean {
        if (!isPipSupported(activity)) return false
        // Either surface will do: the native one, or the WebView's `<video>`,
        // which is what actually plays while experimentalNativeVideo is off.
        // (DR-160)
        return isNativeVideoPath() || html5VideoActive
    }

    /**
     * Enter picture-in-picture, sizing the window to the video's aspect ratio.
     *
     * @return true if the system accepted the transition.
     */
    fun enterPip(activity: Activity): Boolean {
        if (!canEnterPip(activity)) {
            android.util.Log.d(TAG, "Not entering PiP: no local video playing")
            return false
        }
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false

        return try {
            val params = buildParams(activity)
            val entered = activity.enterPictureInPictureMode(params)
            android.util.Log.d(TAG, "enterPictureInPictureMode returned $entered")
            entered
        } catch (e: Exception) {
            // IllegalStateException here means PiP is disallowed (e.g. the user
            // turned it off in system settings). Never crash over it.
            android.util.Log.e(TAG, "Failed to enter PiP", e)
            false
        }
    }

    /**
     * Build PiP params: aspect ratio from the current video, plus a play/pause
     * RemoteAction reflecting the live playback state.
     */
    @RequiresApi(Build.VERSION_CODES.O)
    private fun buildParams(activity: Activity): PictureInPictureParams {
        val builder = PictureInPictureParams.Builder()

        aspectRatioFor()?.let { builder.setAspectRatio(it) }
        builder.setActions(listOf(buildPlayPauseAction(activity)))

        return builder.build()
    }

    /**
     * The video's aspect ratio, clamped to the range Android accepts.
     *
     * The platform rejects ratios outside roughly 1:2.39 - 2.39:1 with an
     * IllegalArgumentException, which would otherwise take down the Activity on
     * unusually tall or wide content.
     */
    private fun aspectRatioFor(): Rational? {
        val player = try {
            JellyTauPlayer.getInstance()
        } catch (e: Exception) {
            null
        }

        // The surface has already been letterboxed to the video's aspect ratio
        // by fitSurfaceToScreen(), so its measured bounds are the video shape.
        val surface = player?.getSurfaceView()
        if (surface != null && surface.width > 0 && surface.height > 0) {
            return clampedRatio(surface.width.toDouble() / surface.height.toDouble())
        }

        // No native surface: the WebView is the video, so use the intrinsic size
        // the frontend reported. (DR-160)
        return html5AspectRatio
    }

    /**
     * Clamp a ratio to the range Android accepts and express it as a [Rational].
     *
     * The platform rejects ratios outside roughly 1:2.39 - 2.39:1 with an
     * IllegalArgumentException, which would otherwise take down the Activity on
     * unusually tall or wide content. Scaled to integers because
     * `Rational(width, height)` can overflow for large surfaces, and the clamped
     * value may not match the raw pixels anyway.
     */
    private fun clampedRatio(ratio: Double): Rational {
        val clamped = ratio.coerceIn(1.0 / 2.39, 2.39)
        return Rational((clamped * 1000).toInt(), 1000)
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun buildPlayPauseAction(activity: Activity): RemoteAction {
        // On the HTML5 path ExoPlayer is idle, so its `isPlaying` is always false
        // and the button would be stuck showing "Play" mid-playback. (DR-160)
        val isPlaying = if (isNativeVideoPath()) {
            try {
                JellyTauPlayer.getInstance().getExoPlayer().isPlaying
            } catch (e: Exception) {
                false
            }
        } else {
            html5VideoPlaying
        }

        val (iconRes, title, controlType, requestCode) = if (isPlaying) {
            Quad(
                android.R.drawable.ic_media_pause,
                "Pause",
                CONTROL_PAUSE,
                REQUEST_PAUSE
            )
        } else {
            Quad(
                android.R.drawable.ic_media_play,
                "Play",
                CONTROL_PLAY,
                REQUEST_PLAY
            )
        }

        val intent = Intent(ACTION_MEDIA_CONTROL)
            .putExtra(EXTRA_CONTROL_TYPE, controlType)
            // Explicit package keeps the broadcast internal to the app.
            .setPackage(activity.packageName)

        val flags = android.app.PendingIntent.FLAG_UPDATE_CURRENT or
            android.app.PendingIntent.FLAG_IMMUTABLE

        val pendingIntent = android.app.PendingIntent.getBroadcast(
            activity,
            requestCode,
            intent,
            flags
        )

        return RemoteAction(
            Icon.createWithResource(activity, iconRes),
            title,
            title,
            pendingIntent
        )
    }

    private data class Quad<A, B, C, D>(
        val first: A,
        val second: B,
        val third: C,
        val fourth: D
    )

    /**
     * Refresh the PiP window's action button so it tracks play/pause state
     * while the window is open. Safe to call when not in PiP (no-op).
     */
    fun updatePipActions(activity: Activity) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
        if (!activity.isInPictureInPictureMode) return
        try {
            activity.setPictureInPictureParams(buildParams(activity))
        } catch (e: Exception) {
            android.util.Log.w(TAG, "Failed to update PiP actions", e)
        }
    }

    /**
     * Called from MainActivity.onPictureInPictureModeChanged.
     *
     * Entering: hide the WebView so only the video surface shows, and register
     * the receiver backing the PiP play/pause button.
     * Leaving: restore the WebView and unregister.
     */
    fun onPipModeChanged(activity: Activity, isInPipMode: Boolean) {
        if (isInPipMode) {
            // Hiding the WebView is correct only when the video is *behind* it on
            // the native surface. On the HTML5 path the WebView is the video, so
            // hiding it would leave an empty black PiP window — the frontend
            // instead strips its own chrome when it hears the event below.
            // (DR-160)
            if (isNativeVideoPath()) {
                hideWebView(activity)
            }
            registerReceiver(activity)
            dispatchWebEvent(activity, "jellytau-pip-entered")
        } else {
            unregisterReceiver(activity)
            showWebView()
            dispatchWebEvent(activity, "jellytau-pip-exited")
            // The surface was laid out against the tiny PiP bounds; re-fit it to
            // the restored full-screen bounds or the video stays postage-stamp sized.
            try {
                JellyTauPlayer.getInstance().fitSurfaceToScreen()
            } catch (e: Exception) {
                android.util.Log.w(TAG, "Failed to re-fit surface after PiP", e)
            }
        }
    }

    /**
     * Fire a DOM event into the WebView.
     *
     * The HTML5 PiP path is a conversation with the frontend rather than
     * something native can do alone: it has to be told to strip its chrome when
     * the window shrinks, and to play/pause the element. (DR-160)
     */
    private fun dispatchWebEvent(activity: Activity, name: String) {
        val webView = findWebView(activity.window.decorView) ?: return
        webView.post {
            webView.evaluateJavascript(
                "window.dispatchEvent(new CustomEvent('$name'));",
                null
            )
        }
    }

    private fun hideWebView(activity: Activity) {
        val webView = findWebView(activity.window.decorView)
        if (webView == null) {
            android.util.Log.w(TAG, "No WebView found to hide for PiP")
            return
        }
        // GONE rather than INVISIBLE: the WebView is opaque, and GONE also stops
        // it from consuming layout space in the shrunken window.
        webView.visibility = android.view.View.GONE
        hiddenWebView = webView
        android.util.Log.d(TAG, "WebView hidden for PiP")
    }

    private fun showWebView() {
        hiddenWebView?.let {
            it.visibility = android.view.View.VISIBLE
            android.util.Log.d(TAG, "WebView restored after PiP")
        }
        hiddenWebView = null
    }

    private fun registerReceiver(activity: Activity) {
        if (receiver != null) return

        val r = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                if (intent?.action != ACTION_MEDIA_CONTROL) return
                val control = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)

                if (isNativeVideoPath()) {
                    val player = try {
                        JellyTauPlayer.getInstance()
                    } catch (e: Exception) {
                        return
                    }
                    when (control) {
                        CONTROL_PLAY -> player.play()
                        CONTROL_PAUSE -> player.pause()
                    }
                } else {
                    // The WebView owns playback here, so the command has to reach
                    // the `<video>` element. Driving ExoPlayer instead would do
                    // nothing at all, which is what a PiP button on the HTML5 path
                    // used to do. (DR-160)
                    val name = when (control) {
                        CONTROL_PLAY -> "jellytau-pip-play"
                        CONTROL_PAUSE -> "jellytau-pip-pause"
                        else -> return
                    }
                    dispatchWebEvent(activity, name)
                    html5VideoPlaying = control == CONTROL_PLAY
                }
                // Swap the button to reflect the new state.
                updatePipActions(activity)
            }
        }

        val filter = IntentFilter(ACTION_MEDIA_CONTROL)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            activity.registerReceiver(r, filter, Context.RECEIVER_NOT_EXPORTED)
        } else {
            @Suppress("UnspecifiedRegisterReceiverFlag")
            activity.registerReceiver(r, filter)
        }
        receiver = r
        android.util.Log.d(TAG, "PiP media control receiver registered")
    }

    private fun unregisterReceiver(activity: Activity) {
        receiver?.let {
            try {
                activity.unregisterReceiver(it)
            } catch (e: IllegalArgumentException) {
                // Already unregistered - harmless.
            }
        }
        receiver = null
    }

    private fun findWebView(view: android.view.View): WebView? {
        if (view is WebView) return view
        if (view is ViewGroup) {
            for (i in 0 until view.childCount) {
                findWebView(view.getChildAt(i))?.let { return it }
            }
        }
        return null
    }
}
