# Based on snippet by
# Author: Michal Ludvig <michal@logix.cz>
#         http://www.logix.cz/michal
#
# modified for args and kwargs by Skylar Saveland http://skyl.org
#
# updated for django 1.6, modified, and packaged by Nicholas Lourie,
# while working for kozbox, llc. http://kozbox.com. Since updated for 
# django 1.8 with various other improvements by numerous contributors, 
# see https://github.com/nalourie/django-macros/ for details.

""" Macros.py, part of django-macros, allows for creation of
macros within django templates.
"""

from re import match as regex_match
from django import template
from django.template.loader import get_template

register = template.Library()


def _setup_macros_dict(parser):
    """ initiates the _macros attribute on the parser
    object, allowing for storage of the macros in the parser.
    """
    # Each macro is stored in a new attribute
    # of the 'parser' class. That way we can access it later
    # in the template when processing 'use_macro' tags.
    try:
        # don't overwrite the attribute if it already exists
        parser._macros
    except AttributeError:
        parser._macros = {}


class DefineMacroNode(template.Node):
    """ The node object for the tag which
    defines a macro.
    """

    def __init__(self, name, nodelist, args, kwargs):
        # the values in the kwargs dictionary are by
        # assumption instances of template.Variable.
        self.name = name
        self.nodelist = nodelist
        self.args = args
        self.kwargs = kwargs

    def render(self, context):
        # convert template variable defaults into resolved
        # variables.
        #
        # recall all defaults are template variables
        self.kwargs = {k: v.resolve(context)
                       for k, v in self.kwargs.items()}

        # empty string - {% macro %} tag has no output
        return ''


@register.tag(name="macro")
def do_macro(parser, token):
    """ the function taking the parsed tag and returning
    a DefineMacroNode object.
    """
    try:
        bits = token.split_contents()
        tag_name, macro_name, arguments = bits[0], bits[1], bits[2:]
    except IndexError:
        raise template.TemplateSyntaxError(
            "'{0}' tag requires at least one argument (macro name)".format(
                token.contents.split()[0]))

    # use regex's to parse the arguments into arg
    # and kwarg definitions

    # the regex for identifying python variable names is:
    #  r'^[A-Za-z_][\w_]*$'

    # args must be proper python variable names
    # we'll want to capture it from the regex also.
    arg_regex = r'^([A-Za-z_][\w_]*)$'

    # kwargs must be proper variable names with a
    # default value, name="value", or name=value if
    # value is a template variable (potentially with
    # filters).
    # we'll want to capture the name and value from
    # the regex as well.
    kwarg_regex = (
        r'^([A-Za-z_][\w_]*)=(".*"|{0}.*{0}|[A-Za-z_][\w_]*)$'.format("'"))
    # leave further validation to the template variable class

    args = []
    kwargs = {}
    for argument in arguments:
        arg_match = regex_match(
            arg_regex, argument)
        if arg_match:
            args.append(arg_match.groups()[0])
        else:
            kwarg_match = regex_match(
                kwarg_regex, argument)
            if kwarg_match:
                kwargs[kwarg_match.groups()[0]] = template.Variable(
                    # convert to a template variable here
                    kwarg_match.groups()[1])
            else:
                raise template.TemplateSyntaxError(
                    "Malformed arguments to the {0} tag.".format(
                        tag_name))

    # parse to the endmacro tag and get the contents
    nodelist = parser.parse(('endmacro',))
    parser.delete_first_token()

    # store macro in parser._macros, creating attribute
    # if necessary
    _setup_macros_dict(parser)
    parser._macros[macro_name] = DefineMacroNode(
        macro_name, nodelist, args, kwargs)
    return parser._macros[macro_name]


class LoadMacrosNode(template.Node):
    """ The template tag node for loading macros from
    an external sheet.
    """

    def __init__(self, macros):
        self.macros = macros

    def render(self, context):
        # render all macro definitions in the current
        # context to set their template variable default
        # arguments:
        for macro in self.macros:
            macro.render(context)

        ## empty string - {% loadmacros %} tag does no output
        return ''


