
""" Generic module for any kind of Python UI, as distinct from the classes these derive from which contains 
stuff also applicable even without this """

import scriptengine, replayer, definitions
import os, sys, logging, subprocess, time, re
from gridformatter import GridFormatter
from itertools import izip

try:
    from collections import OrderedDict
except ImportError:
    from ordereddict import OrderedDict

from locale import getdefaultlocale
from traceback import format_exception

# We really need our ConfigParser to be ordered, copied the one from 2.6 into the repository
if sys.version_info[:2] >= (2, 6):
    from ConfigParser import ConfigParser, ParsingError #@UnusedImport
else: # pragma: no cover - not currently running older than 2.5 in regular tests
    from ConfigParser26 import ConfigParser, ParsingError #@Reimport
    
def encodeToLocale(unicodeText):
    if sys.version_info[0] == 3:
        return unicodeText # don't need to mess about if we're in Python 3 anyway
    if unicodeText:
        try:
            encoding = getdefaultlocale()[1] or "utf-8"
            return unicodeText.encode(encoding, 'replace')
        except ValueError:
            # Get this if locale is invalid for example
            # Return the text as-is and hope for the best
            return unicodeText
    else:
        return ""

class WidgetAdapter:
    adapterClass = None
    @staticmethod
    def setAdapterClass(adapterCls):
        WidgetAdapter.adapterClass = adapterCls
        
    @classmethod
    def adapt(cls, widget):
        return cls.adapterClass(widget)
    
    def __init__(self, widget):
        self.widget = widget

    def __getattr__(self, name):
        return getattr(self.widget, name)

    def __hash__(self):
        return hash(self.widget)

    def __cmp__(self, other):
        return cmp(self.widget, other)

    def getTitle(self):
        try:
            return self.getWidgetTitle()
        except AttributeError:
            return ""

    def getChildren(self):
        return map(self.adapt, self.getChildWidgets())

    def getType(self):
        return self.widget.__class__.__name__

    def isInstanceOf(self, widgetClass):
        return isinstance(self.widget, widgetClass)

    def getBasicUIMapIdentifier(self):
        widgetName = self.getName()
        if not self.isAutoGenerated(widgetName): 
            return "Name=" + widgetName

        title = self.getTitle()
        if title:
            return self.encodeToLocale("Title=" + title)
       
        label = self.getLabel()
        if label:
            return self.encodeToLocale("Label=" + label)

        tooltip = self.getTooltip()
        if tooltip:
            return "Tooltip=" + tooltip
        return "Type=" + self.getType()
    
    def encodeToLocale(self, text):
        return encodeToLocale(text)

    def getTooltip(self):
        return ""

    def findPossibleBasicUIMapIdentifiers(self):
        name = self.getName()
        ids = [ "Name=" + name ] if not self.isAutoGenerated(name) else []
        return ids + [ "Title=" + self.encodeToLocale(self.getTitle()), 
                       "Label=" + self.encodeToLocale(self.getLabel()), "Tooltip=" + self.getTooltip(),
                       "Type=" + self.getType() ]
        
    def getUIMapIdentifier(self):
        return self.addContext(self.getBasicUIMapIdentifier())

    def addContext(self, text):
        if not text.endswith("="):
            context = self.getContextName()
            if context:
                return text + ", Context=" + context
        return text
    
    def getContextName(self):
        return ""

    def findPossibleUIMapIdentifiers(self):
        return map(self.addContext, self.findPossibleBasicUIMapIdentifiers())


class GuiEvent(definitions.UserEvent):
    def __init__(self, name, widget, *args):
        definitions.UserEvent.__init__(self, name)
        self.widget = widget
        self.programmaticChange = False
        self.changeMethod = self.getRealMethod(self.getChangeMethod())
        if self.changeMethod:
            allChangeMethods = [ self.changeMethod ] + self.getProgrammaticChangeMethods()
            for method in allChangeMethods:
                self.interceptMethod(method, ProgrammaticChangeIntercept)

    def getRealMethod(self, method):
        if isinstance(method, MethodIntercept):
            return method.method
        else:
            return method
        
    def interceptMethod(self, method, interceptClass):
        if isinstance(method, MethodIntercept):
            method.addEvent(self)
        else:
            setattr(self.getSelf(method), method.__name__, interceptClass(method, self))

    def getSelf(self, method):
        # seems to be different for built-in and bound methods
        try:
            return method.im_self
        except AttributeError:
            return method.__self__

    def getChangeMethod(self):
        pass

    def getProgrammaticChangeMethods(self):
        return []

    def shouldRecord(self, *args):
        return not self.programmaticChange

    def setProgrammaticChange(self, val, *args, **kwargs):
        self.programmaticChange = val

    @classmethod
    def getAssociatedSignatures(cls, widget):
        return set([ cls.getAssociatedSignal(widget) ])

    def widgetDisposed(self):
        return False

    def checkWidgetStatus(self):
        if self.widgetDisposed():
            raise definitions.UseCaseScriptError, "widget " + self.describeWidget() + \
                  " has already been disposed, cannot simulate event " + repr(self.name)

        if not self.widgetVisible():
            raise definitions.UseCaseScriptError, "widget " + self.describeWidget() + \
                  " is not visible at the moment, cannot simulate event " + repr(self.name)

        if not self.widgetSensitive():
            raise definitions.UseCaseScriptError, "widget " + self.describeWidget() + \
                  " is not sensitive to input at the moment, cannot simulate event " + repr(self.name)


class MethodIntercept:
    def __init__(self, method, event):
        self.method = method
        self.events = [ event ]
    def addEvent(self, event):
        self.events.append(event)

class ProgrammaticChangeIntercept(MethodIntercept):
    def __call__(self, *args, **kwds):
        # Allow for possibly nested programmatic changes, observation can have knock-on effects
        eventsToBlock = filter(lambda event: not event.programmaticChange, self.events)
        for event in eventsToBlock:
            event.setProgrammaticChange(True, *args, **kwds)
        retVal = apply(self.method, args, kwds)
        for event in eventsToBlock:
            event.setProgrammaticChange(False)
        return retVal



