# Template class
#
# This was an attempt to provide a subset of the capabilities of the Production 
# class in a for that was easier to use and understand. The general feeling is 
# that it is still to complicate. It has been superseded by the Templet class 
# and it is anticipated that this class will soon be deprecated.

# Imports {{{1
from __future__ import print_function, division
from .production import Production, CTRL_CODE
from .predefined import Cull, Join, Newline, StrikeLine, Tab
import collections
import re


# Globals {{{1
ATTRIBUTE_SEPARATOR = '|'
NULL_INDICATOR = CTRL_CODE + '0'

# Utilities {{{1
# isAttribute {{{2
IDENT = r'[a-z_][a-z0-9_]*'
ATTRIBUTE = r'\*?{0}(?:\.{0})*'.format(IDENT)
ATTRIBUTE_MATCHER = re.compile(r'\A\s*%s\s*\Z' % ATTRIBUTE, re.I)
def isAttribute(e):
    return bool(ATTRIBUTE_MATCHER.match(e))

# isConditional {{{2
CONDITIONAL_MATCHER = re.compile(
    r'\A\s*%s\s*\Z' % r'\s*'.join([IDENT, r'\?', IDENT, ':', IDENT]), re.I)
def isConditional(e):
    return bool(CONDITIONAL_MATCHER.match(e))

# isString {{{2
STRING = r'''("([^\\"]|(\\.))*")|('([^\\']|(\\.))*')'''
COMPILED_STRING = re.compile(r'''\A\s*%s\s*\Z''' % STRING)
def isString(e):
    return bool(COMPILED_STRING.match(e))

# isJoin {{{2
JOIN_MATCHER = re.compile(r'\A\s*(%s)\s+(%s)\s*\Z' % (ATTRIBUTE, STRING), re.I)
def isJoin(e):
    match = JOIN_MATCHER.match(e)
    if match:
        fields = match.groups()
        return fields[0], fields[1].strip('"' + "'")


