"""Extend the class ``MediaFile`` of the package ``phrydy``."""

import re
from typing import Any, Dict, List, Optional, Tuple

from phrydy import MediaFileExtended
from tmep import Functions

import audiorename.musicbrainz as musicbrainz

Diff = List[Tuple[str, Optional[str], Optional[str]]]
PerformerRaw = List[List[str]]


def compare_dicts(first: Dict[str, str], second: Dict[str, str]) -> Diff:
    """Compare two dictionaries for differenes.

    :param first: First dictionary to diff.
    :param second: Second dicationary to diff.

    :return: As list of key entries whose values differ.
    """
    diff: Diff = []
    for key, _ in sorted(first.items()):
        if key not in second:
            diff.append((key, first[key], None))

    for key, _ in sorted(second.items()):
        if key not in first:
            diff.append((key, None, second[key]))

    all_keys: set[str] = set()
    for key, _ in first.items():
        all_keys.add(key)
    for key, _ in second.items():
        all_keys.add(key)
    for key in sorted(all_keys):
        if key in first and key in second and first[key] != second[key]:
            diff.append((key, first[key], second[key]))

    return diff


class Meta(MediaFileExtended):
    def __init__(self, path: str, shell_friendly: bool = False):
        super(Meta, self).__init__(path, False)
        self.shell_friendly = shell_friendly

    ###############################################################################
    # Public methods
    ###############################################################################

    def export_dict(self, sanitize: bool = True) -> Dict[str, str]:
        """
        Export all fields into a dictionary.

        :param sanitize: Set the parameter to true to trigger the sanitize
          function.
        """
        out: Dict[str, str] = {}
        for field in self.fields_sorted():
            value = getattr(self, field)
            if value:
                if sanitize:
                    out[field] = self._sanitize(str(value))
                else:
                    out[field] = value

        return out

    def enrich_metadata(self) -> None:
        musicbrainz.set_useragent()

        if self.mb_trackid:
            recording = musicbrainz.query("recording", self.mb_trackid)
        else:
            print("No music brainz track id found.")
            return

        release = None
        if self.mb_albumid:
            release = musicbrainz.query("release", self.mb_albumid)
        if release and "release-group" in release:
            release_group = release["release-group"]
            types: List[str] = []
            if "type" in release_group:
                types.append(release_group["type"])
            if "primary-type" in release_group:
                types.append(release_group["primary-type"])
            if "secondary-type-list" in release_group:
                types = types + release_group["secondary-type-list"]
            types = self._uniquify_list(types)
            self.releasegroup_types = "/".join(types).lower()

        work_id = ""
        if self.mb_workid:
            work_id = self.mb_workid
        elif recording:
            try:
                work_id = recording["work-relation-list"][0]["work"]["id"]
            except KeyError:
                pass
        if work_id:
            work_hierarchy = musicbrainz.query_works_recursively(work_id, [])
            if work_hierarchy:
                work_hierarchy.reverse()
                work_bottom = work_hierarchy[-1]
                if "artist-relation-list" in work_bottom:
                    for artist in work_bottom["artist-relation-list"]:
                        if (
                            artist["direction"] == "backward"
                            and artist["type"] == "composer"
                        ):
                            self.composer = artist["artist"]["name"]
                            self.composer_sort = artist["artist"]["sort-name"]
                            break
                self.mb_workid = work_bottom["id"]
                self.work = work_bottom["title"]
                wh_titles: List[str] = []
                wh_ids: List[str] = []
                for work in work_hierarchy:
                    wh_titles.append(work["title"])
                    wh_ids.append(work["id"])
                self.work_hierarchy = " -> ".join(wh_titles)
                self.mb_workhierarchy_ids = "/".join(wh_ids)

    def remap_classical(self) -> None:
        """Remap some fields to fit better for classical music:
        ``composer`` becomes ``artist``, ``work`` becomes ``album``, from the
        ``title`` the work prefix is removed (``Symphonie No. 9: I. Allegro``
        -> ``I. Allegro``) and ``track`` becomes the movement number. All
        overwritten fields are safed in the ``comments`` field. No combined
        properties (like ``ar_combined_composer``) are used and therefore some
        code duplications are done on purpose to avoid circular endless loops.
        """
        safe: List[List[str]] = []

        if self.title:
            safe.append(["title", self.title])
            self.title = re.sub(r"^[^:]*: ?", "", self.title)

            roman = re.findall(r"^([IVXLCDM]*)\.", self.title)
            if roman:
                safe.append(["track", str(self.track)])
                self.track = self._roman_to_int(roman[0])

        if self.composer:
            safe.append(["artist", self.artist])
            self.artist = self.composer

        if self.ar_combined_work_top:
            safe.append(["album", self.album])
            self.ar_performer_short
            album = self.ar_combined_work_top
            if self.ar_performer_short:
                album += " (" + self.ar_performer_short + ")"
            self.album = album

        if safe:
            comments = "Original metadata: "
            for safed in safe:
                comments = comments + str(safed[0]) + ": " + str(safed[1]) + "; "

            self.comments = comments

    ###############################################################################
    # Class methods
    ###############################################################################

    @classmethod
    def fields_phrydy(cls):
        for field in sorted(MediaFileExtended.readable_fields()):
            yield field

    @classmethod
    def fields_audiorename(cls):
        for prop, _ in sorted(cls.__dict__.items()):
            if isinstance(getattr(cls, prop), property):
                if isinstance(prop, bytes):
                    # On Python 2, class field names are bytes. This method
                    # produces text strings.
                    yield prop.decode("utf8", "ignore")
                else:
                    yield prop

    @classmethod
    def fields(cls):
        for field in cls.fields_phrydy():
            yield field
        for field in cls.fields_audiorename():
            yield field

    @classmethod
    def fields_sorted(cls):
        for field in sorted(cls.fields()):
            yield field

    ###############################################################################
    # Static methods
    ###############################################################################

    @staticmethod
    def _find_initials(value: str) -> str:
        """
        Find the first character of a string.

        :param str value: A string to extract the initials.

        :return: A single character in lowercase. The possible return values
            are lowercase letters from the ASCII alphabet (``a-z``), the digit
            ``0`` and the underscore character (``_``).
        """
        # To avoid ae -> a
        value = Functions.tmpl_asciify(value)
        # To avoid “!K7-Compilations” -> “!”
        value = re.sub(r"^\W*", "", value)
        initial = value[0:1].lower()

        if re.match(r"\d", initial):
            return "0"

        if initial == "":
            return "_"

        return initial

    @staticmethod
    def _normalize_performer(ar_performer: List[str]) -> PerformerRaw:
        """
        :param ar_performer: A list of raw ar_performer strings like

        .. code-block:: python

            ['John Lennon (vocals)', 'Ringo Starr (drums)']

        :return: A list

        .. code-block:: python

            [
                ['vocals', 'John Lennon'],
                ['drums', 'Ringo Starr'],
            ]
        """
        out: PerformerRaw = []
        for value in ar_performer:
            value = value[:-1]
            performers: List[str] = value.split(" (")
            if len(performers) == 2:
                out.append([performers[1], performers[0]])
        return out

    @staticmethod
    def _roman_to_int(n: str) -> int:
        numeral_map = tuple(
            zip(
                (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
                ("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"),
            )
        )
        i = result = 0
        for integer, numeral in numeral_map:
            while n[i : i + len(numeral)] == numeral:
                result += integer
                i += len(numeral)
        return result

    @staticmethod
    def _sanitize(value: Any) -> str:
        if isinstance(value, str) or isinstance(value, bytes):
            value = Functions.tmpl_sanitize(str(value))
            value = re.sub(r"\s{2,}", " ", str(value))
        else:
            value = ""
        return value

    @staticmethod
    def _shorten_performer(
        ar_performer: str,
        length: int = 3,
        separator: str = " ",
        abbreviation: str = ".",
    ) -> str:
        out = ""
        count = 0
        for s in ar_performer.split(" "):
            if count < 3:
                if len(s) > length:
                    part = s[:length] + abbreviation
                else:
                    part = s
                out = out + separator + part
            count = count + 1

        return out[len(separator) :]

    @staticmethod
    def _uniquify_list(sequence: List[Any]) -> List[Any]:
        """https://www.peterbe.com/plog/uniqifiers-benchmark"""
        unique: List[Any] = []

        for element in sequence:
            if not unique.count(element):
                unique.append(element)

        return unique

    ###############################################################################
    # Properties
    ###############################################################################

    @property
    def ar_classical_album(self) -> Optional[str]:
        """Uses:

        * ``phrydy.mediafile.MediaFile.work``

        Examples:

        * ``Horn Concerto: I. Allegro`` → ``Horn Concerto``
        * ``Die Meistersinger von Nürnberg``
        """
        if self.work:
            return re.sub(r":.*$", "", (str(self.work)))
        return None

    @property
    def ar_combined_album(self) -> Optional[str]:
        """Uses:

        * ``phrydy.mediafile.MediaFile.album``

        Example:

        * ``Just Friends (Disc 2)`` → ``Just Friends``
        """
        if self.album:
            return re.sub(r" ?\([dD]is[ck].*\)$", "", str(self.album))
        return None

    @property
    def ar_initial_album(self) -> Optional[str]:
        """Uses:

        * :class:`audiorename.meta.Meta.ar_combined_album`

        Examples:

        * ``Just Friends`` → ``j``
        * ``Die Meistersinger von Nürnberg``  → ``d``
        """
        if self.ar_combined_album:
            return self._find_initials(self.ar_combined_album)
        return None

    @property
    def ar_initial_artist(self) -> str:
        """Uses:

        * :class:`audiorename.meta.Meta.ar_combined_artist_sort`

        Examples:

        * ``Just Friends`` → ``j``
        * ``Die Meistersinger von Nürnberg``  → ``d``
        """
        return self._find_initials(self.ar_combined_artist_sort)

    @staticmethod
    def __remove_feat_vs_second_artist(artist: str) -> str:
        """Give only the first artist, remove the second after ``feat.``,
        ``ft.`` or ``vs.``"""
        return re.sub(r"\s+(feat|ft|vs)\.?\s.*", "", artist, flags=re.IGNORECASE)

    @property
    def ar_combined_artist(self) -> str:
        """Uses:

        * ``phrydy.mediafile.MediaFile.albumartist``
        * ``phrydy.mediafile.MediaFile.artist``
        * ``phrydy.mediafile.MediaFile.albumartist_credit``
        * ``phrydy.mediafile.MediaFile.artist_credit``
        * ``phrydy.mediafile.MediaFile.albumartist_sort``
        * ``phrydy.mediafile.MediaFile.artist_sort``

        Removes the second artist after ``feat.``, ``ft.`` or ``vs.``.
        """
        out: str
        if self.albumartist:
            out = self.albumartist
        elif self.artist:
            out = self.artist
        elif self.albumartist_credit:
            out = self.albumartist_credit
        elif self.artist_credit:
            out = self.artist_credit
        # Same as aristsafe_sort
        elif self.albumartist_sort:
            out = self.albumartist_sort
        elif self.artist_sort:
            out = self.artist_sort
        else:
            out = "Unknown"

        return Meta.__remove_feat_vs_second_artist(out)

    @property
    def ar_combined_artist_sort(self) -> str:
        """Uses:

        * ``phrydy.mediafile.MediaFile.albumartist_sort``
        * ``phrydy.mediafile.MediaFile.artist_sort``
        * ``phrydy.mediafile.MediaFile.albumartist``
        * ``phrydy.mediafile.MediaFile.artist``
        * ``phrydy.mediafile.MediaFile.albumartist_credit``
        * ``phrydy.mediafile.MediaFile.artist_credit``

        Removes the second artist after ``feat.``, ``ft.`` or ``vs.``.
        """
        out: str
        if self.albumartist_sort:
            out = self.albumartist_sort
        elif self.artist_sort:
            out = self.artist_sort
        # Same as ar_combined_artist
        elif self.albumartist:
            out = self.albumartist
        elif self.artist:
            out = self.artist
        elif self.albumartist_credit:
            out = self.albumartist_credit
        elif self.artist_credit:
            out = self.artist_credit
        else:
            out = "Unknown"

        out = Meta.__remove_feat_vs_second_artist(out)

        if self.shell_friendly:
            out = out.replace(", ", "_")

        return out

    @property
    def ar_initial_composer(self) -> str:
        """Uses:

        * :class:`audiorename.meta.Meta.ar_combined_composer`
        """
        return self._find_initials(self.ar_combined_composer)

    @property
    def ar_combined_composer(self) -> str:
        """Uses:

        * ``phrydy.mediafile.MediaFile.composer_sort``
        * ``phrydy.mediafile.MediaFile.composer``
        * :class:`audiorename.meta.Meta.ar_combined_artist`
        """
        out: str = ""
        if self.composer_sort:
            out = self.composer_sort
        elif self.composer:
            out = self.composer
        else:
            out = self.ar_combined_artist

        if self.shell_friendly:
            out = out.replace(", ", "_")

        # e. g. 'Mozart, Wolfgang Amadeus/Süßmeyer, Franz Xaver'
        return re.sub(r" ?/.*", "", out)

    @property
    def ar_combined_disctrack(self) -> Optional[str]:
        """
        Generate a combination of track and disc number, e. g.: ``1-04``,
        ``3-06``.

        Uses:

        * ``phrydy.mediafile.MediaFile.disctotal``
        * ``phrydy.mediafile.MediaFile.disc``
        * ``phrydy.mediafile.MediaFile.tracktotal``
        * ``phrydy.mediafile.MediaFile.track``
        """

        if not self.track:
            return None

        if self.disctotal and int(self.disctotal) > 99:
            disk = str(self.disc).zfill(3)
        elif self.disctotal and int(self.disctotal) > 9:
            disk = str(self.disc).zfill(2)
        else:
            disk = str(self.disc)

        if self.tracktotal and int(self.tracktotal) > 99:
            track = str(self.track).zfill(3)
        else:
            track = str(self.track).zfill(2)

        if self.disc and self.disctotal and int(self.disctotal) > 1:
            out = disk + "-" + track
        elif self.disc and not self.disctotal:
            out = disk + "-" + track
        else:
            out = track

        return out

    @property
    def ar_performer(self) -> str:
        """Uses:

        * :class:`audiorename.meta.Meta.ar_performer_raw`
        """
        out: str = ""
        for ar_performer in self.ar_performer_raw:
            out = out + ", " + ar_performer[1]

        out = out[2:]

        return out

    @property
    def ar_classical_performer(self) -> str:
        """http://musicbrainz.org/doc/Style/Classical/Release/Artist

        Uses:

        * :class:`audiorename.meta.Meta.ar_performer_short`
        * ``phrydy.mediafile.MediaFile.albumartist``
        """
        if len(self.ar_performer_short) > 0:
            out = self.ar_performer_short
        elif self.albumartist:
            out = re.sub(r"^.*; ?", "", self.albumartist)
        else:
            out = ""

        return out

    @property
    def ar_performer_raw(self) -> PerformerRaw:
        """Generate a unifed ar_performer list.

        Picard doesn’t store ar_performer values in m4a, alac.m4a, wma, wav,
        aiff.

        :return: A list

        .. code-block:: python

            [
                ['conductor', 'Herbert von Karajan'],
                ['violin', 'Anne-Sophie Mutter'],
            ]

        Uses:

        * ``phrydy.mediafile.MediaFile.mgfile``
        """
        out: PerformerRaw = []

        if (
            self.format == "FLAC" or self.format == "OGG"
        ) and "performer" in self.mgfile:
            out = self._normalize_performer(self.mgfile["performer"])
            if "conductor" in self.mgfile:
                out.insert(0, ["conductor", self.mgfile["conductor"][0]])
        elif self.format == "MP3":
            # 4.2.2 TMCL Musician credits list
            if "TMCL" in self.mgfile:
                out = self.mgfile["TMCL"].people
            # 4.2.2 TIPL Involved people list
            # TIPL is used for producer
            if "TIPL" in self.mgfile:
                out = self.mgfile["TIPL"].people

            # 4.2.2 TPE3 Conductor/ar_performer refinement
            if len(out) > 0 and "conductor" not in out[0] and "TPE3" in self.mgfile:
                out.insert(0, ["conductor", self.mgfile["TPE3"].text[0]])

        else:
            out = []

        return self._uniquify_list(out)

    @property
    def ar_performer_short(self):
        """Uses:

        * ``phrydy.mediafile.MediaFile.ar_performer_raw``
        """
        out: List[str] = []

        performers = self.ar_performer_raw
        picked: PerformerRaw = []
        for performer in performers:
            if performer[0] == "conductor" or performer[0] == "orchestra":
                picked.append(performer)

        if len(picked) > 0:
            performers = picked

        for performer in performers:
            if (
                performer[0] == "producer"
                or performer[0] == "executive producer"
                or performer[0] == "balance engineer"
            ):
                pass
            elif (
                performer[0] == "orchestra"
                or performer[0] == "choir vocals"
                or performer[0] == "string quartet"
            ):
                out.append(
                    self._shorten_performer(performer[1], separator="", abbreviation="")
                )
            else:
                out.append(performer[1].split(" ")[-1])

        return ", ".join(out)

    @property
    def ar_combined_soundtrack(self) -> bool:
        if self.releasegroup_types and "soundtrack" in self.releasegroup_types.lower():
            return True

        if self.albumtype and "soundtrack" in self.albumtype.lower():
            return True

        if self.albumtypes:
            for type in self.albumtypes:
                if "soundtrack" in type.lower():
                    return True

        return False

    @property
    def ar_classical_title(self) -> Optional[str]:
        """Uses:

        * ``phrydy.mediafile.MediaFile.title``

        Example:

        * ``Horn Concerto: I. Allegro``
        """
        if self.title:
            return re.sub(r"^[^:]*: ?", "", self.title)
        return None

    @property
    def ar_classical_track(self) -> Optional[str]:
        """Uses:

        * :class:`audiorename.meta.Meta.ar_classical_title`
        * :class:`audiorename.meta.Meta.ar_combined_disctrack`
        """
        roman = None
        if self.ar_classical_title:
            roman = re.findall(r"^([IVXLCDM]*)\.", self.ar_classical_title)
        if roman:
            return str(self._roman_to_int(roman[0])).zfill(2)
        elif self.ar_combined_disctrack:
            return self.ar_combined_disctrack
        return None

    @property
    def ar_combined_work_top(self) -> Optional[str]:
        """Uses:

        * ``phrydy.mediafile.MediaFile.work_hierarchy``
        * ``phrydy.mediafile.MediaFile.work``

        """
        if self.work_hierarchy:
            return self.work_hierarchy.split(" -> ")[0]
        elif self.ar_classical_album:
            return self.ar_classical_album
        return None

    @property
    def ar_combined_year(self):
        """Uses:

        * ``phrydy.mediafile.MediaFile.original_year``
        * ``phrydy.mediafile.MediaFile.year``
        """
        if self.original_year:
            return self.original_year
        elif self.year:
            return self.year
