#-----------------------------------------------------------------------------
# Name:        PythonEditorModels.py
# Purpose:
#
# Author:      Riaan Booysen
#
# Created:     2002/02/09
# RCS-ID:      $Id: PythonEditorModels.py,v 1.16.2.3 2005/03/07 14:21:38 riaan Exp $
# Copyright:   (c) 2002 - 2005
# Licence:     GPL
#-----------------------------------------------------------------------------

print 'importing PythonEditorModels'

import os, sys, pprint, imp, stat, types, tempfile
from thread import start_new_thread
from time import time, gmtime, strftime
from StringIO import StringIO

from wxPython import wx

import Preferences, Utils

import EditorHelper, ErrorStack
from EditorModels import PersistentModel, SourceModel, EditorModel, BitmapFileModel

import relpath, sourceconst

true,false=1,0

(imgPyAppModel, imgModuleModel, imgPackageModel, imgSetupModel,
 imgPythonBinaryFileModel,
) = EditorHelper.imgIdxRange(5)

class SourcePseudoFile(Utils.PseudoFileOutStore):
    def readlines(self):
        return self.output

class ModuleModel(SourceModel):
    modelIdentifier = 'Module'
    defaultName = 'module'
    bitmap = 'Module.png'
    imgIdx = imgModuleModel
    ext = '.py'

    def __init__(self, data, name, editor, saved, app=None):
        self.app = app
        SourceModel.__init__(self, data, name, editor, saved)
        self.moduleName = os.path.split(self.filename)[1]
        self.lastRunParams = ''
        self.lastDebugParams = ''
        self.useInputStream = false

        if data:
            if Preferences.autoReindent:
                if not self.reindent(false):
                    self.update()
            else:
                self.update()

    def destroy(self):
        SourceModel.destroy(self)
        del self.app

    def load(self, notify = true):
        SourceModel.load(self, false)
        if Preferences.autoReindent:
            if not self.reindent():
                self.update()
        else:
            self.update()
        if notify: self.notify()

    def save(self, overwriteNewer=false):
        if Preferences.autoReindent:
            self.reindent(false)
        SourceModel.save(self, overwriteNewer)

    def saveAs(self, filename):
        oldFilename = self.filename
        SourceModel.saveAs(self, filename)
        if self.app:
            self.app.moduleSaveAsNotify(self, oldFilename, filename)
        self.moduleName = os.path.basename(filename)

        # update breakpoints
        from Debugger.Breakpoint import bplist
        bplist.renameFileBreakpoints(oldFilename, self.filename)
        if self.editor.debugger:
            self.editor.debugger.breakpts.refreshList()

        self.notify()

    _module = None
    def getModule(self):
        if self._module is None:
            wx.wxBeginBusyCursor()
            try:
                import moduleparse
                self._module = moduleparse.Module(
                    self.moduleName, self.getDataAsLines())
            finally:
                wx.wxEndBusyCursor()
        return self._module

    def initModule(self):
        # Don't parse the module until it's needed.
        self._module = None

    def refreshFromModule(self):
        """ Must call this method to apply changes made
        to the module object. """
        self.setDataFromLines(self.getModule().getEOLFixedLines())
        self.notify()

    def renameClass(self, oldname, newname):
        pass

    def update(self):
        self.initModule()
        EditorModel.update(self)

    def runInThread(self, filename, args, interpreterPath, inpLines=[],
                    execStart=None, execFinish=None):
        cwd = os.path.abspath(os.getcwd())
        newCwd = os.path.dirname(os.path.abspath(filename))
        os.chdir(newCwd)
        try:
            cmd = '"%s" %s %s'%(interpreterPath,
                  os.path.basename(filename), args)

            from ModRunner import PopenModuleRunner#, ExecFinishEvent

            runner = PopenModuleRunner(None, newCwd)
            runner.run(cmd, inpLines, execStart)
            #wx.wxPostEvent(self.editor, ExecFinishEvent(runner))
            if execFinish:
                wx.wxCallAfter(execFinish, runner)
        finally:
            if os: os.chdir(cwd)

    def run1(self, args = '', execStart=None, execFinish=None):
        """ Excecute the current saved image of the application. """
        if self.savedAs:
            filename = self.assertLocalFile()

            self.editor.statusBar.setHint('Running %s...' % filename)
            if Preferences.minimizeOnRun:
                self.editor.minimizeBoa()
            inpLines = []
            if self.useInputStream and self.editor.erroutFrm.inputPage:
                inpLines = StringIO(
                      self.editor.erroutFrm.inputPage.GetValue()).readlines()
                
            start_new_thread(self.runInThread, (filename, args,
                  Preferences.getPythonInterpreterPath(), inpLines,
                  execStart, execFinish))

            #self.runInThread(filename, args,
            #      Preferences.getPythonInterpreterPath(), inpLines,
            #      execStart, execFinish)

    def run(self, args = '', execStart=None, execFinish=None):
        """ Excecute the current saved image of the application. """
        if self.savedAs:
            filename = self.assertLocalFile()

            self.editor.statusBar.setHint('Running %s...' % filename)
            if Preferences.minimizeOnRun:
                self.editor.minimizeBoa()

            inpLines = []
            if self.useInputStream and self.editor.erroutFrm.inputPage:
                inpLines = StringIO(
                      self.editor.erroutFrm.inputPage.GetValue()).readlines()
                
            cwd = os.path.abspath(os.getcwd())
            newCwd = os.path.dirname(os.path.abspath(filename))
            interp = Preferences.getPythonInterpreterPath()
            basename = os.path.basename(filename)
            
            os.chdir(newCwd)
            try:
                cmd = '"%s" %s %s'%(interp, basename, args)
    
                from ModRunner import wxPopenModuleRunner
    
                runner = wxPopenModuleRunner(self.editor.erroutFrm, newCwd)
                runner.run(cmd, inpLines, execFinish)
                
                execStart(runner.pid, os.path.basename(interp), basename)

            finally:
                if os: os.chdir(cwd)

    # XXX Not used!
    def runAsScript(self):
        filename = self.assertLocalFile()
        execfile(filename)

    def compile(self):
        import ModRunner
        oldErr = sys.stderr
        sys.stderr = ErrorStack.RecFile()
        try:
            cmr = ModRunner.CompileModuleRunner(self.editor.erroutFrm)
            cmr.run(self.filename, self.data+'\n\n', self.modified)

            serr = ErrorStack.errorList(sys.stderr)

            cmr.checkError(serr, 'Compiled')
        finally:
            sys.stderr = oldErr

        return len(serr)

    def cyclops(self, args='', execStart=None, execFinish=None):
        """ Run the saved application thru Cyclops """
        if self.savedAs:
            cwd = os.path.abspath(os.getcwd())
            filename = self.assertLocalFile()
            os.chdir(os.path.dirname(filename))
            page = ''
            try:
                name = os.path.basename(filename)
                report = tempfile.mktemp()

                # execute Cyclops in Python with module as parameter
                command = '"%s" "%s" "%s" "%s"'%(
                      Preferences.getPythonInterpreterPath(),
                      Utils.toPyPath('RunCyclops.py'), name, report)
                wx.wxExecute(command, true)

                # read report that Cyclops generated
                page = open(report, 'r').read()
                os.remove(report)
            finally:
                os.chdir(cwd)
                return page
        else:
            wx.wxLogWarning('Save before running Cyclops')
            raise 'Not saved yet!'

    def debug(self, params=None, cont_if_running=0, cont_always=0,
              temp_breakpoint=None):
        if self.savedAs:
            debugger = self.editor.debugger
            if not debugger:
                from Debugger import Debugger

                filename = self.assertLocalFile(self.filename)
                debugger = Debugger.DebuggerFrame(self.editor, filename)
                debugger.setDebugClient()
                if params is not None: # pass [] to clear parameters
                    debugger.setParams(params)
                self.editor.debugger = debugger
            debugger.Show()
            debugger.initSashes()
            debugger.ensureRunning(cont_if_running, cont_always,
                                   temp_breakpoint)

    def profile(self):
        filename = self.assertLocalFile()
        #statFile = os.path.splitext(filename)[0]+'.prof'
        statFile = tempfile.mktemp()
        if os.path.exists(statFile):
            modtime = os.stat(statFile)[stat.ST_MTIME]
        else:
            modtime = None
        profDir = os.path.dirname(filename)

        cwd = os.path.abspath(os.getcwd())
        os.chdir(profDir)
        try:
            profCmd = """"%s" -c "import profile;profile.run('execfile('+chr(34)+%s+chr(34)+')', '%s')" """.strip()

            cmd = profCmd % (`Preferences.getPythonInterpreterPath()`[1:-1],
                  `os.path.basename(filename)`, `statFile`[1:-1])

            if hasattr(self, 'app'): app = self.app
            else: app = None

            from ModRunner import ExecuteModuleRunner
            runner = ExecuteModuleRunner(None, profDir)
            self.editor.statusBar.setHint('Profiling %s...'%filename)
            runner.run(cmd)
            self.editor.statusBar.setHint('Finished profiling.')

        finally:
            os.chdir(cwd)

        return statFile, modtime, profDir


    def addModuleInfo(self, prefs):
        # XXX Check that module doesn't already have an info block

        dollar = '$' # has to be obscured from CVS :)
        prefs['Name'] = self.moduleName
        prefs['Created'] = strftime('%Y/%d/%m', gmtime(time()))
        prefs['RCS-ID'] = '%sId: %s %s' % (dollar, self.moduleName , dollar)

        self.data = (sourceconst.defInfoBlock % prefs) + self.data
        self.modified = true
        self.update()
        self.notify()

    def reindent(self, updateModulePage=true):
        from ExternalLib import reindent
        self.refreshFromViews()
        eol = Utils.getEOLMode(self.data)
        file = SourcePseudoFile(self.getDataAsLines())
        ri = reindent.Reindenter(file, eol=eol)
        try:
            if ri.run():
                file.output = []
                ri.write(file)

                newData = ''.join(file.output)
                modified = self.data != newData
                self.modified = self.modified or modified

                if modified:
                    self.data = newData
                    if updateModulePage:
                        self.editor.updateModulePage(self)
                    self.update()
                    self.notify()

                    self.editor.statusBar.setHint(\
                          'Code reformatted (indents and or EOL characters fixed)')
                    return true
        except Exception, error:
            self.editor.statusBar.setHint(\
                  'Reindent failed - %s : %s' % (error.__class__, str(error)) , 'Error')

        return false

    def getSimpleRunnerSrc(self):
        return sourceconst.simpleModuleRunSrc

    def disassembleSource(self):
        import dis
        try:
            code = compile(self.data, self.filename, 'exec')
        except:
            oldOut = sys.stdout
            sys.stdout = Utils.PseudoFileOutStore()
            try:
                print "''' Code does not compile\n\n    Disassembly of Traceback:\n'''"
                try:
                    dis.distb(sys.exc_info()[2])
                except:
                    print "''' Could not disassemble traceback '''\n"
                return sys.stdout.read()
            finally:
                sys.stdout = oldOut

        oldOut = sys.stdout
        sys.stdout = Utils.PseudoFileOutStore()
        try:
            try:
                dis.disco(code)
            except:
                raise
            return sys.stdout.read()
        finally:
            sys.stdout = oldOut

        return 'Invisible code'

    def runLint(self):
        filename = self.assertLocalFile()
        from ExternalLib import pylint
        import StringIO
        pylint.pylint(StringIO.StringIO(self.data), filename)
        if pylint.warnings:
            return ErrorStack.buildLintWarningList(pylint.warnings[:])

    def buildImportSearchPath(self):
        try: filename = self.assertLocalFile()
        except AssertionError: srchpath = []
        else: srchpath = [os.path.dirname(filename)]
        if self.app:
            try: appfilename = self.app.assertLocalFile()
            except AssertionError: pass
            else: srchpath.insert(0, os.path.dirname(appfilename))

        return srchpath

    def findModule(self, modName, impName=''):
        """ Tries it's best to locate given module name or raise ImportError """
        # first search std python modules
        stdPyPath = sys.path[1:]
        srchpath = stdPyPath[:]
        for name in modName.split('.'):
            try:
                file, path, (ext, mode, tpe) = imp.find_module(name, srchpath)
            except ImportError:
                if srchpath == stdPyPath:
                    # else search module and app dirs
                    srchpath = self.buildImportSearchPath()
                    file, path, (ext, mode, tpe) = imp.find_module(name, srchpath)
                else:
                    raise

            if srchpath == stdPyPath:
                srchpath = []

            if tpe == imp.PKG_DIRECTORY:
                srchpath.append(path)
                continue
            elif tpe == imp.PY_SOURCE:
                # handle from [package.]module import name
                return path, 'name'
            if tpe == imp.PY_COMPILED:
                self.editor.setStatus('Compiled file found, check sys.path!',
                      'Warning', true)
                raise ImportError('Compiled file found')
            else:
                raise ImportError('Unhandled import type')
        # handle from package import module
        if srchpath and srchpath != stdPyPath:
            if impName:
                path = os.path.join(srchpath[-1], impName+'.py')
                if os.path.isfile(path):
                    return path, 'module'
            else:
                return srchpath[-1], 'package'

        #print '%s not found in %s'%(modName, `srchpath`)
        raise ImportError('Module not found')

    def importInShell(self):
        modDir, modFile = os.path.split(self.assertLocalFile())
        modName = os.path.splitext(modFile)[0]
        if self.app:
            execDir = os.path.dirname(self.app.assertLocalFile())
            if execDir != modDir:
                p, m = os.path.split(relpath.relpath(execDir, self.assertLocalFile()))
                p = p.replace('/', '.')
                p = p.replace('\\', '.')
                pckName = p
                impExecStr = 'from %s import %s'%(pckName, modName)
            else:
                impExecStr = 'import %s'%modName

        else:
            execDir = modDir
            impExecStr = 'import %s'%modName

        shell = self.editor.shell
        if execDir not in sys.path:
            sys.path.append(execDir)
            shell.pushLine("print '## Appended to sys.path'")
        else:
            info = ''

        shell.pushLine(impExecStr, impExecStr)
        if shell.lastResult != 'stderr':
            return 'Import of %s successfull'%modName, 'Info'
        else:
            return 'Import of %s failed'%modName, 'Error'

    def reloadInShell(self):
        modDir, modFile = os.path.split(self.assertLocalFile())
        modName = os.path.splitext(modFile)[0]
        impExecStr = 'reload(%s)'%modName

        shell = self.editor.shell
        shell.pushLine(impExecStr, impExecStr)

        if shell.lastResult != 'stderr':
            return 'Reload of %s successfull'%modName, 'Info'
        else:
            return 'Reload of %s failed'%modName, 'Error'

    def findGlobalDict(self, name):
        s = name+' ='
        pos = self.data.find(s)
        if pos == -1:
            raise 'Global dict %s not found in the module, please add it.'%name
        end = self.data.find('}\n', pos + len(s) +1) + 1
        if not end:
            end = self.data.find('}\r\n', pos + len(s) +1) + 1
            if not end:
                raise 'Global dict %s not terminated properly, please fix it.'%name
        return pos + len(s), end

    def readGlobalDict(self, name):
        start, end = self.findGlobalDict(name)
        try:
            return eval(Utils.toUnixEOLMode(self.data[start:end]), wx.__dict__)
        except Exception, err:
            raise '"%s" must be a valid dictionary global dict.\nError: %s'%(name, str(err))

    def writeGlobalDict(self, name, dct):
        start, end = self.findGlobalDict(name)
        eol = Utils.getEOLMode(self.data)
        self.data = self.data[:start]+pprint.pformat(dct).replace('\n', eol)+\
              self.data[end:]

    def buildResourceSearchList(self):
        searchPath = [os.path.abspath(os.path.dirname(self.localFilename()))]
        if self.app:
            searchPath.append(os.path.abspath(os.path.dirname(self.app.localFilename())))
        return searchPath

    def loadResource(self, importName, searchPath):
        d={}
        syspath = sys.path[:]
        sys.path[:] = searchPath
        try:
            try:
                exec 'import %s'%importName in d
                exec 'reload(%s)'%importName in d
            finally:
                sys.path[:] = syspath
            imageMod = eval(importName, d)
            del d['__builtins__']
            rootModName, rootMod = d.items()[0]
        finally:
            #try: del sys.modules[importName]
            #except KeyError: pass
            del d

        return imageMod, rootModName, rootMod

    def assureResourceLoaded(self, importName, resources, searchPath=None,
                             specialAttrs=None, report=false):
        if searchPath is None:
            searchPath = self.buildResourceSearchList()

        try:
            f, fn, desc = Utils.find_dotted_module(importName, searchPath)
        except ImportError:
            if report:
                self.editor.setStatus('Could not find %s'%importName, 'Error')
            return false
        
        if f is None:
            return false
        
        f.close()
        
        import Controllers
        Model, main = Controllers.identifyFile(fn)
        for ResourceClass in Controllers.resourceClasses:
            if issubclass(Model, ResourceClass):
                try:
                    imageMod, rootName, rootMod = self.loadResource(importName, 
                                                                    searchPath)
                    resources[importName] = imageMod
                    specialAttrs[rootName] = rootMod
                    if report:
                        self.editor.setStatus('Loaded resource: %s'%importName)
                except ImportError:
                    self.editor.setStatus('Could not load %s'%importName, 'Error')
                    return false
                return true

        if report:
            self.editor.setStatus('%s is not a valid Resource Module'%importName, 'Error')
        return false
    
    def readResources(self, mod, cls, specialAttrs):
        resources = {}
        searchPath = self.buildResourceSearchList()
        for impName in mod.imports.keys():
            self.assureResourceLoaded(impName, resources, searchPath, specialAttrs)
        return resources



