#----------------------------------------------------------------------
# Name:        EditorModels.py
# Purpose:     Model classes usually representing different types of
#              source code
#
# Author:      Riaan Booysen
#
# Created:     1999
# RCS-ID:      $Id: EditorModels.py,v 1.21 2007/07/02 15:01:12 riaan Exp $
# Copyright:   (c) 1999 - 2007 Riaan Booysen
# Licence:     GPL
#----------------------------------------------------------------------

""" The model classes represent different types of source code files,
    Different views can be connected to a model  """

print 'importing Models.EditorModels'

import os, sys, tempfile
from StringIO import StringIO

import wx

import Preferences, Utils, EditorHelper
from Preferences import keyDefs
from Utils import _

_vc_hook = None

class EditorModel:
    defaultName = 'abstract'
    bitmap = 'None'
    imgIdx = -1
    objCnt = 0
    plugins = ()
    def __init__(self, data, name, editor, saved):
        self.active = False
        self.data = data
        self.savedAs = saved
        self.filename = name
        self.editor = editor
        self.transport = None
        self.prevSwitch = None

        self.views = {}
        self.modified = not saved
        self.viewsModified = []
        
        plugins = {}
        for Plugin in self.plugins:
            plugins[Plugin.name] = Plugin(self)
        self.plugins = plugins

    def destroy(self):
        self.views = ()
        self.viewsModified = ()
        self.plugins = ()

    def updateNameFromTransport(self):
        if self.transport:
            self.filename = self.transport.getURI()

    def reorderFollowingViewIdxs(self, idx):
        for view in self.views.values():
            if view.pageIdx > idx:
                view.pageIdx = view.pageIdx - 1

    def getDataAsLines(self):
        return StringIO(self.data).readlines()

    def setDataFromLines(self, lines):
        data = self.data
        strlines = []
        for line in lines:
            # XXX This is an unneeded bottleneck, why not on the joined str ???
            # encodes unicode in the default encoding
            strlines.append(Utils.stringFromControl(line))
        self.data = ''.join(strlines)
        self.modified = self.modified or self.data != data

    def hasUnsavedChanges(self):
        return self.modified or len(self.viewsModified)

    def notify(self):
        """ Update all views connected to this model.
            This method must be called after changes were made to the model """
        for view in self.views.values():
            view.update()

    def update(self):
        """ Rebuild additional derived structure, called when data is changed """
        for plugin in self.plugins:
            self.plugins[plugin].update()

    def refreshFromViews(self):
        for view in self.viewsModified:
            self.views[view].refreshModel()

    def getPageName(self):
        if getattr(Preferences, 'showFilenameExtensions', 0):
            return os.path.basename(self.filename)
        else:
            return os.path.splitext(os.path.basename(self.filename))[0]

    # XXX Move these names into overrideable attrs
    def getSourceView(self):
        views = self.views
        if views.has_key('Source'):
            return views['Source']
        elif views.has_key('ZopeHTML'):
            return views['ZopeHTML']
        return None




class FolderModel(EditorModel):
    modelIdentifier = 'Folder'
    defaultName = 'folder'
    bitmap = 'Folder.png'
    imgIdx = EditorHelper.imgFolder

    def __init__(self, data, name, editor, filepath):
        EditorModel.__init__(self, data, name, editor, True)
        self.filepath = filepath

class SysPathFolderModel(FolderModel):
    modelIdentifier = 'SysPathFolder'
    defaultName = 'syspathfolder'
    bitmap = 'Folder_green.png'
    imgIdx = EditorHelper.imgPathFolder

