#JES- Jython Environment for Students
#Copyright (C) 2002  Jason Ergle, Claire Bailey, David Raines, Joshua Sklare
#See JESCopyright.txt for full licensing information
#Revisions:
# 5/14/03: added removeErrorHighlighting() to be called before any changes take 
#	   place in the text - AdamW
# 5/15/03: added call to removeErrorHighlighting before setting error highlighting
#	   to prevent multiple highlightings which can't be undone. -AdamW
# 5/15/03: added comment and string highlighting. - AdamW

import JESConstants
import java.awt as awt
import javax.swing as swing
import javax.swing.text.DefaultStyledDocument as DefaultStyledDocument
import keyword

WORD_BREAKS = [' ', '\n', '\t', '[', ']', '{', '}', ',', '\'', '-', '+', '=',
               '<', '>',  ':', ';', '_', '(', ')', '.',  '#', '"', '%' ]
KEYWORD_BOLD = 1
INSERT_EVENT = 1
REMOVE_EVENT = 2
MAX_UNDO_EVENTS_TO_RETAIN = 500

ERROR_LINE_FONT_COLOR       = awt.Color.black
ERROR_LINE_BACKGROUND_COLOR = awt.Color.yellow

class JESEditorDocument(DefaultStyledDocument):
################################################################################
# Function name: __init__
# Parameters:
#     -editor: JESEditor object that this object is associated with
# Return:
#     An instance of the JESEditorDocument class.
# Description:
#     Creates an instance of the JESEditorDocument class.
################################################################################
    def __init__(self, editor):
        self.editor = editor
        self.textAttrib = swing.text.SimpleAttributeSet()
        self.keywordAttrib = swing.text.SimpleAttributeSet()
        self.jesEnvironmentWordAttrib = swing.text.SimpleAttributeSet()
        self.errorLineAttrib = swing.text.SimpleAttributeSet()
	self.commentAttrib = swing.text.SimpleAttributeSet()
	self.stringAttrib = swing.text.SimpleAttributeSet()
	
	swing.text.StyleConstants.setForeground(self.stringAttrib, JESConstants.STRING_COLOR)

	swing.text.StyleConstants.setForeground(self.commentAttrib, JESConstants.COMMENT_COLOR)

        swing.text.StyleConstants.setForeground(self.jesEnvironmentWordAttrib, JESConstants.ENVIRONMENT_WORD_COLOR)
        swing.text.StyleConstants.setBold(self.jesEnvironmentWordAttrib, KEYWORD_BOLD)
        swing.text.StyleConstants.setFontSize(self.jesEnvironmentWordAttrib, self.editor.program.userFont)
        
        swing.text.StyleConstants.setFontSize(self.textAttrib, self.editor.program.userFont)
        swing.text.StyleConstants.setBackground(self.textAttrib, awt.Color.white)
        
        swing.text.StyleConstants.setForeground(self.keywordAttrib, JESConstants.KEYWORD_COLOR)
        swing.text.StyleConstants.setBold(self.keywordAttrib, KEYWORD_BOLD)
        swing.text.StyleConstants.setFontSize(self.keywordAttrib, self.editor.program.userFont)

        swing.text.StyleConstants.setForeground(self.errorLineAttrib, ERROR_LINE_FONT_COLOR)
        swing.text.StyleConstants.setBackground(self.errorLineAttrib, ERROR_LINE_BACKGROUND_COLOR)
        swing.text.StyleConstants.setFontSize(self.errorLineAttrib,  self.editor.program.userFont)
        self.undoEvents = []

        #The following variables are set when showErrorLine is called.  They
        #are then used to unhighlight the line when the next text modification
        #is made.
        self.errorLineStart = -1
        self.errorLineLen   = -1

################################################################################
# Function name: insertString
# Parameters:
#     -offset: offset where the text will be inserted
#     -str: string that is being inserted
#     -a: attribute for the text that is being innserted.
# Description:
#     This function overrides the inherited insertString function.  It inserts
#     the target text and then calls the keywordHighlightEvent function to
#     highlight keywords.
################################################################################
    def insertString(self, offset, str, a, addUndoEvent=1):
	lineUpdateNeeded = 0
	if self.errorLineStart >= 0:
	    self.removeErrorHighlighting()
        if str == '\t':
            str = JESConstants.TAB
        self.editor.modified = 1
        self.editor.gui.loadButton.enabled = 1
	for char in str:
	    if (char == '#') or (char == '"') or (char == '"'):
		lineUpdateNeeded = 1
        if addUndoEvent:
            self.addUndoEvent(INSERT_EVENT,
                              offset,
                              str)
        DefaultStyledDocument.insertString(self, offset, str, self.textAttrib)
	if lineUpdateNeeded:
	    self.updateLineHighlighting(offset)
        self.keywordHighlightEvent(offset, len(str))

