"""
This module contains a native python syntax highlighter, strongly inspired from
spyderlib.widgets.source_code.syntax_higlighter.PythonSH but modified to
highlight docstrings with a different color than the string color and to
highlight decorators and self parameters.

It is approximately 3 time faster then :class:`pyqode.core.modes.PygmentsSH`.

"""
import builtins
import re
from pyqode.core.api import SyntaxHighlighter as BaseSH
from pyqode.core.api import TextBlockHelper


def any(name, alternates):
    """Return a named group pattern matching list of alternates."""
    return "(?P<%s>" % name + "|".join(alternates) + ")"


kwlist = [
    'self',
    'False',
    'None',
    'True',
    'assert',
    'break',
    'class',
    'continue',
    'def',
    'del',
    'elif',
    'else',
    'except',
    'finally',
    'for',
    'global',
    'if',
    'lambda',
    'nonlocal',
    'pass',
    'raise',
    'return',
    'try',
    'while',
    'with',
    'yield',
]

kw_namespace_list = ['from', 'import', 'as']
wordop_list = ['and', 'or', 'not', 'in', 'is']


def make_python_patterns(additional_keywords=[], additional_builtins=[]):
    """Strongly inspired from idlelib.ColorDelegator.make_pat"""
    kw = r"\b" + any("keyword", kwlist + additional_keywords) + r"\b"
    kw_namespace = r"\b" + any("namespace", kw_namespace_list) + r"\b"
    word_operators = r"\b" + any("operator_word", wordop_list) + r"\b"
    builtinlist = [str(name) for name in dir(builtins)
                   if not name.startswith('_')] + additional_builtins
    for v in ['None', 'True', 'False']:
        builtinlist.remove(v)
    builtin = r"([^.'\"\\#]\b|^)" + any("builtin", builtinlist) + r"\b"
    builtin_fct = any("builtin_fct", [r'_{2}[a-zA-Z_]*_{2}'])
    comment = any("comment", [r"#[^\n]*"])
    instance = any("instance", [r"\bself\b", r"\bcls\b"])
    decorator = any('decorator', [r'@\w*', r'.setter'])
    number = any("number",
                 [r"\b[+-]?[0-9]+[lLjJ]?\b",
                  r"\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b",
                  r"\b[+-]?0[oO][0-7]+[lL]?\b",
                  r"\b[+-]?0[bB][01]+[lL]?\b",
                  r"\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?[jJ]?\b"])
    sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*'?"
    dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*"?'
    uf_sqstring = r"(\b[rRuU])?'[^'\\\n]*(\\.[^'\\\n]*)*(\\)$(?!')$"
    uf_dqstring = r'(\b[rRuU])?"[^"\\\n]*(\\.[^"\\\n]*)*(\\)$(?!")$'
    sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
    dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
    uf_sq3string = r"(\b[rRuU])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(\\)?(?!''')$"
    uf_dq3string = r'(\b[rRuU])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(\\)?(?!""")$'
    string = any("string", [sq3string, dq3string, sqstring, dqstring])
    ufstring1 = any("uf_sqstring", [uf_sqstring])
    ufstring2 = any("uf_dqstring", [uf_dqstring])
    ufstring3 = any("uf_sq3string", [uf_sq3string])
    ufstring4 = any("uf_dq3string", [uf_dq3string])
    return "|".join([instance, decorator, kw, kw_namespace, builtin,
                     word_operators, builtin_fct, comment,
                     ufstring1, ufstring2, ufstring3, ufstring4, string,
                     number, any("SYNC", [r"\n"])])


