from typing import List
import musicbrainzngs

from src.music_kraken.utils.shared import *
from src.music_kraken.utils.object_handeling import get_elem_from_obj, parse_music_brainz_date

logger = SEARCH_LOGGER

MAX_PARAMETERS = 3
OPTION_TYPES = ['artist', 'release_group', 'release', 'recording']


class Option:
    def __init__(self, type_: str, id_: str, name: str, additional_info: str = "") -> None:
        # print(type_, id_, name)
        if type_ not in OPTION_TYPES:
            raise ValueError(f"type: {type_} doesn't exist. Legal Values: {OPTION_TYPES}")
        self.type = type_
        self.name = name
        self.id = id_

        self.additional_info = additional_info

    def __getitem__(self, item):
        map_ = {
            "id": self.id,
            "type": self.type,
            "kind": self.type,
            "name": self.name
        }
        return map_[item]

    def __repr__(self) -> str:
        type_repr = {
            'artist': 'artist\t\t',
            'release_group': 'release group\t',
            'release': 'release\t\t',
            'recording': 'recording\t'
        }
        return f"{type_repr[self.type]}: \"{self.name}\"{self.additional_info}"


class MultipleOptions:
    def __init__(self, option_list: List[Option]) -> None:
        self.option_list = option_list

    def __repr__(self) -> str:
        return "\n".join([f"{str(i).zfill(2)}) {choice.__repr__()}" for i, choice in enumerate(self.option_list)])