# Template class {{{1
class Template(Production):
    # Description {{{2
    """A Description of a Syntax Fragment

    A Template takes a string as input, which is converted to a production rule.

    :param template:
        A string that may contain brace delimited regions. The contents of 
        the brace delimited regions is evaluated and the result is 
        interpolated into the string. Within the braces may be a simple 
        identifier or it may be a python expression. If it is an identifier, 
        it must be known by the production object itself. Otherwise it is 
        expected to be a python expression.  There are two ways of informing 
        the production object of values of attributes, or the meaning of 
        names passed to the python expression: they are either passed as 
        a dictionary to the symbols argument or they are passed as extra 
        keyword arguments to the constructor.

        Normally any simple attributes that are interpolated into a template 
        are not further processed. However, if the attribute name is 
        preceded by an asterisk, then the attribute value is itself treated 
        as a template and so any attributes it contains would be elaborated. 
        In this case, the values of these attributes must also be known by 
        the production object.

        There is more features available, see the users guide for more 
        information.

    :param symbols:
        A dictionary containing names that should be made available during 
        Python expression evaluation within templates.

    :param tabstop:
        Tab stop to use if a tab character is found in the template.

    :param nullify:
        Treat inaccessible attributes as if they were null.

    :param * args:
        Unnamed arguments should be either class instances or dictionaries and 
        will be searched for attribute values in the order in which they are 
        given.

    :param ** kwargs:
        Any otherwise unrecognized  keyword arguments are saved and their 
        values made available as brace-delimited attributes in templates.
    """

    # Constructor {{{2
    def __init__(self, template, *args, **kwargs):
        #self.__dict__.update(kwargs)
        self.add_rule(Template._elaborate(self, template, *args, **kwargs))

    # add_template {{{2
    def add_template(self, template, *args, **kwargs):
        self.rule = self._clean(
            Template._elaborate(self, template, *args, **kwargs))

    # _elaborate {{{2
    @staticmethod
    def _elaborate(rule, template, *args, **kwargs):
        if type(template) == str:
            return Template._expand(rule, template, *args, **kwargs)
        elif isinstance(template, collections.Iterable):
            for t in template:
                res = Template._expand(rule, t, *args, **kwargs)
                if res:
                    break
            return res
        raise AssertionError("Invalid type for template value")

    # _expand {{{2
    @staticmethod
    def _expand(self, template, *args, **kwargs):
        # Description {{{3
        # Takes a string that contains nested braces and splits it into a list 
        # of productions. Everything outside the lowest level braces is treated 
        # as simple strings.  Things within the first level braces may be 
        # attribute names, in which case they are interpolated into the output.  
        # For example, in:
        #
        #   template = "Hello {user}."
        #
        # {user} is replaced by the value of user where user is either a kwarg 
        # or a named value associated with an arg in args.
        #
        # It is also possible for the contents of the first level braces to be a
        # python expression that evaluates to things that can be contained in
        # productions (strings, lists, and productions). In this case it is
        # generally required to specify the symbols argument so items in the
        # expression can be found.
        #
        #   template = "/* Created: {str(datetime.date.today())} */"
        #
        # If the thing in the first level of braces is a python expression (it 
        # is not a simple identifier), then it is possible to have a second 
        # level of braces. This second level of braces may only contain 
        # attribute names.
        #
        #   template = dedent('''\
        #       always @(*) begin
        #           {Indent(Join(
        #               ['In[%s] = {in}%s;' % (i, i) for i in range(4)],
        #               sep=Newline()
        #           ))}
        #       end''')
        #
        # You escape braces by doubling or tripling them up. When multiple 
        # braces are encountered, they are passed through except one level of 
        # braces are removed. In this way, the previous example could be 
        # rewritten as:
        #
        #   template = dedent('''\
        #       always @(*) begin
        #           {Indent(Lines(
        #               ['In[{{0}}] = {in}{{0}};'.format(i) for i in range(4)]
        #           ))}
        #       end''')
        #
        # In this case {in} is replaced by the value of rule.in and {{0}} is 
        # replaced with {0}, and so if rule.in is left_in, then python is 
        # passed:
        #
        #   Indent(Lines(['In[{0}] = left_in{0};'.format(i) for i in range(4)))
        #
        # This is a staticmethod with self passed in because we need to apply it 
        # to user classes that are not children of Production (those classes 
        # would be expected to have docgen_templates).

        # Fields class {{{3
        class Fields(object):
            # This is a class that mimics a dictionary. It must mimic 
            # a dictionary because its objects are passed to Python's eval() 
            # function, which requires dictionaries, but it is a class so that 
            # we can access the values dynamically. When requested, we search 
            # through the keyword arguments (kwargs is a dict), the arguments 
            # (each may be a dict or an object) and the base rule (object) 
            # looking for the desired key.
            #
            # It is expected that getValue will try __getitem__ before 
            # __getattr__, so our attributes should not interfere with the 
            # users, but I added leading and trailing underscores just in case.
            def __init__(self, kwargs, args, parent):
                self._local_symbols_ = {}
                self._kwargs_ = kwargs
                self._args_ = args
                self._parent_ = parent

            def __getitem__(self, key):
                for arg in (
                   [self._local_symbols_, self._kwargs_] +
                   list(self._args_) +
                   [self._parent_]
                ):
                    try:
                        return getattr(arg, key)
                    except (AttributeError, TypeError):
                        pass
                    try:
                        return arg[key]
                    except (KeyError, AttributeError, TypeError):
                        pass
                raise KeyError('%s: not found.' % key)

            def __contains__(self, key):
                try:
                    self._get(key)
                    return True
                except KeyError:
                    return False

            def __setitem__(self, key, value):
                # This is needed because eval() needs to store values for 
                # temporary variables (like: ['x[%s]' % n for n in ['p', 'n']]) 
                self._local_symbols_[key] = value

        # mismatch {{{3
        def mismatch():
            raise ValueError("unbalanced braces in '%s'" % template)

        # too_many_levels {{{3
        def too_many_levels():
            raise ValueError(
                "too many levels of braces (> 2) in '%s'" % template)

        # expected_identifier {{{3
        def expected_identifier():
            raise ValueError("expected identifier for '%s'" % template)

        # outputString {{{3
        def outputString(text):
            if text:
                return [text]
            else:
                return []

        # getValue {{{3
        def getValue(collection, path):
            # the path may be string of names separated with dots, if so call 
            # getValue recursively to track down the desired value, otherwise 
            # just return the value associated with the name contained in path.

            def get(collection, name):
                # collection may be an object, in which case name is the name of 
                # an attribute, or it may be a dictionary, in which case name is 
                # a key. collection may also be a Field object, which purposely 
                # has no attributes and instead mimics a dictionary.
                #
                # try as a dictionary first, in this way any attributes of Field 
                # will not be accessed by accident.
                try:
                    return collection[name]
                except (KeyError, AttributeError, TypeError):
                    pass
                try:
                    return getattr(collection, name)
                except AttributeError:
                    pass

                # The value was not found.
                # Either an error can be generated or None can be returned.
                # Choose your poison ...
                if nullify:
                    return None
                else:
                    raise KeyError('%s: not found.' % name)

            if '.' in path:
                name, remainder = path.split('.', 1)
                return getValue(get(collection, name), remainder)
            else:
                value = get(collection, path)
                assert Production._eligibleToBeRule(value), (
                    '%s: bad type: %s' % (path, type(value))
                )
                return value

        # expandValue {{{3
        def expandValue(attr):
            # Given a name, find the value associated with that name and return 
            # it.  If the name is preceded by an asterisk treat it as 
            # a template, which causes any attributes it contains to be expanded 
            # as well.
            if attr[0] == '*':
                # first, make sure that attribute value is a production so that 
                # the conversion back to a string works without a hitch
                #     this is weak in that we do not pass the attribute values 
                #     into any productions we find here, thus these productions 
                #     act on the attribute names rather than the attribute 
                #     values, meaning that any None processing is defeated.
                template = Production(getValue(fields, attr[1:]))
                # now, convert value into a string that is treated as a template
                # in this way, any attributes it contains will be evaluated
                rule = Template(str(template), fields)
                return rule
            # simply return the value of the expression
            return getValue(fields, attr)

        # evalExpression {{{3
        def evalExpression(expr):
            """Evaluate Python expression"""
            # should pass sumbols as 2nd argument to eval (globals) and fields 
            # to 3rd argument (locals). But I believe there is a bug in eval 
            # that sometimes skip looking in the local symbol table for local 
            # variables, so add kwargs to symbols and pass that in for the 
            # global symbol table. It will not fix the problem completely, but 
            # does allow of a work around: if it is not working, pass the 
            # offending value in as a kwarg.
            env = dict(list(symbols.items()) + list(kwargs.items()))
            result = eval(expr, env, fields)
            return result

        # outputNewlines {{{3
        def outputNewlines(newlines):
            return [Newline(newlines-1)]

        # _expand {{{3
        result = []
        start = 0
        depth = 0
        null = False
        last = len(template)
        symbols = kwargs.pop('symbols', {})
        tabstop = kwargs.pop('tabstop', None)
        nullify = kwargs.pop('nullify', None)
        fields = Fields(kwargs, args, self)

        iterator = iter(range(last))
        string = ''
        expr = ''
        for i in iterator:
            if template[i] == '{':
                # found opening brace
                braces = 1
                remainder = template[i+1:]
                if remainder and remainder[0] == '{':
                    # found escaped brace
                    # braces are escaped by doubling or tripling them up
                    for char in remainder:
                        if char == '{':
                            braces += 1
                            next(iterator)
                        else:
                            break
                    # add what we have seen before the braces to value to
                    # output and then add the number of braces found minus one.
                    if depth == 0:
                        string += template[start:i] + (braces-1)*'{'
                        expr = ''
                    elif depth == 1:
                        expr += template[start:i] + (braces-1)*'{'
                    else:
                        too_many_levels()
                else:
                    # found single opening brace
                    if depth == 0:
                        # output what we have so far because we may be entering
                        # a python expression
                        result += outputString(string + template[start:i])
                        string = ''
                    elif depth == 1:
                        # this is the start of an attribute
                        # save what we already have of the expression
                        expr += template[start:i]
                    else:
                        too_many_levels()
                    depth += 1
                start = i + braces
            elif template[i] == '}':
                # found closing brace
                braces = 1
                remainder = template[i+1:]
                if remainder and remainder[0] == '}':
                    # found multiple closing braces
                    for char in remainder:
                        if char == '}':
                            braces += 1
                            next(iterator)
                        else:
                            break
                    if depth == 0:
                        string += template[start:i] + (braces-1)*'}'
                    elif depth == 1:
                        expr += template[start:i] + (braces-1)*'}'
                    else:
                        too_many_levels()
                else:
                    # found single closing brace
                    if depth == 0:
                        mismatch()
                    elif depth == 1:
                        expr += template[start:i]
                        for term in expr.split(ATTRIBUTE_SEPARATOR): # | split
                            if isAttribute(term):
                                rule = expandValue(term)
                            elif term == '!strike':
                                rule = StrikeLine()
                            elif term == '':
                                rule = ''
                            elif isString(term):
                                if NULL_INDICATOR in term:
                                    rule = None
                                else:
                                    rule = term[1:-1]
                            elif isConditional(term): # match ?: oper
                                cond, rest = term.split('?')
                                iftrue, iffalse = rest.split(':')
                                if fields[cond]:
                                    rule = expandValue(iftrue)
                                else:
                                    rule = expandValue(iffalse)
                            else:
                                join = isJoin(term) # match join {name 'str'}
                                if join:
                                    rule = Join(
                                        Cull(expandValue(join[0])), sep=join[1]
                                    )
                                else:
                                    # is python expression
                                    if NULL_INDICATOR in term:
                                        rule = None
                                    else:
                                        rule = evalExpression(term)
                            rule = Production._clean(rule)
                            if rule is not None:
                                result += [rule]
                                break
                        else:
                            null = True
                        expr = ''
                    elif depth == 2:
                        name = template[start:i]
                        if not isAttribute(name):
                            expected_identifier()
                        value = getValue(fields, name)
                        if value is None:
                            expr += NULL_INDICATOR
                        elif type(value) == str:
                            expr += value
                        elif Production._eligibleToBeRule(value):
                            expr += Production._clean(value).expand()
                        else:
                            raise AssertionError(
                                ' '.join([
                                    "Values interpolated into Python",
                                    "expressions must be strings.",
                                    "name=%s, type=%s" % (name, type(value))
                                ])
                            )
                    else:
                        too_many_levels()
                    depth -= 1
                start = i + braces
            elif template[i] == '\n' and depth == 0:
                newlines = 1
                remainder = template[i+1:]
                if remainder and remainder[0] == '\n':
                    # found multiple newlines
                    for char in remainder:
                        if char == '\n':
                            newlines += 1
                            next(iterator)
                        else:
                            break
                result += outputString(template[start:i])
                result += outputNewlines(newlines)
                start = i + newlines
            elif template[i] == '\t' and depth == 0:
                result += outputString(template[start:i])
                result += [Tab(tabstop)]
                start = i + 1

        if depth != 0:
            mismatch()

        if null:
            return None
        else:
            return result + [s for s in [string, template[start:]] if s]


