from copy import deepcopy

from cached_property import cached_property

from deploy.Windows.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write
from module.base.timer import timer
from module.config.server import VALID_SERVER
from module.config.utils import *

CONFIG_IMPORT = '''
import datetime

# This file was automatically generated by module/config/config_updater.py.
# Don't modify it manually.


class GeneratedConfig:
    """
    Auto generated configuration
    """
'''.strip().split('\n')

DICT_GUI_TO_INGAME = {
    'zh-CN': 'cn',
    'en-US': 'en',
}


def get_generator():
    from module.base.code_generator import CodeGenerator
    return CodeGenerator()


class ConfigGenerator:
    @cached_property
    def argument(self):
        """
        Load argument.yaml, and standardise its structure.

        <group>:
            <argument>:
                type: checkbox|select|textarea|input
                value:
                option (Optional): Options, if argument has any options.
                validate (Optional): datetime
        """
        data = {}
        raw = read_file(filepath_argument('argument'))

        def option_add(keys, options):
            options = deep_get(raw, keys=keys, default=[]) + options
            deep_set(raw, keys=keys, value=options)

        # Insert packages
        option_add(keys='Emulator.PackageName.option', options=list(VALID_SERVER.keys()))

        # Load
        for path, value in deep_iter(raw, depth=2):
            arg = {
                'type': 'input',
                'value': '',
                # option
            }
            if not isinstance(value, dict):
                value = {'value': value}
            arg['type'] = data_to_type(value, arg=path[1])
            if arg['type'] == 'stored':
                value['value'] = {}
                arg['display'] = 'hide'  # Hide `stored` by default
            if isinstance(value['value'], datetime):
                arg['type'] = 'datetime'
                arg['validate'] = 'datetime'
            # Manual definition has the highest priority
            arg.update(value)
            deep_set(data, keys=path, value=arg)

        return data

    @cached_property
    def task(self):
        """
        <task_group>:
            <task>:
                <group>:
        """
        return read_file(filepath_argument('task'))

    @cached_property
    def default(self):
        """
        <task>:
            <group>:
                <argument>: value
        """
        return read_file(filepath_argument('default'))

    @cached_property
    def override(self):
        """
        <task>:
            <group>:
                <argument>: value
        """
        return read_file(filepath_argument('override'))

    @cached_property
    def gui(self):
        """
        <i18n_group>:
            <i18n_key>: value, value is None
        """
        return read_file(filepath_argument('gui'))

    @cached_property
    @timer
    def args(self):
        """
        Merge definitions into standardised json.

            task.yaml ---+
        argument.yaml ---+-----> args.json
        override.yaml ---+
         default.yaml ---+

        """
        # Construct args
        data = {}
        for path, groups in deep_iter(self.task, depth=3):
            if 'tasks' not in path:
                continue
            task = path[2]
            # Add storage to all task
            # groups.append('Storage')
            for group in groups:
                if group not in self.argument:
                    print(f'`{task}.{group}` is not related to any argument group')
                    continue
                deep_set(data, keys=[task, group], value=deepcopy(self.argument[group]))

        def check_override(path, value):
            # Check existence
            old = deep_get(data, keys=path, default=None)
            if old is None:
                print(f'`{".".join(path)}` is not a existing argument')
                return False
            # Check type
            # But allow `Interval` to be different
            old_value = old.get('value', None) if isinstance(old, dict) else old
            value = old.get('value', None) if isinstance(value, dict) else value
            if type(value) != type(old_value) \
                    and old_value is not None \
                    and path[2] not in ['SuccessInterval', 'FailureInterval']:
                print(
                    f'`{value}` ({type(value)}) and `{".".join(path)}` ({type(old_value)}) are in different types')
                return False
            # Check option
            if isinstance(old, dict) and 'option' in old:
                if value not in old['option']:
                    print(f'`{value}` is not an option of argument `{".".join(path)}`')
                    return False
            return True

        # Set defaults
        for p, v in deep_iter(self.default, depth=3):
            if not check_override(p, v):
                continue
            deep_set(data, keys=p + ['value'], value=v)
        # Override non-modifiable arguments
        for p, v in deep_iter(self.override, depth=3):
            if not check_override(p, v):
                continue
            if isinstance(v, dict):
                typ = v.get('type')
                if typ == 'state':
                    pass
                elif typ == 'lock':
                    deep_default(v, keys='display', value="disabled")
                elif deep_get(v, keys='value') is not None:
                    deep_default(v, keys='display', value='hide')
                for arg_k, arg_v in v.items():
                    deep_set(data, keys=p + [arg_k], value=arg_v)
            else:
                deep_set(data, keys=p + ['value'], value=v)
                deep_set(data, keys=p + ['display'], value='hide')
        # Set command
        for path, groups in deep_iter(self.task, depth=3):
            if 'tasks' not in path:
                continue
            task = path[2]
            if deep_get(data, keys=f'{task}.Scheduler.Command'):
                deep_set(data, keys=f'{task}.Scheduler.Command.value', value=task)
                deep_set(data, keys=f'{task}.Scheduler.Command.display', value='hide')

        return data

    @timer
    def generate_code(self):
        """
        Generate python code.

        args.json ---> config_generated.py

        """
        visited_group = set()
        visited_path = set()
        lines = CONFIG_IMPORT
        for path, data in deep_iter(self.argument, depth=2):
            group, arg = path
            if group not in visited_group:
                lines.append('')
                lines.append(f'    # Group `{group}`')
                visited_group.add(group)

            option = ''
            if 'option' in data and data['option']:
                option = '  # ' + ', '.join([str(opt) for opt in data['option']])
            path = '.'.join(path)
            lines.append(f'    {path_to_arg(path)} = {repr(parse_value(data["value"], data=data))}{option}')
            visited_path.add(path)

        with open(filepath_code(), 'w', encoding='utf-8', newline='') as f:
            for text in lines:
                f.write(text + '\n')

    @timer
    def generate_stored(self):
        import module.config.stored.classes as classes
        gen = get_generator()
        gen.add('from module.config.stored.classes import (')
        with gen.tab():
            for cls in sorted([name for name in dir(classes) if name.startswith('Stored')]):
                gen.add(cls + ',')
        gen.add(')')
        gen.Empty()
        gen.Empty()
        gen.Empty()
        gen.CommentAutoGenerage('module/config/config_updater.py')

        with gen.Class('StoredGenerated'):
            for path, data in deep_iter(self.args, depth=3):
                cls = data.get('stored')
                if cls:
                    gen.add(f'{path[-1]} = {cls}("{".".join(path)}")')

        gen.write('module/config/stored/stored_generated.py')

    @timer
    def generate_i18n(self, lang):
        """
        Load old translations and generate new translation file.

                     args.json ---+-----> i18n/<lang>.json
        (old) i18n/<lang>.json ---+

        """
        new = {}
        old = read_file(filepath_i18n(lang))

        def deep_load(keys, default=True, words=('name', 'help')):
            for word in words:
                k = keys + [str(word)]
                d = ".".join(k) if default else str(word)
                v = deep_get(old, keys=k, default=d)
                deep_set(new, keys=k, value=v)

        # Menu
        for path, data in deep_iter(self.task, depth=3):
            if 'tasks' not in path:
                continue
            task_group, _, task = path
            deep_load(['Menu', task_group])
            deep_load(['Task', task])
        # Arguments
        visited_group = set()
        for path, data in deep_iter(self.argument, depth=2):
            if path[0] not in visited_group:
                deep_load([path[0], '_info'])
                visited_group.add(path[0])
            deep_load(path)
            if 'option' in data:
                deep_load(path, words=data['option'], default=False)

        # Package names
        # for package, server in VALID_PACKAGE.items():
        #     path = ['Emulator', 'PackageName', package]
        #     if deep_get(new, keys=path) == package:
        #         deep_set(new, keys=path, value=server.upper())
        # for package, server_and_channel in VALID_CHANNEL_PACKAGE.items():
        #     server, channel = server_and_channel
        #     name = deep_get(new, keys=['Emulator', 'PackageName', to_package(server)])
        #     if lang == SERVER_TO_LANG[server]:
        #         value = f'{name} {channel}渠道服 {package}'
        #     else:
        #         value = f'{name} {package}'
        #     deep_set(new, keys=['Emulator', 'PackageName', package], value=value)
        # Game server names
        # for server, _list in VALID_SERVER_LIST.items():
        #     for index in range(len(_list)):
        #         path = ['Emulator', 'ServerName', f'{server}-{index}']
        #         prefix = server.split('_')[0].upper()
        #         prefix = '国服' if prefix == 'CN' else prefix
        #         deep_set(new, keys=path, value=f'[{prefix}] {_list[index]}')

        # GUI i18n
        for path, _ in deep_iter(self.gui, depth=2):
            group, key = path
            deep_load(keys=['Gui', group], words=(key,))

        write_file(filepath_i18n(lang), new)

    @cached_property
    def menu(self):
        """
        Generate menu definitions

        task.yaml --> menu.json

        """
        data = {}
        for task_group in self.task.keys():
            value = deep_get(self.task, keys=[task_group, 'menu'])
            if value not in ['collapse', 'list']:
                value = 'collapse'
            deep_set(data, keys=[task_group, 'menu'], value=value)
            value = deep_get(self.task, keys=[task_group, 'page'])
            if value not in ['setting', 'tool']:
                value = 'setting'
            deep_set(data, keys=[task_group, 'page'], value=value)
            tasks = deep_get(self.task, keys=[task_group, 'tasks'], default={})
            tasks = list(tasks.keys())
            deep_set(data, keys=[task_group, 'tasks'], value=tasks)

        return data

    @cached_property
    def stored(self):
        import module.config.stored.classes as classes
        data = {}
        for path, value in deep_iter(self.args, depth=3):
            if value.get('type') != 'stored':
                continue
            name = path[-1]
            stored = value.get('stored')
            stored_class = getattr(classes, stored)
            row = {
                'name': name,
                'path': '.'.join(path),
                'i18n': f'{path[1]}.{path[2]}.name',
                'stored': stored,
                'attrs': stored_class('')._attrs,
                'order': value.get('order', 0),
                'color': value.get('color', '#777777')
            }
            data[name] = row

        # sort by `order` ascending, but `order`==0 at last
        data = sorted(data.items(), key=lambda kv: (kv[1]['order'] == 0, kv[1]['order']))
        data = {k: v for k, v in data}
        return data

    @staticmethod
    def generate_deploy_template():
        template = poor_yaml_read(DEPLOY_TEMPLATE)
        cn = {
            'Repository': 'cn',
            'PypiMirror': 'https://pypi.tuna.tsinghua.edu.cn/simple',
            'Language': 'zh-CN',
        }
        aidlux = {
            'GitExecutable': '/usr/bin/git',
            'PythonExecutable': '/usr/bin/python',
            'RequirementsFile': './deploy/AidLux/0.92/requirements.txt',
            'AdbExecutable': '/usr/bin/adb',
        }

        docker = {
            'GitExecutable': '/usr/bin/git',
            'PythonExecutable': '/usr/local/bin/python',
            'RequirementsFile': './deploy/docker/requirements.txt',
            'AdbExecutable': '/usr/bin/adb',
        }

        def update(suffix, *args):
            file = f'./config/deploy.{suffix}.yaml'
            new = deepcopy(template)
            for dic in args:
                new.update(dic)
            poor_yaml_write(data=new, file=file)

        update('template')
        update('template-cn', cn)
        # update('template-AidLux', aidlux)
        # update('template-AidLux-cn', aidlux, cn)
        # update('template-docker', docker)
        # update('template-docker-cn', docker, cn)

        tpl = {
            'Repository': '{{repository}}',
            'GitExecutable': '{{gitExecutable}}',
            'PythonExecutable': '{{pythonExecutable}}',
            'AdbExecutable': '{{adbExecutable}}',
            'Language': '{{language}}',
            'Theme': '{{theme}}',
        }

        def update(file, *args):
            new = deepcopy(template)
            for dic in args:
                new.update(dic)
            poor_yaml_write(data=new, file=file)

        update('./webapp/packages/main/public/deploy.yaml.tpl', tpl)

    @timer
    def generate(self):
        _ = self.args
        _ = self.menu
        _ = self.stored
        # _ = self.event
        # self.insert_server()
        write_file(filepath_args(), self.args)
        write_file(filepath_args('menu'), self.menu)
        write_file(filepath_args('stored'), self.stored)
        self.generate_code()
        self.generate_stored()
        for lang in LANGUAGES:
            self.generate_i18n(lang)
        self.generate_deploy_template()