################################################################################
# Function name: remove
# Parameters:
#     -offset: offset of the text that is being removed
#     -len: length of the text that is being removed
# Description:
#     This function overrides the inherited remove function.  It removes the
#     target text and then calls the keywordHighlightEvent function to highlight
#     keywords.
################################################################################
    def remove(self, offset, len, addUndoEvent=1):
	lineUpdateNeeded = 0
	if self.errorLineStart >= 0:
	    self.removeErrorHighlighting()
        self.editor.modified = 1
        self.editor.gui.loadButton.enabled = 1
	for char in self.getText(offset, len):
	    if (char == '#') or (char == '"') or (char == '"'):
		lineUpdateNeeded = 1
        if addUndoEvent:
            self.addUndoEvent(REMOVE_EVENT,
                              offset,
                              self.getText(offset, len))
        DefaultStyledDocument.remove(self, offset, len)
	if lineUpdateNeeded:
	    self.updateLineHighlighting(offset)
        else:
	    self.keywordHighlightEvent(offset, len)
	

################################################################################
# Function name: removeErrorHighlighting
# Description:
#     This funciton will remove any error highlighting set between 
#     errorLineStart and errorLineLen.
################################################################################
    def removeErrorHighlighting(self):
	#Unhighlight a line if showErrorLine was called earlier
        if self.errorLineStart >= 0:
            self.updateKeywordHighlightInRange(self.errorLineStart,
                                               self.errorLineLen)
            self.errorLineStart = -1
            self.errorLineLen   = -1


################################################################################
# Function name: keywordHighlightEvent
# Parameters:
#     -modifiedTextOffset:
#     -modifiedTextLen:
# Description:
#     This function is called when text is either inserted or removed from the
#     document.  It takes in the text that was modified, and then calculates
#     where the first word before that text begins as well as where the next
#     word after that text ends.  This can then be used when calling the
#     updateKeywordHighlightInRange function to let it know where to update
#     keyword highlights.
################################################################################
    def keywordHighlightEvent(self, modifiedTextOffset, modifiedTextLen):
        #Unhighlight a line if showErrorLine was called earlier
        if self.errorLineStart >= 0:
            self.updateKeywordHighlightInRange(self.errorLineStart,
                                               self.errorLineLen)
            self.errorLineStart = -1
            self.errorLineLen   = -1

        #Find start of the word before the modified text
        startOffset = modifiedTextOffset - 1
        while startOffset > 0:
            startOffset -= 1
            curChar = self.getText(startOffset, 1)
            if curChar in WORD_BREAKS:
                break

        #Find end of the word after the modified text
        endOffset = modifiedTextOffset + modifiedTextLen
        while endOffset <= self.length:
            curChar = self.getText(endOffset, 1)
            if curChar in WORD_BREAKS:
                break
            endOffset += 1

        #Ensure that the start and end offsets are within vaild document range
        if endOffset >= self.length:
            endOffset = self.length
        if startOffset < 0:
            startOffset = 0


        self.updateKeywordHighlightInRange(startOffset, endOffset - startOffset)


################################################################################
# Function name: updateKeywordHighlightInRange
# Parameters:
#     -offset: offset where text begins that needs to be checked for keywords
#     -len: length of the text which needs to be checked for keywords
# Description:
#     This function takes in a text offset and length, searches for keywords in
#     the text, and highlights those keywords.
#     It does this by first removing the entire text and replacing it with
#     default attribute text.  This ensures that any text that once was a
#     keyword will not be highlighted anymore.  Then the text is searched for
#     keywords;  when a keyword is found, that text is replaced with the keyword
#     highlighted text.
################################################################################
    def updateKeywordHighlightInRange(self, offset, len):
        #Save the cursor position so it can be reset later
        try:
            curOffset = self.editor.getCaretPosition()
            rangeText = self.getText(offset, len)

            #Delete all text within range and replace with default style text
            DefaultStyledDocument.remove(self, offset, len)
            DefaultStyledDocument.insertString(self, offset, rangeText, self.textAttrib)
            i = 0
            iWordStart = 0

            #Read each character in the range until an entire word is found
            fun = self.setTextAttrib
            r = rangeText[:len]
            for curChar in r:
                #If the currect char is a word break char, then an entire word has
                #been read
                if curChar in WORD_BREAKS:
		    #Make sure to comment color word breaks if they are in comments
		    #blocks (AW 5/15/03)
		    if self.isComment(offset + i):
		    	DefaultStyledDocument.remove(self, offset + i, 1)
            		DefaultStyledDocument.insertString(self, offset + i, curChar, self.commentAttrib)
		    #Make sure to color strings: (AW 5/15/03)
		    elif self.isString(offset + i):
		    	DefaultStyledDocument.remove(self, offset + i, 1)
            		DefaultStyledDocument.insertString(self, offset + i, curChar, self.stringAttrib)
		    self.setTextAttrib(offset + iWordStart, rangeText[iWordStart:i])
                    iWordStart = i + 1
                else:              
                    pass
                i += 1

            self.setTextAttrib(offset + iWordStart, rangeText[iWordStart:i])
            self.editor.setCaretPosition(curOffset)
        except:

            pass