class ClassModel(ModuleModel):
    """ Represents access to 1 maintained main class in the module.
        This class is identified by the 3rd header entry  #Boa:Model:Class """
    def __init__(self, data, name, main, editor, saved, app = None):
        self.main = main
        self.mainConstr = None
        ModuleModel.__init__(self, data, name, editor, saved, app)

    def renameMain(self, oldName, newName):
        self.getModule().renameClass(oldName, newName)
        self.main = newName

        idx = 0
        for line in self.getModule().source:
            if line:
                if line[0] != '#': break

                header = line.strip().split(':')
                if (len(header) == 3) and (header[0] == sourceconst.boaIdent):
                    self.getModule().source[idx] = \
                    ':'.join((header[0], header[1], newName))
                    break
            else: break
            idx = idx + 1

class ImportRelationshipMix:
    def buildImportRelationshipDict(self, modules):
        relationships = {}

        tot = len(modules)
        self.editor.statusBar.progress.SetRange(tot)
        try:
            prog = 0
            totLOC = 0
            classCnt = 0
            # XXX Rewrite in terms of transport
            for module in modules:
                self.editor.statusBar.progress.SetValue(prog)
                prog = prog + 1
                self.editor.setStatus('Parsing '+module+'...')
                #module = self.modules[moduleName]
                #filename = self.normaliseModuleRelativeToApp(module[2])
                if module[:7] != 'file://':
                    print '%s skipped, only local files supported for Imports View'
                else:
                    module = module[7:]
                try: f = open(module)
                except IOError:
                    print "couldn't load %s" % module
                    continue
                else:
                    data = f.read()
                    f.close()
                    name = os.path.splitext(os.path.basename(module))[0]
                    model = ModuleModel(data, name, self.editor, 1)
                    relationships[name] = model.getModule() #.imports

                totLOC = totLOC + model.getModule().loc
                classCnt = classCnt + len(model.getModule().classes)

            print 'Project LOC: %d,\n%d classes in %d modules.'%(totLOC, classCnt, len(modules))
        finally:
            self.editor.statusBar.progress.SetValue(0)
            self.editor.statusBar.setHint('')
        return relationships

