/*
 * Bitstream audio passthrough adapted from Moonfin's TV backends, which carry the same
 * feature on Android TV (Media3) and Apple TV (AetherEngine): a hardware probe that says
 * what the receiver accepts, a manual per-codec override for the sets that misreport it,
 * and a decode-to-PCM path underneath so a track always plays.
 *
 * 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

/**
 * The surround formats a receiver can be handed as a bitstream.
 *
 * Deliberately holds no media3 or Android type: this file is the rule, and the rule has to
 * be readable by a plain JUnit test. The mapping to media3's own encoding constants lives
 * next to the probe that needs them, in `AudioCapabilityProbe.kt`.
 *
 * [embyCodecs] is what Emby calls the format in a device profile, and is a *list* because
 * ffmpeg's name and Emby's are not always the one word — DTS is `dts` or `dca` depending on
 * which side of the library named the stream, and both appear in real libraries.
 *
 * [decodeMimeTypes] is a list for a different reason: an extension carried inside another
 * format is decoded by that format's decoder. Atmos is E-AC-3 to a decoder that has never
 * heard of it, and DTS-HD carries a plain DTS core, so either plays as PCM on a platform
 * holding only the base decoder — which is the fallback this whole feature rests on.
 */
enum class SurroundCodec(
    val slug: String,
    val label: String,
    val description: String,
    val mimeType: String,
    val embyCodecs: List<String>,
    val decodeMimeTypes: List<String> = listOf(mimeType),
) {
    AC3(
        slug = "ac3",
        label = "Dolby Digital",
        description = "The 5.1 track on most films and broadcasts.",
        mimeType = "audio/ac3",
        embyCodecs = listOf("ac3"),
    ),
    EAC3(
        slug = "eac3",
        label = "Dolby Digital Plus",
        description = "The newer Dolby stream, common on streaming rips.",
        mimeType = "audio/eac3",
        embyCodecs = listOf("eac3"),
    ),
    ATMOS(
        slug = "atmos",
        label = "Dolby Atmos",
        description = "Height channels carried inside a Dolby Digital Plus stream.",
        mimeType = "audio/eac3-joc",
        // Atmos is not a container of its own: it rides inside E-AC-3 (and TrueHD, which
        // has its own row). Advertising eac3 is what lets the stream through at all.
        embyCodecs = listOf("eac3"),
        decodeMimeTypes = listOf("audio/eac3-joc", "audio/eac3"),
    ),
    DTS(
        slug = "dts",
        label = "DTS",
        description = "The 5.1 track on most Blu-ray rips.",
        mimeType = "audio/vnd.dts",
        embyCodecs = listOf("dts", "dca"),
    ),
    DTS_HD(
        slug = "dts_hd",
        label = "DTS-HD",
        description = "DTS-HD Master Audio and High Resolution.",
        mimeType = "audio/vnd.dts.hd",
        embyCodecs = listOf("dts", "dca", "dtshd"),
        decodeMimeTypes = listOf("audio/vnd.dts.hd", "audio/vnd.dts"),
    ),
    TRUEHD(
        slug = "truehd",
        label = "Dolby TrueHD",
        description = "Lossless Dolby, including TrueHD Atmos.",
        mimeType = "audio/true-hd",
        embyCodecs = listOf("truehd", "mlp"),
    ),
    ;

    companion object {
        fun from(slug: String): SurroundCodec? =
            entries.firstOrNull { it.slug.equals(slug.trim(), ignoreCase = true) }

        fun setFrom(slugs: String): Set<SurroundCodec> = slugs.split(',')
            .mapNotNull(::from)
            .toSet()

        fun Set<SurroundCodec>.asSlugs(): String =
            entries.filter { it in this }.joinToString(",") { it.slug }
    }
}

/**
 * What this television and whatever is plugged into it turned out to be able to do.
 *
 * [passthrough] is the hardware's own answer — media3's probe of the HDMI sink — and
 * [decode] is what the platform holds a decoder for, which is the half that matters even
 * when there is no receiver at all: a format Memby can decode is one Emby never has to
 * transcode the *video* alongside.
 *
 * [probed] separates "asked, and the answer was nothing" from "never asked". A set that has
 * not been probed must not have its silence read as a receiver that accepts nothing.
 */
data class DeviceAudioCapabilities(
    val passthrough: Set<SurroundCodec> = emptySet(),
    val decode: Set<SurroundCodec> = emptySet(),
    val maxChannelCount: Int = STEREO_CHANNELS,
    val probed: Boolean = false,
) {
    companion object {
        const val STEREO_CHANNELS = 2

        /**
         * What a bitstreamed track is worth asking Emby for once *any* passthrough is on.
         * The receiver, not this television, is what will decode it, so the set's own
         * output channel count says nothing about how many channels may be sent.
         */
        const val BITSTREAM_CHANNELS = 8

        /**
         * Formats media3 decodes on every build, with no probe and no receiver involved.
         * These are advertised unconditionally, which is the whole reason a FLAC or Opus
         * track in an MKV no longer drags the video through an encoder with it.
         */
        val ALWAYS_DECODABLE = listOf(
            "aac", "mp3", "flac", "opus", "vorbis", "pcm_s16le", "pcm_s24le",
        )
    }
}

