import re, os, vim, string, random
from collections import deque, namedtuple

_Placeholder = namedtuple("_FrozenPlaceholder", ["current_text", "start", "end"])
_VisualContent = namedtuple("_VisualContent", ["mode", "text"])
_Position = namedtuple("_Position", ["line", "col"])


class _SnippetUtilCursor(object):
    def __init__(self, cursor):
        self._cursor = [cursor[0] - 1, cursor[1]]
        self._set = False

    def preserve(self):
        self._set = True
        self._cursor = [vim.buf.cursor[0], vim.buf.cursor[1]]

    def is_set(self):
        return self._set

    def set(self, line, column):
        self.__setitem__(0, line)
        self.__setitem__(1, column)

    def to_vim_cursor(self):
        return (self._cursor[0] + 1, self._cursor[1])

    def __getitem__(self, index):
        return self._cursor[index]

    def __setitem__(self, index, value):
        self._set = True
        self._cursor[index] = value

    def __len__(self):
        return 2

    def __str__(self):
        return str((self._cursor[0], self._cursor[1]))


class IndentUtil(object):

    """Utility class for dealing properly with indentation."""

    def __init__(self):
        self.reset()

    def reset(self):
        """Gets the spacing properties from Vim."""
        self.shiftwidth = int(
            vim.eval("exists('*shiftwidth') ? shiftwidth() : &shiftwidth")
        )
        self._expandtab = vim.eval("&expandtab") == "1"
        self._tabstop = int(vim.eval("&tabstop"))

    def ntabs_to_proper_indent(self, ntabs):
        """Convert 'ntabs' number of tabs to the proper indent prefix."""
        line_ind = ntabs * self.shiftwidth * " "
        line_ind = self.indent_to_spaces(line_ind)
        line_ind = self.spaces_to_indent(line_ind)
        return line_ind

    def indent_to_spaces(self, indent):
        """Converts indentation to spaces respecting Vim settings."""
        indent = indent.expandtabs(self._tabstop)
        right = (len(indent) - len(indent.rstrip(" "))) * " "
        indent = indent.replace(" ", "")
        indent = indent.replace("\t", " " * self._tabstop)
        return indent + right

    def spaces_to_indent(self, indent):
        """Converts spaces to proper indentation respecting Vim settings."""
        if not self._expandtab:
            indent = indent.replace(" " * self._tabstop, "\t")
        return indent


class SnippetUtil(object):

    """Provides easy access to indentation, etc.

    This is the 'snip' object in python code.

    """

    def __init__(self, _initial_indent, start, end, context):
        self._ind = IndentUtil()
        self._visual = _VisualContent(
            vim.eval("visualmode()"), vim.eval('get(g:,"coc_selected_text","")')
        )
        self._initial_indent = _initial_indent
        self._reset("")
        self._start = start
        self._end = end
        self._context = context

    def _reset(self, cur):
        """Gets the snippet ready for another update.

        :cur: the new value for c.

        """
        self._ind.reset()
        self._cur = cur
        self._rv = ""
        self._changed = False
        self.reset_indent()

    def shift(self, amount=1):
        """Shifts the indentation level. Note that this uses the shiftwidth
        because thats what code formatters use.

        :amount: the amount by which to shift.

        """
        self.indent += " " * self._ind.shiftwidth * amount

    def unshift(self, amount=1):
        """Unshift the indentation level. Note that this uses the shiftwidth
        because thats what code formatters use.

        :amount: the amount by which to unshift.

        """
        by = -self._ind.shiftwidth * amount
        try:
            self.indent = self.indent[:by]
        except IndexError:
            self.indent = ""

    def mkline(self, line="", indent=""):
        """Creates a properly set up line.

        :line: the text to add
        :indent: the indentation to have at the beginning
                 if None, it uses the default amount

        """
        return indent + line

    def reset_indent(self):
        """Clears the indentation."""
        self.indent = self._initial_indent

    # Utility methods
    @property
    def fn(self):  # pylint:disable=no-self-use,invalid-name
        """The filename."""
        return vim.eval('expand("%:t")') or ""

    @property
    def basename(self):  # pylint:disable=no-self-use
        """The filename without extension."""
        return vim.eval('expand("%:t:r")') or ""

    @property
    def ft(self):  # pylint:disable=invalid-name
        """The filetype."""
        return self.opt("&filetype", "")

    @property
    def rv(self):  # pylint:disable=invalid-name
        """The return value.

        The text to insert at the location of the placeholder.

        """
        return self._rv

    @rv.setter
    def rv(self, value):  # pylint:disable=invalid-name
        """See getter."""
        self._changed = True
        self._rv = value

    @property
    def _rv_changed(self):
        """True if rv has changed."""
        return self._changed

    @property
    def c(self):  # pylint:disable=invalid-name
        """The current text of the placeholder."""
        return ""

    @property
    def v(self):  # pylint:disable=invalid-name
        """Content of visual expansions."""
        return self._visual

    @property
    def p(self):
        if "coc_last_placeholder" in vim.vars:
            p = vim.vars["coc_last_placeholder"]
            start = _Position(p["start"]["line"], p["start"]["col"])
            end = _Position(p["end"]["line"], p["end"]["col"])
            return _Placeholder(p["current_text"], start, end)
        return None

    @property
    def context(self):
        return self._context

    def opt(self, option, default=None):  # pylint:disable=no-self-use
        """Gets a Vim variable."""
        if vim.eval("exists('%s')" % option) == "1":
            try:
                return vim.eval(option)
            except vim.error:
                pass
        return default

    def __add__(self, value):
        """Appends the given line to rv using mkline."""
        self.rv += "\n"  # pylint:disable=invalid-name
        self.rv += self.mkline(value)
        return self

    def __lshift__(self, other):
        """Same as unshift."""
        self.unshift(other)

    def __rshift__(self, other):
        """Same as shift."""
        self.shift(other)

    @property
    def snippet_start(self):
        """
        Returns start of the snippet in format (line, column).
        """
        return self._start

    @property
    def snippet_end(self):
        """
        Returns end of the snippet in format (line, column).
        """
        return self._end

    @property
    def buffer(self):
        return vim.buf


class ContextSnippet(object):
    def __init__(self):
        self.buffer = vim.current.buffer
        self.window = vim.current.window
        self.cursor = _SnippetUtilCursor(vim.current.window.cursor)
        self.line = vim.call("line", ".") - 1
        self.column = vim.call("col", ".") - 1
        line = vim.call("getline", ".")
        self.after = line[self.column :]
        if "coc_selected_text" in vim.vars:
            self.visual_mode = vim.eval("visualmode()")
            self.visual_text = vim.vars["coc_selected_text"]
        else:
            self.visual_mode = None
            self.visual_text = ""
        if "coc_last_placeholder" in vim.vars:
            p = vim.vars["coc_last_placeholder"]
            start = _Position(p["start"]["line"], p["start"]["col"])
            end = _Position(p["end"]["line"], p["end"]["col"])
            self.last_placeholder = _Placeholder(p["current_text"], start, end)
        else:
            self.last_placeholder = None
