""" Utilities for processing ANSI escape codes and special ASCII characters.
"""
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
from collections import namedtuple
import re

# System library imports
from qtpy import QtGui

# Local imports
from qtconsole.styles import dark_style

#-----------------------------------------------------------------------------
# Constants and datatypes
#-----------------------------------------------------------------------------

# An action for erase requests (ED and EL commands).
EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])

# An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
# and HVP commands).
# FIXME: Not implemented in AnsiCodeProcessor.
MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])

# An action for scroll requests (SU and ST) and form feeds.
ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])

# An action for the carriage return character
CarriageReturnAction = namedtuple('CarriageReturnAction', ['action'])

# An action for the \n character
NewLineAction = namedtuple('NewLineAction', ['action'])

# An action for the beep character
BeepAction = namedtuple('BeepAction', ['action'])

# An action for backspace
BackSpaceAction = namedtuple('BackSpaceAction', ['action'])

# Regular expressions.
CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu'
CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS
OSC_SUBPATTERN = '\](.*?)[\x07\x1b]'
ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \
                (CSI_SUBPATTERN, OSC_SUBPATTERN))
ANSI_OR_SPECIAL_PATTERN = re.compile('(\a|\b|\r(?!\n)|\r?\n)|(?:%s)' % ANSI_PATTERN)
SPECIAL_PATTERN = re.compile('([\f])')

#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------

class AnsiCodeProcessor(object):
    """ Translates special ASCII characters and ANSI escape codes into readable
        attributes. It also supports a few non-standard, xterm-specific codes.
    """

    # Whether to increase intensity or set boldness for SGR code 1.
    # (Different terminals handle this in different ways.)
    bold_text_enabled = False

    # We provide an empty default color map because subclasses will likely want
    # to use a custom color format.
    default_color_map = {}

    #---------------------------------------------------------------------------
    # AnsiCodeProcessor interface
    #---------------------------------------------------------------------------

    def __init__(self):
        self.actions = []
        self.color_map = self.default_color_map.copy()
        self.reset_sgr()

    def reset_sgr(self):
        """ Reset graphics attributs to their default values.
        """
        self.intensity = 0
        self.italic = False
        self.bold = False
        self.underline = False
        self.foreground_color = None
        self.background_color = None

    def split_string(self, string):
        """ Yields substrings for which the same escape code applies.
        """
        self.actions = []
        start = 0

        # strings ending with \r are assumed to be ending in \r\n since
        # \n is appended to output strings automatically.  Accounting
        # for that, here.
        last_char = '\n' if len(string) > 0 and string[-1] == '\n' else None
        string = string[:-1] if last_char is not None else string

        for match in ANSI_OR_SPECIAL_PATTERN.finditer(string):
            raw = string[start:match.start()]
            substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
            if substring or self.actions:
                yield substring
                self.actions = []
            start = match.end()

            groups = [g for g in match.groups() if (g is not None)]
            g0 = groups[0]
            if g0 == '\a':
                self.actions.append(BeepAction('beep'))
                yield None
                self.actions = []
            elif g0 == '\r':
                self.actions.append(CarriageReturnAction('carriage-return'))
                yield None
                self.actions = []
            elif g0 == '\b':
                self.actions.append(BackSpaceAction('backspace'))
                yield None
                self.actions = []
            elif g0 == '\n' or g0 == '\r\n':
                self.actions.append(NewLineAction('newline'))
                yield g0
                self.actions = []
            else:
                params = [ param for param in groups[1].split(';') if param ]
                if g0.startswith('['):
                    # Case 1: CSI code.
                    try:
                        params = list(map(int, params))
                    except ValueError:
                        # Silently discard badly formed codes.
                        pass
                    else:
                        self.set_csi_code(groups[2], params)

                elif g0.startswith(']'):
                    # Case 2: OSC code.
                    self.set_osc_code(params)

        raw = string[start:]
        substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
        if substring or self.actions:
            yield substring

        if last_char is not None:
            self.actions.append(NewLineAction('newline'))
            yield last_char

    def set_csi_code(self, command, params=[]):
        """ Set attributes based on CSI (Control Sequence Introducer) code.

        Parameters
        ----------
        command : str
            The code identifier, i.e. the final character in the sequence.

        params : sequence of integers, optional
            The parameter codes for the command.
        """
        if command == 'm':   # SGR - Select Graphic Rendition
            if params:
                self.set_sgr_code(params)
            else:
                self.set_sgr_code([0])

        elif (command == 'J' or # ED - Erase Data
              command == 'K'):  # EL - Erase in Line
            code = params[0] if params else 0
            if 0 <= code <= 2:
                area = 'screen' if command == 'J' else 'line'
                if code == 0:
                    erase_to = 'end'
                elif code == 1:
                    erase_to = 'start'
                elif code == 2:
                    erase_to = 'all'
                self.actions.append(EraseAction('erase', area, erase_to))

        elif (command == 'S' or # SU - Scroll Up
              command == 'T'):  # SD - Scroll Down
            dir = 'up' if command == 'S' else 'down'
            count = params[0] if params else 1
            self.actions.append(ScrollAction('scroll', dir, 'line', count))

    def set_osc_code(self, params):
        """ Set attributes based on OSC (Operating System Command) parameters.

        Parameters
        ----------
        params : sequence of str
            The parameters for the command.
        """
        try:
            command = int(params.pop(0))
        except (IndexError, ValueError):
            return

        if command == 4:
            # xterm-specific: set color number to color spec.
            try:
                color = int(params.pop(0))
                spec = params.pop(0)
                self.color_map[color] = self._parse_xterm_color_spec(spec)
            except (IndexError, ValueError):
                pass

    def set_sgr_code(self, params):
        """ Set attributes based on SGR (Select Graphic Rendition) codes.

        Parameters
        ----------
        params : sequence of ints
            A list of SGR codes for one or more SGR commands. Usually this
            sequence will have one element per command, although certain
            xterm-specific commands requires multiple elements.
        """
        # Always consume the first parameter.
        if not params:
            return
        code = params.pop(0)

        if code == 0:
            self.reset_sgr()
        elif code == 1:
            if self.bold_text_enabled:
                self.bold = True
            else:
                self.intensity = 1
        elif code == 2:
            self.intensity = 0
        elif code == 3:
            self.italic = True
        elif code == 4:
            self.underline = True
        elif code == 22:
            self.intensity = 0
            self.bold = False
        elif code == 23:
            self.italic = False
        elif code == 24:
            self.underline = False
        elif code >= 30 and code <= 37:
            self.foreground_color = code - 30
        elif code == 38 and params:
            _color_type = params.pop(0)
            if _color_type == 5 and params:
                # xterm-specific: 256 color support.
                self.foreground_color = params.pop(0)
            elif _color_type == 2:
                # 24bit true colour support.
                self.foreground_color = params[:3]
                params[:3] = []
        elif code == 39:
            self.foreground_color = None
        elif code >= 40 and code <= 47:
            self.background_color = code - 40
        elif code == 48 and params:
            _color_type = params.pop(0)
            if _color_type == 5 and params:
                # xterm-specific: 256 color support.
                self.background_color = params.pop(0)
            elif _color_type == 2:
                # 24bit true colour support.
                self.background_color = params[:3]
                params[:3] = []
        elif code == 49:
            self.background_color = None

        # Recurse with unconsumed parameters.
        self.set_sgr_code(params)

    #---------------------------------------------------------------------------
    # Protected interface
    #---------------------------------------------------------------------------

    def _parse_xterm_color_spec(self, spec):
        if spec.startswith('rgb:'):
            return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
        elif spec.startswith('rgbi:'):
            return tuple(map(lambda x: int(float(x) * 255),
                             spec[5:].split('/')))
        elif spec == '?':
            raise ValueError('Unsupported xterm color spec')
        return spec

    def _replace_special(self, match):
        special = match.group(1)
        if special == '\f':
            self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
        return ''