/**
 * How the viewer wants the probe treated.
 *
 * [MANUAL] is deliberately *authoritative* rather than a narrowing of the probe. The whole
 * reason it exists is a set whose platform lies about what its receiver accepts — a mode
 * that could only ever subtract from the probe would be no use to the case it was added
 * for. That cuts both ways, which is why the settings page states what was detected beside
 * the switches rather than only offering them.
 */
enum class AudioPassthroughMode(val value: String) {
    AUTO("auto"),
    MANUAL("manual"),
    ;

    companion object {
        val DEFAULT = AUTO

        fun from(value: String?): AudioPassthroughMode =
            entries.firstOrNull { it.value.equals(value?.trim(), ignoreCase = true) } ?: DEFAULT
    }
}

/** The viewer's answer to the probe: which mode, and what they chose if they chose. */
data class AudioPassthroughPreference(
    val mode: AudioPassthroughMode = AudioPassthroughMode.DEFAULT,
    val codecs: Set<SurroundCodec> = emptySet(),
) {
    companion object {
        val AUTOMATIC = AudioPassthroughPreference()
    }
}

/**
 * The formats that will actually be bitstreamed. Pure, and the one place the two modes are
 * resolved — the player, the device profile and the gateway tokens all read this rather
 * than each deciding what "manual" means.
 */
fun DeviceAudioCapabilities.passthroughFor(
    preference: AudioPassthroughPreference,
): Set<SurroundCodec> = when (preference.mode) {
    AudioPassthroughMode.AUTO -> passthrough
    AudioPassthroughMode.MANUAL -> preference.codecs
}.withCarrierCodecs()

/**
 * Extension formats cannot be handed to an audio sink without their carrier. This is the
 * same dependency Moonfin applies to DTS-HD and its DTS core; Atmos similarly rides an
 * E-AC-3 stream. Keeping it here means the player, Emby profile and gateway all agree.
 */
private fun Set<SurroundCodec>.withCarrierCodecs(): Set<SurroundCodec> = buildSet {
    addAll(this@withCarrierCodecs)
    if (SurroundCodec.DTS_HD in this@withCarrierCodecs) add(SurroundCodec.DTS)
    if (SurroundCodec.ATMOS in this@withCarrierCodecs) add(SurroundCodec.EAC3)
}

/**
 * Every surround format this television can put through the speakers one way or another.
 *
 * The union is the point of the feature. A track that is bitstreamed reaches the receiver
 * untouched; one that is only decodable is turned into PCM here — and either way Emby is
 * told it can send the file as it is, so the audio track alone stops being a reason to
 * re-encode somebody's video.
 */
fun DeviceAudioCapabilities.playableFor(
    preference: AudioPassthroughPreference,
): Set<SurroundCodec> = passthroughFor(preference) + decode

/**
 * The `MaxAudioChannels` Emby is told to respect.
 *
 * With any passthrough on, the answer belongs to the receiver rather than to this set, so
 * it is the full bitstream width. With none, it is what the sink reported it can render,
 * floored at stereo — a probe that returned nothing must never advertise zero channels,
 * which Emby reads as "downmix everything to mono".
 */
fun DeviceAudioCapabilities.channelLimitFor(preference: AudioPassthroughPreference): Int =
    if (passthroughFor(preference).isNotEmpty()) {
        DeviceAudioCapabilities.BITSTREAM_CHANNELS
    } else {
        maxOf(maxChannelCount, DeviceAudioCapabilities.STEREO_CHANNELS)
    }

/**
 * The Emby audio codec list for a direct-play profile: the baseline formats plus whatever
 * surround the resolved preference makes playable, deduplicated and in a stable order.
 */
fun DeviceAudioCapabilities.embyAudioCodecs(preference: AudioPassthroughPreference): String {
    val surround = playableFor(preference).flatMap(SurroundCodec::embyCodecs)
    return (DeviceAudioCapabilities.ALWAYS_DECODABLE + surround).distinct().joinToString(",")
}

/**
 * What Emby should convert an *unplayable* audio track into.
 *
 * A 5.1 track that has to be converted must not arrive as stereo AAC when there is a
 * receiver on the other end of the cable, so a set that can bitstream Dolby asks for Dolby
 * and keeps its channels. AAC stays on the end as the format every build can decode.
 */
fun DeviceAudioCapabilities.transcodeAudioCodecs(
    preference: AudioPassthroughPreference,
): String {
    val playable = playableFor(preference)
    val preferred = listOf(SurroundCodec.EAC3, SurroundCodec.AC3)
        .filter { it in playable }
        .flatMap(SurroundCodec::embyCodecs)
    return (preferred + listOf("aac", "mp3")).distinct().joinToString(",")
}

/**
 * The capability tokens the gateway builds its own device profile from.
 *
 * Passthrough and decode are reported separately because they are different claims, and
 * the gateway needs both: the union decides what may be direct-played, and the passthrough
 * half alone decides how many channels may be sent and what a conversion should target.
 */
fun DeviceAudioCapabilities.gatewayAudioTokens(
    preference: AudioPassthroughPreference,
): List<String> = buildList {
    val bitstreamed = passthroughFor(preference)
    SurroundCodec.entries.forEach { codec ->
        if (codec in bitstreamed) add("audio_${codec.slug}_passthrough")
        if (codec in decode) add("audio_${codec.slug}_decode")
    }
    add("audio_max_channels_${channelLimitFor(preference)}")
}
