/*
 * Bitstream audio passthrough adapted from Moonfin's TV backends.
 *
 * Moonfin: https://github.com/Moonfin-Client/Moonfin-Core
 *
 * Modifications Copyright (C) 2026 Memby contributors
 * SPDX-License-Identifier: GPL-2.0-only
 */

package com.ponzischeme89.memby.data.playback

import android.content.Context
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.audio.AudioCapabilities

/**
 * Asks the platform what the thing on the end of the HDMI cable accepts, and what this
 * television can decode for itself if the answer is nothing.
 *
 * The probe needs a Context, which the video probe beside it does not, so it is installed
 * from [com.ponzischeme89.memby.ServiceLocator] rather than being a plain `by lazy`. It is
 * still evaluated on first *use* — the first gateway request or the first playback
 * negotiation — and never during `Application.onCreate`.
 */
private object AudioProbeHolder {
    @Volatile
    var appContext: Context? = null

    @Volatile
    var cached: DeviceAudioCapabilities? = null

    @Volatile
    var watchingRoute: Boolean = false
}

/** Called once from the service locator, with the application context. */
fun installAudioCapabilityProbe(context: Context) {
    AudioProbeHolder.appContext = context.applicationContext
}

/**
 * What this set and its receiver turned out to support.
 *
 * A result is memoised only once it has actually been probed. Reading this before the
 * context is installed answers "nothing known" *without* caching it, because the
 * alternative — a `by lazy` that happened to be touched one call too early — would leave
 * the television advertising a stereo-only profile for the rest of the process.
 */
val deviceAudioCapabilities: DeviceAudioCapabilities
    get() = AudioProbeHolder.cached ?: probeAudioCapabilities()

@UnstableApi
private fun probeAudioCapabilities(): DeviceAudioCapabilities {
    val context = AudioProbeHolder.appContext ?: return DeviceAudioCapabilities()
    // Route callbacks are useful but optional. A handful of vendor AudioManager
    // implementations throw while registering one; that must not escape the otherwise
    // defensive probe and take the launcher down with it.
    runCatching { watchAudioRoute(context) }
    val probed = runCatching { AndroidAudioCapabilityProbe(context).probe() }
        // A vendor build with broken audio metadata must still play something: an empty
        // probe means PCM through whatever the platform decoders can manage, which is the
        // behaviour this app had before passthrough existed.
        .getOrElse { DeviceAudioCapabilities(probed = true) }
    AudioProbeHolder.cached = probed
    return probed
}

/**
 * Moonfin re-probes when an AVR appears or disappears. Keep that behaviour here: Media3's
 * automatic sink already follows the live route, and invalidating this snapshot makes the
 * next gateway request and Settings visit describe the same route as the player.
 */
private fun watchAudioRoute(context: Context) {
    if (AudioProbeHolder.watchingRoute) return
    synchronized(AudioProbeHolder) {
        if (AudioProbeHolder.watchingRoute) return
        val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
            ?: return
        audioManager.registerAudioDeviceCallback(
            object : AudioDeviceCallback() {
                override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
                    AudioProbeHolder.cached = null
                }

                override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
                    AudioProbeHolder.cached = null
                }
            },
            Handler(Looper.getMainLooper()),
        )
        AudioProbeHolder.watchingRoute = true
    }
}

@UnstableApi
private class AndroidAudioCapabilityProbe(private val context: Context) {

    fun probe(): DeviceAudioCapabilities {
        // The same attributes the player is built with. Passthrough support is routing
        // dependent, so probing under different attributes than playback uses would be
        // asking a question about a different audio path.
        val capabilities = AudioCapabilities.getCapabilities(
            context,
            AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(),
            /* routedDevice = */ null,
        )
        return DeviceAudioCapabilities(
            passthrough = SurroundCodec.entries
                .filterTo(mutableSetOf()) { capabilities.supportsEncoding(it.encoding()) },
            // The platform probe remains useful for preferring its hardware decoder, but
            // the packaged FFmpeg Media3 extension covers every format below when it does
            // not. Advertise that real software fallback to Emby so audio alone never
            // causes a video transcode.
            decode = SurroundCodec.entries.toSet(),
            maxChannelCount = capabilities.maxChannelCount,
            probed = true,
        )
    }

}

/**
 * The media3 encoding a bitstreamed track is written to the audio track as.
 *
 * Kept here rather than on [SurroundCodec] so the rule itself stays free of media3 types
 * and testable without Android. This is also what the player's manual-mode override is
 * built from, so the set the probe is asked about and the set the sink is told about can
 * never fall out of step.
 */
@UnstableApi
internal fun SurroundCodec.encoding(): Int = when (this) {
    SurroundCodec.AC3 -> C.ENCODING_AC3
    SurroundCodec.EAC3 -> C.ENCODING_E_AC3
    SurroundCodec.ATMOS -> C.ENCODING_E_AC3_JOC
    SurroundCodec.DTS -> C.ENCODING_DTS
    SurroundCodec.DTS_HD -> C.ENCODING_DTS_HD
    SurroundCodec.TRUEHD -> C.ENCODING_DOLBY_TRUEHD
}
