from   collections    import ChainMap, OrderedDict
from   palm.Immutable import Immutable
from   palm.convert   import str2type
import re


class PalmConfig(ChainMap):

    class Rule(Immutable):

        __slots__ = ('name','path','pathname','format','validate','default')

        REQUIRED = '==REQUIRED=='

        @staticmethod
        def __format( name, value ): return ''
        @staticmethod
        def __validate( value, name=None, context=None ): return True

        def __init__( self, name=None, path=None, format=None, validate=None, default=None ):
            self.name     = name
            self.path     = path
            self.pathname = '_'.join(path) if path     else None
            self.format   = format         if format   else PalmConfig.Rule.__format
            self.validate = validate       if validate else PalmConfig.Rule.__validate
            self.default  = default


    def __init__(self, *values, rules=None, override=True, validate=True):

        super().__init__( *values )

        # helper
        #
        is_predefined = lambda n: n is not None and super(PalmConfig,self).__contains__(n)

        defaults   = {}
        index      = OrderedDict()

        for rule in rules or []:

            name    = rule.name
            path    = rule.pathname
            default = rule.default

            if path is not None:
                index[path] = rule
            if name is not None:
                index[name] = rule

            # NOTE: store default using its
            #       - predefined name or
            #       - rule's pathname or
            #       - rule's name
            #
            if is_predefined(path):
                defaults[path] = default

            elif is_predefined(name):
                defaults[name] = default

            elif path is not None:
                defaults[path] = default

            elif name is not None:
                defaults[name] = default

        self.maps.append( defaults )

        self.override = override
        self.validate = validate
        self.managed  = self.maps[0]
        self.rules    = rules
        self.__ridx   = index
        self.__init   = True   # see __setitem__


    def rule(self, name):
        return self.__ridx[name] if name in self.__ridx else PalmConfig.Rule(name)
    def rules(self):
        for rule in self.__ridx.values():
            yield rule

    def aliases(self, name):
        result = []
        if name is not None:
            rule = self.rule(name)
            if rule.pathname is not None:
                result.append(rule.pathname)
            if rule.name is not None:
                if rule.name != rule.pathname:
                    result.append(rule.name)
        return tuple(result)

    def managing(self, name):
        names = self.aliases(name)
        for name in names:
            try:
                if name in self.managed:
                    return True
            except:
                pass
        return False


    def clear(self):
        if self.override:
            self.managed.clear()

    def __contains__(self, name):
        names = self.aliases(name)
        for mapping in self.maps:
            for name in names:
                try:
                    if name in mapping:
                        return True
                except:
                    pass
        return False

    def __getitem__(self, name):
        names = self.aliases(name)
        for mapping in self.maps:
            for name in names:
                try:
                    return mapping[name]
                except:
                    pass

        return self.__missing__(name)

    def __setitem__(self, name, value):
        if name is None:
            raise KeyError('name is None')

        elif name in self.__dict__ or '_PalmConfig__init' not in self.__dict__:  # name is attr of self
            self.__dict__[name] = value

        else:
            names = [ n for n in self.aliases(name) if n in self.managed ]
            if len(names) == 0:
                if self.validate:
                    rule = self.rule(name)
                    rule.validate( value, name, self )

                self.managed[name] = value

            elif self.override:
                if self.validate:
                    rule = self.rule(name)
                    rule.validate( value, name, self )

                for name in names:
                    self.managed[name] = value

    def __delitem__(self, name):
        names = [ n for n in self.aliases(name) if n in self.managed ]
        if len(names) == 0:
            raise KeyError("Key not managed: '{}'".format(name) )

        elif self.override:
            for name in names:
                del self.managed[name]


    def __getattr__(self, name):
        return self[name] if name in self else None
    def __setattr__(self, name, value):
        self[name] = value
    def __delattr__(self, name ):
        del self[name]


    # __next = re.compile( r'\$(\$|\w+|\{\w+\})', re.ASCII ).search
    __next = re.compile( r'\$(\$|[-a-zA-Z0-9_]+|\{[-a-zA-Z0-9_]+\})', re.ASCII ).search

    def expand( self, string, context=None, keep_unknown=True, ignore=None ):

        """expands variables (e.g. $name or ${name}) by values of context

        string:       (str)   string to expand
        context:      (dict)  dictionary containing name-value-pairs
        keep_unknown: (bool)  keep variables not in context or delete from string
        ignore:       (tuple) tuple of names to ignore

        returns:
        expanded string (as bool, int, float if convertable)"""

        if type(string) is not str:
            return string

        if context is None:
            context = self

        i = 0
        while True:
            m = PalmConfig.__next( string, i )
            if not m:
                break

            i, j = m.span(0)
            name = m.group(1)
            if name.startswith('{') and name.endswith('}'):
                name = name[1:-1]

            value = None
            if name == '$':
                value = '$'

            elif ignore is not None and name in ignore:
                value = '??'+name+'??'

            elif name in context:
                value = context.get(name)
                if type(value) is str:
                    names = self.aliases(name)
                    value = self.expand(
                        value,
                        context,
                        ignore=names if ignore is None else ignore + names,
                        keep_unknown=keep_unknown
                        )

            if value is not None: ####### replace ${name} by value
                tail    = string[j:]
                string  = string[:i] + str(value)
                i       = len(string)
                string += tail

            elif name in context: ####### delete ${name}
                string = string[:i] + string[j:]

            elif keep_unknown == True: ## keep ${name}
                i = j

            else: ####################### delete ${name}
                string = string[:i] + string[j:]


        # return as bool, int, float or string
        #
        return str2type(string)

    def eval( self, name, context=None, keep_unknown=True, ignore=None ):
        result = self.get(name)
        if type(result) is str:
            names  = self.aliases(name)
            result = self.expand(
                result,
                context=context,
                ignore=names if ignore is None else ignore + names,
                keep_unknown=keep_unknown
                )

        return result



    # def palmnames(self):
    #     return [ rule.name for rule in self.rules if not rule.name is None ]

    # def pathnames(self):
    #     return [ rule.pathname for rule in self.rules if not rule.pathname is None ]


    # def allKeys(self):

    #     keys = set()

    #     def addKey( key ):
    #         result = False
    #         if key is not None:
    #             if key not in keys:
    #                 keys.add( key )
    #                 result = True
    #         return result

    #     for key in self.__values.keys():
    #         if addKey(key):
    #             yield key

    #     for key in self.__default.keys():
    #         if addKey(key):
    #             yield key

    #     for rule in self.rules:
    #         if addKey( rule.name ):
    #             yield rule.name
    #         elif addKey( rule.pathname ):
    #             yield rule.pathname


    # def trace( self, name=None ):

    #     def stackOf( name, *context ):
    #         result = []
    #         for current in context:
    #             result.append( current.get(name))
    #         return result

    #     def valuesOf( name ):
    #         result = [
    #             self.get(name),
    #             stackOf( name, self.__values, *self.__default.maps )
    #             ]
    #         return result

    #     def trace( name ):
    #         result = {}

    #         rule = self.rule(name)
    #         if rule.name:
    #             result[rule.name] = valuesOf(rule.name)
    #         if rule.pathname:
    #             result[rule.pathname] = valuesOf(rule.pathname)

    #         return result

    #     # returns: {
    #     #    name: {
    #     #         rule.name:     [value,[default,...]],
    #     #         rule.pathname: [value,[default,...]]
    #     #         }
    #     #    ...
    #     #     }
    #     result = {}
    #     if name:
    #         result[name] = trace( name )

    #     else:
    #         for name in self.palmnames():
    #             result[name] = trace( name )

    #     return result


    # def __make_index( rules ):
    #     result = OrderedDict()
    #     for rule in rules:
    #         if not rule.name is None:
    #             result[rule.name] = rule
    #         if rule.pathname:
    #             result[rule.pathname] = rule

    #     return result