class CVSFolderModel(FolderModel):
    modelIdentifier = 'CVS Folder'
    defaultName = 'cvsfolder'
    bitmap = 'Folder_cyan.png'
    imgIdx = EditorHelper.imgCVSFolder

    def __init__(self, data, name, editor, filepath):
        FolderModel.__init__(self, data, name, editor, filepath)
        self.readFiles()

    def readFile(self, filename):
        f = open(filename, 'r')
        try: return f.read().strip()
        finally: f.close()

    def readFiles(self):
        self.root = self.readFile(os.path.join(self.filepath, 'Root'))
        self.repository = self.readFile(os.path.join(self.filepath, 'Repository'))
        self.entries = []

        f = open(os.path.join(self.filepath, 'Entries'), 'r')
        dirpos = 0
        try:
            txtEntries = f.readlines()
            for txtEntry in txtEntries:
                txtEntry = txtEntry.strip()
                if txtEntry:
                    if txtEntry == 'D':
                        pass
                        # maybe add all dirs?
                    elif txtEntry[0] == 'D':
                        self.entries.insert(dirpos, CVSDir(txtEntry))
                        dirpos = dirpos + 1
                    else:
                        try:
                            self.entries.append(CVSFile(txtEntry, self.filepath))
                        except IOError: pass
        finally:
            f.close()

class BasePersistentModel(EditorModel):
    fileModes = ('rb', 'wb')
    saveBmp = 'Images/Editor/Save.png'
    saveAsBmp = 'Images/Editor/SaveAs.png'

    def load(self, notify=True):
        """ Loads contents of data from file specified by self.filename.
            Note: Load's not really used much currently cause objects are
                  constructed with their data as parameter """
        if not self.transport:
            raise Exception, _('No transport for loading')

        self.data = self.transport.load(mode=self.fileModes[0])
        self.modified = False
        self.saved = False
        self.update()
        if notify: self.notify()

    def save(self, overwriteNewer=False):
        """ Saves contents of data to file specified by self.filename. """
        if not self.transport:
            raise Exception, _('No transport for saving')

        if self.filename:
            filename = self.transport.assertFilename(self.filename)
            # this check is to minimise interface change.
            if overwriteNewer:
                self.transport.save(filename, self.data, mode=self.fileModes[1],
                      overwriteNewer=True)
            else:
                self.transport.save(filename, self.data, mode=self.fileModes[1])
            self.modified = False
            self.saved = True

            for view in self.views.values():
                view.saveNotification()

            if _vc_hook:
                _vc_hook.save(filename, self.data, mode=self.fileModes[1])
        else:
            raise Exception, _('No filename')

    def saveAs(self, filename):
        """ Saves contents of data to file specified by filename.
            Override this to catch name changes. """
        # Catch transport changes
        from Explorers.Explorer import splitURI, getTransport
        protO, catO, resO, uriO = splitURI(self.filename)
        protN, catN, resN, uriN = splitURI(filename)

        if protO != protN:
            self.transport = getTransport(protN, catN, resN,
                  self.editor.explorerStore.transports)#explorer.tree.transports)

        # Rename and save
        oldname = self.filename
        self.filename = filename
        try:
            self.save(overwriteNewer=True)
        except:
            self.filename = oldname
            raise
        self.savedAs = True

    def localFilename(self, filename=None):
        if filename is None: filename = self.filename
        from Explorers.Explorer import splitURI
        return splitURI(filename)[2]

    def assertLocalFile(self, filename=None):
        # XXX depreciated (and silly!)

        if filename is None:
            filename = self.filename
        from Explorers.Explorer import splitURI
        prot, cat, filename, uri = splitURI(filename)
        assert prot=='file', _('Operation only supported on the filesystem.')
        return filename

    def checkLocalFile(self, filename=None):
        """ Either return the model's uri as a local filepath or raise an error """

        if filename is None:
            filename = self.filename
        from Explorers.Explorer import splitURI, TransportError
        prot, cat, filename, uri = splitURI(filename)
        if prot != 'file':
            raise TransportError, _('Operation only supported on the filesystem.')
        return filename

    def getDefaultData(self):
        return ''

    def new(self):
        self.data = self.getDefaultData()
        self.savedAs = False
        self.modified = True
        self.update()
        self.notify()

class PersistentModel(BasePersistentModel):
    def __init__(self, data, name, editor, saved):
        BasePersistentModel.__init__(self, data, name, editor, saved)
        if data: self.update()

    def load(self, notify=True):
        BasePersistentModel.load(self, False)
        self.update()
        if notify: self.notify()