@register.tag(name="loadmacros")
def do_loadmacros(parser, token):
    """ The function taking a parsed tag and returning
    a LoadMacrosNode object, while also loading the macros
    into the page.
    """
    try:
        tag_name, filename = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "'{0}' tag requires exactly one argument (filename)".format(
                token.contents.split()[0]))
    if filename[0] in ('"', "'") and filename[-1] == filename[0]:
        filename = filename[1:-1]
    else:
        raise template.TemplateSyntaxError(
            "Malformed argument to the {0} template tag."
            " Argument must be in quotes.".format(tag_name)
        )
    t = get_template(filename)
    try:
        # Works for Django 1.8
        nodelist = t.template.nodelist
    except AttributeError:
        # Works for Django < 1.8
        nodelist = t.nodelist
    macros = nodelist.get_nodes_by_type(DefineMacroNode)
    # make sure the _macros attribute dictionary is instantiated
    # on the parser, then add the macros to it.
    _setup_macros_dict(parser)
    for macro in macros:
        parser._macros[macro.name] = macro
    # pass macros to LoadMacrosNode so that it can
    # resolve the macros template variable kwargs on render
    return LoadMacrosNode(macros)


class UseMacroNode(template.Node):
    """ Template tag Node object for the tag which
    uses a macro.
    """

    def __init__(self, macro, args, kwargs):
        # all the values kwargs and the items in args
        # are by assumption template.Variable instances.
        self.macro = macro
        self.args = args
        self.kwargs = kwargs

    def render(self, context):

        # add all of the use_macros args into context
        for i, arg in enumerate(self.macro.args):
            try:
                template_variable = self.args[i]
                context[arg] = template_variable.resolve(context)
            except IndexError:
                context[arg] = ""

        # add all of use_macros kwargs into context
        for name, default in self.macro.kwargs.items():
            if name in self.kwargs:
                context[name] = self.kwargs[name].resolve(context)
            else:
                if isinstance(default, template.Variable):
                    # variables must be resolved explicitly,
                    # because otherwise if macro's loaded from
                    # a separate file things will break
                    context[name] = default.resolve(context)
                else:
                    context[name] = default

        # return the nodelist rendered in the adjusted context
        return self.macro.nodelist.render(context)


def parse_macro_params(token):
    """
    Common parsing logic for both use_macro and macro_block
    """
    try:
        bits = token.split_contents()
        tag_name, macro_name, values = bits[0], bits[1], bits[2:]
    except IndexError:
        raise template.TemplateSyntaxError(
            "{0} tag requires at least one argument (macro name)".format(
                token.contents.split()[0]))

    args = []
    kwargs = {}

    # leaving most validation up to the template.Variable
    # class, but use regex here so that validation could
    # be added in future if necessary.
    kwarg_regex = (
        r'^([A-Za-z_][\w_]*)=(".*"|{0}.*{0}|[A-Za-z_][\w_]*)$'.format(
            "'"))
    arg_regex = r'^([A-Za-z_][\w_]*|".*"|{0}.*{0}|(\d+))$'.format(
        "'")
    for value in values:
        # must check against the kwarg regex first
        # because the arg regex matches everything!
        kwarg_match = regex_match(
            kwarg_regex, value)
        if kwarg_match:
            kwargs[kwarg_match.groups()[0]] = template.Variable(
                # convert to a template variable here
                kwarg_match.groups()[1])
        else:
            arg_match = regex_match(
                arg_regex, value)
            if arg_match:
                args.append(template.Variable(arg_match.groups()[0]))
            else:
                raise template.TemplateSyntaxError(
                    "Malformed arguments to the {0} tag.".format(
                        tag_name))

    return tag_name, macro_name, args, kwargs


@register.tag(name="use_macro")
def do_usemacro(parser, token):
    """ The function taking a parsed template tag
    and returning a UseMacroNode.
    """
    tag_name, macro_name, args, kwargs = parse_macro_params(token)
    try:
        macro = parser._macros[macro_name]
    except (AttributeError, KeyError):
        raise template.TemplateSyntaxError(
            "Macro '{0}' is not defined previously to the {1} tag".format(
                macro_name, tag_name))
    macro.parser = parser
    return UseMacroNode(macro, args, kwargs)


class MacroBlockNode(UseMacroNode):
    """ Template node object for the extended
    syntax macro useage.
    """

    def __init__(self, macro, nodelist, args, kwargs):
        self.nodelist = nodelist
        super(MacroBlockNode, self).__init__(macro, args, kwargs)


