# Copyright (c) 2000 Phil Thompson <phil@river-bank.demon.co.uk>


import re
import string
import keyword
from qt import Qt, QCanvas, QCanvasText, QFont, QFontMetrics, QMessageBox
from qt import QCanvasLine, QCanvasRectangle, QApplication, QBrush
from qt import QCanvasPixmapArray, QCanvasSprite, QPoint

from Config import *
from Icons import IconBreak


ScannerStatusColWidth = 22

ScannerBreakPix = QCanvasPixmapArray([IconBreak],[QPoint(0,0)])


def ScannerMakeGrp(name,list):
    """ScannerMakeGrp(name,list) -> string

    Return a named regular expression group that comprises a ored list of
    separate expressions.

    """
    return "(?P<%s>" % name + string.join(list,"|") + ")"
 

def ScannerMakePat():
    """ScannerMakePat() -> string

    Return a regular expression to tokenise a Python source file.

    """
    comment = ScannerMakeGrp('COMMENT', [r"#[^\n]*"])
    sqstring = r"(\b[rR])?'[^'\\\n]*(\\.[^'\\\n]*)*'?"
    dqstring = r'(\b[rR])?"[^"\\\n]*(\\.[^"\\\n]*)*"?'
    sq3string = r"(\b[rR])?'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
    dq3string = r'(\b[rR])?"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
    string = ScannerMakeGrp('STRING', [sq3string, dq3string, sqstring, dqstring])
    kw = ScannerMakeGrp('NAME',[r'[a-zA-Z_][\.\w]*'])
    sync = ScannerMakeGrp('NL',[r"\n"])

    return comment + "|" + string + "|" + kw + "|" + sync


ScannerLexRe = re.compile(ScannerMakePat(),re.S)


class ScannerToken(QCanvasText):
    """ScannerToken(token,value,font,canvas)

    A class representing a token from a Python source file that will be
    displayed as text on a canvas.  token is the type of the token.  value
    is the text.  font is the font to use.  canvas is the canvas on which
    the text is displayed.

    """
    def __init__(self,tok,value,font,canvas):
        QCanvasText.__init__(self,value,font,canvas)
        self.token = tok


class Scanner(QCanvas):
    """Scanner(dbs,fn,parent=None)

    A class used to display a Python source file that is broken down into
    separate, colour coded, tokens.  dbs is the debug server.  fn is the file
    to display.  parent is the optional QObject parent.

    """
    def __init__(self,dbs,fn,parent=None):
        QCanvas.__init__(self,parent)

        self.dbs = dbs

        # The font to use.
        self.font = QFont(ConfigScannerFont,ConfigScannerFontSize)

        # The colour coding to use.
        self.colours = {
            'KEYWORD': ConfigScannerColorKeyword,
            'NAME': ConfigScannerColorName,
            'COMMENT': ConfigScannerColorComment,
            'STRING': ConfigScannerColorString,
            'DEFAULT': ConfigScannerColorDefault
        }

        # Calculate the line height.
        self.lineHeight = QFontMetrics(self.font).height()

        try:
            f = open(str(fn),'r')
        except:
            QMessageBox.warning(parent,'Open File',
                            'The file <b>%s</b> could not be opened.' % (fn))
            raise

        QApplication.setOverrideCursor(Qt.waitCursor)

        py = f.read().expandtabs(ConfigScannerTabStop)

        # Clear the current contents.
        self.items = []
        self.breaks = {}

        # Lex the file.
        self.x = ScannerStatusColWidth
        self.y = 0
        lastend = 0
        self.width = 0

        m = ScannerLexRe.search(py)

        while m:
            for key, value in m.groupdict().items():
                if value:
                    # Handle anything that didn't match.
                    newstart = m.start()

                    if newstart > lastend:
                        self.addToken('DEFAULT',py[lastend:newstart])

                    if key == 'STRING':
                        nlpend = 0
                        for l in value.split('\n'):
                            if nlpend:
                                self.addToken('NL',None)

                            if l:
                                self.addToken('STRING',l)
                            nlpend = 1
                    else:
                        if key == 'NAME' and keyword.iskeyword(value):
                            key = 'KEYWORD'

                        self.addToken(key,value)

            lastend = m.end()
            m = ScannerLexRe.search(py,lastend)

        # Handle anything non-matching at the end.
        tail = py[lastend:]

        if tail:
            self.addToken('DEFAULT',tail)

        f.close()

        QApplication.restoreOverrideCursor()

        self.resize(self.width,self.y)

        # Create a line delineating the status column.
        self.scol = QCanvasLine(self)
        self.scol.setPoints(ScannerStatusColWidth - 4,0,
                            ScannerStatusColWidth - 4,self.y)
        self.scol.show()

        self.update()

        self.fileName = fn

        # Create the highlight bar with the right size, but make it hidden.
        self.hlBar = QCanvasRectangle(0,0,
                                      self.width - ScannerStatusColWidth,
                                      self.lineHeight,self)
        self.hlBar.setBrush(QBrush(ConfigScannerColorCurrentLine))
        self.hlBar.setZ(-1.0)

    def addToken(self,tok,value):
        if tok == "NL":
            self.x = ScannerStatusColWidth
            self.y = self.y + self.lineHeight
        else:
            itm = ScannerToken(tok,value,self.font,self)

            itm.setColor(self.colours[tok])
            itm.setX(self.x)
            itm.setY(self.y)
            itm.show()

            self.items.append(itm)

            self.x = itm.boundingRect().right()

            if self.width < self.x:
                self.width = self.x

    def getFileName(self):
        """
        Public method to return the name of the file being displayed.
        """
        return self.fileName

    def highlight(self,line=None):
        """
        Public method to highlight (or de-highlight) a particular line.
        """
        if line is None:
            self.hlBar.hide()
        else:
            self.hlBar.move(ScannerStatusColWidth,(line - 1) * self.lineHeight)
            self.hlBar.show()

        self.update()

    def getHighlightPosition(self):
        """
        Public method to return the y position of the highlight bar.
        """
        return int(self.hlBar.y())

    def mouseClick(self,x,y):
        """
        Public method to handle a mouse click.
        """
        # Ignore anything outside of the status column.
        if x >= ScannerStatusColWidth:
            return

        # Convert to a line.
        line = int(y / self.lineHeight) + 1

        try:
            pm = self.breaks[line]
        except:
            pm = QCanvasSprite(ScannerBreakPix,self)
            self.breaks[line] = pm
            pm.move(0,(line - 1) * self.lineHeight)
            pm.show()

            self.dbs.remoteBreakpoint(self.fileName,line,1)
        else:
            del self.breaks[line]
            del pm

            self.dbs.remoteBreakpoint(self.fileName,line,0)

        self.update()