class ScriptEngine(scriptengine.ScriptEngine):
    defaultMapFile = os.path.join(scriptengine.ScriptEngine.storytextHome, "ui_map.conf")
    def __init__(self, enableShortcuts=False, uiMapFiles=[ defaultMapFile ],
                 customEventTypes=[], universalLogging=True, binDir="", **kw):
        self.uiMap = self.createUIMap(uiMapFiles)
        self.binDir = binDir
        self.addCustomEventTypes(customEventTypes)
        self.importCustomEventTypes()
        scriptengine.ScriptEngine.__init__(self, enableShortcuts, universalLogging=universalLogging, **kw)

    def createUIMap(self, uiMapFiles):
        return UIMap(self, uiMapFiles)

    def importCustomEventTypes(self):
        try:
            from customwidgetevents import customEventTypes
            self.addCustomEventTypes(customEventTypes)
        except ImportError, e:
            msg = str(e).strip()
            if msg != "No module named customwidgetevents":
                raise

    def addCustomEventTypes(self, customEventTypes):
        for customWidgetClass, customEventClasses in customEventTypes:
            for widgetClass, currEventClasses in self.eventTypes:
                if widgetClass is customWidgetClass:
                    # Insert at the start, to give first try to the custom events
                    currEventClasses[0:0] = customEventClasses
                    break
            self.eventTypes.insert(0, (customWidgetClass, customEventClasses))

    def findEventClassesFor(self, widget):
        eventClasses = []
        currClass = None
        for widgetClass, currEventClasses in self.eventTypes:
            if widget.isInstanceOf(widgetClass):
                if not currClass or issubclass(widgetClass, currClass):
                    eventClasses = currEventClasses
                    currClass = widgetClass
                elif not issubclass(currClass, widgetClass):
                    eventClasses = eventClasses + currEventClasses # make a copy
        return eventClasses

    def monitorSignal(self, eventName, signalName, widget, *args, **kw):
        if self.active():
            return self._monitorSignal(eventName, signalName, WidgetAdapter.adapt(widget), *args, **kw)

    def _monitorSignal(self, eventName, signalName, widget, argumentParseData=None):
        signalEvent = self._createSignalEvent(eventName, signalName, widget, argumentParseData)
        if signalEvent:
            self._addEventToScripts(signalEvent)
            return signalEvent

    def _addEventToScripts(self, event):
        if event.name and self.replayerActive():
            self.replayer.addEvent(event)
        if event.name and self.recorderActive():
            event.connectRecord(self.recorder.writeEvent)

    def _createSignalEvent(self, eventName, eventDescriptor, widget, argumentParseData):
        for eventClass in self.findEventClassesFor(widget):
            if eventDescriptor in eventClass.getAssociatedSignatures(widget):
                return eventClass(eventName, widget, argumentParseData)
            
    def getUsecaseNameChooserEnv(self):
        new_env = {}
        for var, value in os.environ.items():
            if var == "PATH":
                new_env[var] = value + os.pathsep + self.binDir
            elif not var.startswith("USECASE_RE"): # Don't transfer our record scripts!
                new_env[var] = value
        return new_env

    def getUsecaseNameChooserCmdArgs(self, recordScript, interface):
        mapFiles = self.uiMap.getMapFileNames()
        return [ "usecase_name_chooser", "-m", ",".join(mapFiles), 
                 "-r", recordScript, "-i", interface ]

    def replaceAutoRecordingForUsecase(self, interface):
        self.recorder.closeScripts()
        recordScript = os.getenv("USECASE_RECORD_SCRIPT")
        if self.uiMap and recordScript and os.path.isfile(recordScript) and self.recorder.hasAutoRecordings:
            sys.stdout.flush()
            cmdArgs = self.getUsecaseNameChooserCmdArgs(recordScript, interface)
            env = self.getUsecaseNameChooserEnv()
            if os.name == "posix":
                os.execvpe(cmdArgs[0], cmdArgs, env) #@UndefinedVariable
            else:
                subprocess.call(cmdArgs, env=env)

    def replaceAutoRecordingForShortcut(self, script):
        if self.uiMap and self.binDir:
            cmdArgs = self.getUsecaseNameChooserCmdArgs(script.scriptName, "gtk")
            subprocess.call(cmdArgs, env=self.getUsecaseNameChooserEnv())
    
    def getClassName(self, widgetClass, module):
        return module + "." + widgetClass.__name__

    def getFormatted(self, text, html, title):
        if html:
            return '<div class="Text_Header">' + title + "</div>\n" + \
                '<div class="Text_Normal">' + text + "</div>"
        else:
            return text

    def run(self, options, args):
        if options.supported:
            return self.describeSupportedWidgets()
        elif options.supported_html:
            return self.describeSupportedWidgets(html=True)
        else:
            try:
                return scriptengine.ScriptEngine.run(self, options, args)
            finally:
                if not options.disable_usecase_names:
                    self.replaceAutoRecordingForUsecase(options.interface)

    def handleAdditionalOptions(self, options):
        if options.maxoutputwidth:
            Describer.maxOutputWidth = int(options.maxoutputwidth)
        if options.imagedescription:
            Describer.imageDescriptionType = options.imagedescription
        if options.pathstoimages:
            Describer.imagePaths = options.pathstoimages.split(",")

    def run_python_or_java(self, args):
        # Two options here: either a Jython program and hence a .py file, or a Java class
        # If it's a file, assume it's Python
        if os.path.isfile(args[0]):
            self.run_python_file(args)
        else:
            exec "import " + args[0] + " as _className"
            # Java doesn't use the standard convention of having the first item in args be the
            # actual program name
            _className.main(args[1:]) #@UndefinedVariable

    def describeSupportedWidgets(self, html=False):
        toolkit, module, actionWord, linkPrefix = self.getDescriptionInfo()
        intro = """The following lists the %s widget types and the associated %s on them which 
StoryText %s is currently capable of recording and replaying. Any type derived from the listed
types is also supported.
""" % (toolkit, actionWord, definitions.__version__)
        print self.getFormatted(intro, html, toolkit + " Widgets and " + actionWord + " supported for record/replay")
        classes = self.getRecordReplayInfo(module)
        classNames = sorted(classes.keys())
        if html:
            self.writeHtmlTable(classNames, classes, linkPrefix)
        else:
            self.writeAsciiTable(classNames, classes)

        logIntro = """
The following lists the %s widget types whose status and changes StoryText %s is 
currently capable of monitoring and logging. Any type derived from the listed types 
is also supported but will only have features of the listed type described.
""" % (toolkit, definitions.__version__)
        print self.getFormatted(logIntro, html, toolkit + " Widgets supported for automatic logging")
        classNames = [ self.getClassName(w, module) for w in self.getSupportedLogWidgets() ]
        classNames.sort()
        if html:
            self.writeHtmlList(classNames, module, linkPrefix)
        else:
            for className in classNames:
                print className
        return True

    def getRecordReplayInfo(self, module):
        classes = {}
        for widgetClass, currEventClasses in self.eventTypes:
            if len(currEventClasses):
                self.addSignals(classes, widgetClass, currEventClasses, module)
        return classes

    def addSignals(self, classes, widgetClass, currEventClasses, module):
        signalNames = set()
        for eventClass in currEventClasses:
            signatures = eventClass.getAssociatedSignatures(None)
            signalNames.update(signatures)
        className = self.getClassName(widgetClass, module)
        classes[className] = sorted(signalNames)

    def writeAsciiTable(self, classNames, classes):
        for className in classNames:
            print className.ljust(self.getClassNameColumnSize()) + ":", " , ".join(classes[className])

    def getClassNameColumnSize(self):
        return 25 # seems to work, mostly

    def writeHtmlTable(self, classNames, classes, linkPrefix):
        print '<div class="Text_Normal"><table border=1 cellpadding=1 cellspacing=1>'
        for className in classNames:
            print '<tr><td>' + self.getLink(className, linkPrefix) + '</td><td><div class="Table_Text_Normal">' + \
                " , ".join(classes[className]) + "</div></td></tr>"
        print "</table></div>"

    def getLink(self, className, linkPrefix):
        docName = self.getDocName(className)
        return '<a class="Text_Link" href=' + linkPrefix + \
            docName + '.html>' + className + '</a>'

    def getDocName(self, className):
        return className.split(".")[-1].lower()

    def writeHtmlList(self, classNames, module, linkPrefix):
        print '<div class="Text_Normal">'
        for className in classNames:
            print '<li>' + self.getLink(className, linkPrefix)
        print '</div><div class="Text_Normal"><i>(Note that a textual version of this page can be auto-generated by running "storytext -s -i ' + module.lower() + '")</i></div>'

    @classmethod
    def getDisplayName(cls, signalName):
        return cls.signalDescs.get(signalName)

    @classmethod
    def getColumnDisplayName(cls, signalName):
        return cls.columnSignalDescs.get(signalName, signalName)


