#!/usr/bin/env python
# encoding: utf-8

"""Snippet representation after parsing."""

import re

from UltiSnips.compatibility import as_unicode
from UltiSnips.text_objects import SnippetInstance
from UltiSnips.indent_util import IndentUtil
import UltiSnips._vim as _vim


def _words_for_line(trigger, before, num_words=None):
    """ Gets the final 'num_words' words from 'before'.
    If num_words is None, then use the number of words in
    'trigger'.
    """
    if not len(before):
        return ''

    if num_words is None:
        num_words = len(trigger.split())

    word_list = before.split()
    if len(word_list) <= num_words:
        return before.strip()
    else:
        before_words = before
        for i in range(-1, -(num_words + 1), -1):
            left = before_words.rfind(word_list[i])
            before_words = before_words[:left]
        return before[len(before_words):].strip()


class SnippetDefinition(object):
    """Represents a snippet as parsed from a file."""

    _INDENT = re.compile(r"^[ \t]*")
    _TABS = re.compile(r"^\t*")

    def __init__(self, priority, trigger, value, description, options, globals):
        self._priority = priority
        self._trigger = as_unicode(trigger)
        self._value = as_unicode(value)
        self._description = as_unicode(description)
        self._opts = options
        self._matched = ""
        self._last_re = None
        self._globals = globals

    def __repr__(self):
        return "SnippetDefinition(%r,%s,%s,%s)" % (
                self._priority, self._trigger, self._description, self._opts)

    def _re_match(self, trigger):
        """ Test if a the current regex trigger matches
        `trigger`. If so, set _last_re and _matched.
        """
        for match in re.finditer(self._trigger, trigger):
            if match.end() != len(trigger):
                continue
            else:
                self._matched = trigger[match.start():match.end()]

            self._last_re = match
            return match
        return False

    def has_option(self, opt):
        """ Check if the named option is set """
        return opt in self._opts

    def matches(self, trigger):
        """Returns True if this snippet matches 'trigger'."""
        # If user supplies both "w" and "i", it should perhaps be an
        # error, but if permitted it seems that "w" should take precedence
        # (since matching at word boundary and within a word == matching at word
        # boundary).
        self._matched = ""

        # Don't expand on whitespace
        if trigger and trigger.rstrip() != trigger:
            return False

        words = _words_for_line(self._trigger, trigger)

        if "r" in self._opts:
            match = self._re_match(trigger)
        elif "w" in self._opts:
            words_len = len(self._trigger)
            words_prefix = words[:-words_len]
            words_suffix = words[-words_len:]
            match = (words_suffix == self._trigger)
            if match and words_prefix:
                # Require a word boundary between prefix and suffix.
                boundary_chars = words_prefix[-1:] + words_suffix[:1]
                boundary_chars = boundary_chars.replace('"', '\\"')
                match = _vim.eval('"%s" =~# "\\\\v.<."' % boundary_chars) != '0'
        elif "i" in self._opts:
            match = words.endswith(self._trigger)
        else:
            match = (words == self._trigger)

        # By default, we match the whole trigger
        if match and not self._matched:
            self._matched = self._trigger

        # Ensure the match was on a word boundry if needed
        if "b" in self._opts and match:
            text_before = trigger.rstrip()[:-len(self._matched)]
            if text_before.strip(" \t") != '':
                self._matched = ""
                return False
        return match

    def could_match(self, trigger):
        """Return True if this snippet could match the (partial) 'trigger'."""
        self._matched = ""

        # List all on whitespace.
        if trigger and trigger[-1] in (" ", "\t"):
            trigger = ""
        if trigger and trigger.rstrip() is not trigger:
            return False

        words = _words_for_line(self._trigger, trigger)

        if "r" in self._opts:
            # Test for full match only
            match = self._re_match(trigger)
        elif "w" in self._opts:
            # Trim non-empty prefix up to word boundary, if present.
            qwords = words.replace('"', '\\"')
            words_suffix = _vim.eval(
                    'substitute("%s", "\\\\v^.+<(.+)", "\\\\1", "")' % qwords)
            match = self._trigger.startswith(words_suffix)
            self._matched = words_suffix

            # TODO: list_snippets() function cannot handle partial-trigger
            # matches yet, so for now fail if we trimmed the prefix.
            if words_suffix != words:
                match = False
        elif "i" in self._opts:
            # TODO: It is hard to define when a inword snippet could match,
            # therefore we check only for full-word trigger.
            match = self._trigger.startswith(words)
        else:
            match = self._trigger.startswith(words)

        # By default, we match the words from the trigger
        if match and not self._matched:
            self._matched = words

        # Ensure the match was on a word boundry if needed
        if "b" in self._opts and match:
            text_before = trigger.rstrip()[:-len(self._matched)]
            if text_before.strip(" \t") != '':
                self._matched = ""
                return False

        return match

    @property
    def description(self):
        """Descriptive text for this snippet."""
        return ("(%s) %s" % (self._trigger, self._description)).strip()

    @property
    def priority(self):
        """The snippets priority, which defines which snippet will be preferred
        over others with the same trigger."""
        return self._priority

    @property
    def trigger(self):
        """The trigger text for the snippet."""
        return self._trigger

    @property
    def matched(self):
        """The last text that matched this snippet in match() or
        could_match()."""
        return self._matched

    def launch(self, text_before, visual_content, parent, start, end):
        """Launch this snippet, overwriting the text 'start' to 'end' and
        keeping the 'text_before' on the launch line. 'Parent' is the parent
        snippet instance if any."""
        indent = self._INDENT.match(text_before).group(0)
        lines = (self._value + "\n").splitlines()
        ind_util = IndentUtil()

        # Replace leading tabs in the snippet definition via proper indenting
        snippet_definition = []
        for line_num, line in enumerate(lines):
            if "t" in self._opts:
                tabs = 0
            else:
                tabs = len(self._TABS.match(line).group(0))

            line_ind = ind_util.ntabs_to_proper_indent(tabs)

            if line_num != 0:
                line_ind = indent + line_ind

            snippet_definition.append(line_ind + line[tabs:])
        snippet_definition = '\n'.join(snippet_definition)

        si = SnippetInstance(self, parent, indent, snippet_definition, start,
                end, visual_content, last_re=self._last_re,
                globals=self._globals)

        return si