from typing import Union, List
from argparse import ArgumentParser, Namespace, SUPPRESS
from tiramisu import Option, OptionDescription, Config, BoolOption, StrOption, IntOption, \
                     ChoiceOption, SymLinkOption
from tiramisu.error import PropertiesOptionError


class TiramisuNamespace(Namespace):
    def _populate(self):
        for tiramisu_key, tiramisu_value in self._config.value.dict().items():
            option = self._config.option(tiramisu_key)
            if not isinstance(option.option.get(), SymLinkOption):
                if tiramisu_value == [] and option.option.ismulti() and option.owner.isdefault():
                    tiramisu_value = None
                super().__setattr__(tiramisu_key, tiramisu_value)

    def __init__(self, config):
        self._config = config
        super().__init__()

    def __setattr__(self, key, value):
        if key == '_config':
            super().__setattr__(key, value)
            return
        self._config.property.read_write()
        option = self._config.option(key)
        if option.option.ismulti() and value is not None and not isinstance(value, list):
            value = [value]
        option.value.set(value)

    def __getattribute__(self, key):
        if key == '__dict__' and hasattr(self, '_config'):
            self._config.property.read_only()
            self._populate()
            self._config.property.read_write()
        return super().__getattribute__(key)


class TiramisuParser(ArgumentParser):
    def __init__(self, *args, **kwargs):
        self.config = None
        super().__init__(*args, **kwargs)

    def _match_arguments_partial(self, actions, arg_string_pattern):
        # used only when check first proposal for first value
        # we have to remove all actions with propertieserror
        # so only first settable option will be returned
        actions_pop = []
        for idx, action in enumerate(actions):
            if self.config.unrestraint.option(action.dest).property.get(only_raises=True):
                actions_pop.append(idx)
            else:
                break
        for idx in actions_pop:
            actions.pop(0)
        return super()._match_arguments_partial(actions, arg_string_pattern)

    def add_argument(self, *args, **kwargs):
        if args == ('-h', '--help'):
            super().add_argument(*args, **kwargs)
        else:
            raise NotImplementedError('do not use add_argument')

    def add_arguments(self, tiramisu: Union[Config, Option, List[Option], OptionDescription]) -> None:
        if not isinstance(tiramisu, Config):
            if not isinstance(tiramisu, OptionDescription):
                if isinstance(tiramisu, Option):
                    tiramisu = [tiramisu]
                tiramisu = OptionDescription('root', 'root', tiramisu)
            tiramisu = Config(tiramisu)
        self.config = tiramisu
        actions = {}
        for obj in tiramisu.unrestraint.option.list():
            if obj.option.properties(only_raises=True) or 'frozen' in obj.option.properties():
                continue
            option = obj.option
            tiramisu_option = option.get()
            name = option.name()
            if name.startswith(self.prefix_chars):
                raise ValueError('name cannot startswith "{}"'.format(self.prefix_chars))
            properties = obj.property.get()
            kwargs = {'help': option.doc(),
                      'default': SUPPRESS}
            if 'positional' in properties:
                #if not 'mandatory' in properties:
                #    raise ValueError('"positional" argument must be "mandatory" too')
                args = [name]
                if option.requires():
                    kwargs['nargs'] = '?'
            else:
                if len(name) == 1 and 'longargument' not in properties:
                    args = [self.prefix_chars + name]
                else:
                    args = [self.prefix_chars * 2 + name]
                if 'mandatory' in properties:
                    kwargs['required'] = True
            if isinstance(tiramisu_option, BoolOption):
                if 'mandatory' in properties:
                    raise ValueError('"mandatory" property is not allowed for BoolOption')
                #if not isinstance(option.default(), bool):
                #    raise ValueError('default value is mandatory for BoolOption')
                if option.default() is False:
                    action = 'store_true'
                else:
                    action = 'store_false'
                kwargs['action'] = action
            else:
                if option.default() not in [None, []]:
                    #kwargs['default'] = kwargs['const'] = option.default()
                    #kwargs['action'] = 'store_const'
                    kwargs['nargs'] = '?'
                if option.ismulti():
                    if 'mandatory' in properties:
                        kwargs['nargs'] = '+'
                    else:
                        kwargs['nargs'] = '*'
                if isinstance(tiramisu_option, StrOption):
                    pass
                elif isinstance(tiramisu_option, IntOption):
                    kwargs['type'] = int
                elif isinstance(tiramisu_option, SymLinkOption):
                    tiramisu_option = tiramisu_option.impl_getopt()
                    actions[tiramisu_option.impl_getname()][0].insert(0, args[0])
                    continue
                elif isinstance(tiramisu_option, ChoiceOption):
                    kwargs['choices'] = obj.value.list()
                else:
                    pass
                    #raise NotImplementedError('not supported yet')
            actions[option.name()] = (args, kwargs)
        for args, kwargs in actions.values():
            super().add_argument(*args, **kwargs)

    def parse_args(self, *args, **kwargs):
        kwargs['namespace'] = TiramisuNamespace(self.config)
        try:
            namespaces = super().parse_args(*args, **kwargs)
        except PropertiesOptionError as err:
            # import traceback
            # traceback.print_exc()
            if err.proptype == ('mandatory',):
                self.error('the following arguments are required: {}'.format(err._option_bag.option.impl_getname()))
            else:
                self.error('unexpected error: {}'.format(err))
        del namespaces.__dict__['_config']
        return namespaces

    def get_config(self):
        return self.config