class WriteParserHandler:
    def __init__(self, fileName, parser):
        self.fileName = fileName
        self.parser = parser
        self.changed = False

    def write(self):
        if self.changed:
            dirName = os.path.dirname(self.fileName)
            if dirName and not os.path.isdir(dirName):
                os.makedirs(dirName)
            self.parser.write(open(self.fileName, "w"))
            self.changed = False

    def add_section(self, *args):
        self.changed = True
        self.parser.add_section(*args)

    def set(self, *args):
        self.changed = True
        self.parser.set(*args)

    def __getattr__(self, name):
        return getattr(self.parser, name)
    
    
class ParserSectionDict(OrderedDict):
    def __init__(self, fileName, *args, **kw):
        OrderedDict.__init__(self, *args, **kw)
        self.readingFiles = fileName
        
    def __getitem__(self, key):
        if self.readingFiles:
            msg = "UI map file(s) at " + self.readingFiles + " has duplicated sections for widgets identified by '" + key + "', the earlier ones will be ignored"
            sys.stderr.write("WARNING: " + msg + ".\n")
        return OrderedDict.__getitem__(self, key)

    def values(self):
        # Fix for python 2.7... which calls __getitem__ internally
        origFile = self.readingFiles
        self.readingFiles = None
        ret = OrderedDict.values(self)
        self.readingFiles = origFile
        return ret

class UIMapFileHandler:
    def __init__(self, uiMapFiles): 
        self.readFiles(uiMapFiles)

    def readFiles(self, uiMapFiles):
        # See top of file: uses the version from 2.6
        self.writeParsers = [ WriteParserHandler(f, self.makeParser([ f ])) for f in uiMapFiles ]
        if len(self.writeParsers) == 1:
            self.readParser = self.writeParsers[0]
        else:
            self.readParser = self.makeParser(uiMapFiles)
            
    def makeParser(self, filenames):
        parser = ConfigParser(dict_type=OrderedDict)
        parser.optionxform = str # don't automatically lower-case everything
        # There isn't a nice way to change the behaviour on getting a duplicate section
        # so we use a nasty way :)
        parser._sections = ParserSectionDict(",".join(filenames))
        try:
            parser.read(filenames)
            parser._sections.readingFiles = None
            return parser
        except ParsingError:
            raise definitions.UseCaseScriptError, "ERROR: could not parse UI map file(s) at " + ",".join(filenames)

    def storeInfo(self, sectionName, signature, eventName):
        if not self.readParser.has_section(sectionName):
            self.writeParsers[-1].add_section(sectionName)
           
        signature = signature.replace("::", "-") # Can't store :: in ConfigParser unfortunately
        if not self.readParser.has_option(sectionName, signature):
            for writeParser in self.writeParsers:
                if writeParser.has_section(sectionName):
                    writeParser.set(sectionName, signature, eventName)
            
    def findWriteParser(self, section):
        for parser in self.writeParsers:
            if parser.has_section(section):
                return parser

    def updateSectionName(self, section, newName):
        """ Note, we only add a new section, don't delete the old one as we once did
        This is in case some other widget is using it, which is possible and not easily detected currently
        Let the user clean away the old section if they want to."""
        writeParser = self.findWriteParser(section)
        if not writeParser.has_section(newName):
            writeParser.add_section(newName)
        for name, value in self.readParser.items(section):
            writeParser.set(newName, name, value)
        return newName

    def write(self, *args):
        for parserHandler in self.writeParsers:
            parserHandler.write()

    def __getattr__(self, name):
        return getattr(self.readParser, name)

    def findSectionAndOption(self, valueString):
        for section in self.readParser.sections():
            for optionName, value in self.readParser.items(section):
                if value and valueString.startswith(value):
                    return section, optionName
        return None, None

    def hasInfo(self):
        return len(self.readParser.sections()) > 0