@register.tag(name="macro_block")
def do_macro_block(parser, token):
    """ Function taking parsed template tag
    to a MacroBlockNode.
    """
    tag_name, macro_name, args, kwargs = parse_macro_params(token)
    # could add extra validation on the macro_name tag
    # here, but probably don't need to since we're checking
    # if there's a macro by that name anyway.
    try:
        # see if the macro is in the context.
        macro = parser._macros[macro_name]
    except (AttributeError, KeyError):
        raise template.TemplateSyntaxError(
            "Macro '{0}' is not defined ".format(macro_name) +
            "previously to the {0} tag".format(tag_name))
    # get the arg and kwarg nodes from the nodelist
    nodelist = parser.parse(('endmacro_block',))
    parser.delete_first_token()

    # Loop through nodes, sorting into args/kwargs
    # (we could do this more semantically, but we loop
    # only once like this as an optimization).
    for node in nodelist:
        if isinstance(node, MacroArgNode) and not isinstance(node, MacroKwargNode):
            # note that MacroKwargNode is also a MacroArgNode (via inheritance),
            # so we must check against this.
            args.append(node)
        elif isinstance(node, MacroKwargNode):
            if node.keyword in macro.kwargs:
                # check that the keyword is defined as an argument for
                # the macro.
                if node.keyword not in kwargs:
                    # add the keyword argument to the dict
                    # if it's not in there
                    kwargs[node.keyword] = node
                else:
                    # raise a template syntax error if the
                    # keyword is already in the dict (thus a keyword
                    # argument was passed twice.
                    raise template.TemplateSyntaxError(
                        "{0} template tag was supplied "
                        "the same keyword argument multiple times.".format(
                            tag_name))
            else:
                raise template.TemplateSyntaxError(
                    "{0} template tag was supplied with a "
                    "keyword argument not defined by the {1} macro.".format(
                        tag_name, macro_name))
        # The following is a check that only whitespace is inside the macro_block tag,
        # but it's currently removed for reasons of backwards compatibility/potential 
        # uses people might have to put extra stuff in te macro_block tag.
        # elif not isinstance(node, template.TextNode) or node.s.strip() != "":
        #     # whitespace is allowed, anything else is not
        #     raise template.TemplateSyntaxError(
        #         "{0} template tag received an argument that "
        #         "is neither a arg or a kwarg tag. Make sure there's "
        #         "text or template tags directly descending "
        #         "from the {0} tag.".format(tag_name))

    # check that there aren't more arg tags than args
    # in the macro.
    if len(args) > len(macro.args):
        raise template.TemplateSyntaxError(
            "{0} template tag was supplied too many arg block tags.".format(
                tag_name))

    macro.parser = parser
    return MacroBlockNode(macro, nodelist, args, kwargs)


class MacroArgNode(template.Node):
    """ Template node object for defining a
    positional argument to a MacroBlockNode.
    """

    def __init__(self, nodelist):
        # save the tag's contents
        self.nodelist = nodelist

    def render(self, context):
        # macro_arg tags output nothing.
        return ''

    def resolve(self, context):
        # we have a "resolve" method similar with Variable,
        # so rendering code won't have to make any distinctions
        return self.nodelist.render(context)


@register.tag(name="macro_arg")
def do_macro_arg(parser, token):
    """ Function taking a parsed template tag
    to a MacroArgNode.
    """
    # macro_arg takes no arguments, so we don't
    # need to split the token/do validation.
    nodelist = parser.parse(('endmacro_arg',))
    parser.delete_first_token()
    # simply save the contents to a MacroArgNode.
    return MacroArgNode(nodelist)


class MacroKwargNode(MacroArgNode):
    """ Template node object for defining a
    keyword argument to a MacroBlockNode.
    """

    def __init__(self, keyword, nodelist):
        # save keyword so we know where to substitute it later.
        self.keyword = keyword
        super(MacroKwargNode, self).__init__(nodelist)

    def render(self, context):
        # macro_kwarg tags output nothing.
        return ''


@register.tag(name="macro_kwarg")
def do_macro_kwarg(parser, token):
    """ Function taking a parsed template tag
    to a MacroKwargNode.
    """
    try:
        tag_name, keyword = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "{0} tag requires exactly one argument, a keyword".format(
                token.contents.split()[0]))

    # add some validation of the keyword argument here.
    nodelist = parser.parse(('endmacro_kwarg',))
    parser.delete_first_token()
    return MacroKwargNode(keyword, nodelist)