class PackageModel(ModuleModel, ImportRelationshipMix):
    """ Must be constructed in a valid path, name being filename, actual
        name will be derived from path """

    modelIdentifier = 'Package'
    defaultName = 'package'
    bitmap = 'Package.png'
    imgIdx = imgPackageModel
    pckgIdnt = '__init__.py'
    ext = '.py'

    def __init__(self, data, name, editor, saved, app=None):
        ModuleModel.__init__(self, data, name, editor, saved, app)
        self.packagePath = os.path.split(self.filename)[0]
        self.packageName = os.path.split(self.packagePath)[1]
        self.savedAs = true
        self.modified = false

    def openPackage(self, name):
        if self.views.has_key('Folder'):
            notebook = self.views['Folder']
        else:
            notebook = None
        self.editor.openOrGotoModule(os.path.join(self.packagePath, name,
              self.pckgIdnt), notebook=notebook)

    def openFile(self, name):
        if self.views.has_key('Folder'):
            notebook = self.views['Folder']
        else:
            notebook = None
        self.editor.openOrGotoModule(os.path.join(self.packagePath,
              name + self.ext), notebook=notebook)

    def generateFileList(self):
        """ Generate a list of modules and packages in the package path """

        from Explorers.Explorer import openEx
        transp = openEx(self.packagePath)

        filtered = []
        for item in transp.openList():
            if item.treename != '__init__.py' and \
                  (os.path.splitext(item.treename)[1] == self.ext or \
                   item.imgIdx == imgPackageModel):
                filtered.append(item)
        return filtered

    def getPageName(self):
        return self.packageName

    def buildImportRelationshipDict(self):
        mods = []
        for module in self.generateFileList():
            mods.append('file://'+module.resourcepath)

        return ImportRelationshipMix.buildImportRelationshipDict(self, mods)