################################################################################
# Function name: setTextAttrib
# Parameters:
#     -offset: offset where text begins the will be updated
#     -text: text that will be updated
# Description:
#     This function is called set the text attribute of the specified text.  It
#     will check to see if that text is a keyword, and if so, will set the
#     text attribute so that the word has the correct keyword highlighting.
################################################################################
    def setTextAttrib(self, offset, text):
	if self.isComment(offset):
            DefaultStyledDocument.remove(self, offset, len(text))
            DefaultStyledDocument.insertString(self, offset, text, self.commentAttrib)
	elif self.isString(offset):
	    DefaultStyledDocument.remove(self, offset, len(text))
            DefaultStyledDocument.insertString(self, offset, text, self.stringAttrib)
        else:
	    if keyword.iskeyword(text):
		DefaultStyledDocument.remove(self, offset, len(text))
            	DefaultStyledDocument.insertString(self, offset, text, self.keywordAttrib)
            if self.isJESEnvironmentWord(text):
                DefaultStyledDocument.remove(self, offset, len(text))
                DefaultStyledDocument.insertString(self, offset, text, self.jesEnvironmentWordAttrib)

    def isJESEnvironmentWord(self,text):

        varsToHighlight = self.editor.program.getVarsToHighlight()

        if varsToHighlight.has_key(text):

            return not None

        return None

################################################################################
# Funciton name: updateLineHighlighting
# Parameters:
#     -offset: the location in the text which has been changed
# Description:
#     This should be called whenever a ', ", or # is added or removed from the
#     text.  It updates the string and comment highlighting to match the current
#     formatting.
################################################################################
    def updateLineHighlighting(self, offset):
	text = self.getText(0, self.getLength())
	linestart = text.rfind("\n", 0, offset)
	if linestart == -1:
	     linestart = 0
	lineend = text.find("\n", offset)
	if lineend == -1:
	     lineend = self.getLength()
	self.updateKeywordHighlightInRange(linestart, (lineend - linestart))

################################################################################
# Function name: isComment
# Parameters:
#     -offset: the location of the text to see if it's a comment(#)
# Description:
#     Takes in a location of text, and then examines the line that it's on to 
#     determine if the location is inside a comment.  Will also find out if
#     the comment is inside a string and not color it.
################################################################################
    def isComment(self, offset):
	isSingleString = 0
	isDoubleString = 0	
	text = self.getText(0, self.getLength())
	linestart = text.rfind("\n", 0, offset)
	if linestart == -1:
	     linestart = 0
	for loc in range(linestart, offset + 1):
	     if text[loc] == '"':
	          if (isDoubleString == 1) and (isSingleString == 0):
		       isDoubleString = 0
		  elif (isDoubleString == 0) and (isSingleString == 0):
		       isDoubleString = 1
   	     if text[loc] == "'":
	          if (isSingleString == 1) and (isDoubleString == 0):
		       isSingleString = 0
		  elif (isSingleString == 0) and (isDoubleString == 0):
		       isSingleString = 1
	     if text[loc] == '#':
	          if (isSingleString == 0) and (isDoubleString == 0):
		       return 1
        return 0

################################################################################
# Function name: isString
# Parameters:
#     -offset: the location of the text to see if it's a string
# Description:
#     Takes in a location, and determines if that location is inside a string
################################################################################
    def isString(self, offset):
	isSingleString = 0
	isDoubleString = 0
	nextDOff = 0
	nextSOff = 0	
	text = self.getText(0, self.getLength())
	linestart = text.rfind("\n", 0, offset)
	if linestart == -1:
	     linestart = 0
	for loc in range(linestart, offset + 1):
	     if nextDOff:	
		  isDoubleString = 0
		  nextDOff = 0
	     if nextSOff:	
		  isSingleString = 0
		  nextSOff = 0
	     if text[loc] == '"':
		  if (isDoubleString == 0) and (isSingleString == 0):
		       isDoubleString = 1
		  elif (isDoubleString == 1) and (isSingleString == 0):
		       nextDOff = 1
   	     if text[loc] == "'":
		  if (isSingleString == 0) and (isDoubleString == 0):
		       isSingleString = 1
	          elif (isSingleString == 1) and (isDoubleString == 0):
		       nextSOff = 1
        return (isSingleString or isDoubleString)

