# This file is part of the Coral project.
# Please read the copyright notice in the LICENSE file.

from qt import *
import coral
from coral.modeler.unicode import QStringToFileName, PyStringToFileName
from coral.modeler.editor import Editor
#from coral.core.transform.spyrun import spyrun

from coral.modeler.getFQName import *
from coral.modeler.extension import PropertyEditor, ModelerTool
from coral.modeler import iconcache 
from coral.modeler.window import CoralActionHandler
import os
import codecs
import string
import sys
import traceback
import copy

from coral import configuration

from coral.modeler import CoralAction

from code import InteractiveInterpreter as Interpreter
from coral import kernel

from py_compile import PyCompileError

from coral import log
logger=log.getLogger("shell")

def doNothing():
    """dummy function that does exactly what the name says"""
    pass


class Shell(Editor,ModelerTool,CoralActionHandler,QWidget):
    def __init__(self,parent):
        QWidget.__init__(self,parent)
    
        CoralActionHandler.__init__(self)
    
        Editor.__init__(self)
        ModelerTool.__init__(self)

 
        self.setFocusPolicy(QWidget.StrongFocus)
        
        self.setIcon(iconcache.getIcon("shell"))
        self.setName('Shell')
        self.setCaption(self.trUtf8("Shell"))

        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
                                         QSizePolicy.Expanding))
        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
                                         QSizePolicy.Expanding))
        self.setMinimumSize(QApplication.globalStrut())
     
        self.fileName=""


        Form1Layout = QVBoxLayout(self)
        Form1Layout.setSpacing(0)
        Form1Layout.setMargin(0)

        self.bFrame = QFrame(self,'bFrame')
        self.bFrame.setMinimumSize(QSize(30,30))
        self.bFrame.setFrameShape(QFrame.StyledPanel)
        self.bFrame.setFrameShadow(QFrame.Raised)
        self.bFrame.setLineWidth(0)

        self.ShellMain = InterpreterShell(locals(),'',self)

        self.clearButton = QPushButton(self.bFrame,"clearButton")
        self.clearButton.setIconSet(QIconSet(iconcache.getIcon("edittrash")))
        self.clearButton.setAccel(Qt.CTRL+Qt.Key_L)
        self.clearButton.setGeometry(QRect(120,0,30,30))
        self.clearButton.show()
        