class UIMap:
    ignoreWidgetTypes = []
    def __init__(self, scriptEngine, uiMapFiles):
        self.fileHandler = UIMapFileHandler(uiMapFiles)
        self.scriptEngine = scriptEngine
        self.windows = []
        self.logger = logging.getLogger("gui map")

    def readFiles(self, uiMapFiles):
        self.fileHandler.readFiles(uiMapFiles)

    def findWidgetDetails(self, scriptCommand):
        return self.fileHandler.findSectionAndOption(scriptCommand)

    def getMapFileNames(self):
        return [ parser.fileName for parser in self.fileHandler.writeParsers ]

    def monitorAndStoreWindow(self, window):
        if window not in self.windows:
            self.windows.append(window)
            self.monitorWindow(WidgetAdapter.adapt(window))

    def monitorWindow(self, window):
        self.logger.debug("Monitoring new window with title " + repr(window.getTitle()))
        self.monitor(window)

    def monitor(self, widget, excludeWidgets=[]):
        if widget.widget not in excludeWidgets:
            self.monitorWidget(widget)
            self.monitorChildren(widget, excludeWidgets)
        
    def monitorChildren(self, widget, *args, **kw):
        for child in widget.getChildren():
            self.monitor(child, *args, **kw)

    def monitorWidget(self, widget):
        signaturesInstrumented, autoInstrumented = self.instrumentFromMapFile(widget)
        if self.scriptEngine.recorderActive() or not self.fileHandler.hasInfo():
            widgetType = widget.getType()
            for signature in self.findAutoInstrumentSignatures(widget, signaturesInstrumented):
                identifier = widget.getUIMapIdentifier().replace("'", "<APOSTROPHE>")
                autoEventName = "Auto." + widgetType + "." + signature + ".'" + identifier + "'"
                signalName, argumentParseData = self.parseSignature(signature)
                self.autoInstrument(autoEventName, signalName, widget, argumentParseData, widgetType)
        return autoInstrumented

    def tryImproveSectionName(self, widget, section):
        if not section.startswith("Name="):
            newName = widget.getUIMapIdentifier()
            if newName != section:
                self.logger.debug("Updating section " + repr(section) + " to " + repr(newName))
                return self.fileHandler.updateSectionName(section, newName)
        return section

    def instrumentFromMapFile(self, widget):
        widgetType = widget.getType()
        if widgetType in self.ignoreWidgetTypes:
            return set(), False
        signaturesInstrumented = set()
        autoInstrumented = False
        section = self.findSection(widget)
        if section:
            section = self.tryImproveSectionName(widget, section)
            self.logger.debug("Reading map file section " + repr(section) + " for widget of type " + widgetType)
            for signature, eventName in self.fileHandler.items(section):
                if self.tryAutoInstrument(eventName, signature, signaturesInstrumented, widget, widgetType):
                    autoInstrumented = True
        return signaturesInstrumented, autoInstrumented

    def tryAutoInstrument(self, eventName, signature, signaturesInstrumented, widget, widgetType):
        try:
            signalName, argumentParseData = self.parseSignature(signature)
            if self.autoInstrument(eventName, signalName, widget, argumentParseData, widgetType):
                signaturesInstrumented.add(signature)
                # sometimes extra data is optional configuration, need to note that we don't need 
                # several variants in the recorder
                signaturesInstrumented.add(signalName) 
                return True
        except definitions.UseCaseScriptError, e:
            sys.stderr.write("ERROR in UI map file: " + str(e) + "\n")
        return False

    def findAutoInstrumentSignatures(self, widget, preInstrumented):
        signatures = []
        for eventClass in self.scriptEngine.findEventClassesFor(widget):
            for signature in eventClass.getAssociatedSignatures(widget):
                if signature not in signatures and signature not in preInstrumented:
                    signatures.append(signature)
        return signatures

    def findSection(self, widget):
        sectionNames = widget.findPossibleUIMapIdentifiers()
        for sectionName in sectionNames:
            self.logger.debug("Looking up section name " + repr(sectionName))
            if self.fileHandler.has_section(sectionName):
                return sectionName

    def parseSignature(self, signature):
        parts = signature.split(".", 1)
        signalName = parts[0]
        if len(parts) > 1:
            return signalName, parts[1]
        else:
            return signalName, None

    def autoInstrument(self, eventName, signalName, widget, argumentParseData, *args):
        self.logger.debug("Monitor " + eventName + ", " + signalName + ", " + widget.getType() + ", " + str(argumentParseData))
        self.scriptEngine._monitorSignal(eventName, signalName, widget, argumentParseData)
        return True
        
# Base class for all replayers using a GUI
class UseCaseReplayer(replayer.UseCaseReplayer):
    def __init__(self, uiMap, universalLogging, recorder, **kw):
        replayer.UseCaseReplayer.__init__(self, **kw)
        self.readingEnabled = False
        self.uiMap = uiMap
        self.loggerActive = universalLogging
        self.recorder = recorder
        self.delay = float(os.getenv("USECASE_REPLAY_DELAY", 0.0))
        
    def enableReading(self):
        self.readingEnabled = True
    
    def getParseError(self, scriptCommand):
        widgetDescriptor, actionName = self.uiMap.findWidgetDetails(scriptCommand)
        if widgetDescriptor:
            return "Could not execute script command '" + scriptCommand + "'.\n" + \
                   "No widget found with descriptor '" + widgetDescriptor + "' to perform action '" + actionName + "' on."
        else:
            return replayer.UseCaseReplayer.getParseError(self, scriptCommand)