class BitmapFileModel(PersistentModel):
    modelIdentifier = 'Bitmap'
    defaultName = 'bitmap'
    bitmap = 'Bitmap.png'
    imgIdx = EditorHelper.imgBitmapFileModel
    ext = '.bmp'

    fileModes = ('rb', 'wb')

    extTypeMap = {'.bmp': wx.BITMAP_TYPE_BMP, #'.gif': wx.BITMAP_TYPE_GIF,
                  '.jpg': wx.BITMAP_TYPE_JPEG, '.png': wx.BITMAP_TYPE_PNG}

    def save(self, overwriteNewer=False):
        ext = os.path.splitext(self.filename)[1].lower()
        if ext == '.gif':
            raise Exception, _('Saving .gif format not supported')

        PersistentModel.save(self, overwriteNewer)

    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
            import cStringIO
            bmp = wx.BitmapFromImage(wx.ImageFromStream(
                  cStringIO.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)

        if updateViews:
            self.notify()

class SourceModel(BasePersistentModel):
    modelIdentifier = 'Source'
    def __init__(self, data, name, editor, saved):
        BasePersistentModel.__init__(self, data, name, editor, saved)

    def getCVSConflicts(self):
        # needless obscurity
        # numedLines = apply(map, (None,) + (lines, range(len(lines))) )

        # use model.module.source
        conflictStart = -1
        confCnt = 0
        lineNo = 0
        conflicts =[]
        for line in self.getDataAsLines():
            if line[:8] == '<<<<<<< ' and \
                  line[8:].strip() == os.path.basename(self.filename):
                conflictStart = lineNo
            if line[:8] == '>>>>>>> ':
                rev = line[8:]
                conflicts.append( (rev, conflictStart, lineNo - conflictStart) )
                confCnt = confCnt + 1
            lineNo = lineNo + 1
        return conflicts

    def applyChangeBlock(self, conflict, blockIdx):
        rev, start, size = conflict
        lines = self.getDataAsLines()

        blocks = Utils.split_seq(lines[start+1 : start+size], '=======')
        lines[start:start+size+1] = blocks[blockIdx]
        self.setDataFromLines(lines)

        self.update()
        self.notify()

        self.editor.updateModuleState(self)

    def acceptConflictChange(self, conflict):
        self.applyChangeBlock(conflict, 1)

    def rejectConflictChange(self, conflict):
        self.applyChangeBlock(conflict, 0)


class TextModel(PersistentModel):
    modelIdentifier = 'Text'
    defaultName = 'text'
    bitmap = 'Text.png'
    imgIdx = EditorHelper.imgTextModel
    ext = '.txt'

class UnknownFileModel(TextModel):
    modelIdentifier = 'Unknown'
    defaultName = '*'
    bitmap = 'Unknown.png'
    imgIdx = EditorHelper.imgUnknownFileModel
    ext = '.*'

class InternalFileModel(TextModel):
    modelIdentifier = 'Internal'
    defaultName = ''
    bitmap = 'InternalFile.png'
    imgIdx = EditorHelper.imgInternalFileModel
    ext = '.intfile'

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

modelReg = EditorHelper.modelReg
extMap = EditorHelper.extMap

# model registry: add to this dict to register a Model (needed for explorer images)
modelReg.update({
            TextModel.modelIdentifier: TextModel,
            UnknownFileModel.modelIdentifier: UnknownFileModel,
            BitmapFileModel.modelIdentifier: BitmapFileModel,
            InternalFileModel.modelIdentifier: InternalFileModel,
            })

extMap[''] = TextModel
extMap['.jpg'] = extMap['.gif'] = extMap['.png'] = extMap['.ico'] = BitmapFileModel

EditorHelper.imageExtReg.extend(['.bmp', '.jpg', '.gif', '.png', '.ico'])
EditorHelper.internalFilesReg.extend(['.umllay', '.implay', '.brk', '.trace', '.stack', '.cycles', '.prof', '.cached'])
EditorHelper.binaryFilesReg.extend(['.zexp', '.prof'])