#        #Form1Layout.addWidget(self.clearButton,5,0)
#
#        self.runButton = QPushButton(self.bFrame,"runButton")
#        self.runButton.setIconSet(QIconSet(iconcache.getIcon("misc")))
#        self.runButton.setAccel(Qt.CTRL+Qt.Key_Return)
#        self.runButton.setGeometry(QRect(80,0,30,30))
#        self.runButton.show()

        self.strictButton=QCheckBox(self.bFrame,"strict")
        self.strictButton.setText("Enforce WFR")
        self.strictButton.move(160,0)
        self.strictButton.show()
        
        #Form1Layout.addWidget(self.runButton,4,0)

        self.openButton = QPushButton(self.bFrame,"openButton")
        self.openButton.setIconSet(QIconSet(iconcache.getIcon("fileopen")))
        self.openButton.setGeometry(QRect(0,0,30,30))
        self.openButton.show()
        
        #Form1Layout.addWidget(self.openButton,2,0)

        self.saveButton = QPushButton(self.bFrame,"saveButton")
        self.saveButton.setIconSet(QIconSet(iconcache.getIcon("filesave")))
        self.saveButton.setGeometry(QRect(30,0,30,30))
        self.saveButton.show()
        
        #Form1Layout.addWidget(self.saveButton,3,0)

        self.selfLabel = QLabel("self=",self)
        self.selfLabel.setMinimumSize(QSize(30,30))
        self.selfLabel.setGeometry(QRect(0,0,0,30))
        self.selfLabel.setIndent ( 5 )
        self.selfLabel.show()

        self.clearButton.connect(self.clearButton,SIGNAL("clicked()"),self.clear)
        self.openButton.connect(self.openButton,SIGNAL("clicked()"),self.open)
        self.saveButton.connect(self.saveButton,SIGNAL("clicked()"),self.save)


        self.setIcon(iconcache.getIcon("shell"))
        self.bFrame.show()
        Form1Layout.addWidget(self.bFrame)
        Form1Layout.addWidget(self.selfLabel)
        Form1Layout.addWidget(self.ShellMain)
        self.resize(500,300)
        self.clear()

    def doIt(self):
        pass
    
    def initActions(self):
        return [CoralAction(self,"shellOpenScript",
                            "shell.open",
                            "Open Script",
                            QIconSet(iconcache.getIcon("misc")),
                            0),
                
                CoralAction(self,"shellSaveScript",
                            "shell.save",
                            "Save Script",
                            QIconSet(iconcache.getIcon("misc")),
                            0),
    
                CoralAction(self,"shellSetCurrentScript",
                            "shell.setScript",
                            "Set Script",
                            QIconSet(iconcache.getIcon("misc")),
                            0),
    
                CoralAction(self,"shellGetOutput",
                            "shell.getOutput",
                            "Get Output",
                            QIconSet(iconcache.getIcon("misc")),
                            0),
                CoralAction(self,"shellClear",
                            "shell.clear",
                            "Clear TextEditor",
                            QIconSet(iconcache.getIcon("misc")),
                            0),
                CoralAction(self,"shellExecuteCurrentScript",
                            "shell.executeScript",
                                "Execute Current Script",
                                QIconSet(iconcache.getIcon("misc")),
                                0),
             ]

    def executeScript(self):
        self.ShellMain.executePendingScript()
        
    def open(self,fn=None):

        """open, if provided a filename as fn, opens the specified
        python script file, else it shows a dialog box that allows the
        user to select what script to load """
        if not fn:
            fn = QStringToFileName(QFileDialog.getOpenFileName(self.fileName,'Coral Scripts (*.spy;*.py);;All files (*.*)',self))

        if not fn:
            return
        
        self.fileName=fn
        text=loadTextFile(self.fileName)         
        self.ShellMain.loadScript(text)
    
    def save(self,fileName=None):
        
        """save provides the user with a dialog box to allow the user
        to specify what name the current script should be saved as """

        if not fileName:
            fileName = QStringToFileName(QFileDialog.getSaveFileName(self.fileName,'Coral Scripts (*.spy; *.py);;All files (*.*)',self))

        if not fileName:
            return

        saveTextFile(fileName,self.ShellMain.getHistoryString())


    def setColor(self,c):
        colors=[ [0,0,0], [0,150,0], [150,0,0] ]
        self.ShellMain.setColor(QColor(colors[c][0],colors[c][1],colors[c][2]))

    def setScript(self,s):
        self.ShellMain.loadScript(s)
        
    def canEdit(self,e):
        """the shell can edit anything"""
        return 1

    def apply(self):
        pass

    def focusOn(self,element):
        self.focus=element
        Editor.focusOn(self,self.focus)
        if not self.focus:
            self.selfLabel.setText("No element selected")
            return
        t = self.focus.__type__()
        self.selfLabel.setText("self= "+getFQName(self.focus)+": "+getFQTypeName(t))
        
    def clear(self):
        self.ShellMain.clear()
        self.ShellMain.clearHistory()

    def getOutput(self):
        # We split the text from the shell between the length before and after execution
        if self.ShellMain.startOutput < self.ShellMain.endOutput:
            return str(self.ShellMain.text())[self.ShellMain.startOutput:self.ShellMain.endOutput]
        else:
            return ""
                      
def loadTextFile(fileName):
     text=""
     fd = open(fileName,'r')
     try:
         text=fd.read()       
     finally:
         fd.close()
     return text
            
def saveTextFile(fileName,text):
    """Saves text in a file with name fileName, using UTF-8 encoding.
    If text is an 8-bit string type, it is assumed to be in ISO 8859-15 encoding.
 
    It may create new directories specified in fileName if necessary"""

    f = PyStringToFileName(fileName)
    if os.path.dirname(f):
        try:
            os.makedirs(os.path.dirname(f))
        except:
            pass

    fd = codecs.open(f,'w','UTF-8','replace')
    try:
        if isinstance(text, str):
            text = unicode(text, 'ISO-8859-15')
        else:
            text = unicode(text)
    
        fd.write(text)
    finally:
        fd.close()