# Use the idle handlers instead of a separate thread for replay execution
# Used for GTK, tkinter, wxPython
class IdleHandlerUseCaseReplayer(UseCaseReplayer):
    def __init__(self, *args, **kw):
        UseCaseReplayer.__init__(self, *args, **kw)
        self.idleHandler = None
        self.tryAddDescribeHandler()

    def isMonitoring(self):
        return self.loggerActive or (self.recorder.isActive() and self.uiMap)
        
    def tryAddDescribeHandler(self):
        if self.idleHandler is None and self.isMonitoring():
            self.idleHandler = self.makeDescribeHandler(self.handleNewWindows)
        else:
            self.idleHandler = None

    def enableReading(self):
        self.readingEnabled = True
        self._disableIdleHandlers()
        self.enableReplayHandler()

    def makeDescribeHandler(self, method):
        return self.makeIdleHandler(method)

    def makeIdleReplayHandler(self, method):
        return self.makeIdleHandler(method)

    def callReplayHandlerAgain(self, *args):
        self.enableReplayHandler()

    def _disableIdleHandlers(self):
        if self.idleHandler is not None:
            self.removeHandler(self.idleHandler)
            self.idleHandler = None
    
    def enableReplayHandler(self):
        self.idleHandler = self.makeReplayHandler(self.describeAndRun)

    def handleNewWindows(self):
        for window in self.findWindowsForMonitoring():
            if self.uiMap and (self.isActive() or self.recorder.isActive()):
                self.uiMap.monitorAndStoreWindow(window)
            if self.loggerActive:
                self.describeNewWindow(window)
        return True

    def makeReplayHandler(self, method):
        if self.delay:
            milliseconds = int(self.delay * 1000)
            return self.makeTimeoutReplayHandler(method, milliseconds)
        else:
            return self.makeIdleReplayHandler(method)

    def describeAndRun(self, *args):
        self.handleNewWindows()
        if self.readingEnabled:
            self.readingEnabled = self.runNextCommand()[0]
            if not self.readingEnabled:
                self.idleHandler = None
                self.tryAddDescribeHandler()
                if not self.idleHandler and self.uiMap: # pragma: no cover - cannot test with replayer disabled
                    # End of shortcut: reset for next time
                    self.logger.debug("Shortcut terminated: Resetting UI map ready for next shortcut")
                    self.uiMap.windows = [] 
                    self.events = {}
        if self.readingEnabled:
            return self.callReplayHandlerAgain(*args)
        else:
            return False
        
# Base class for Java replayers, both of which run in a separate thread
class ThreadedUseCaseReplayer(UseCaseReplayer):
    def waitForReenable(self):
        self.logger.debug("Waiting for replaying to be re-enabled...")
        while not self.readingEnabled:
            time.sleep(0.1) # don't use the whole CPU while waiting

    def describeAndRun(self, describeMethod):
        if not self.readingEnabled:
            self.waitForReenable()
        while True:
            if self.delay:
                self.logger.debug("Sleeping for " + str(self.delay) + " seconds...")
                time.sleep(self.delay)
            proceed, wait = self.runNextCommand(describeMethod=describeMethod)
            if not proceed:
                self.readingEnabled = self.waitingCompleted()
                if wait:
                    self.waitForReenable()
                else:
                    self.logger.debug("No command to run, no waiting to do: exiting replayer")
                    break

    def tryParseRepeatedly(self, command):
        attemptCount = 30
        for attempt in range(attemptCount):
            try:
                return self.parseCommand(command)
            except definitions.UseCaseScriptError:
                # We don't terminate scripts if they contain errors
                if attempt == attemptCount - 1:
                    value = sys.exc_info()[1]
                    self.write("ERROR: " + str(value))
                else:
                    time.sleep(0.1)
        
        return None, None

    def parseAndProcess(self, command, describeMethod):
        commandName, argumentString = self.tryParseRepeatedly(command)
        if commandName:
            describeMethod()
            self.describeAppEventsHappened()
            self.logger.debug("About to perform " + repr(commandName) + " with arguments " + repr(argumentString))
            try:
                self.processCommand(commandName, argumentString)
            except definitions.UseCaseScriptError:
                value = sys.exc_info()[1]
                self.write("ERROR: " + str(value))
                


class WidgetCounter:
    def __init__(self, equalityMethod=None):
        self.widgetNumbers = []
        self.nextWidgetNumber = 1
        self.describedNumber = 0
        self.customEqualityMethod = equalityMethod

    def widgetsEqual(self, widget1, widget2):
        if self.customEqualityMethod:
            return widget1 is widget2 or self.customEqualityMethod(widget1, widget2)
        else:
            return widget1 is widget2

    def getWidgetNumber(self, widget):
        for currWidget, number in self.widgetNumbers:
            if (not hasattr(currWidget, "isDisposed") or not currWidget.isDisposed()) and self.widgetsEqual(widget, currWidget):
                return number
        return 0

    def getId(self, widget):
        number = self.getWidgetNumber(widget)
        if not number:
            number = self.nextWidgetNumber
            self.widgetNumbers.append((widget, self.nextWidgetNumber))
            self.nextWidgetNumber += 1
        return str(number)

    def getWidgetsForDescribe(self):
        widgets = self.widgetNumbers[self.describedNumber:]
        self.describedNumber = len(self.widgetNumbers)
        return widgets



