from __future__ import annotations

import math
from enum import Enum
from typing import Any, Optional, Union

from envied.core.tracks.track import Track


class Audio(Track):
    class Codec(str, Enum):
        AAC = "AAC"  # https://wikipedia.org/wiki/Advanced_Audio_Coding
        AC3 = "DD"  # https://wikipedia.org/wiki/Dolby_Digital
        EC3 = "DD+"  # https://wikipedia.org/wiki/Dolby_Digital_Plus
        OPUS = "OPUS"  # https://wikipedia.org/wiki/Opus_(audio_format)
        OGG = "VORB"  # https://wikipedia.org/wiki/Vorbis
        DTS = "DTS"  # https://en.wikipedia.org/wiki/DTS_(company)#DTS_Digital_Surround
        ALAC = "ALAC"  # https://en.wikipedia.org/wiki/Apple_Lossless_Audio_Codec
        FLAC = "FLAC"  # https://en.wikipedia.org/wiki/FLAC

        @property
        def extension(self) -> str:
            return self.name.lower()

        @staticmethod
        def from_mime(mime: str) -> Audio.Codec:
            mime = mime.lower().strip().split(".")[0]
            if mime == "mp4a":
                return Audio.Codec.AAC
            if mime == "ac-3":
                return Audio.Codec.AC3
            if mime == "ec-3":
                return Audio.Codec.EC3
            if mime == "opus":
                return Audio.Codec.OPUS
            if mime == "dtsc":
                return Audio.Codec.DTS
            if mime == "alac":
                return Audio.Codec.ALAC
            if mime == "flac":
                return Audio.Codec.FLAC
            raise ValueError(f"The MIME '{mime}' is not a supported Audio Codec")

        @staticmethod
        def from_codecs(codecs: str) -> Audio.Codec:
            for codec in codecs.lower().split(","):
                mime = codec.strip().split(".")[0]
                try:
                    return Audio.Codec.from_mime(mime)
                except ValueError:
                    pass
            raise ValueError(f"No MIME types matched any supported Audio Codecs in '{codecs}'")

        @staticmethod
        def from_netflix_profile(profile: str) -> Audio.Codec:
            profile = profile.lower().strip()
            if profile.startswith("heaac"):
                return Audio.Codec.AAC
            if profile.startswith("dd-"):
                return Audio.Codec.AC3
            if profile.startswith("ddplus"):
                return Audio.Codec.EC3
            if profile.startswith("playready-oggvorbis"):
                return Audio.Codec.OGG
            raise ValueError(f"The Content Profile '{profile}' is not a supported Audio Codec")

    def __init__(
        self,
        *args: Any,
        codec: Optional[Audio.Codec] = None,
        bitrate: Optional[Union[str, int, float]] = None,
        channels: Optional[Union[str, int, float]] = None,
        joc: Optional[int] = None,
        descriptive: Union[bool, int] = False,
        **kwargs: Any,
    ):
        """
        Create a new Audio track object.

        Parameters:
            codec: An Audio.Codec enum representing the audio codec.
                If not specified, MediaInfo will be used to retrieve the codec
                once the track has been downloaded.
            bitrate: A number or float representing the average bandwidth in bytes/s.
                Float values are rounded up to the nearest integer.
            channels: A number, float, or string representing the number of audio channels.
                Strings may represent numbers or floats. Expanded layouts like 7.1.1 is
                not supported. All numbers and strings will be cast to float.
            joc: The number of Joint-Object-Coding Channels/Objects in the audio stream.
            descriptive: Mark this audio as being descriptive audio for the blind.

        Note: If codec, bitrate, channels, or joc is not specified some checks may be
        skipped or assume a value. Specifying as much information as possible is highly
        recommended.
        """
        super().__init__(*args, **kwargs)

        if not isinstance(codec, (Audio.Codec, type(None))):
            raise TypeError(f"Expected codec to be a {Audio.Codec}, not {codec!r}")
        if not isinstance(bitrate, (str, int, float, type(None))):
            raise TypeError(f"Expected bitrate to be a {str}, {int}, or {float}, not {bitrate!r}")
        if not isinstance(channels, (str, int, float, type(None))):
            raise TypeError(f"Expected channels to be a {str}, {int}, or {float}, not {channels!r}")
        if not isinstance(joc, (int, type(None))):
            raise TypeError(f"Expected joc to be a {int}, not {joc!r}")
        if not isinstance(descriptive, (bool, int)) or (isinstance(descriptive, int) and descriptive not in (0, 1)):
            raise TypeError(f"Expected descriptive to be a {bool} or bool-like {int}, not {descriptive!r}")

        self.codec = codec

        try:
            self.bitrate = int(math.ceil(float(bitrate))) if bitrate else None
        except (ValueError, TypeError) as e:
            raise ValueError(f"Expected bitrate to be a number or float, {e}")

        try:
            self.channels = self.parse_channels(channels) if channels else None
        except (ValueError, NotImplementedError) as e:
            raise ValueError(f"Expected channels to be a number, float, or a string, {e}")

        self.joc = joc
        self.descriptive = bool(descriptive)

    @property
    def atmos(self) -> bool:
        """Return True if the audio track contains Dolby Atmos."""
        return bool(self.joc)

    def __str__(self) -> str:
        return " | ".join(
            filter(
                bool,
                [
                    "AUD",
                    f"[{self.codec.value}]" if self.codec else None,
                    str(self.language),
                    ", ".join(
                        filter(
                            bool,
                            [
                                str(self.channels) if self.channels else None,
                                "Atmos" if self.atmos else None,
                                f"JOC {self.joc}" if self.joc else None,
                            ],
                        )
                    ),
                    f"{self.bitrate // 1000} kb/s" if self.bitrate else None,
                    self.get_track_name(),
                    self.edition,
                ],
            )
        )

    @staticmethod
    def parse_channels(channels: Union[str, int, float]) -> float:
        """
        Converts a Channel string to a float representing audio channel count and layout.
        E.g. "3" -> "3.0", "2.1" -> "2.1", ".1" -> "0.1".

        This does not validate channel strings as genuine channel counts or valid layouts.
        It does not convert the value to assume a sub speaker channel layout, e.g. 5.1->6.0.
        It also does not support expanded surround sound channel layout strings like 7.1.2.
        """
        if isinstance(channels, str):
            # TODO: Support all possible DASH channel configurations (https://datatracker.ietf.org/doc/html/rfc8216)
            if channels.upper() == "A000":
                return 2.0
            elif channels.upper() == "F801":
                return 5.1
            elif channels.replace("ch", "").replace(".", "", 1).isdigit():
                # e.g., '2ch', '2', '2.0', '5.1ch', '5.1'
                return float(channels.replace("ch", ""))
            raise NotImplementedError(f"Unsupported Channels string value, '{channels}'")

        return float(channels)

    def get_track_name(self) -> Optional[str]:
        """Return the base Track Name."""
        track_name = super().get_track_name() or ""
        flag = self.descriptive and "Descriptive"
        if flag:
            if track_name:
                flag = f" ({flag})"
            track_name += flag
        return track_name or None


__all__ = ("Audio",)