class PythonBinaryFileModel(PersistentModel):
    modelIdentifier = 'PythonBinary'
    defaultName = ''
    bitmap = 'PythonBinary.png'
    imgIdx = imgPythonBinaryFileModel
    ext = '.pybin'

SimpleTypes = [types.StringType, types.IntType, types.FloatType, types.NoneType,
               types.DictionaryType, types.ListType, types.TupleType]
try: SimpleTypes.append(types.UnicodeType)
except AttributeError: pass

FunctionTypes = [types.FunctionType, types.BuiltinFunctionType]

MethodTypes = [types.MethodType, types.BuiltinMethodType]
PrivMethodTypeNames = ['method_descriptor', 'method-wrapper']

class PyExtTypeData:
    def __init__(self, Type):
        self.methods = []
        self.attrs = {}
        for name in dir(Type):
            attr = getattr(Type, name)
            AttrType = type(attr)
            if AttrType in MethodTypes or \
                  AttrType.__name__ in PrivMethodTypeNames:
                self.methods.append(name)
            else:
                self.attrs[name] = attr

class PyExtModuleData:
    def __init__(self, module):
        self.classes = {}
        self.functions = {}
        self.attrs = {}
        self.modules = {}
        for name in dir(module):
            attr = getattr(module, name)
            AttrType = type(attr)
            if AttrType in SimpleTypes:
                self.attrs[name] = attr
            elif AttrType is types.ClassType:
                self.classes[name] = PyExtTypeData(attr)
            elif AttrType in FunctionTypes:
                self.functions[name] = attr
            elif AttrType is types.ModuleType:
                self.modules[name] = PyExtModuleData(attr)
            elif hasattr(attr, '__class__'):
                self.classes[name] = PyExtTypeData(attr)
            else:
                # fallback attributes
                self.attrs[name] = attr


