# coding: utf-8

"""
Exposes a parse() function to parse template strings.

"""

import bisect
import re

from pystache import defaults
from pystache.common import FormatterNotFoundError, ParsingError
from pystache.parsed import ParsedTemplate


END_OF_LINE_CHARACTERS = ['\r', '\n']
NON_BLANK_RE = re.compile(r'^(.)', re.M)


class Location(object):
    def __init__(self, name, line, col):
        self.name = name
        self.line = line
        self.col = col

    def __str__(self):
        return "{}:{}.{}".format(self.name, self.line, self.col)


class Locator(object):
    def __init__(self, data, name):
        self.line_offsets = [0]
        self.name = name
        for line_len in map(len, data.splitlines(True)):
            self.line_offsets.append(self.line_offsets[-1] + line_len)

    def locate(self, offset):
        line = bisect.bisect_right(self.line_offsets, offset)
        col = offset - self.line_offsets[line - 1]
        return Location(self.name, line, col)


# TODO: add some unit tests for this.
# TODO: add a test case that checks for spurious spaces.
# TODO: add test cases for delimiters.
def parse(template, delimiters=None, name='<string>'):
    """
    Parse a template string and return a ParsedTemplate instance.

    Arguments:

      template: a template string.

      delimiters: a 2-tuple of delimiters.  Defaults to the package default.

    Examples:

    >>> parsed = parse(u"Hey {{#who}}{{name}}!{{/who}}")
    >>> print(str(parsed).replace('u', ''))  # This is a hack to get the test to pass both in Python 2 and 3.
    ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_InterpolateNode(key='name'), '!'])]

    """
    if type(template) is not str:
        raise Exception("Template is not str: %s" % type(template))
    parser = _Parser(delimiters)
    return parser.parse(template, name)


def _compile_template_re(delimiters):
    """
    Return a regular expresssion object (re.RegexObject) instance.

    """
    # The possible tag type characters following the opening tag,
    # excluding "=" and "{".
    tag_types = "!>&/#^"

    # TODO: are we following this in the spec?
    #
    #   The tag's content MUST be a non-whitespace character sequence
    #   NOT containing the current closing delimiter.
    #
    tag = r"""
        (?P<whitespace>[\ \t]*)
        %(otag)s \s*
        (?:
          (?P<change>=) \s* (?P<delims>.+?)   \s* = |
          (?P<raw>{)    \s* (?P<raw_name>.+?) \s* } |
          (?P<tag>[%(tag_types)s]?)  \s* (?P<tag_key>[\s\S]+?)
        )
        \s* %(ctag)s
    """ % {'tag_types': tag_types, 'otag': re.escape(delimiters[0]), 'ctag': re.escape(delimiters[1])}

    return re.compile(tag, re.VERBOSE)


## Node types

def _format(obj, exclude=None):
    if exclude is None:
        exclude = []
    exclude.append('key')
    attrs = obj.__dict__
    names = list(set(attrs.keys()) - set(exclude))
    names.sort()
    names.insert(0, 'key')
    args = ["%s=%s" % (name, repr(attrs[name])) for name in names]
    return "%s(%s)" % (obj.__class__.__name__, ", ".join(args))


class _CommentNode(object):

    def __repr__(self):
        return _format(self)

    def render(self, engine, context):
        return ''


class _ChangeNode(object):

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

    def __repr__(self):
        return _format(self)

    def render(self, engine, context):
        return ''


class _InterpolateNode(object):

    def __init__(self, key, formatter, location):
        self.key = key
        self.formatter = formatter
        self.location = location

    def __repr__(self):
        return _format(self, exclude=['formatter', 'location'])

    def render(self, engine, context):
        val = engine.fetch_value(context, self.key, self.location)
        return engine.interpolate(val, self.formatter, self.location)


class _PartialNode(object):

    def __init__(self, key, indent, location):
        self.key = key
        self.indent = indent
        self.location = location

    def __repr__(self):
        return _format(self)

    def render(self, engine, context):
        template = engine.resolve_partial(self.key, self.location)
        # Indent before rendering.
        template = re.sub(NON_BLANK_RE, self.indent + r'\1', template)

        return engine.render(template, context)


class _InvertedNode(object):

    def __init__(self, key, parsed_section, location):
        self.key = key
        self.parsed_section = parsed_section
        self.location = location

    def __repr__(self):
        return _format(self)

    def render(self, engine, context):
        # TODO: is there a bug because we are not using the same
        #   logic as in fetch_string()?
        data = engine.resolve_context(context, self.key, self.location)
        # Note that lambdas are considered truthy for inverted sections
        # per the spec.
        if data:
            return ''
        return self.parsed_section.render(engine, context)


