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

# Standard library imports
from collections import namedtuple
import re

# System library imports
from PyQt4 import QtCore, QtGui

#-----------------------------------------------------------------------------
# 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'])

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

class AnsiCodeProcessor(object):
    """ Translates special ASCII characters and ANSI escape codes into readable
        attributes.
    """

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

    # Protected class variables.
    _ansi_commands = 'ABCDEFGHJKSTfmnsu'
    _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
    _special_pattern = re.compile('([\f])')

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

    def __init__(self):
        self.actions = []
        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

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

            self.actions = []
            try:
                params = []
                for param in match.group(1).split(';'):
                    if param:
                        params.append(int(param))
            except ValueError:
                # Silently discard badly formed escape codes.
                pass
            else:
                self.set_csi_code(match.group(2), params)

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

    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:
                for code in params:
                    self.set_sgr_code(code)
            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_sgr_code(self, code):
        """ Set attributes based on SGR (Select Graphic Rendition) code.
        """
        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 == 39:
            self.foreground_color = None
        elif code >= 40 and code <= 47:
            self.background_color = code - 40
        elif code == 49:
            self.background_color = None

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

    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 color codes to RGB colors.
    default_map = (# Normal,      Bright/Light    ANSI color code
                   ('black',      'grey'),        # 0: black
                   ('darkred',    'red'),         # 1: red
                   ('darkgreen',  'lime'),        # 2: green
                   ('brown',      'yellow'),      # 3: yellow
                   ('darkblue',   'deepskyblue'), # 4: blue
                   ('darkviolet', 'magenta'),     # 5: magenta
                   ('steelblue',  'cyan'),        # 6: cyan
                   ('grey',       'white'))       # 7: white

    def __init__(self):
        super(QtAnsiCodeProcessor, self).__init__()
        self.color_map = self.default_map
    
    def get_format(self):
        """ Returns a QTextCharFormat that encodes the current style attributes.
        """
        format = QtGui.QTextCharFormat()

        # Set foreground color
        if self.foreground_color is not None:
            color = self.color_map[self.foreground_color][self.intensity]
            format.setForeground(QtGui.QColor(color))

        # Set background color
        if self.background_color is not None:
            color = self.color_map[self.background_color][self.intensity]
            format.setBackground(QtGui.QColor(color))

        # 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, color):
        """ Given a background color (a QColor), attempt to set a color map
            that will be aesthetically pleasing.
        """
        if color.value() < 127:
            # Colors appropriate for a terminal with a dark background.
            self.color_map = self.default_map

        else:
            # Colors appropriate for a terminal with a light background. For 
            # now, only use non-bright colors...
            self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ]

            # ...and replace white with black.
            self.color_map[7] = ('black', 'black')