# Base class for everything except GTK's describer, which works a bit differently
class Describer(object):
    maxOutputWidth = 130
    imagePaths = []
    imageDescriptionType = None
    def __init__(self):
        self.logger = logging.getLogger("gui log")
        self.windows = set()
        self.widgetsWithState = OrderedDict()
        self.imageCounter = WidgetCounter(self.imagesEqual)
        self.structureLogger = logging.getLogger("widget structure")

    def imagesEqual(self, image1, image2):
        return image1 == image2

    def describe(self, window):
        if window in self.windows or not self.checkWindow(window):
            return
        
        if self.structureLogger.isEnabledFor(logging.DEBUG):
            self.describeStructure(window)

        self.windows.add(window)
        title = self.getSpecificState(window)
        message = "-" * 10 + " " + self.getWindowString() + " '" + title + "' " + "-" * 10
        self.widgetsWithState[window] = title
        self.logger.info("\n" + message)
        self.logger.info(self.getWindowContentDescription(window))
        footerLength = min(len(message), 100) # Don't let footers become too huge, they become ugly...
        self.logger.info("-" * footerLength)

    def checkWindow(self, window):
        return True

    def getWindowContentDescription(self, window):
        return self.getChildrenDescription(window)

    def getWindowString(self):
        return "Window"

    def findStateChanges(self, *args):
        defunctWidgets = []
        stateChanges = []
        for widget, oldState in self.widgetsWithState.items():
            if not self.shouldCheckForUpdates(widget, *args):
                continue
            try:
                state = self.getState(widget)
            except:
                # If the frame where it existed has been removed, for example...
                message = "Warning: The following exception has been thrown:\n"
                self.logger.debug(message + getExceptionString())
                if self.logger.isEnabledFor(logging.DEBUG):
                    exc = sys.exc_info()[1]
                    if hasattr(exc, "printStackTrace"):
                        exc.printStackTrace()
                defunctWidgets.append(widget)
                continue

            if state != oldState:
                stateChanges.append((widget, oldState, state))
                self.widgetsWithState[widget] = state
            
        for widget in defunctWidgets:
            del self.widgetsWithState[widget]
        return stateChanges

    def shouldCheckForUpdates(self, *args):
        return True

    def describeStateChanges(self, stateChanges, describedForAppearance=[]):
        for widget, oldState, state in stateChanges:
            if not describedForAppearance or not self.hasMarkedAncestor(widget, describedForAppearance):
                changeDesc = self.getStateChangeDescription(widget, oldState, state).rstrip()
                if changeDesc:
                    self.logger.info(changeDesc)

    def describeUpdates(self):
        stateChanges = self.findStateChanges()
        self.describeStateChanges(stateChanges)

    def getStateChangeDescription(self, widget, oldState, state):
        if isinstance(widget, self.getWindowClasses()):
            return "Changed title of " + self.getWindowString().lower() + " to '" + state + "'"
        else:
            return self.getUpdatePrefix(widget, oldState, state) + self.getDescription(widget)

    def getUpdatePrefix(self, widget, oldState, state):
        if isinstance(widget, self.getTextEntryClass()):
            return "Updated "
        else:
            return "\n"

    def addToDescription(self, desc, newText):
        if newText:
            if desc:
                desc += "\n"
            desc += newText.rstrip() + "\n"
        return desc

    def getDescription(self, *args, **kw):
        return self.convertToString(self._getDescription(*args, **kw))

    def getChildrenDescription(self, *args, **kw):
        return self.convertToString(self._getChildrenDescription(*args, **kw))

    def convertToString(self, obj):
        # Bit of a pain, unicode doesn't inherit from string for some reason
        return str(obj) if isinstance(obj, GridFormatter) else obj

    def _getDescription(self, widget):
        desc = ""
        widgetDesc = self.getWidgetDescription(widget)
        if isinstance(widgetDesc, GridFormatter):
            return widgetDesc
        desc = self.addToDescription(desc, widgetDesc)
        childDesc = self._getChildrenDescription(widget)
        if desc or not isinstance(childDesc, GridFormatter):
            desc = self.addToDescription(desc, self.convertToString(childDesc))
            return desc.rstrip()
        else:
            return childDesc
    
    def getWidgetDescription(self, widget):
        for widgetClass in self.stateWidgets + self.statelessWidgets:
            if isinstance(widget, widgetClass):
                methodName = "get" + widgetClass.__name__.replace("$", "") + "Description"
                return getattr(self, methodName)(widget)

        for widgetClass in self.ignoreWidgets:
            if isinstance(widget, widgetClass):
                return ""
        
        return self.widgetTypeDescription(widget.__class__.__name__) # pragma: no cover - should be unreachable

    def widgetTypeDescription(self, typeName): # pragma: no cover - should be unreachable
        return "A widget of type '" + typeName + "'" 

    def getState(self, widget):
        state = self.getSpecificState(widget)
        return state.strip()

    def getSpecificState(self, widget):
        for widgetClass in self.stateWidgets:
            if isinstance(widget, widgetClass):
                methodName = "get" + widgetClass.__name__ + "State"
                return getattr(self, methodName)(widget)
        return ""

    def combineElements(self, elements):
        elements = filter(len, elements)
        if len(elements) <= 1:
            return "".join(elements)
        else:
            rows = elements[0].split("\n")
            rows[0] += " ("
            basicLengths = map(len, rows)
            for elIx, el in enumerate(elements[1:]):
                elRows = el.split("\n")
                if len(elRows) > 1:
                    self.equaliseRows(rows)

                for i, elRow in enumerate(elRows):
                    while len(rows) <= i:
                        rows.append(" " * len(rows[-1]))
                    rows[i] += elRow
                if len(elRows) > 1:
                    self.equaliseRows(rows)
                if elIx != len(elements) - 2:
                    rows[0] += ", "
            strippedRows = [ r.rstrip() for r in rows ]
            strippedRows[self.getLastDetailRow(strippedRows, basicLengths)] += ")"
            return "\n".join(strippedRows)

    def getLastDetailRow(self, rows, basicLengths):
        for ix in range(-1, -1 - len(rows), -1):
            if len(rows[ix]) > basicLengths[ix]:
                return ix
        
    def equaliseRows(self, rows):
        if len(rows) > 1:
            maxLen = max((len(r) for r in rows))
            for i, r in enumerate(rows):
                rows[i] = r.ljust(maxLen)

    ##Debug code
    def getRawData(self, widget, useModule=False,
                   visibleMethodNameOverride=None, layoutMethodNameOverride=None):
        basic = ""
        if useModule:
            basic = widget.__class__.__module__ + "."
        basic += widget.__class__.__name__ + " " + str(id(widget))
        if hasattr(widget, "isDisposed") and widget.isDisposed():
            return basic
        layout = None
        layoutMethodName = layoutMethodNameOverride or "getLayout"
        if hasattr(widget, layoutMethodName):
            layout = getattr(widget, layoutMethodName)()

        if layout is not None:
            layoutText = layout.__class__.__name__
            if useModule:
                layoutText = layout.__class__.__module__ + "." + layoutText
            elements = [ layoutText ] + self.getRawDataLayoutDetails(layout, widget)
            basic += " (" + ", ".join(elements) + ")"
        visibleMethodName = visibleMethodNameOverride or self.visibleMethodName
        if hasattr(widget, visibleMethodName) and not self.isVisible(widget, visibleMethodName):
            basic += " (invisible)"
        return basic

    def addHeaderAndFooter(self, widget, text):
        header = "=" * 10 + " " + widget.__class__.__name__ + " " + "=" * 10
        return header + "\n" + self.fixLineEndings(text.rstrip()) + "\n" + "=" * len(header)

    def getRawDataLayoutDetails(self, *args):
        return []

    def getWidgetChildren(self, widget):
        return getattr(widget, self.childrenMethodName)() if hasattr(widget, self.childrenMethodName) else []
        
    def describeStructure(self, widget, indent=0, **kw):
        rawData = self.getRawData(widget, useModule=True, **kw)
        self.structureLogger.info("-" * 2 * indent + rawData)
        for child in self.getWidgetChildren(widget):
            self.describeStructure(child, indent+1, **kw)
                
    def getAndStoreState(self, widget):
        state = self.getState(widget)
        self.widgetsWithState[widget] = state
        return state

    def getItemDescription(self, item, prefix, *args):
        elements = []
        if hasattr(item, "getText") and item.getText():
            elements.append(item.getText())
        elements += self.getPropertyElements(item, *args)
        desc = self.combineElements(elements)
        if desc:
            return prefix + desc

    def getItemBarDescription(self, *args, **kw):
        return "\n".join(self.getAllItemDescriptions(*args, **kw))

    def formatTable(self, headerRow, rows, columnCount):
        if columnCount == 0:
            return ""

        colWidths = GridFormatter([ headerRow ] + rows, columnCount, allowOverlap=False).findColumnWidths()
        header = GridFormatter([ headerRow ], columnCount).formatCellsInGrid(colWidths)
        body = GridFormatter(rows, columnCount).formatCellsInGrid(colWidths)
        line = "_" * sum(colWidths) + "\n"
        return self.formatWithSeparators(header, body, line)

    def formatWithSeparators(self, header, body, line):
        return line + header + "\n" + line + body + "\n" + line

    def shouldDescribeChildren(self, widget):
        return hasattr(widget, self.childrenMethodName) and not isinstance(widget, self.ignoreChildren)

    def _getChildrenDescription(self, widget):
        return self.formatChildrenDescription(widget) if self.shouldDescribeChildren(widget) else ""

    def shouldFormatAsGrid(self, columns):
        return columns > 1

    def layoutSortsChildren(self, widget):
        return True

    def isVisible(self, widget, visibleMethodNameOverride=None):
        visibleMethodName = visibleMethodNameOverride or self.visibleMethodName
        return getattr(widget, visibleMethodName)()

    def getHorizontalSpan(self, *args):
        return 1

    def formatChildrenDescription(self, widget):
        children = self.getWidgetChildren(widget)
        visibleChildren = filter(self.isVisible, children)
        sortedChildren = self.sortChildren(widget, visibleChildren)
        childDescriptions = map(self._getDescription, sortedChildren)
        if not self.usesGrid(widget):
            self.removeEmptyDescriptions(sortedChildren, childDescriptions)
        if len(childDescriptions) > 1:
            grid, columns = self.tryMakeGrid(widget, sortedChildren, childDescriptions)
            if self.shouldFormatAsGrid(columns):
                maxWidth = self.getMaxDescriptionWidth(widget)
                formatter = GridFormatter(grid, columns, maxWidth)
                return self.handleGridFormatter(formatter)
            elif grid:
                childDescriptions = [ row[0] for row in grid ]
        return self.formatInColumn(childDescriptions)

    def handleGridFormatter(self, formatter):
        # Try to combine horizontal rows into one, so we can take one decision about if they're too wide
        return formatter if formatter.isHorizontalRow() else str(formatter)

    def tryMakeGrid(self, widget, sortedChildren, childDescriptions):
        columns = self.getLayoutColumns(widget, len(childDescriptions), sortedChildren)
        if columns > 1:
            horizontalSpans = self.getHorizontalSpans(sortedChildren, columns)
            return self.makeGrid(childDescriptions, horizontalSpans, columns)
        return None, 0

    def getHorizontalSpans(self, sortedChildren, columns):
        return [ self.getHorizontalSpan(c, columns) for c in sortedChildren ]

    def usesGrid(self, widget):
        return False

    def makeGrid(self, cellObjects, spans, numColumns):
        grid = []
        index = 0
        horizontalRow = len(cellObjects) == numColumns
        newColumns = numColumns
        for cellObject, span in izip(cellObjects, spans):
            if index % numColumns == 0:
                grid.append([])
            if horizontalRow and isinstance(cellObject, GridFormatter):
                grid[-1] += cellObject.grid[-1]
            else:
                grid[-1].append(self.convertToString(cellObject))
            index += span
            if index % numColumns != 0:
                # If we aren't at line end, introduce extra cells for padding
                for _ in range(span - 1):
                    grid[-1].append("")
        if horizontalRow:
            newColumns = len(grid[-1])
        return grid, newColumns

    def removeEmptyDescriptions(self, sortedChildren, childDescriptions):
        for child, desc in zip(sortedChildren, childDescriptions):
            if not desc:
                sortedChildren.remove(child)
                childDescriptions.remove(desc)

    def getMaxDescriptionWidth(self, widget):
        return self.maxOutputWidth # About a screen or so...

    def formatInColumn(self, childDescriptions):
        if len(childDescriptions) == 1:
            return childDescriptions[0]
        desc = ""
        for childDesc in childDescriptions:
            desc = self.addToDescription(desc, self.convertToString(childDesc))
        
        return desc.rstrip()

    def sortChildren(self, widget, visibleChildren):
        if len(visibleChildren) <= 1 or self.layoutSortsChildren(widget):
            # Trust in the layout, if there is one
            return visibleChildren
        
        xDivides = self.getVerticalDividePositions(visibleChildren)
        # Children don't always come in order, sort them...
        def getChildPosition(child):
            loc = child.getLocation()
            # With a divider, want to make sure everything ends up on the correct side of it
            return self.getDividerIndex(loc.x, xDivides), loc.y, loc.x
            
        visibleChildren.sort(key=getChildPosition)
        return visibleChildren

    def getDividerIndex(self, pos, dividers):
        for i, dividePos in enumerate(dividers):
            if pos < dividePos:
                return i
        return len(dividers)
    
    def fixLineEndings(self, text):
        # Methods return text 'raw' with Windows line endings
        if os.linesep != "\n":
            return text.replace(os.linesep, "\n")
        else:
            return text

    def describeAppearedWidgets(self, stateChangeWidgets):
        newWindows, commonParents = self.categoriseAppearedWidgets(stateChangeWidgets)
        for window in newWindows:
            self.describe(window)
            
        descriptions = map(self.getDescriptionForVisibilityChange, commonParents)
        if self.structureLogger.isEnabledFor(logging.DEBUG):
            for parent in commonParents:
                self.describeStructure(parent)
            
        for desc in sorted(descriptions):
            self.logger.info("\nNew widgets have appeared: describing common parent :\n")
            self.logger.info(desc)
        return commonParents
    
    def getMarkedAncestor(self, widget, markedWidgets):
        if widget in markedWidgets:
            return widget
        elif widget.getParent():
            return self.getMarkedAncestor(widget.getParent(), markedWidgets)

    def categoriseAppearedWidgets(self, stateChangeWidgets):
        newWindows, commonParents = [], []
        # Windows only get title changes described
        stateChangesFullDescribe = filter(lambda w: not isinstance(w, self.getWindowClasses()), stateChangeWidgets)
        markedWidgets = self.widgetsAppeared + stateChangesFullDescribe
        for widget in self.widgetsAppeared:
            if not self.widgetShowing(widget):
                continue

            if isinstance(widget, self.getWindowClasses()): 
                newWindows.append(widget)
            else:
                appearedWidget = self.getWidgetToDescribeForAppearance(widget, markedWidgets)
                if appearedWidget:
                    markedWidgets.append(appearedWidget)
                    commonParents.append(appearedWidget)
                    
        commonParents = [ w for w in commonParents if not self.hasMarkedAncestor(w, markedWidgets) ]
        return newWindows, commonParents

    def hasMarkedAncestor(self, widget, markedWidgets):
        return widget.getParent() is not None and self.getMarkedAncestor(widget.getParent(), markedWidgets) is not None

    def getWidgetToDescribeForAppearance(self, widget, markedWidgets):
        parent = widget.getParent()
        if parent is not None:
            markedAncestor = self.getMarkedAncestor(parent, markedWidgets)
            if markedAncestor:
                if self.logger.isEnabledFor(logging.DEBUG):
                    self.logger.debug("Not describing " + self.getRawData(widget) + " - marked " +
                                          self.getRawData(markedAncestor))
            else:
                return parent

    def getDescriptionForVisibilityChange(self, widget):
        if self.shouldDescribeChildren(widget):
            return self.getChildrenDescription(widget)
        else:
            return self.getDescription(widget)


