'''
Function:
    Implementation of MusicClient
Author:
    Zhenchao Jin
WeChat Official Account (微信公众号):
    Charles的皮卡丘
'''
import sys
import copy
import click
import json_repair
from threading import Lock
from concurrent.futures import ThreadPoolExecutor
from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn, MofNCompleteColumn
if __name__ == '__main__':
    from __init__ import __version__
    from modules import BuildMusicClient, LoggerHandle, MusicClientBuilder, smarttrunctable, colorize, printfullline
else:
    from .__init__ import __version__
    from .modules import BuildMusicClient, LoggerHandle, MusicClientBuilder, smarttrunctable, colorize, printfullline


'''BASIC_INFO'''
BASIC_INFO = '''Function: Music Downloader v%s
Author: Zhenchao Jin
WeChat Official Account (微信公众号): Charles_pikachu (Charles的皮卡丘)
Instructions:
    Enter r: reinitialize the program (i.e., return to the main menu)
    Enter q: exit the program
    Download multiple songs: when selecting songs to download, enter "1,2,5" to download songs 1, 2, and 5 simultaneously
Music Files Save Path:
    %s (root dir is the current directory if using relative path).'''
'''DEFAULT_MUSIC_SOURCES'''
DEFAULT_MUSIC_SOURCES = ['MiguMusicClient', 'NeteaseMusicClient', 'QQMusicClient', 'KuwoMusicClient', 'QianqianMusicClient', 'KugouMusicClient']


'''MusicClient'''
class MusicClient():
    def __init__(self, music_sources: list = [], init_music_clients_cfg: dict = {}, clients_threadings: dict = {}, requests_overrides: dict = {}, search_rules: dict = {}):
        # assert
        assert isinstance(music_sources, list) and isinstance(init_music_clients_cfg, dict) and isinstance(clients_threadings, dict) and \
               isinstance(requests_overrides, dict) and isinstance(search_rules, dict)
        music_sources, init_music_clients_cfg, clients_threadings, requests_overrides, search_rules = \
            copy.deepcopy(music_sources), copy.deepcopy(init_music_clients_cfg), copy.deepcopy(clients_threadings), copy.deepcopy(requests_overrides), copy.deepcopy(search_rules)
        # set attributes
        self.work_dirs = {}
        self.search_rules = search_rules
        self.clients_threadings = clients_threadings
        self.requests_overrides = requests_overrides
        self.music_sources = music_sources if music_sources else DEFAULT_MUSIC_SOURCES
        self.music_sources = list(set(self.music_sources))
        # init
        self.logger_handle, self.music_clients = LoggerHandle(), dict()
        for music_source in self.music_sources:
            if music_source not in MusicClientBuilder.REGISTERED_MODULES.keys(): continue
            init_music_client_cfg = {
                'search_size_per_source': 5, 'auto_set_proxies': False, 'random_update_ua': False, 'max_retries': 3,
                'maintain_session': False, 'logger_handle': self.logger_handle, 'disable_print': True, 'work_dir': 'musicdl_outputs',
                'freeproxy_settings': None, 'default_search_cookies': {}, 'default_download_cookies': {}, 'type': music_source,
                'search_size_per_page': 10, 'strict_limit_search_size_per_page': True, 'quark_parser_config': {}
            }
            init_music_client_cfg.update(init_music_clients_cfg.get(music_source, {}))
            self.music_clients[music_source] = BuildMusicClient(module_cfg=init_music_client_cfg)
            self.work_dirs[music_source] = init_music_client_cfg['work_dir']
            if music_source not in self.clients_threadings:
                self.clients_threadings[music_source] = 5 if music_source not in ['GDStudioMusicClient'] else 10
            if music_source not in self.requests_overrides:
                self.requests_overrides[music_source] = {}
            if music_source not in self.search_rules:
                self.search_rules[music_source] = {}
    '''printbasicinfo'''
    def printbasicinfo(self):
        printfullline(ch='-')
        print(BASIC_INFO % (__version__, ', '.join([f'"{v} for {k}"' for k, v in self.work_dirs.items()])))
        printfullline(ch='-')
    '''printsearchresults'''
    def printsearchresults(self, search_results: dict):
        print_titles, print_items, song_infos, song_info_pointer = ['ID', 'Singers', 'Songname', 'Filesize', 'Duration', 'Album', 'Source'], [], {}, 0
        for _, per_search_results in search_results.items():
            for search_result in per_search_results:
                song_info_pointer += 1
                song_infos[str(song_info_pointer)] = search_result
                print_items.append([
                    colorize(str(song_info_pointer), 'number'), 
                    colorize(search_result['singers'][:12] + '...' if len(search_result['singers']) > 15 else search_result['singers'], 'singer'), 
                    search_result['song_name'], 
                    search_result['file_size'] if search_result['ext'] not in ['flac', 'wav', 'alac', 'ape', 'wv', 'tta', 'dsf', 'dff'] else colorize(search_result['file_size'], 'flac'), 
                    search_result['duration'], search_result['album'], 
                    colorize('|'.join([s.upper() for s in [search_result['source'].removesuffix('MusicClient'), search_result['root_source']] if s]), 'highlight'),
                ])
        print(smarttrunctable(headers=print_titles, rows=print_items, no_trunc_cols=[0, 1, 3, 4, 6]))
        return song_infos
    '''startcmdui'''
    def startcmdui(self):
        while True:
            self.printbasicinfo()
            # process user inputs, music file search
            user_input_keyword = self.processinputs('Please enter keywords to search for songs: ')
            search_results = self.search(keyword=user_input_keyword)
            # print search_results
            song_infos = self.printsearchresults(search_results=search_results)
            # process user inputs, music file download
            user_input_select_song_info_pointer = self.processinputs('Please enter music IDs to download (e.g., "1,2"): ').replace(' ', '').split(',')
            user_input_select_song_info_pointer = [idx for idx in user_input_select_song_info_pointer if idx in song_infos]
            user_input_select_song_info_pointer = list(set(user_input_select_song_info_pointer))
            selected_song_infos = []
            for idx in user_input_select_song_info_pointer: selected_song_infos.append(song_infos[idx])
            self.download(selected_song_infos)
    '''search'''
    def search(self, keyword):
        self.logger_handle.info(f'Searching {colorize(keyword, "highlight")} From {colorize("|".join(self.music_sources), "highlight")}')
        max_workers, main_progress_lock = min(len(self.music_sources), 10), Lock()
        with Progress(TextColumn("{task.description}"), BarColumn(bar_width=None), MofNCompleteColumn(), TimeRemainingColumn(), refresh_per_second=10) as main_process_context:
            main_progress_id = main_process_context.add_task(f"ALL sources >>> completed (0/0)", total=0)
            def _search(ms):
                try:
                    return ms, self.music_clients[ms].search(
                        keyword=keyword, num_threadings=self.clients_threadings[ms], request_overrides=self.requests_overrides[ms], rule=self.search_rules[ms], 
                        main_process_context=main_process_context, main_progress_id=main_progress_id, main_progress_lock=main_progress_lock,
                    )
                except Exception as err:
                    self.logger_handle.error(f'MusicClient.{ms}.search >>> {keyword} (Error: {err})')
                    return ms, []
            with ThreadPoolExecutor(max_workers=max_workers) as ex:
                return dict(ex.map(_search, self.music_sources))
    '''download'''
    def download(self, song_infos):
        classified_song_infos = {}
        for song_info in song_infos:
            if song_info['source'] in classified_song_infos:
                classified_song_infos[song_info['source']].append(song_info)
            else:
                classified_song_infos[song_info['source']] = [song_info]
        for source, source_song_infos in classified_song_infos.items():
            self.music_clients[source].download(
                song_infos=source_song_infos, num_threadings=self.clients_threadings[source], request_overrides=self.requests_overrides[source]
            )
    '''processinputs'''
    def processinputs(self, input_tip='', prefix: str = '\n'):
        # accept user inputs
        user_input = input(prefix + input_tip)
        # quit
        if user_input.lower() == 'q':
            self.logger_handle.info('Goodbye — thanks for using musicdl; come back anytime!')
            sys.exit()
        # restart
        elif user_input.lower() == 'r':
            self.startcmdui()
        # common inputs
        else:
            return user_input
    '''str'''
    def __str__(self):
        return 'Welcome to use musicdl!\nYou can visit https://github.com/CharlesPikachu/musicdl for more details.'