class InterpreterShell(QTextEdit):
    def __init__(self, locals=None, log='', parent=None):
        """
        This class takes care of input and passing it to the Interpreter.
        """

        QTextEdit.__init__(self, parent)
        
        self.config=configuration.getCoralConfiguration()
        self.shell = parent
        assert parent
        self.namespace=locals
        
        # session log
        self.log = log or ''

        self.startOutput=0
        self.endOutput=0
        
        # capture all interactive input/output 
        self.old_stdout = sys.stdout
        self.old_stderr = sys.stderr
        self.old_stdin = sys.stdin

        # last line + last incomplete lines
        self.line    = QString()
        self.lines   = []
        # the cursor position in the last line
        self.point   = 0
        # flag: the interpreter needs more input to run the last lines. 
        self.more    = 0
        # flag: readline() is being used for e.g. raw_input() and input()
        self.reading = 0
        # After loading a script we wait for Enter to run it or Backspace to discard
        self.loadedScriptPending=0
        # history
        self.history = []
        self.pointer = 0
        self.xLast   = 0
        self.yLast   = 0

        # user interface setup
        self.setTextFormat(QTextEdit.PlainText)
        self.setWrapPolicy(QTextEdit.Anywhere)

        # font
        try:
            size= int(self.config.dGet(["GUI","fixed_font_size"],"12"))
        except:
            size=12
                      
        font = QFont(self.config.dGet(["GUI","fixed_font_name"],"Fixed"),size)
                    
        font.setFixedPitch(1)
        self.setFont(font)

        # geometry
        height = 40*QFontMetrics(font).lineSpacing()
        request = QSize(600, height)
        self.resize(request)

        # interpreter prompt.
        try:
            sys.ps1
        except AttributeError:
            sys.ps1 = ">>> "
        try:
            sys.ps2
        except AttributeError:
            sys.ps2 = "... "

        # interpreter banner
        self.write('Coral Shell running Python %s.\n' %
                   (sys.version,))
        self.write('Type "copyright", "credits" or "license"'
                   ' for more information on Coral and Python.\n')
        self.write(sys.ps1)

    def flush(self):
        """
        Simulate stdin, stdout, and stderr.
        """
        pass

    def isatty(self):
        """
        Simulate stdin, stdout, and stderr.
        """
        return 1

    def readline(self):
        """
        Simulate stdin, stdout, and stderr.
        """
        self.reading = 1
        self.__clearLine()
        self.moveCursor(QTextEdit.MoveEnd, 0)
        while self.reading:
            qApp.processOneEvent()
        if self.line.length() == 0:
            return '\n'
        else:
            return str(self.line) 
    
    def write(self, text):
        """
        Simulate stdin, stdout, and stderr.
        """
        # The output of self.append(text) contains to many newline characters,
        # so work around QTextEdit's policy for handling newline characters.

        self.insert(text)
        self.moveCursor(QTextEdit.MoveEnd, 0)
        self.yLast, self.xLast = self.getCursorPosition()
        
    def writelines(self, text):
        """
        Simulate stdin, stdout, and stderr.
        """
        map(self.write, text)
        logger.debug("DO WE EVER GET HERE? IF YES, OPTIMIZATION POSSIBLE")

    def fakeUser(self, lines):
        """
        Simulate a user: lines is a sequence of strings, (Python statements).
        """
        for line in lines:
            self.line = QString(line.rstrip())
            self.write(self.line)
            self.write('\n')
            self.__run()
            
    def getLocals(self):
        """
        Sets the self pointer to the selected Element
        """
        self.namespace["self"]=self.parent().focus
        self.namespace["sys"]=sys
        self.namespace["coral"] = coral
        self.namespace["gfx"] = coral.gfx
        self.namespace["sys"].argv=["<SMWShell>","smw://localeditor/currentModel"]
        return self.namespace

    def __run(self):
        """
        Append the last line to the history list, let the interpreter execute
        the last line(s), and clean up accounting for the interpreter results
        """
        self.pointer = 0
        self.history.append(QString(self.line))
        self.lines.append(str(self.line))
        source = '\n'.join(self.lines)

        locals=self.getLocals()
        selfBefore=locals.get('self',None)
        self.interpreter = Interpreter(locals)

        kernel.getTransactionManager().beginModifyCmd(self.parent().strictButton.isChecked())
        try:
            sys.stdout   = self
            sys.stderr   = self
            sys.stdin    = self
            self.more = self.interpreter.runsource(source)
        finally:
            sys.stdout   = self.old_stdout
            sys.stderr   = self.old_stderr
            sys.stdin    = self.old_stdin
            kernel.getTransactionManager().endModifyCmd()
        
        # Incase someone is trying to asign weird things to self
        newself = locals.get("self", None)
        if newself and not isinstance(newself, coral.Element):
            self.write("%s %s can not be assigned to self\n" % ( locals.get('self',None) , type(locals.get('self',None) ) ))
            locals['self']=selfBefore
            newself = selfBefore


        # Change focused element after a script has changed self
        # in the execution locals
        if selfBefore != newself:
            # Don't notify others if self is orphan
            if newself and newself.__getRootsProject__():
                self.parent().editorSet.notifyFocusOn(newself)
            else:
                self.shell.focusOn(newself)
                
        if self.more:
            self.write(sys.ps2)
        else:
            self.write(sys.ps1)
            self.lines = []
        
        self.__clearLine()



    def __clearLine(self):
        """
        Clear input line buffer
        """
        self.line.truncate(0)
        self.point = 0
        
    def __insertText(self, text):
        """
        Insert text at the current cursor position.
        """
        y, x = self.getCursorPosition()
        self.insertAt(text, y, x)
        self.line.insert(self.point, text)
        self.point += text.length()
        self.setCursorPosition(y, x + text.length())

    def keyPressEvent(self, e):
        """
        Handle user input a key at a time.
        """
        text  = e.text()
        key   = e.key()
        ascii = e.ascii()

        if self.loadedScriptPending:
            if(key == Qt.Key_Enter or key == Qt.Key_Return):
                self.loadedScriptPending=0
                self.executePendingScript()
            elif key == Qt.Key_Backspace:
                self.loadedScriptPending=0
                self.code=None
                self.setText(self.oldText);
                self.moveCursor(QTextEdit.MoveEnd, 0)
            else:
                self.append("Press ENTER to execute or BACKSPACE to cancel script\n")
                self.moveCursor(QTextEdit.MoveEnd, 0)
            return

            
        if text.length() and ascii>=32 and ascii<127:
            self.__insertText(text)
            return

        if e.state() & Qt.ControlButton or e.state() & Qt.ShiftButton:
            e.ignore()
            return

        if key == Qt.Key_Backspace:
            if self.point:
                self.doKeyboardAction(QTextEdit.ActionBackspace)
                self.point -= 1
                self.line.remove(self.point, 1)
                
        elif key == Qt.Key_Delete:
            self.doKeyboardAction(QTextEdit.ActionDelete)
            self.line.remove(self.point, 1)
            
        elif key == Qt.Key_Return or key == Qt.Key_Enter:
            self.moveCursor(QTextEdit.MoveLineEnd, 0)
            self.point = self.line.length()
            self.write('\n')
            if self.reading:
                self.reading = 0
            else:
                self.__run()

        elif key == Qt.Key_Tab:
            self.__insertText(text)
        elif key == Qt.Key_Left:
            if self.point:
                self.moveCursor(QTextEdit.MoveBackward, 0)
                self.point -= 1
        elif key == Qt.Key_Right:
            if self.point < self.line.length():
                self.moveCursor(QTextEdit.MoveForward, 0)
                self.point += 1
        elif key == Qt.Key_Home:
            self.setCursorPosition(self.yLast, self.xLast)
            self.point = 0
        elif key == Qt.Key_End:
            self.moveCursor(QTextEdit.MoveLineEnd, 0)
            self.point = self.line.length()
        elif key == Qt.Key_Up:
            if len(self.history):
                if self.pointer == 0:
                    self.pointer = len(self.history)
                self.pointer -= 1
                self.__recall()
        elif key == Qt.Key_Down:
            if len(self.history):
                self.pointer += 1
                if self.pointer == len(self.history):
                    self.pointer = 0
                self.__recall()
        else:
            e.ignore()

    def __recall(self): #display last command line
        """
        Display the current item from the command history.
        """
        self.setCursorPosition(self.yLast, self.xLast)
        self.setSelection(self.yLast, self.xLast,
                          self.yLast, self.paragraphLength(self.yLast))
        self.removeSelectedText()
        self.__clearLine()
        self.__insertText(self.history[self.pointer])

        
    def focusNextPrevChild(self, next):
        """
        Suppress tabbing to the next window in multi-line commands. 
        """
        if next and self.more:
            return 0
        return QTextEdit.focusNextPrevChild(self, next)

    def mousePressEvent(self, e):
        """
        Keep the cursor after the last prompt.
        """
        if e.button() == Qt.LeftButton:
            self.moveCursor(QTextEdit.MoveEnd, 0)
        return

    def loadScript(self,text):
        """
        Insert script to be executed. Execution is done by calling executePendingScript.
        """
        self.scriptText=text
        self.oldText=self.text()