class PythonExtensionFileModel(PythonBinaryFileModel):
    modelIdentifier = 'PythonExtension'
    defaultName = ''
    bitmap = 'PythonBinary.png'
    imgIdx = imgPythonBinaryFileModel
    ext = '.pyd'

    def __init__(self, data, name, editor, saved):
        # XXX data not read as binary anyway
        PythonBinaryFileModel.__init__(self, '', name, editor, true)

        filename = self.checkLocalFile()
        dirName, pydName = os.path.split(filename)
        modName = os.path.splitext(pydName)[0]
        sys.path.insert(0, dirName)
        try:
            self.module = __import__(modName)
        finally:
            del sys.path[0]

        self.moduleData = PyExtModuleData(self.module)

class PythonCompiledFileModel(PythonBinaryFileModel):
    modelIdentifier = 'PythonCompiled'
    defaultName = ''
    bitmap = 'PythonBinary.png'
    imgIdx = imgPythonBinaryFileModel
    ext = '.pyc'


class BaseAppModel(ClassModel, ImportRelationshipMix):
    def __init__(self, data, name, main, editor, saved, openModules):
        self.moduleModels = {}
        self.textInfos = {}
        self.unsavedTextInfos = []
        self.modules = {}
        self.app = self
        ClassModel.__init__(self, data, name, main, editor, saved, self)
        if data:
            self.update()
            self.notify()

        # Connect all open modules to this app obj if they are defined in
        # the app's modules
        import Controllers
        abspaths = self.absModulesPaths()
        for modPage in openModules.values():
            if modPage.model.modelIdentifier not in Controllers.appModelIdReg \
                  and hasattr(modPage.model, 'app') and \
                  modPage.model.filename in abspaths:
                modPage.model.app = self

    def absModulesPaths(self):
        modules = self.modules.keys()
        abspaths = []
        for moduleName in modules:
            abspaths.append(self.normaliseModuleRelativeToApp(self.modules[moduleName][2]))
        return abspaths

    def convertToUnixPath(self, filename):
        # Don't convert absolute windows paths, will stay illegal until saved
        if os.path.splitdrive(filename)[0] != '':
            return filename
        else:
            return filename.replace('\\', '/')

    def save(self, overwriteNewer=false):
        ClassModel.save(self, overwriteNewer)
        for tin in self.unsavedTextInfos:
            fn = os.path.join(os.path.dirname(self.filename), tin)
            data = self.textInfos[tin]
            if data:
                from Explorers.Explorer import openEx, TransportError
                try:
                    f = openEx(fn)
                    f.save(f.currentFilename(), data)
                except TransportError, err:
                    pass
        self.unsavedTextInfos = []

    def saveAs(self, filename):
        for mod in self.modules.keys():
            self.modules[mod][2] = self.convertToUnixPath(\
              relpath.relpath(os.path.dirname(filename),
              self.normaliseModuleRelativeToApp(self.modules[mod][2])))

        self.writeModules()

        ClassModel.saveAs(self, filename)

        self.notify()

    def findImports(self):
        impPos = self.data.find(sourceconst.defImport.strip())
        impPos = self.data.find('import', impPos + 1)

        # XXX Add if not found
        if impPos == -1: raise 'Module import list not found in application'
        impEnd = self.data.find('\012', impPos + len('import') +1) + 1
        if impEnd == -1: raise 'Module import list not terminated'
        return impPos + len('import'), impEnd

    def idModel(self, name, src=None):
        # XXX This should be cached until rename or delete
        absPath = self.normaliseModuleRelativeToApp(self.modules[name][2])
        import Controllers

        from Explorers.Explorer import splitURI
        prot, cat, res, fn = splitURI(absPath)

        if src is None:
            if self.editor.modules.has_key(name):
                self.moduleModels[name], main = identifySource(
                    self.editor.modules[name].model.getDataAsLines())
            if self.editor.modules.has_key(absPath):
                self.moduleModels[name], main = identifySource(
                    self.editor.modules[absPath].model.getDataAsLines())
            else:
                try: self.moduleModels[name], main = \
                           Controllers.identifyFile(res, localfs=prot=='file')
                except: pass
        else:
            self.moduleModels[name], main = identifySource(src)

    def readModules(self):
        self.modules = self.readGlobalDict('modules')

        for mod in self.modules.keys():
            self.idModel(mod)

    def writeModules(self, notify=true):
        self.writeGlobalDict('modules', self.modules)

        self.modified = true
        self.editor.updateTitle()
        self.editor.updateModulePage(self)

        if notify: self.notify()

    def viewAddModule(self):
        fn = self.editor.openFileDlg()
        if fn:
            self.addModule(fn, '')

    def addModule(self, filename, descr, source=None):
        name, ext = os.path.splitext(os.path.basename(filename))
        if self.modules.has_key(name):
            raise Exception('Module name exists in application')
        if self.savedAs:
            relative = relpath.relpath(os.path.dirname(self.filename), filename)
        else:
            relative = filename
        self.modules[name] = [0, descr, self.convertToUnixPath(relative)]

        self.idModel(name, source)

        self.writeModules()

    def removeModule(self, name):
        if not self.modules.has_key(name): raise 'No such module in application'

        del self.modules[name]
        self.writeModules()

    def editModule(self, oldname, newname, main, descr):
        relpath = self.modules[oldname][2]
        if oldname != newname:
            del self.modules[oldname]
        self.modules[newname] = [main, descr, relpath]
        self.writeModules()

    def splitProtFile(self, uri):
        protsplit = uri.split('://')
        if len(protsplit) == 1:
            return 'file', uri
        elif len(protsplit) == 2:
            return protsplit
        else:
            raise 'Unhandled protocol %s'%uri

    def moduleFilename(self, name):
        """ Return absolute filename of the given module """
        if not self.modules.has_key(name):
            raise 'No such module in application: '+name

        prot, modFilename = self.splitProtFile(self.modules[name][2])
        if self.savedAs:
            if os.path.isabs(modFilename) or prot != 'file':
                absPath = self.modules[name][2]
            else:
                appProt, appFilename = self.splitProtFile(self.filename)
                absPath = appProt+'://'+self.convertToUnixPath(os.path.normpath(
                      os.path.join(os.path.dirname(appFilename), modFilename)))
        else:
            #absPath = name + ModuleModel.ext
            absPath = self.modules[name][2]
        return absPath

    def updateAutoCreateImports(self, oldName, newName):
        """ Rename module in import list.

            Only autocreated modules should be on this list.
            The module is modified and the model is not updated"""
        module = self.getModule()
        if module.imports.has_key(oldName):
            impLine = module.imports[oldName][0]-1
            # read in the import line
            line = module.source[impLine]
            impIndent = line.find('import')
            imports = line[7+impIndent:].strip().split(', ')
            impIdx = imports.index(oldName)
            imports[impIdx] = newName
            module.imports[newName] = module.imports[oldName]
            del module.imports[oldName]
            module.source[impLine] = 'import '+', '.join(imports)
            return impIdx
        return None

    def updateMainFrameModuleRefs(self, oldName, newName):
        """ Replace references to old main module with new main module """
        module = self.getModule()
        block = module.classes[sourceconst.boaClass].methods['OnInit']
        mainDef = 'self.main = %s.'
        fndOldStr = mainDef % oldName
        repNewStr = mainDef % newName

        for idx in range(block.start, block.end):
            line = module.source[idx]
            newLine = line.replace(fndOldStr, repNewStr)
            if newLine != line:
                module.source[idx] = newLine

    def changeMainFrameModule(self, newMainFrameModule):
        """ Select a new main frame module """
        if len(self.viewsModified):
            self.refreshFromViews()

        # determine which module is the main module
        module = self.getModule()
        #for mod, props in filter(lambda v: v[1][0], self.modules.items()):
        for mod, props in [i for i in self.modules.items() if i[1][0]]:
            impLine = module.imports[mod][0]-1
            line = module.source[impLine]
            impIndent = line.find('import')
            imports = line[7+impIndent:].split(', ')
            if len(imports) and imports[0] == mod:
                try:
                    impIdx = imports.index(newMainFrameModule)
                except ValueError:
                    impIdx = 0
                del imports[impIdx]

                imports.insert(0, newMainFrameModule)
                module.source[impLine] = impIndent*' '+'import '+', '.join(imports)

                self.updateMainFrameModuleRefs(mod, newMainFrameModule)
                self.refreshFromModule()

                # Update autocreation status
                props[0] = 0
                self.modules[newMainFrameModule][0] = 1
                self.writeModules(false)

                self.update()
                self.notify()
                break
        else:
            raise 'No main frame module found in application'

    def moduleSaveAsNotify(self, module, oldFilename, newFilename):
        if module != self:
            newName, ext = os.path.splitext(os.path.basename(newFilename))
            oldName = os.path.splitext(os.path.basename(oldFilename))[0]

            if not self.modules.has_key(oldName):
                raise 'Module does not exists in application'

            if self.savedAs:
                relative = relpath.relpath(os.path.dirname(self.filename), newFilename)
            else:
                relative = newFilename

            if newName != oldName:
                self.modules[newName] = self.modules[oldName]
                del self.modules[oldName]
            self.modules[newName][2] = self.convertToUnixPath(relative)

            # Check if it's autocreated module
            if self.modules[newName][0]:
                if len(self.viewsModified):
                    self.refreshFromViews()

                impIdx = self.updateAutoCreateImports(oldName, newName)

                if impIdx is not None:
                    # check if it's the main module, first in the import list is
                    # always the main module
                    if not impIdx:
                        self.updateMainFrameModuleRefs(oldName, newName)

                    # preserve modified modules
                    mods = self.modules
                    self.refreshFromModule()
                    self.modules = mods

            self.writeModules()
            self.update()

    def crashLog(self):
        err = ErrorStack.crashError(os.path.splitext(self.assertLocalFile())[0]+'.trace')
        if err:
            frm = self.editor.erroutFrm
            if frm:
                frm.updateCtrls(err)
                frm.display(err)
                return frm
        else:
            wx.wxLogError('Trace file not found. Run with command line param -T')
            return None

    def openModule(self, name):
        from Explorers.Explorer import TransportError
        try:
            return self.editor.openOrGotoModule(self.moduleFilename(name), self)
        except TransportError, err:
            if str(err) == 'Unhandled transport' and err[1][0] == 'none':
                if wx.wxMessageBox('Unsaved file no longer open in the Editor.\n'
                      'Remove it from application modules ?', 'Missing file',
                      wx.wxYES_NO | wx.wxICON_QUESTION) == wx.wxYES:
                    self.removeModule(name)
                return None, None
            else:
                raise

    def normaliseModuleRelativeToApp(self, relFilename):
        """ Normalise relative paths to absolute paths """
        if not self.savedAs or relFilename.startswith('none://'):
            return relFilename
        else:
            protsplit = self.filename.split('://')
            if len(protsplit) == 1:
                prot, appFilename = 'file', self.filename
            elif len(protsplit) == 2:
                prot, appFilename = protsplit
            elif len(protsplit) == 3:
                prot, archive, appFilename = protsplit
            else:
                raise Exception, 'Unhandled protocol during normalisation:%s'%protsplit

            if prot == 'zip':
                return relFilename
            
            normedpath = os.path.normpath(os.path.join(os.path.dirname(appFilename),
                  relFilename))
            if prot == 'file':
                return '%s://%s' %(prot, normedpath)
            else:
                return '%s://%s' %(prot, normedpath.replace('\\', '/'))

    def buildImportRelationshipDict(self):
        return ImportRelationshipMix.buildImportRelationshipDict(self,
               self.absModulesPaths())

    def update(self):
        self.readModules()
        ClassModel.update(self)

    def loadTextInfo(self, viewName):
        from Explorers.Explorer import openEx, TransportError
        fn = os.path.join(os.path.dirname(self.filename), viewName)
        ti = openEx(fn)
        try:
            data = ti.load()
        except TransportError, err:
            data = ''
        self.textInfos[viewName] = data