class QtAnsiCodeProcessor(AnsiCodeProcessor):
    """ Translates ANSI escape codes into QTextCharFormats.
    """

    # A map from ANSI color codes to SVG color names or RGB(A) tuples.
    darkbg_color_map = {
        0  : 'black',       # black
        1  : 'darkred',     # red
        2  : 'darkgreen',   # green
        3  : 'brown',       # yellow
        4  : 'darkblue',    # blue
        5  : 'darkviolet',  # magenta
        6  : 'steelblue',   # cyan
        7  : 'grey',        # white
        8  : 'grey',        # black (bright)
        9  : 'red',         # red (bright)
        10 : 'lime',        # green (bright)
        11 : 'yellow',      # yellow (bright)
        12 : 'deepskyblue', # blue (bright)
        13 : 'magenta',     # magenta (bright)
        14 : 'cyan',        # cyan (bright)
        15 : 'white' }      # white (bright)

    # Set the default color map for super class.
    default_color_map = darkbg_color_map.copy()

    def get_color(self, color, intensity=0):
        """ Returns a QColor for a given color code or rgb list, or None if one
            cannot be constructed.
        """

        if isinstance(color, int):
            # Adjust for intensity, if possible.
            if color < 8 and intensity > 0:
                color += 8
            constructor = self.color_map.get(color, None)
        elif isinstance(color, (tuple, list)):
            constructor = color
        else:
            return None

        if isinstance(constructor, str):
            # If this is an X11 color name, we just hope there is a close SVG
            # color name. We could use QColor's static method
            # 'setAllowX11ColorNames()', but this is global and only available
            # on X11. It seems cleaner to aim for uniformity of behavior.
            return QtGui.QColor(constructor)

        elif isinstance(constructor, (tuple, list)):
            return QtGui.QColor(*constructor)

        return None

    def get_format(self):
        """ Returns a QTextCharFormat that encodes the current style attributes.
        """
        format = QtGui.QTextCharFormat()

        # Set foreground color
        qcolor = self.get_color(self.foreground_color, self.intensity)
        if qcolor is not None:
            format.setForeground(qcolor)

        # Set background color
        qcolor = self.get_color(self.background_color, self.intensity)
        if qcolor is not None:
            format.setBackground(qcolor)

        # Set font weight/style options
        if self.bold:
            format.setFontWeight(QtGui.QFont.Bold)
        else:
            format.setFontWeight(QtGui.QFont.Normal)
        format.setFontItalic(self.italic)
        format.setFontUnderline(self.underline)

        return format

    def set_background_color(self, style):
        """
        Given a syntax style, attempt to set a color map that will be
        aesthetically pleasing.
        """
        # Set a new default color map.
        self.default_color_map = self.darkbg_color_map.copy()

        if not dark_style(style):
            # Colors appropriate for a terminal with a light background. For
            # now, only use non-bright colors...
            for i in range(8):
                self.default_color_map[i + 8] = self.default_color_map[i]

            # ...and replace white with black.
            self.default_color_map[7] = self.default_color_map[15] = 'black'

        # Update the current color map with the new defaults.
        self.color_map.update(self.default_color_map)