#        self.write("\n")
        self.write(text)
        #self.write("\n---\n")

        self.moveCursor(QTextEdit.MoveEnd, 0)
        self.loadedScriptPending=1

    def executePendingScript(self):
        """
        Executes the loaded or pasted script.
        """
        self.history=self.history+map(QString, string.split(self.scriptText,"\n"))
        locals=self.getLocals()
        selfBefore=locals.get('self',None)
        sys.stdout   = self
        sys.stderr   = self
        sys.stdin    = self
        self.lastException=None
        try:
            try:
                self.code=compile(self.scriptText,'',"exec")
                if self.code:
                    kernel.getTransactionManager().beginModifyCmd(self.parent().strictButton.isChecked())
                    try:
                        self.write("\n")
                        # Store the length of teh text before execution
                        self.startOutput=self.length()
                        exec(self.code,locals)
                        # Store the length of the text after execution
                        # A substring between these indexes is the result + newline
                      
                    finally:
                        kernel.getTransactionManager().endModifyCmd()
                    
            
            except Exception, e: 
                self.write(str(e))
                self.lastException=e
        finally:
            self.endOutput=self.length()
            sys.stdout   = self.old_stdout
            sys.stderr   = self.old_stderr
            sys.stdin    = self.old_stdin

        # In case someone is trying to asign weird things to self
        newself = locals.get("self", None)
        if newself and not isinstance(newself, coral.Element):
            self.write("%s %s can not be assigned to self\n" % ( locals.get('self',None) , type(locals.get('self',None ) )))
            locals['self']=selfBefore
            newself = selfBefore
        
        # Change focused element after a script has changed self
        # in the execution locals
        if selfBefore != newself:
            # Don't notify others if self is orphan
            if newself and newself.__getRootsProject__():
                self.parent().editorSet.notifyFocusOn(newself)
            else:
                self.shell.focusOn(newself)

        self.write("\n")
        self.write(sys.ps1)
        
    def getHistoryString(self):
        return "\n".join(map(str,self.history))

    def clearHistory(self):
        self.history=[]
        self.code=None
        self.loadedScriptPending=0
        
    def displayPrompt(self):
        """
        Show the banner at the top
        """
        # interpreter banner
        self.write('This is Coral %s using Python %s on %s\n' %
                   (coral.version, sys.version, sys.platform))
        self.write('Type "coral.copyright", "coral.credits" or "coral.license"'
                   ' for more information on Coral.\n')
        self.write(sys.ps1)

    def paste(self):
        """
        Override paste to enable script loading, using loadScript.
        """
        cb=QApplication.clipboard()
        self.loadScript(unicode(cb.text("plain", QClipboard.Selection).utf8()))

    def clear(self):
        """
        Clear the editor and displayPrompt
        """
        self.setText("")
        self.displayPrompt()
        

