# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""Console base class"""

import re

from qtconsole.styles import dark_color
from qtpy.QtCore import Signal
from qtpy.QtGui import QColor, QFont, QTextCharFormat, QTextCursor
from qtpy.QtWidgets import QApplication

from spyder.plugins.editor.widgets.base import TextEditBaseWidget
from spyder.plugins.console.utils.ansihandler import ANSIEscapeCodeHandler
from spyder.utils.palette import SpyderPalette


MAIN_BG_COLOR = SpyderPalette.COLOR_BACKGROUND_1
MAIN_DEFAULT_FG_COLOR = SpyderPalette.COLOR_TEXT_1
MAIN_ERROR_FG_COLOR = SpyderPalette.COLOR_ERROR_1
MAIN_TB_FG_COLOR = SpyderPalette.COLOR_ACCENT_3
MAIN_PROMPT_FG_COLOR = SpyderPalette.COLOR_SUCCESS_1

def insert_text_to(cursor, text, fmt):
    """Helper to print text, taking into account backspaces"""
    while True:
        index = text.find(chr(8))  # backspace
        if index == -1:
            break
        cursor.insertText(text[:index], fmt)
        if cursor.positionInBlock() > 0:
            cursor.deletePreviousChar()
        text = text[index+1:]
    cursor.insertText(text, fmt)


class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler):
    def __init__(self):
        ANSIEscapeCodeHandler.__init__(self)
        self.base_format = None
        self.current_format = None

    def set_color_scheme(self, foreground_color, background_color):
        """Set color scheme (foreground and background)."""
        if dark_color(foreground_color):
            self.default_foreground_color = 30
        else:
            self.default_foreground_color = 37

        if dark_color(background_color):
            self.default_background_color = 47
        else:
            self.default_background_color = 40

    def set_base_format(self, base_format):
        self.base_format = base_format

    def get_format(self):
        return self.current_format

    def set_style(self):
        """
        Set font style with the following attributes:
        'foreground_color', 'background_color', 'italic',
        'bold' and 'underline'
        """
        if self.current_format is None:
            assert self.base_format is not None
            self.current_format = QTextCharFormat(self.base_format)
        # Foreground color
        if self.foreground_color is None:
            qcolor = self.base_format.foreground()
        else:
            cstr = self.ANSI_COLORS[self.foreground_color-30][self.intensity]
            qcolor = QColor(cstr)
        self.current_format.setForeground(qcolor)
        # Background color
        if self.background_color is None:
            qcolor = self.base_format.background()
        else:
            cstr = self.ANSI_COLORS[self.background_color-40][self.intensity]
            qcolor = QColor(cstr)
        self.current_format.setBackground(qcolor)

        font = self.current_format.font()
        # Italic
        if self.italic is None:
            italic = self.base_format.fontItalic()
        else:
            italic = self.italic
        font.setItalic(italic)
        # Bold
        if self.bold is None:
            bold = self.base_format.font().bold()
        else:
            bold = self.bold
        font.setBold(bold)
        # Underline
        if self.underline is None:
            underline = self.base_format.font().underline()
        else:
            underline = self.underline
        font.setUnderline(underline)
        self.current_format.setFont(font)


def inverse_color(color):
    color.setHsv(color.hue(), color.saturation(), 255-color.value())


class ConsoleFontStyle(object):
    def __init__(self, foregroundcolor, backgroundcolor,
                 bold, italic, underline):
        self.foregroundcolor = foregroundcolor
        self.backgroundcolor = backgroundcolor
        self.bold = bold
        self.italic = italic
        self.underline = underline
        self.format = None

    def apply_style(self, font, is_default):
        self.format = QTextCharFormat()
        self.format.setFont(font)
        foreground = QColor(self.foregroundcolor)
        self.format.setForeground(foreground)
        background = QColor(self.backgroundcolor)
        self.format.setBackground(background)
        font = self.format.font()
        font.setBold(self.bold)
        font.setItalic(self.italic)
        font.setUnderline(self.underline)
        self.format.setFont(font)