class Search:
    def __init__(self) -> None:
        self.options_history = []
        self.current_option: Option

    def append_new_choices(self, new_choices: List[Option]) -> MultipleOptions:
        self.options_history.append(new_choices)
        return MultipleOptions(new_choices)

    def get_previous_options(self):
        self.options_history.pop(-1)
        return MultipleOptions(self.options_history[-1])

    @staticmethod
    def fetch_new_options_from_artist(artist: Option):
        """
        returning list of artist and every release group
        """
        result = musicbrainzngs.get_artist_by_id(artist.id, includes=["release-groups", "releases"])
        artist_data = get_elem_from_obj(result, ['artist'], return_if_none={})

        result = [artist]

        # sort all release groups by date and add album sort to have them in chronological order.
        release_group_list = artist_data['release-group-list']
        for i, release_group in enumerate(release_group_list):
            release_group_list[i]['first-release-date'] = parse_music_brainz_date(release_group['first-release-date'])
        release_group_list.sort(key=lambda x: x['first-release-date'])
        release_group_list = [Option("release_group", get_elem_from_obj(release_group_, ['id']),
                                     get_elem_from_obj(release_group_, ['title']),
                                     additional_info=f" ({get_elem_from_obj(release_group_, ['type'])}) from {get_elem_from_obj(release_group_, ['first-release-date'])}")
                              for release_group_ in release_group_list]

        result.extend(release_group_list)
        return result

    @staticmethod
    def fetch_new_options_from_release_group(release_group: Option):
        """
        returning list including the artists, the releases and the tracklist of the first release
        """
        results = []

        result = musicbrainzngs.get_release_group_by_id(release_group.id,
                                                        includes=["artist-credits", "releases"])
        release_group_data = get_elem_from_obj(result, ['release-group'], return_if_none={})
        artist_datas = get_elem_from_obj(release_group_data, ['artist-credit'], return_if_none={})
        release_datas = get_elem_from_obj(release_group_data, ['release-list'], return_if_none={})

        # appending all the artists to results
        for artist_data in artist_datas:
            results.append(Option('artist', get_elem_from_obj(artist_data, ['artist', 'id']),
                                  get_elem_from_obj(artist_data, ['artist', 'name'])))

        # appending initial release group
        results.append(release_group)

        # appending all releases
        first_release = None
        for i, release_data in enumerate(release_datas):
            results.append(
                Option('release', get_elem_from_obj(release_data, ['id']), get_elem_from_obj(release_data, ['title']),
                       additional_info=f" ({get_elem_from_obj(release_data, ['status'])})"))
            if i == 0:
                first_release = results[-1]

        # append tracklist of first release
        if first_release is not None:
            results.extend(Search.fetch_new_options_from_release(first_release, only_tracklist=True))

        return results

    @staticmethod
    def fetch_new_options_from_release(release: Option, only_tracklist: bool = False):
        """
        artists
        release group
        release
        tracklist
        """
        results = []
        result = musicbrainzngs.get_release_by_id(release.id,
                                                  includes=["recordings", "labels", "release-groups", "artist-credits"])
        release_data = get_elem_from_obj(result, ['release'], return_if_none={})
        label_data = get_elem_from_obj(release_data, ['label-info-list'], return_if_none={})
        recording_datas = get_elem_from_obj(release_data, ['medium-list', 0, 'track-list'], return_if_none=[])
        release_group_data = get_elem_from_obj(release_data, ['release-group'], return_if_none={})
        artist_datas = get_elem_from_obj(release_data, ['artist-credit'], return_if_none={})

        # appending all the artists to results
        for artist_data in artist_datas:
            results.append(Option('artist', get_elem_from_obj(artist_data, ['artist', 'id']),
                                  get_elem_from_obj(artist_data, ['artist', 'name'])))

        # appending the according release group
        results.append(Option("release_group", get_elem_from_obj(release_group_data, ['id']),
                              get_elem_from_obj(release_group_data, ['title']),
                              additional_info=f" ({get_elem_from_obj(release_group_data, ['type'])}) from {get_elem_from_obj(release_group_data, ['first-release-date'])}"))

        # appending the release
        results.append(release)

        # appending the tracklist, but first putting it in a list, in case of only_tracklist being True to
        # return this instead
        tracklist = []
        for i, recording_data in enumerate(recording_datas):
            recording_data = recording_data['recording']
            tracklist.append(Option('recording', get_elem_from_obj(recording_data, ['id']),
                                    get_elem_from_obj(recording_data, ['title']),
                                    f" ({get_elem_from_obj(recording_data, ['length'])}) from {get_elem_from_obj(recording_data, ['artist-credit-phrase'])}"))

        if only_tracklist:
            return tracklist
        results.extend(tracklist)
        return results

    @staticmethod
    def fetch_new_options_from_record(recording: Option):
        """
        artists, release, record
        """
        results = []

        result = musicbrainzngs.get_recording_by_id(recording.id, includes=["artists", "releases"])
        recording_data = result['recording']
        release_datas = get_elem_from_obj(recording_data, ['release-list'])
        artist_datas = get_elem_from_obj(recording_data, ['artist-credit'], return_if_none={})

        # appending all the artists to results
        for artist_data in artist_datas:
            results.append(Option('artist', get_elem_from_obj(artist_data, ['artist', 'id']),
                                  get_elem_from_obj(artist_data, ['artist', 'name'])))

        # appending all releases
        for i, release_data in enumerate(release_datas):
            results.append(
                Option('release', get_elem_from_obj(release_data, ['id']), get_elem_from_obj(release_data, ['title']),
                       additional_info=f" ({get_elem_from_obj(release_data, ['status'])})"))

        results.append(recording)

        return results

    def fetch_new_options(self) -> MultipleOptions:
        if self.current_option is None:
            return -1

        result = []
        if self.current_option.type == 'artist':
            result = self.fetch_new_options_from_artist(self.current_option)
        elif self.current_option.type == 'release_group':
            result = self.fetch_new_options_from_release_group(self.current_option)
        elif self.current_option.type == 'release':
            result = self.fetch_new_options_from_release(self.current_option)
        elif self.current_option.type == 'recording':
            result = self.fetch_new_options_from_record(self.current_option)

        return self.append_new_choices(result)

    def choose(self, index: int) -> MultipleOptions:
        if len(self.options_history) == 0:
            logging.error("initial query neaded before choosing")
            return MultipleOptions([])

        latest_options = self.options_history[-1]
        if index >= len(latest_options):
            logging.error("index outside of options")
            return MultipleOptions([])

        self.current_option = latest_options[index]
        return self.fetch_new_options()

    @staticmethod
    def search_recording_from_text(artist: str = None, release_group: str = None, recording: str = None,
                                   query: str = None):
        result = musicbrainzngs.search_recordings(artist=artist, release=release_group, recording=recording,
                                                  query=query)
        recording_list = get_elem_from_obj(result, ['recording-list'], return_if_none=[])

        resulting_options = [
            Option("recording", get_elem_from_obj(recording_, ['id']), get_elem_from_obj(recording_, ['title']),
                   additional_info=f" of {get_elem_from_obj(recording_, ['release-list', 0, 'title'])} by {get_elem_from_obj(recording_, ['artist-credit', 0, 'name'])}")
            for recording_ in recording_list]
        return resulting_options

    @staticmethod
    def search_release_group_from_text(artist: str = None, release_group: str = None, query: str = None):
        result = musicbrainzngs.search_release_groups(artist=artist, releasegroup=release_group, query=query)
        release_group_list = get_elem_from_obj(result, ['release-group-list'], return_if_none=[])

        resulting_options = [Option("release_group", get_elem_from_obj(release_group_, ['id']),
                                    get_elem_from_obj(release_group_, ['title']),
                                    additional_info=f" by {get_elem_from_obj(release_group_, ['artist-credit', 0, 'name'])}")
                             for release_group_ in release_group_list]
        return resulting_options

    @staticmethod
    def search_artist_from_text(artist: str = None, query: str = None):
        result = musicbrainzngs.search_artists(artist=artist, query=query)
        artist_list = get_elem_from_obj(result, ['artist-list'], return_if_none=[])

        resulting_options = [Option("artist", get_elem_from_obj(artist_, ['id']), get_elem_from_obj(artist_, ['name']),
                                    additional_info=f": {', '.join([i['name'] for i in get_elem_from_obj(artist_, ['tag-list'], return_if_none=[])])}")
                             for artist_ in artist_list]
        return resulting_options

    def search_from_text(self, artist: str = None, release_group: str = None, recording: str = None) -> MultipleOptions:
        logger.info(
            f"searching specified artist: \"{artist}\", release group: \"{release_group}\", recording: \"{recording}\"")
        if artist is None and release_group is None and recording is None:
            logger.error("either artist, release group or recording has to be set")
            return MultipleOptions([])

        if recording is not None:
            logger.info("search for recording")
            results = self.search_recording_from_text(artist=artist, release_group=release_group, recording=recording)
        elif release_group is not None:
            logger.info("search for release group")
            results = self.search_release_group_from_text(artist=artist, release_group=release_group)
        else:
            logger.info("search for artist")
            results = self.search_artist_from_text(artist=artist)

        return self.append_new_choices(results)

    def search_from_text_unspecified(self, query: str) -> MultipleOptions:
        logger.info(f"searching unspecified: \"{query}\"")

        results = []
        results.extend(self.search_artist_from_text(query=query))
        results.extend(self.search_release_group_from_text(query=query))
        results.extend(self.search_recording_from_text(query=query))

        return self.append_new_choices(results)

    def search_from_query(self, query: str) -> MultipleOptions:
        if query is None:
            return MultipleOptions([])
        """
        mit # wird ein neuer Parameter gestartet
        der Buchstabe dahinter legt die Art des Parameters fest
        "#a Psychonaut 4 #r Tired, Numb and #t Drop by Drop"
        if no # is in the query it gets treated as "unspecified query"
        :param query:
        :return:
        """

        if not '#' in query:
            return self.search_from_text_unspecified(query)

        artist = None
        release_group = None
        recording = None

        query = query.strip()
        parameters = query.split('#')
        parameters.remove('')

        if len(parameters) > MAX_PARAMETERS:
            raise ValueError(f"too many parameters. Only {MAX_PARAMETERS} are allowed")

        for parameter in parameters:
            splitted = parameter.split(" ")
            type_ = splitted[0]
            input_ = " ".join(splitted[1:]).strip()

            if type_ == "a":
                artist = input_
                continue
            if type_ == "r":
                release_group = input_
                continue
            if type_ == "t":
                recording = input_
                continue

        return self.search_from_text(artist=artist, release_group=release_group, recording=recording)


def automated_demo():
    search = Search()
    search.search_from_text(artist="I Prevail")

    # choose an artist
    search.choose(0)
    # choose a release group
    search.choose(9)
    # choose a release
    search.choose(2)
    # choose a recording
    search.choose(4)


def interactive_demo():
    search = Search()
    while True:
        input_ = input(
            "q to quit, .. for previous options, int for this element, str to search for query, ok to download: ")
        input_.strip()
        if input_.lower() == "ok":
            break
        if input_.lower() == "q":
            break
        if input_.lower() == "..":
            search.get_previous_options()
            continue
        if input_.isdigit():
            search.choose(int(input_))
            continue
        search.search_from_query(input_)


if __name__ == "__main__":
    interactive_demo()