'''MusicClientCMD'''
@click.command()
@click.version_option()
@click.option(
    '-k', '--keyword', default=None, help='The keywords for the music search. If left empty, an interactive terminal will open automatically.', type=str, show_default=True,
)
@click.option(
    '-m', '--music-sources', '--music_sources', default=','.join(DEFAULT_MUSIC_SOURCES), help='The music search and download sources.', type=str, show_default=True, 
)
@click.option(
    '-i', '--init-music-clients-cfg', '--init_music_clients_cfg', default=None, help='Config such as `work_dir` for each music client as a JSON string.', type=str, show_default=True,
)
@click.option(
    '-r', '--requests-overrides', '--requests_overrides', default=None, help='Requests.get / Requests.post kwargs such as `headers` and `proxies` for each music client as a JSON string.', type=str, show_default=True,
)
@click.option(
    '-c', '--clients-threadings', '--clients_threadings', default=None, help='Number of threads used for each music client as a JSON string.', type=str, show_default=True,
)
@click.option(
    '-s', '--search-rules', '--search_rules', default=None, help='Search rules for each music client as a JSON string.', type=str, show_default=True,
)
def MusicClientCMD(keyword: str, music_sources: str, init_music_clients_cfg: str, requests_overrides: str, clients_threadings: str, search_rules: str):
    # load json string
    def _safe_load(string):
        if string is not None:
            result = json_repair.loads(string) or {}
        else:
            result = {}
        return result
    init_music_clients_cfg = _safe_load(init_music_clients_cfg)
    requests_overrides = _safe_load(requests_overrides)
    clients_threadings = _safe_load(clients_threadings)
    search_rules = _safe_load(search_rules)
    # instance music client
    music_sources = music_sources.replace(' ', '').split(',')
    music_client = MusicClient(
        music_sources=music_sources, init_music_clients_cfg=init_music_clients_cfg, clients_threadings=clients_threadings, 
        requests_overrides=requests_overrides, search_rules=search_rules,
    )
    # switch according to keyword
    if keyword is None:
        music_client.startcmdui()
    else:
        print(music_client)
        # --search
        search_results = music_client.search(keyword=keyword)
        # print search_results
        song_infos = music_client.printsearchresults(search_results=search_results)
        # process user inputs, music file download
        user_input_select_song_info_pointer = music_client.processinputs('Please enter music IDs to download (e.g., "1,2"): ').replace(' ', '').split(',')
        user_input_select_song_info_pointer = [idx for idx in user_input_select_song_info_pointer if idx in song_infos]
        user_input_select_song_info_pointer = list(set(user_input_select_song_info_pointer))
        selected_song_infos = []
        for idx in user_input_select_song_info_pointer: selected_song_infos.append(song_infos[idx])
        # --download
        music_client.download(song_infos=selected_song_infos)


'''tests'''
if __name__ == '__main__':
    music_client = MusicClient()
    music_client.startcmdui()