#
# Pygments Syntax highlighter
#
class PythonSH(BaseSH):
    """
    Highlights python syntax in the editor.
    """
    mimetype = 'text/x-python'

    # Syntax highlighting rules:
    PROG = re.compile(make_python_patterns(), re.S)
    IDPROG = re.compile(r"\s+(\w+)", re.S)
    ASPROG = re.compile(r".*?\b(as)\b")
    # Syntax highlighting states (from one text block to another):
    (NORMAL, INSIDE_SQ3STRING, INSIDE_DQ3STRING,
     INSIDE_SQSTRING, INSIDE_DQSTRING) = list(range(5))

    # Comments suitable for Outline Explorer
    OECOMMENT = re.compile('^(# ?--[-]+|##[#]+ )[ -]*[^- ]+')

    def __init__(self, parent, color_scheme=None):
        super(PythonSH, self).__init__(parent, color_scheme)
        self.import_statements = []
        self.global_import_statements = []
        self.docstrings = []

    def highlight_block(self, text, block):
        prev_block = block.previous()
        prev_state = TextBlockHelper.get_state(prev_block)
        if prev_state == self.INSIDE_DQ3STRING:
            offset = -4
            text = r'""" ' + text
        elif prev_state == self.INSIDE_SQ3STRING:
            offset = -4
            text = r"''' " + text
        elif prev_state == self.INSIDE_DQSTRING:
            offset = -2
            text = r'" ' + text
        elif prev_state == self.INSIDE_SQSTRING:
            offset = -2
            text = r"' " + text
        else:
            offset = 0

        import_stmt = None
        # set docstring dynamic attribute, used by the fold detector.
        block.docstring = False

        self.setFormat(0, len(text), self.formats["normal"])

        state = self.NORMAL
        match = self.PROG.search(text)
        while match:
            for key, value in list(match.groupdict().items()):
                if value:
                    start, end = match.span(key)
                    start = max([0, start + offset])
                    end = max([0, end + offset])
                    if key == "uf_sq3string":
                        self.setFormat(start, end - start,
                                       self.formats["docstring"])
                        block.docstring = True
                        state = self.INSIDE_SQ3STRING
                    elif key == "uf_dq3string":
                        self.setFormat(start, end - start,
                                       self.formats["docstring"])
                        block.docstring = True
                        state = self.INSIDE_DQ3STRING
                    elif key == "uf_sqstring":
                        self.setFormat(start, end - start,
                                       self.formats["string"])
                        state = self.INSIDE_SQSTRING
                    elif key == "uf_dqstring":
                        self.setFormat(start, end - start,
                                       self.formats["string"])
                        state = self.INSIDE_DQSTRING
                    elif key == 'builtin_fct':
                        # trick to highlight __init__, __add__ and so on with
                        # builtin color
                        self.setFormat(start, end - start,
                                       self.formats["constant"])
                    else:
                        if ('"""' in value or "'''" in value) and \
                                key != 'comment':
                            # highlight docstring with a different color
                            block.docstring = True
                            self.setFormat(start, end - start,
                                           self.formats["docstring"])
                        elif key == 'decorator':
                            # highlight decorators
                            self.setFormat(start, end - start,
                                           self.formats["decorator"])
                        elif value in ['self', 'cls']:
                            # highlight self attribute
                            self.setFormat(start, end - start,
                                           self.formats["self"])
                        else:
                            # highlight all other tokens
                            self.setFormat(start, end - start,
                                           self.formats[key])
                        if key == "keyword":
                            if value in ("def", "class"):
                                match1 = self.IDPROG.match(text, end)
                                if match1:
                                    start1, end1 = match1.span(1)
                                    fmt_key = ('definition' if value == 'class'
                                               else 'function')
                                    fmt = self.formats[fmt_key]
                                    self.setFormat(start1, end1 - start1, fmt)
                        if key == 'namespace':
                            import_stmt = text.strip()
                            # color all the "as" words on same line, except
                            # if in a comment; cheap approximation to the
                            # truth
                            if '#' in text:
                                endpos = text.index('#')
                            else:
                                endpos = len(text)
                            while True:
                                match1 = self.ASPROG.match(text, end,
                                                           endpos)
                                if not match1:
                                    break
                                start, end = match1.span(1)
                                self.setFormat(start, end - start,
                                               self.formats["namespace"])
            # next match
            match = self.PROG.search(text, match.end())
        TextBlockHelper.set_state(block, state)

        # update import zone
        if import_stmt is not None:
            block.import_stmt = import_stmt
            self.import_statements.append(block)
            block.import_stmt = True
        elif block.docstring:
            self.docstrings.append(block)

    def rehighlight(self):
        self.import_statements[:] = []
        self.global_import_statements[:] = []
        self.docstrings[:] = []
        super(PythonSH, self).rehighlight()
