# coding=utf8

import vim
from UltiSnips import vim_helper
from UltiSnips.diff import diff
from UltiSnips.error import PebkacError
from UltiSnips.position import Position

from contextlib import contextmanager


@contextmanager
def use_proxy_buffer(snippets_stack, vstate):
    """
    Forward all changes made in the buffer to the current snippet stack while
    function call.
    """
    buffer_proxy = VimBufferProxy(snippets_stack, vstate)
    old_buffer = vim_helper.buf
    try:
        vim_helper.buf = buffer_proxy
        yield
    finally:
        vim_helper.buf = old_buffer
    buffer_proxy.validate_buffer()


@contextmanager
def suspend_proxy_edits():
    """
    Prevents changes being applied to the snippet stack while function call.
    """
    if not isinstance(vim_helper.buf, VimBufferProxy):
        yield
    else:
        try:
            vim_helper.buf._disable_edits()
            yield
        finally:
            vim_helper.buf._enable_edits()


class VimBufferProxy(vim_helper.VimBuffer):
    """
    Proxy object used for tracking changes that made from snippet actions.

    Unfortunately, vim by itself lacks of the API for changing text in
    trackable maner.

    Vim marks offers limited functionality for tracking line additions and
    deletions, but nothing offered for tracking changes withing single line.

    Instance of this class is passed to all snippet actions and behaves as
    internal vim.current.window.buffer.

    All changes that are made by user passed to diff algorithm, and resulting
    diff applied to internal snippet structures to ensure they are in sync with
    actual buffer contents.
    """

    def __init__(self, snippets_stack, vstate):
        """
        Instantiate new object.

        snippets_stack is a slice of currently active snippets.
        """
        self._snippets_stack = snippets_stack
        self._buffer = vim.current.buffer
        self._change_tick = int(vim.eval("b:changedtick"))
        self._forward_edits = True
        self._vstate = vstate

    def is_buffer_changed_outside(self):
        """
        Returns true, if buffer was changed without using proxy object, like
        with vim.command() or through internal vim.current.window.buffer.
        """
        return self._change_tick < int(vim.eval("b:changedtick"))

    def validate_buffer(self):
        """
        Raises exception if buffer is changes beyound proxy object.
        """
        if self.is_buffer_changed_outside():
            raise PebkacError(
                "buffer was modified using vim.command or "
                + "vim.current.buffer; that changes are untrackable and leads to "
                + "errors in snippet expansion; use special variable `snip.buffer` "
                "for buffer modifications.\n\n"
                + "See :help UltiSnips-buffer-proxy for more info."
            )

    def __setitem__(self, key, value):
        """
        Behaves as vim.current.window.buffer.__setitem__ except it tracks
        changes and applies them to the current snippet stack.
        """
        if isinstance(key, slice):
            value = [line for line in value]
            changes = list(self._get_diff(key.start, key.stop, value))
            self._buffer[key.start : key.stop] = [line.strip("\n") for line in value]
        else:
            value = value
            changes = list(self._get_line_diff(key, self._buffer[key], value))
            self._buffer[key] = value

        self._change_tick += 1

        if self._forward_edits:
            for change in changes:
                self._apply_change(change)
            if self._snippets_stack:
                self._vstate.remember_buffer(self._snippets_stack[0])

    def __setslice__(self, i, j, text):
        """
        Same as __setitem__.
        """
        self.__setitem__(slice(i, j), text)

    def __getitem__(self, key):
        """
        Just passing call to the vim.current.window.buffer.__getitem__.
        """
        return self._buffer[key]

    def __getslice__(self, i, j):
        """
        Same as __getitem__.
        """
        return self.__getitem__(slice(i, j))

    def __len__(self):
        """
        Same as len(vim.current.window.buffer).
        """
        return len(self._buffer)

    def append(self, line, line_number=-1):
        """
        Same as vim.current.window.buffer.append(), but with tracking changes.
        """
        if line_number < 0:
            line_number = len(self)
        if not isinstance(line, list):
            line = [line]
        self[line_number:line_number] = [l for l in line]

    def __delitem__(self, key):
        if isinstance(key, slice):
            self.__setitem__(key, [])
        else:
            self.__setitem__(slice(key, key + 1), [])

    def _get_diff(self, start, end, new_value):
        """
        Very fast diffing algorithm when changes are across many lines.
        """
        for line_number in range(start, end):
            if line_number < 0:
                line_number = len(self._buffer) + line_number
            yield ("D", line_number, 0, self._buffer[line_number], True)

        if start < 0:
            start = len(self._buffer) + start
        for line_number in range(0, len(new_value)):
            yield ("I", start + line_number, 0, new_value[line_number], True)

    def _get_line_diff(self, line_number, before, after):
        """
        Use precise diffing for tracking changes in single line.
        """
        if before == "":
            for change in self._get_diff(line_number, line_number + 1, [after]):
                yield change
        else:
            for change in diff(before, after):
                yield (change[0], line_number, change[2], change[3])

    def _apply_change(self, change):
        """
        Apply changeset to current snippets stack, correctly moving around
        snippet itself or its child.
        """
        if not self._snippets_stack:
            return

        change_type, line_number, column_number, change_text = change[0:4]

        line_before = line_number <= self._snippets_stack[0]._start.line
        column_before = column_number <= self._snippets_stack[0]._start.col
        if line_before and column_before:
            direction = 1
            if change_type == "D":
                direction = -1

            diff = Position(direction, 0)
            if len(change) != 5:
                diff = Position(0, direction * len(change_text))

            self._snippets_stack[0]._move(Position(line_number, column_number), diff)
        else:
            if line_number > self._snippets_stack[0]._end.line:
                return
            if column_number >= self._snippets_stack[0]._end.col:
                return
            self._snippets_stack[0]._do_edit(change[0:4])

    def _disable_edits(self):
        """
        Temporary disable applying changes to snippets stack. Should be done
        while expanding anonymous snippet in the middle of jump to prevent
        double tracking.
        """
        self._forward_edits = False

    def _enable_edits(self):
        """
        Enables changes forwarding back.
        """
        self._forward_edits = True