class _SectionNode(object):

    # TODO: the template_ and parsed_template_ arguments don't both seem
    # to be necessary.  Can we remove one of them?  For example, if
    # callable(data) is True, then the initial parsed_template isn't used.
    def __init__(self, key, parsed, delimiters, template, index_begin, index_end, location):
        self.delimiters = delimiters
        self.key = key
        self.parsed = parsed
        self.template = template
        self.index_begin = index_begin
        self.index_end = index_end
        self.location = location

    def __repr__(self):
        return _format(self, exclude=['delimiters', 'template', 'location'])

    def render(self, engine, context):
        data = engine.fetch_section_data(context, self.key, self.location)

        parts = []
        for val in data:
            if hasattr(val, '__call__'):
                # Lambdas special case section rendering and bypass pushing
                # the data value onto the context stack.  From the spec--
                #
                #   When used as the data value for a Section tag, the
                #   lambda MUST be treatable as an arity 1 function, and
                #   invoked as such (passing a String containing the
                #   unprocessed section contents).  The returned value
                #   MUST be rendered against the current delimiters, then
                #   interpolated in place of the section.
                #
                #  Also see--
                #
                #   https://github.com/defunkt/pystache/issues/113
                #
                # TODO: should we check the arity?
                val = val(self.template[self.index_begin:self.index_end])
                val = engine._render_value(val, context, delimiters=self.delimiters,
                                           location=self.location)
                parts.append(val)
                continue

            context.push(val)
            parts.append(self.parsed.render(engine, context))
            context.pop()

        return str(''.join(parts))


class _Parser(object):

    _delimiters = None
    _template_re = None

    def __init__(self, delimiters=None):
        if delimiters is None:
            delimiters = defaults.DELIMITERS

        self._delimiters = delimiters

    def _compile_delimiters(self):
        self._template_re = _compile_template_re(self._delimiters)

    def _change_delimiters(self, delimiters):
        self._delimiters = delimiters
        self._compile_delimiters()

    def parse(self, template, name):
        """Parse a template string starting at some index.

        Name is used to provide detailed information about the name of the
        template which is used in debug messages.

        This method uses the current tag delimiter.

        Arguments:

          template: a string that is the template to parse.

          index: the index at which to start parsing.

        Returns:

          a ParsedTemplate instance.

        """
        self._compile_delimiters()

        start_index = 0
        content_end_index, parsed_section, section_key = None, None, None
        parsed_template = ParsedTemplate()

        states = []

        locator = Locator(template, name)

        while True:
            match = self._template_re.search(template, start_index)

            if match is None:
                break

            match_index = match.start()
            end_index = match.end()

            matches = match.groupdict()

            # Normalize the matches dictionary.
            if matches['change'] is not None:
                matches.update(tag='=', tag_key=matches['delims'])
            elif matches['raw'] is not None:
                matches.update(tag='&', tag_key=matches['raw_name'])

            tag_type = matches['tag']
            tag_key = matches['tag_key']
            leading_whitespace = matches['whitespace']

            # Standalone (non-interpolation) tags consume the entire line,
            # both leading whitespace and trailing newline.
            did_tag_begin_line = match_index == 0 or template[match_index - 1] in END_OF_LINE_CHARACTERS
            did_tag_end_line = end_index == len(template) or template[end_index] in END_OF_LINE_CHARACTERS
            is_tag_interpolating = tag_type in ['', '&']

            if did_tag_begin_line and did_tag_end_line and not is_tag_interpolating:
                if end_index < len(template):
                    end_index += template[end_index] == '\r' and 1 or 0
                if end_index < len(template):
                    end_index += template[end_index] == '\n' and 1 or 0
            elif leading_whitespace:
                match_index += len(leading_whitespace)
                leading_whitespace = ''

            # Do this after match_index has been updated to take
            # whitespace into account
            location = locator.locate(match_index)

            # Avoid adding spurious empty strings to the parse tree.
            if start_index != match_index:
                parsed_template.add(template[start_index:match_index])

            start_index = end_index

            if tag_type in ('#', '^'):
                # Cache current state.
                state = (tag_type, end_index, section_key, parsed_template)
                states.append(state)

                # Initialize new state
                section_key, parsed_template = tag_key, ParsedTemplate()
                continue

            if tag_type == '/':
                if tag_key != section_key:
                    msg = "Section end tag mismatch: {} != {}".format(tag_key, section_key)
                    raise ParsingError(msg, location)

                # Restore previous state with newly found section data.
                parsed_section = parsed_template

                (tag_type, section_start_index, section_key, parsed_template) = states.pop()
                node = self._make_section_node(template, tag_type, tag_key, parsed_section,
                                               section_start_index, match_index, location)

            else:
                node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace, location)

            parsed_template.add(node)

        # Avoid adding spurious empty strings to the parse tree.
        if start_index != len(template):
            parsed_template.add(template[start_index:])

        return parsed_template

    def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace, location):
        """
        Create and return a non-section node for the parse tree.

        """
        # TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
        if tag_type == '!':
            return _CommentNode()

        if tag_type == '=':
            delimiters = tag_key.split()
            self._change_delimiters(delimiters)
            return _ChangeNode(delimiters)

        if tag_type == '':
            key_formatter = tag_key.rsplit(defaults.FORMAT_DELIMITER, 1)
            tag_key, formatter = (key_formatter[0], '') if len(key_formatter) == 1 else key_formatter
            return _InterpolateNode(tag_key, formatter, location)

        if tag_type == '&':
            return _InterpolateNode(tag_key, 'literal', location)

        if tag_type == '>':
            return _PartialNode(tag_key, leading_whitespace, location)

        raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type))

    def _make_section_node(self, template, tag_type, tag_key, parsed_section,
                           section_start_index, section_end_index, location):
        """
        Create and return a section node for the parse tree.

        """
        if tag_type == '#':
            return _SectionNode(tag_key, parsed_section, self._delimiters,
                               template, section_start_index, section_end_index, location)

        if tag_type == '^':
            return _InvertedNode(tag_key, parsed_section, location)

        raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