class ConsoleBaseWidget(TextEditBaseWidget):
    """Console base widget"""
    BRACE_MATCHING_SCOPE = ('sol', 'eol')
    COLOR_PATTERN = re.compile(r'\x01?\x1b\[(.*?)m\x02?')

    # --- Signals
    # This signal emits an error text, which corresponds to a Python
    # traceback.
    sig_exception_occurred = Signal(dict)
    userListActivated = Signal(int, str)
    completion_widget_activated = Signal(str)

    CONF_SECTION = 'internal_console'

    def __init__(self, parent=None):
        TextEditBaseWidget.__init__(self, parent)

        # To adjust some things for the internal console
        self.setObjectName('console')

        self.setMaximumBlockCount(300)

        # ANSI escape code handler
        self.ansi_handler = QtANSIEscapeCodeHandler()

        # Disable undo/redo (nonsense for a console widget...):
        self.setUndoRedoEnabled(False)

        self.userListActivated.connect(
            lambda user_id, text: self.completion_widget_activated.emit(text))

        background_color = MAIN_BG_COLOR
        default_foreground_color = MAIN_DEFAULT_FG_COLOR
        error_foreground_color = MAIN_ERROR_FG_COLOR
        traceback_foreground_color = MAIN_TB_FG_COLOR
        prompt_foreground_color = MAIN_PROMPT_FG_COLOR

        self.default_style = ConsoleFontStyle(
                            foregroundcolor=default_foreground_color,
                            backgroundcolor=background_color,
                            bold=False, italic=False, underline=False)
        self.error_style = ConsoleFontStyle(
                            foregroundcolor=error_foreground_color,
                            backgroundcolor=background_color,
                            bold=False, italic=False, underline=False)
        self.traceback_link_style = ConsoleFontStyle(
                            foregroundcolor=traceback_foreground_color,
                            backgroundcolor=background_color,
                            bold=True, italic=False, underline=True)
        self.prompt_style = ConsoleFontStyle(
                            foregroundcolor=prompt_foreground_color,
                            backgroundcolor=background_color,
                            bold=True, italic=False, underline=False)
        self.font_styles = (self.default_style, self.error_style,
                            self.traceback_link_style, self.prompt_style)

        self.set_color_scheme(default_foreground_color, background_color)
        self.setMouseTracking(True)

    def set_color_scheme(self, foreground_color, background_color):
        """Set color scheme of the console (foreground and background)."""
        self.ansi_handler.set_color_scheme(foreground_color, background_color)

        background_color = QColor(background_color)
        foreground_color = QColor(foreground_color)

        self.set_palette(background=background_color,
                         foreground=foreground_color)

        self.set_pythonshell_font()

    # ----- Python shell
    def insert_text(self, text):
        """Reimplement TextEditBaseWidget method"""
        # Eventually this maybe should wrap to insert_text_to if
        # backspace-handling is required
        self.textCursor().insertText(text, self.default_style.format)

    def paste(self):
        """Reimplement Qt method"""
        if self.has_selected_text():
            self.remove_selected_text()
        self.insert_text(QApplication.clipboard().text())

    def append_text_to_shell(self, text, error, prompt):
        """
        Append text to Python shell
        In a way, this method overrides the method 'insert_text' when text is
        inserted at the end of the text widget for a Python shell

        Handles error messages and show blue underlined links
        Handles ANSI color sequences
        Handles ANSI FF sequence
        """
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.End)
        if '\r' in text:    # replace \r\n with \n
            text = text.replace('\r\n', '\n')
            text = text.replace('\r', '\n')
        while True:
            index = text.find(chr(12))
            if index == -1:
                break
            text = text[index+1:]
            self.clear()

        if error:
            is_traceback = False
            is_warning = False
            for line in text.splitlines(True):
                if (line.startswith('  File')
                        and not line.startswith('  File "<')):
                    is_traceback = True
                    is_warning = False
                    # Show error links in blue underlined text
                    cursor.insertText('  ', self.default_style.format)
                    cursor.insertText(line[2:],
                                      self.traceback_link_style.format)
                else:
                    # Detect if line is a warning.
                    if (re.findall('[A-Z].*Warning', line) != [] or
                            'warnings.warn' in line or
                            'WARNING' in line):
                        is_warning = True

                    # Show error/warning messages in red
                    cursor.insertText(line, self.error_style.format)

                # Don't report warnings as internal errors
                if not is_warning:
                    self.sig_exception_occurred.emit(
                        dict(text=line, is_traceback=is_traceback)
                    )
        elif prompt:
            # Show prompt in green
            insert_text_to(cursor, text, self.prompt_style.format)
        else:
            # Show other outputs in black
            last_end = 0
            for match in self.COLOR_PATTERN.finditer(text):
                insert_text_to(cursor, text[last_end:match.start()],
                               self.default_style.format)
                last_end = match.end()
                try:
                    for code in [int(_c) for _c in match.group(1).split(';')]:
                        self.ansi_handler.set_code(code)
                except ValueError:
                    pass
                self.default_style.format = self.ansi_handler.get_format()
            insert_text_to(cursor, text[last_end:], self.default_style.format)
#            # Slower alternative:
#            segments = self.COLOR_PATTERN.split(text)
#            cursor.insertText(segments.pop(0), self.default_style.format)
#            if segments:
#                for ansi_tags, text in zip(segments[::2], segments[1::2]):
#                    for ansi_tag in ansi_tags.split(';'):
#                        self.ansi_handler.set_code(int(ansi_tag))
#                    self.default_style.format = self.ansi_handler.get_format()
#                    cursor.insertText(text, self.default_style.format)
        self.set_cursor_position('eof')
        self.setCurrentCharFormat(self.default_style.format)

    def set_pythonshell_font(self, font=None):
        """Python Shell only"""
        if font is None:
            font = QFont()
        for style in self.font_styles:
            style.apply_style(font=font,
                              is_default=style is self.default_style)
        self.ansi_handler.set_base_format(self.default_style.format)
