# -*- coding: utf-8 -*-
""" Contains smart indent modes """
import re
from pyqode.core.api import TextHelper, get_block_symbol_data
from pyqode.qt.QtGui import QTextCursor
from pyqode.core.modes import AutoIndentMode, SymbolMatcherMode
from pyqode.core.modes.matcher import CLOSE, PAREN, SQUARE, BRACE, OPEN


class PyAutoIndentMode(AutoIndentMode):
    """ Automatically indents text, respecting the PEP8 conventions.

    Customised :class:`pyqode.core.modes.AutoIndentMode` for python
    that tries its best to follow the pep8 indentation guidelines.

    """
    def __init__(self):
        super(PyAutoIndentMode, self).__init__()
        self._helper = None

    def on_install(self, editor):
        super(PyAutoIndentMode, self).on_install(editor)
        self._helper = TextHelper(editor)

    def _get_indent(self, cursor):
        ln, column = self._helper.cursor_position()
        fullline = self._get_full_line(cursor).rstrip()
        line = fullline[:column]
        pre, post = AutoIndentMode._get_indent(self, cursor)
        if self._at_block_start(cursor, line):
            return pre, post
        # return pressed in comments
        c2 = QTextCursor(cursor)
        if c2.atBlockEnd():
            c2.movePosition(c2.Left)
        if (self._helper.is_comment_or_string(
                c2, formats=['comment', 'docstring']) or
                fullline.endswith(('"""', "'''"))):
            if line.strip().startswith("#") and column != len(fullline):
                post += '# '
            return pre, post
        # between parens
        elif self._between_paren(cursor, column):
            return self._handle_indent_between_paren(
                column, line, (pre, post), cursor)
        else:
            lastword = self._get_last_word(cursor)
            lastwordu = self._get_last_word_unstripped(cursor)
            end_with_op = fullline.endswith(
                ('+', '-', '*', '/', '=', ' and', ' or', '%'))
            in_string_def, char = self._is_in_string_def(fullline, column)
            if in_string_def:
                post, pre = self._handle_indent_inside_string(
                    char, cursor, fullline, post)
            elif (fullline.rstrip().endswith(":") and
                    lastword.rstrip().endswith(':') and
                    self._at_block_end(cursor, fullline)):
                post = self._handle_new_scope_indentation(
                    cursor, fullline)
            elif line.endswith("\\"):
                # if user typed \ and press enter -> indent is always
                # one level higher
                post += self.editor.tab_length * " "
            elif (fullline.endswith((')', '}', ']')) and
                    lastword.endswith((')', '}', ']'))):
                post = self._handle_indent_after_paren(cursor, post)
            elif (not fullline.endswith("\\") and
                    (end_with_op or
                     not self._at_block_end(cursor, fullline))):
                post, pre = self._handle_indent_in_statement(
                    fullline, lastwordu, post, pre)
            elif ((self._at_block_end(cursor, fullline) and
                    fullline.strip().startswith('return')) or
                    lastword == "pass"):
                post = post[:-self.editor.tab_length]
        return pre, post

    @staticmethod
    def _is_in_string_def(full_line, column):
        count = 0
        char = "'"
        for i in range(len(full_line)):
            if full_line[i] == "'" or full_line[i] == '"':
                count += 1
            if full_line[i] == '"' and i < column:
                char = '"'
        count_after_col = 0
        for i in range(column, len(full_line)):
            if full_line[i] == "'" or full_line[i] == '"':
                count_after_col += 1
        return count % 2 == 0 and count_after_col % 2 == 1, char

    @staticmethod
    def _is_paren_open(paren):
        return (paren.character == "(" or paren.character == "[" or
                paren.character == '{')

    @staticmethod
    def _is_paren_closed(paren):
        return (paren.character == ")" or paren.character == "]" or
                paren.character == '}')

    @staticmethod
    def _get_full_line(tc):
        tc2 = QTextCursor(tc)
        tc2.select(QTextCursor.LineUnderCursor)
        full_line = tc2.selectedText()
        return full_line

    def _parens_count_for_block(self, col, block):
        open_p = []
        closed_p = []
        lists = get_block_symbol_data(self.editor, block)
        for symbols in lists:
            for paren in symbols:
                if paren.position >= col:
                    continue
                if self._is_paren_open(paren):
                    if not col:
                        return -1, -1, [], []
                    open_p.append(paren)
                if self._is_paren_closed(paren):
                    closed_p.append(paren)
        return len(open_p), len(closed_p), open_p, closed_p

    def _between_paren(self, tc, col):
        try:
            self.editor.modes.get('SymbolMatcherMode')
        except KeyError:
            return False
        block = tc.block()
        nb_open = nb_closed = 0
        while block.isValid() and block.text().strip():
            o, c, _, _ = self._parens_count_for_block(col, block)
            nb_open += o
            nb_closed += c
            block = block.previous()
            col = len(block.text())
        return nb_open > nb_closed

    @staticmethod
    def _get_last_word(tc):
        tc2 = QTextCursor(tc)
        tc2.movePosition(QTextCursor.Left, tc.KeepAnchor, 1)
        tc2.movePosition(QTextCursor.WordLeft, tc.KeepAnchor)
        w = tc2.selectedText().strip()
        return w

    @staticmethod
    def _get_last_word_unstripped(tc):
        tc2 = QTextCursor(tc)
        tc2.movePosition(QTextCursor.Left, tc.KeepAnchor, 1)
        tc2.movePosition(QTextCursor.WordLeft, tc.KeepAnchor)
        return tc2.selectedText()

    def _get_indent_of_opening_paren(self, tc):
        tc.movePosition(tc.Left, tc.KeepAnchor)
        char = tc.selectedText()
        tc.movePosition(tc.Right, tc.MoveAnchor)
        mapping = {
            ')': (OPEN, PAREN),
            ']': (OPEN, SQUARE),
            '}': (OPEN, BRACE)
        }
        try:
            character, char_type = mapping[char]
        except KeyError:
            return None
        else:
            ol, oc = self.editor.modes.get(SymbolMatcherMode).symbol_pos(
                tc, character, char_type)
            line = self._helper.line_text(ol)
            return len(line) - len(line.lstrip())

    def _get_first_open_paren(self, tc, column):
        pos = None
        char = None
        ln = tc.blockNumber()
        tc_trav = QTextCursor(tc)
        mapping = {
            '(': (CLOSE, PAREN),
            '[': (CLOSE, SQUARE),
            '{': (CLOSE, BRACE)
        }
        while ln >= 0 and tc.block().text().strip():
            tc_trav.movePosition(tc_trav.StartOfLine, tc_trav.MoveAnchor)
            lists = get_block_symbol_data(self.editor, tc_trav.block())
            all_symbols = []
            for symbols in lists:
                all_symbols += [s for s in symbols]
            symbols = sorted(all_symbols, key=lambda x: x.position)
            for paren in reversed(symbols):
                if paren.position < column:
                    if self._is_paren_open(paren):
                        if paren.position > column:
                            continue
                        else:
                            pos = tc_trav.position() + paren.position
                            char = paren.character
                            # ensure it does not have a closing paren on
                            # the same line
                            tc3 = QTextCursor(tc)
                            tc3.setPosition(pos)
                            try:
                                ch, ch_type = mapping[paren.character]
                                l, c = self.editor.modes.get(
                                    SymbolMatcherMode).symbol_pos(
                                    tc3, ch, ch_type)
                            except KeyError:
                                continue
                            if l == ln and c < column:
                                continue
                            return pos, char
            # check previous line
            tc_trav.movePosition(tc_trav.Up, tc_trav.MoveAnchor)
            ln = tc_trav.blockNumber()
            column = len(self._helper.line_text(ln))
        return pos, char

    def _get_paren_pos(self, tc, column):
        pos, char = self._get_first_open_paren(tc, column)
        mapping = {'(': PAREN, '[': SQUARE, '{': BRACE}
        tc2 = QTextCursor(tc)
        tc2.setPosition(pos)
        import sys
        ol, oc = self.editor.modes.get(SymbolMatcherMode).symbol_pos(
            tc2, OPEN, mapping[char])
        cl, cc = self.editor.modes.get(SymbolMatcherMode).symbol_pos(
            tc2, CLOSE, mapping[char])
        return (ol, oc), (cl, cc)

    @staticmethod
    def _get_next_char(tc):
        tc2 = QTextCursor(tc)
        tc2.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor)
        char = tc2.selectedText()
        return char

    @staticmethod
    def _get_prev_char(tc):
        tc2 = QTextCursor(tc)
        tc2.movePosition(QTextCursor.PreviousCharacter, QTextCursor.KeepAnchor)
        char = tc2.selectedText()
        while char == ' ':
            tc2.movePosition(tc2.PreviousCharacter, tc2.KeepAnchor)
            char = tc2.selectedText()
        return char.strip()

    def _handle_indent_between_paren(self, column, line, parent_impl, tc):
        """
        Handle indent between symbols such as parenthesis, braces,...
        """
        pre, post = parent_impl
        next_char = self._get_next_char(tc)
        prev_char = self._get_prev_char(tc)
        prev_open = prev_char in ['[', '(', '{']
        next_close = next_char in [']', ')', '}']
        (open_line, open_symbol_col), (close_line, close_col) = \
            self._get_paren_pos(tc, column)
        open_line_txt = self._helper.line_text(open_line)
        open_line_indent = len(open_line_txt) - len(open_line_txt.lstrip())
        if prev_open:
            post = (open_line_indent + self.editor.tab_length) * ' '
        elif next_close and prev_char != ',':
            post = open_line_indent * ' '
        elif tc.block().blockNumber() == open_line:
            post = open_symbol_col * ' '

        # adapt indent if cursor on closing line and next line have same
        # indent -> PEP8 compliance
        if close_line and close_col:
            txt = self._helper.line_text(close_line)
            bn = tc.block().blockNumber()
            flg = bn == close_line
            next_indent = self._helper.line_indent(bn + 1) * ' '
            if flg and txt.strip().endswith(':') and next_indent == post:
                # | look at how the previous line ( ``':'):`` ) was
                # over-indented, this is actually what we are trying to
                # achieve here
                post += self.editor.tab_length * ' '

        # breaking string
        if next_char in ['"', "'"]:
            tc.movePosition(tc.Left)
        is_string = self._helper.is_comment_or_string(tc, formats=['string'])
        if next_char in ['"', "'"]:
            tc.movePosition(tc.Right)
        if is_string:
            trav = QTextCursor(tc)
            while self._helper.is_comment_or_string(
                    trav, formats=['string']):
                trav.movePosition(trav.Left)
            trav.movePosition(trav.Right)
            symbol = '%s' % self._get_next_char(trav)
            pre += symbol
            post += symbol

        return pre, post

    @staticmethod
    def _at_block_start(tc, line):
        """
        Improve QTextCursor.atBlockStart to ignore spaces
        """
        if tc.atBlockStart():
            return True
        column = tc.columnNumber()
        indentation = len(line) - len(line.lstrip())
        return column <= indentation

    @staticmethod
    def _at_block_end(tc, fullline):
        if tc.atBlockEnd():
            return True
        column = tc.columnNumber()
        return column >= len(fullline.rstrip()) - 1

    def _handle_indent_inside_string(self, char, cursor, fullline, post):
        # break string with a '\' at the end of the original line, always
        # breaking strings enclosed by parens is done in the
        # _handle_between_paren method
        n = self.editor.tab_length
        pre = '%s \\' % char
        post += n * ' '
        if fullline.endswith(':'):
            post += n * " "
        post += char
        return post, pre

    def _handle_new_scope_indentation(self, cursor, fullline):
        try:
            indent = (self._get_indent_of_opening_paren(cursor) +
                      self.editor.tab_length)
            post = indent * " "
        except TypeError:
            # e.g indent is None (meaning the line does not ends with ):, ]:
            # or }:
            kw = ["if", "class", "def", "while", "for", "else", "elif",
                  "except", "finally", "try", "with"]
            l = fullline
            ln = cursor.blockNumber()

            def check_kw_in_line(kwds, lparam):
                for kwd in kwds:
                    if kwd in lparam:
                        return True
                return False

            while not check_kw_in_line(kw, l) and ln:
                ln -= 1
                l = self._helper.line_text(ln)
            indent = (len(l) - len(l.lstrip())) * " "
            indent += self.editor.tab_length * " "
            post = indent
        return post

    def _handle_indent_after_paren(self, cursor, post):
        indent = self._get_indent_of_opening_paren(cursor)
        if indent is not None:
            post = indent * " "
        return post

    def _handle_indent_in_statement(self, fullline, lastword, post, pre):
        if lastword[-1] != ':':
            if lastword and lastword[-1] != " ":
                pre += " \\"
            else:
                pre += '\\'
        post += self.editor.tab_length * " "
        if fullline.endswith(':'):
            post += self.editor.tab_length * " "
        return post, pre