class TableIndexer:
    allIndexers = {}
    def __init__(self, table):
        self.table = table
        self.logger = logging.getLogger("TableIndexer")
        self.primaryKeyColumn, self.rowNames = self.findRowNames()
        self.logger.debug("Creating table indexer with rows " + repr(self.rowNames))

    def updateTableInfo(self):
        if self.primaryKeyColumn is None:
            self.primaryKeyColumn, self.rowNames = self.findRowNames()
            self.logger.debug("Rebuilding indexer, primary key " + str(self.primaryKeyColumn) +
                              ", row names now " + repr(self.rowNames))
        else:
            currRowNames = self.getColumn(self.primaryKeyColumn)
            if set(currRowNames) != set([ "<unnamed>" ]):
                self.rowNames = currRowNames
                self.logger.debug("Model changed, row names now " + repr(self.rowNames))

    @classmethod
    def getIndexer(cls, table):
        # Don't just do setdefault, shouldn't create the TableIndexer if it already exists
        if table in cls.allIndexers:
            return cls.allIndexers.get(table)
        else:
            return cls.allIndexers.setdefault(table, cls(table))
        
    def getColumn(self, col):
        return [ self.getCellValue(row, col) for row in range(self.getRowCount()) ]
    
    def findRowNames(self):
        if self.getRowCount() > 1:
            for colIndex in range(self.table.getColumnCount()):
                column = self.getColumn(colIndex)
                if len(column) > 1 and len(set(column)) == len(column):
                    return colIndex, column
                else:
                    self.logger.debug("Rejecting column " + str(colIndex) + " as primary key : names were " + repr(column))
        # No unique columns to use as row names. Use the first column and add numbers
        return None, self.addIndexes(self.getColumn(0))
        
    def getIndexedValue(self, index, value, mapping):
        indices = mapping.get(value)
        if len(indices) == 1:
            return value
        else:
            return value + " (" + str(indices.index(index) + 1) + ")"

    def addIndexes(self, values):
        mapping = {}
        for i, value in enumerate(values):
            mapping.setdefault(value, []).append(i)

        return [ self.getIndexedValue(i, v, mapping) for i, v in enumerate(values) ]

    def findColumnIndex(self, columnName):
        for col in range(self.table.getColumnCount()):
            if self.getColumnText(col) == columnName:
                return col

    def parseDescription(self, description):
        if " for " in description:
            columnName, rowName = description.split(" for ")
            colIndex = self.findColumnIndex(columnName)
            if colIndex is None:
                raise definitions.UseCaseScriptError, "Could not find column labelled '" + columnName + "' in table."
            return rowName, colIndex
        else:
            return description, 0
    
    def getViewCellIndices(self, description):
        rowName, columnIndex = self.parseDescription(description)
        try:
            rowIndex = self.rowNames.index(rowName)
            return rowIndex, columnIndex
        except ValueError:
            raise definitions.UseCaseScriptError, "Could not find row identified by '" + rowName + "' in table."
                    
    def useColumnTextInDescription(self, **kw):
        return self.table.getColumnCount() > 1

    def getCellDescription(self, row, col, **kw):
        rowName = self.rowNames[row]
        if self.useColumnTextInDescription(**kw):
            return self.getColumnText(col) + " for " + rowName
        else:
            return rowName