class ConfigUpdater:
    # source, target, (optional)convert_func
    # redirection = [
    #     ('Dungeon.Dungeon.Support', 'Dungeon.DungeonSupport.Use'),
    #     ('Dungeon.Dungeon.SupportCharacter', 'Dungeon.DungeonSupport.Character'),
    #     ('Dungeon.Dungeon.Name', 'Dungeon.Dungeon.Name', convert_daily),
    #     ('Dungeon.Dungeon.NameAtDoubleCalyx', 'Dungeon.Dungeon.NameAtDoubleCalyx', convert_daily),
    #     ('Dungeon.DungeonDaily.CalyxCrimson', 'Dungeon.DungeonDaily.CalyxCrimson', convert_daily),
    # ]

    @cached_property
    def args(self):
        return read_file(filepath_args())

    def config_update(self, old, is_template=False):
        """
        Args:
            old (dict):
            is_template (bool):

        Returns:
            dict:
        """
        new = {}

        def deep_load(keys):
            data = deep_get(self.args, keys=keys, default={})
            value = deep_get(old, keys=keys, default=data['value'])
            typ = data['type']
            display = data.get('display')
            if (is_template or value is None or value == ''
                    or typ in ['lock', 'state'] or (display == 'hide' and typ != 'stored')):
                value = data['value']
            value = parse_value(value, data=data)
            deep_set(new, keys=keys, value=value)

        for path, _ in deep_iter(self.args, depth=3):
            deep_load(path)

        # if not is_template:
        #     new = self.config_redirect(old, new)
        # new = self.update_state(new)

        return new

    # def config_redirect(self, old, new):
    #     """
    #     Convert old settings to the new.
    #
    #     Args:
    #         old (dict):
    #         new (dict):
    #
    #     Returns:
    #         dict:
    #     """
    #     for row in self.redirection:
    #         if len(row) == 2:
    #             source, target = row
    #             update_func = None
    #         elif len(row) == 3:
    #             source, target, update_func = row
    #         else:
    #             continue
    #
    #         if isinstance(source, tuple):
    #             value = []
    #             error = False
    #             for attribute in source:
    #                 tmp = deep_get(old, keys=attribute)
    #                 if tmp is None:
    #                     error = True
    #                     continue
    #                 value.append(tmp)
    #             if error:
    #                 continue
    #         else:
    #             value = deep_get(old, keys=source)
    #             if value is None:
    #                 continue
    #
    #         if update_func is not None:
    #             value = update_func(value)
    #
    #         if isinstance(target, tuple):
    #             for k, v in zip(target, value):
    #                 # Allow update same key
    #                 if (deep_get(old, keys=k) is None) or (source == target):
    #                     deep_set(new, keys=k, value=v)
    #         elif (deep_get(old, keys=target) is None) or (source == target):
    #             deep_set(new, keys=target, value=value)
    #
    #     return new

    # @staticmethod
    # def update_state(data):
    #     def set_daily(quest, value):
    #         if value is True:
    #             value = 'achievable'
    #         if value is False:
    #             value = 'not_set'
    #         deep_set(data, keys=['DailyQuest', 'AchievableQuest', quest], value=value)
    #
    #     set_daily('Complete_1_Daily_Mission', 'not_supported')
    #     # Dungeon
    #     dungeon = deep_get(data, keys='Dungeon.Scheduler.Enable')
    #     set_daily('Clear_Calyx_Golden_1_times',
    #               dungeon and deep_get(data, 'Dungeon.DungeonDaily.CalyxGolden') != 'do_not_achieve')
    #     set_daily('Complete_Calyx_Crimson_1_time',
    #               dungeon and deep_get(data, 'Dungeon.DungeonDaily.CalyxCrimson') != 'do_not_achieve')
    #     set_daily('Clear_Stagnant_Shadow_1_times',
    #               dungeon and deep_get(data, 'Dungeon.DungeonDaily.StagnantShadow') != 'do_not_achieve')
    #     set_daily('Clear_Cavern_of_Corrosion_1_times',
    #               dungeon and deep_get(data, 'Dungeon.DungeonDaily.CavernOfCorrosion') != 'do_not_achieve')
    #     # Combat requirements
    #     set_daily('In_a_single_battle_inflict_3_Weakness_Break_of_different_Types', 'achievable')
    #     set_daily('Inflict_Weakness_Break_5_times', 'achievable')
    #     set_daily('Defeat_a_total_of_20_enemies', 'achievable')
    #     set_daily('Enter_combat_by_attacking_enemy_Weakness_and_win_3_times', 'achievable')
    #     set_daily('Use_Technique_2_times', 'achievable')
    #     # Other game systems
    #     set_daily('Go_on_assignment_1_time', deep_get(data, 'Assignment.Scheduler.Enable'))
    #     set_daily('Take_1_photo', 'achievable')
    #     set_daily('Destroy_3_destructible_objects', 'achievable')
    #     set_daily('Complete_Forgotten_Hall_1_time', 'achievable')
    #     set_daily('Complete_Echo_of_War_1_times', deep_get(data, 'Weekly.Scheduler.Enable'))
    #     set_daily('Complete_1_stage_in_Simulated_Universe_Any_world', 'not_supported')
    #     set_daily('Obtain_victory_in_combat_with_support_characters_1_time',
    #               dungeon and deep_get(data, 'Dungeon.DungeonSupport.Use') in ['when_daily', 'always_use'])
    #     set_daily('Use_an_Ultimate_to_deal_the_final_blow_1_time', 'achievable')
    #     # Build
    #     set_daily('Level_up_any_character_1_time', 'not_supported')
    #     set_daily('Level_up_any_Light_Cone_1_time', 'not_supported')
    #     set_daily('Level_up_any_Relic_1_time', 'not_supported')
    #     # Items
    #     set_daily('Salvage_any_Relic', 'achievable')
    #     set_daily('Synthesize_Consumable_1_time', 'achievable')
    #     set_daily('Synthesize_material_1_time', 'achievable')
    #     set_daily('Use_Consumables_1_time', 'achievable')
    #     return data

    def read_file(self, config_name, is_template=False):
        """
        Read and update config file.

        Args:
            config_name (str): ./config/{file}.json
            is_template (bool):

        Returns:
            dict:
        """
        old = read_file(filepath_config(config_name))
        new = self.config_update(old, is_template=is_template)
        # The updated config did not write into file, although it doesn't matters.
        # Commented for performance issue
        # self.write_file(config_name, new)
        return new

    @staticmethod
    def write_file(config_name, data, mod_name='alas'):
        """
        Write config file.

        Args:
            config_name (str): ./config/{file}.json
            data (dict):
            mod_name (str):
        """
        write_file(filepath_config(config_name, mod_name), data)

    @timer
    def update_file(self, config_name, is_template=False):
        """
        Read, update and write config file.

        Args:
            config_name (str): ./config/{file}.json
            is_template (bool):

        Returns:
            dict:
        """
        data = self.read_file(config_name, is_template=is_template)
        self.write_file(config_name, data)
        return data


if __name__ == '__main__':
    """
    Process the whole config generation.

                 task.yaml -+----------------> menu.json
             argument.yaml -+-> args.json ---> config_generated.py
             override.yaml -+       |
                  gui.yaml --------\|
                                   ||
    (old) i18n/<lang>.json --------\\========> i18n/<lang>.json
    (old)    template.json ---------\========> template.json
    """
    # Ensure running in Alas root folder
    import os

    os.chdir(os.path.join(os.path.dirname(__file__), '../../'))

    ConfigGenerator().generate()
    ConfigUpdater().update_file('template', is_template=True)