class PyAppModel(BaseAppModel):
    modelIdentifier = 'PyApp'
    defaultName = 'PyApp'
    bitmap = 'PythonApplication.png'
    imgIdx = imgPyAppModel

    def getDefaultData(self):
        return (sourceconst.defEnvPython + sourceconst.defSig + \
                sourceconst.defPyApp) %{'modelIdent': self.modelIdentifier,
                                        'main': 'main'}

class SetupModuleModel(ModuleModel):
    modelIdentifier = 'setup'
    defaultName = 'Setup'
    bitmap = 'Setup.png'
    imgIdx = imgSetupModel
    def __init__(self, data, name, editor, saved, app=None):
        ModuleModel.__init__(self, data, name, editor, saved, app)
        if data:
            self.update()
            self.notify()

    def getDefaultData(self):
        return (sourceconst.defSetup_py) % {'name': 'default', 'version': '0.1',
                                            'scripts': ''}

    def getPageName(self):
        return 'setup (%s)' % os.path.basename(os.path.dirname(self.filename))

   
##    def saveAs(self, filename):
##        # catch image type changes
##        newExt = os.path.splitext(filename)[1].lower()
##        oldExt = os.path.splitext(self.filename)[1].lower()
##        updateViews = 0
##        if newExt != oldExt:
##            updateViews = 1
##            bmp = wx.wxBitmapFromImage(wx.wxImageFromStream(StringIO(self.data)))
##            fn = tempfile.mktemp(newExt)
##            try:
##                bmp.SaveFile(fn, self.extTypeMap[newExt])
##            except KeyError:
##                raise Exception, '%s image file types not supported'%newExt
##            try:
##                # convert data to new image format
##                self.data = open(fn, 'rb').read()
##            finally:
##                os.remove(fn)
##
##        # Actually save the file
##        PersistentModel.saveAs(self, filename)


#-------------------------------------------------------------------------------

def identifyHeader(headerStr):
    header = headerStr.split(':')
    if len(header) and (header[0] == sourceconst.boaIdent) and \
          EditorHelper.modelReg.has_key(header[1]):
        return EditorHelper.modelReg[header[1]], header[2]
    return ModuleModel, ''

def identifySource(source):
    """ Return appropriate model for given Python source.
        The logic is a copy paste from above func """
    for line in source:
        if line:
            if line[0] != '#':
                return ModuleModel, ''

            headerInfo = identifyHeader(line.strip())

            if headerInfo[0] != ModuleModel:
                return headerInfo
        else:
            return ModuleModel, ''
    return ModuleModel, ''    


#-------------------------------------------------------------------------------

EditorHelper.modelReg.update({
            PyAppModel.modelIdentifier: PyAppModel,
            ModuleModel.modelIdentifier: ModuleModel,
            SetupModuleModel.modelIdentifier: SetupModuleModel,
            PackageModel.modelIdentifier: PackageModel,
            PythonBinaryFileModel.modelIdentifier: PythonBinaryFileModel,
            PythonExtensionFileModel.modelIdentifier: PythonExtensionFileModel,
            })

EditorHelper.inspectableFilesReg['.py'] = ModuleModel