def getExceptionString():
    type, value, traceback = sys.exc_info()
    return "".join(format_exception(type, value, traceback))

def findPrecedingLabel(widget, children, labelClass, ignoreLabels=[], textMethod="getText"):
    textPos = children.index(widget)
    while textPos > 0:
        prevWidget = children[textPos -1]
        if isinstance(prevWidget, labelClass):
            text = getattr(prevWidget, textMethod)()
            if text and text not in ignoreLabels:
                return text
            else:
                textPos -= 1
        else:
            return ""
    return ""

def getTextLabel(widget, childrenMethodName, labelClass, ignoreLabels=[]):
    """ Text widgets often are preceeded by a label, use this as their text, if it exists """
    parent = widget.getParent()
    if parent:
        children = getattr(parent, childrenMethodName)()
        if len(children) == 1:
            return getTextLabel(parent, childrenMethodName, labelClass, ignoreLabels)
        else:
            return findPrecedingLabel(widget, children, labelClass, ignoreLabels)
    
    return ""

def removeMarkup(text):
    removed = re.sub("<[^>]*>", "", text)
    return text if removed == text else removed.strip()

# Jython has problems with exceptions thrown from Java callbacks
# Print them out and continue, don't just lose them...
def catchAll(method, *args, **kw):
    try:
        method(*args, **kw)
    except:
        sys.stderr.write(getExceptionString() + "\n")

