# Standard library imports
import re

# System library imports
from PyQt4 import QtCore, QtGui


class AnsiAction(object):
    """ Represents an action requested by an ANSI escape sequence.
    """
    def __init__(self, kind):
        self.kind = kind

class MoveAction(AnsiAction):
    """ An AnsiAction for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, 
        CHA, and CUP commands).
    """
    def __init__(self):
        raise NotImplementedError

class EraseAction(AnsiAction):
    """ An AnsiAction for erase requests (ED and EL commands).
    """
    def __init__(self, area, erase_to):
        super(EraseAction, self).__init__('erase')
        self.area = area
        self.erase_to = erase_to


class AnsiCodeProcessor(object):
    """ Translates ANSI escape codes into readable attributes.
    """

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

    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):
            substring = string[start:match.start()]
            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)

        substring = string[start:]
        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
            for code in params:
                self.set_sgr_code(code)

        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(area, erase_to))
        
    def set_sgr_code(self, code):
        """ Set attributes based on SGR (Select Graphic Rendition) code.
        """
        if code == 0:
            self.reset_sgr()
        elif code == 1:
            self.intensity = 1
            self.bold = True
        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
        

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

    # A map from color codes to RGB colors.
    ansi_colors = (# Normal,      Bright/Light    ANSI color code
                   ('black',      'grey'),        # 0: black
                   ('darkred',    'red'),         # 1: red
                   ('darkgreen',  'green'),       # 2: green
                   ('gold',       'yellow'),      # 3: yellow
                   ('darkblue',   'blue'),        # 4: blue
                   ('darkviolet', 'magenta'),     # 5: magenta
                   ('steelblue',  'cyan'),        # 6: cyan
                   ('grey',       'white'))       # 7: white
    
    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.ansi_colors[self.foreground_color][self.intensity]
            format.setForeground(QtGui.QColor(color))

        # Set background color
        if self.background_color is not None:
            color = self.ansi_colors[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