################################################################################
# Function name: addUndoEvent 
# Parameters:
#     -eventType: identifies the type of event that occured (insert or remove)
#     -offset: offset in the text that the event occured in
#     -str: text that is being inserted or removed
# Description:
#     Adds an undo event to the event array.  This is needed so that text
#     modification events can be undone.  If the array reaches it's maximum
#     capacity, then the oldest event is removed from the array before adding
#     the new one.
################################################################################
    def addUndoEvent(self, eventType, offset, str):
        if len(self.undoEvents) > MAX_UNDO_EVENTS_TO_RETAIN:
            del self.undoEvents[0]
            
        self.undoEvents.append([eventType,
                                offset,
                                str])

################################################################################
# Function name: undo
# Description:
#     Undoes the last text modification that is in the undo events array and
#     removes it from the array.
################################################################################
    def undo(self):
        if len(self.undoEvents) > 0:
            lastEvent = self.undoEvents.pop()
            if lastEvent[0] == INSERT_EVENT:
                self.remove(lastEvent[1],
                            len(lastEvent[2]),
                            0)
            else:
                self.insertString(lastEvent[1],
                                  lastEvent[2],
                                  self.textAttrib,
                                  0)

################################################################################
# Function name: showErrorLine
# Parameters:
#      -lineNumber: number of the line that should be highlighted
# Description:
#     When this function is called, the specified line will be highlighted so
#     that the user can tell which line contains an error.
################################################################################
    def showErrorLine(self, lineNumber):
	
	#remove any old error highlighting, because we only want to show one error
	# at a time.  Plus, the system only keeps track of one error, so we need to
	# unhighlight the old error before setting the new one (AW 5/15/03)
	if self.errorLineStart > 0:
	    self.removeErrorHighlighting()

        #Search for the start offset of the error line
        docText = self.getText(0, self.getLength())

        line   = 1
        offset = 0

        while line < lineNumber:
            offset = docText.find('\n', offset) + 1
            line += 1

        #Search for the end offset of the error line
        #offset += 1
        endOffset = docText.find('\n', offset)
  
        if endOffset == -1:
            endOffset = len(docText)

        #Set error line position and length object variables
        self.errorLineStart = offset
        self.errorLineLen   = endOffset - offset

        #Set the correct text attribute for the error line
        self.setCharacterAttributes(self.errorLineStart,
                                    self.errorLineLen,
                                    self.errorLineAttrib,
                                    0)

        #Set cusor to error line to ensure that the error line will be visible
        self.editor.setCaretPosition(self.errorLineStart)


################################################################################
# Function name: gotoLine
# Parameters:
#      -lineNumber: number of the line that should be highlighted
# Description:
#     When this function is called, the specified line will be highlighted so
#     that the user can tell which line contains an error.
################################################################################
    def gotoLine(self, lineNumber):
        #Search for the start offset of the error line
        docText = self.getText(0, self.getLength())

        line   = 1
        offset = 0

        while line < lineNumber:
            offset = docText.find('\n', offset) + 1
            line += 1

        #Search for the end offset of the target line
        #offset += 1
        endOffset = docText.find('\n', offset)
         
        if endOffset == -1:
            endOffset = len(docText)
    
        #Set target line position and length object variables
        self.targetLineStart = offset
        self.targetLineLen   = endOffset - offset

        #Set cusor to target line to ensure that the error line will be visible
        self.editor.setCaretPosition(self.targetLineStart)

    def searchForward(self,toFind):
        try:
            offset = self.editor.getCaretPosition()
            text=self.getText(offset,self.getLength()-offset)
            location=text.find(toFind)
            if location != -1:
                #Highlight Text
                self.setCharacterAttributes(0,
                                        self.getLength(),
                                        self.textAttrib,
                                        0)
                self.setCharacterAttributes(location+offset,
                                        len(toFind),
                                        self.errorLineAttrib,
                                        0)
                self.editor.setCaretPosition(location+offset+len(toFind))
            else:
                if self.editor.getCaretPosition()>1:
                    self.editor.setCaretPosition(1)
                    self.searchForward(toFind)
        except:
            print "Exception thrown in searchForward"
            import sys
            a,b,c=sys.exc_info()
            print a,b,c

    def searchBackward(self,toFind):
        try:
            offset = self.editor.getCaretPosition()
            text=self.getText(0,offset)
            location=text.rfind(toFind)
            if location != -1:
                #Unhighlight Text
                self.setCharacterAttributes(0,
                                        self.getLength(),
                                        self.textAttrib,
                                        0)
                #Highlight Text
                self.setCharacterAttributes(location,
                                        len(toFind),
                                        self.errorLineAttrib,
                                        0)
                self.editor.setCaretPosition(location)
            else:
                if self.editor.getCaretPosition()<self.getLength():
                    self.editor.setCaretPosition(self.getLength())
                    self.searchBackward(toFind)
        except:
            import sys
            a,b,c=sys.exc_info()
            print a,b,c
            print "Exception thrown in searchBackward"

                                                                             