"""apetype configs module defines the core class ConfigBase
that is also used as the starting point for Tasks and 
ReportTasks, and wrapped in Pipelines
"""

import typing
from collections import OrderedDict

class ConfigBase(object):
    """Takes in a list of settings, which will be exposed
    as CLI arguments. Each settings tuple should have the
    following format:
    ('--name', keyword dict for the parser.add_argument function)

    The recommended way to build a SettingsBase object, is to
    inherit from it and define the `setup` method
    (see SettingsBase.setup docstring)

    Args:
      parse (bool | list): if True, already parse arguments.
        Can also be a list that will then be passed on as args.
    """

    def __init__(self, parse=True):
        self.groups = False
        self._help = self.get_arg_docs()
        self.setup()
        self.make_call()
        self.make_parser()
        if parse:
            self.parse_args(None if isinstance(parse, bool) else parse)

    def __getitem__(self, key):
        return self.settings.__getattribute__(key)

    def __getattr__(self, name):
        if 'settings' in self.__dict__ and name in self.settings:
            return self.settings.__getattribute__(name)
        else:
            # Trick from pandas to ensure jedi completion works
            return object.__getattribute__(self, name)

    def __repr__(self):
        if 'settings' in self.__dict__:
            return str(self.settings)
        else:
            return f'<Uninitialised {self.__class__.__name__}>'

    def __str__(self):
        return self.__repr__().replace(
            'Namespace(', 'Settings:\n\n').replace(', ', '\n')[:-1]

    def setup(self):
        """Can be overwritten by inheriting classes.
        Allows defining parameters with type hints.
        Overwritten setup methods need to call `super().setup()`
        at the end.

        Example:
        >>> class Settings(SettingsBase):
        ...     def setup(_, a: int = 5, b: float = .1, c: str = 'a'):
        ...          super().setup()
        ... settings = Settings()
        """
        import inspect
        cls = type(self)
        cls_vars = vars(cls)
        # getting hints from self does not always work
        self._annotations = annotations = typing.get_type_hints(cls)
        self._annotation_groups = annotation_groups = [
            attr for attr in cls_vars if isinstance(cls_vars[attr], type)
        ]
        if annotations or annotation_groups:
            # get_attr does not work so requesting vars
            self._settings = [
                self.add_arg_format(p, cls_vars[p] if p in cls_vars else None,
                                    annotations[p], p not in cls_vars,
                                    self._help[p] if p in self._help else None
                )
                for p in annotations
            ]
            if annotation_groups:
                self.groups = True
                self._settings = OrderedDict(('default', self._settings)) if self._settings else OrderedDict()
                for annot_grp in annotation_groups:
                    grp_annotations = typing.get_type_hints(cls_vars[annot_grp])
                    grp_cls_vars = vars(cls_vars[annot_grp])
                    self._settings[annot_grp] = [
                        self.add_arg_format(
                            p, grp_cls_vars[p] if p in grp_cls_vars else None,
                            grp_annotations[p], p not in grp_cls_vars,
                            self._help[p] if p in self._help else None
                        )
                        for p in grp_annotations
                    ]
        else:
            sig = inspect.signature(self.setup)
            self._settings = [
                self.add_arg_format(
                    p, sig.parameters[p].default,
                    sig.parameters[p].annotation,
                    not(bool(sig.parameters[p].default)),
                    self._help[p] if p in self._help else None
                )
                for p in sig.parameters
            ]

    @staticmethod
    def add_arg_format(name, default, typ, positional, help):
        if typ is bool:
            return (f'--{name}', {
                    'default': default,
                    'action': 'store_const',
                    'const': not(default),
                    'help': help
            }
            )
        else:
            return (name if positional else f'--{name}', {
                    'default': default,
                    'type': typ,
                    'help': help
            }
            )

    @classmethod
    def get_arg_docs(cls):
        import re
        import inspect
        commentdoc = re.compile(r'\s*(\w+)\s*:\s*\w+.*?#(.+)')
        multilinedoc = re.compile(
            r'\s*(?P<identifier>\w+)\s*:\s*\w+.*?\n\s*(?P<delimiter>"""|\'\'\')'
            r'(?P<annot>(.*\n)+?)\s*?(?P=delimiter)',
            re.MULTILINE)
        try:
            argdocs = dict([
                commentdoc.match(line).groups()
                for line in inspect.getsourcelines(cls)[0] if commentdoc.match(line)
            ])
            # Clean if not identifier and strip
            argdocs = {arg:argdocs[arg].strip() for arg in argdocs if arg.isidentifier()}
            argdocs.update({
                match.group('identifier'):match.group('annot').replace('\n', ' ').strip()
                for match in multilinedoc.finditer(inspect.getsource(cls))
                if match.group('identifier').isidentifier()
            })
            return argdocs
        except TypeError:
            # TypeError will be raised when class is defined interactively
            # Returning an empty dict in that case
            return {}

    def set_settings(self, vardict):
        if 'self' in vardict: vardict.pop('self')
        for attr in vardict:
            # TODO check type
            self.__setattr__(attr, vardict[attr])
            # print(attr,vardict[attr])
        self.settings = vardict

    def make_call(self):
        from types import FunctionType
        variables = ', '.join([
            s[0].replace('-','') + ('' if s[1]['default'] is None else
                                     '='+repr(s[1]['default'])
            )
            for s in sorted(
                self._settings,
                key=lambda x: x[1]['default'] is None,
                reverse=True
            )
        ])
        call_code = compile(f'''def set_variables(self, {variables}):
            self.set_settings(locals())
        ''', "<string>", "exec")
        # Find code sub object:
        #for i in range(len(call_code.co_consts)):
        #    if type(call_code) is type(call_code.co_consts[i]):
        #        code = call_code.co_consts[i]
        #        break
        code_pos = 0 if call_code.co_consts[-1] is None else -4
        call_func = FunctionType(
            call_code.co_consts[code_pos], globals(), "set_variables",
            argdefs = call_code.co_consts[-1] if '=' in variables else None
        )
        setattr(self.__class__, '__call__', call_func)
        # could also orverwrite __init__, if call to super().__init__ is included, but this should be optional as otherwise it will not work for the argparse workflow side
        
    def make_parser(self, **kwargs):
        import argparse
        self.parser = argparse.ArgumentParser(**kwargs)
        if self.groups: self.group_parsers = {}
        for grp in self._settings:
            if self.groups:
                parser = self.parser.add_argument_group(grp)
                self.group_parsers[grp] = parser
            else:
                parser = self.parser
            for setting in (self._settings[grp] if self.groups else self._settings):
                parser.add_argument(
                    setting[0],
                    **setting[1]
                )
            if not self.groups:
                break  # if no groups need to break

    def parse_args(self, args):
        self.settings = self.parser.parse_args(args)
        # Overwrite default typing values
        for a in self._annotations:
            if a in self.settings:
                self.__setattr__(a, self.settings.__getattribute__(a))
        return self.settings
