#-----------------------------------------------------------------------------
# Name:        SVNExplorer.py
# Purpose:
#
# Author:      Riaan Booysen
#
# Created:     2006
# RCS-ID:      $Id: SVNSupport.plug-in.py,v 1.5 2007/07/05 14:03:34 riaan Exp $
# Copyright:   (c) 2007 Riaan Booysen
# Licence:     GPL
#-----------------------------------------------------------------------------
""" Explorer classes for SVN browsing and operations """

import time, stat, os

import wx
from wx.lib.dialogs import ScrolledMessageDialog


from Explorers import ExplorerNodes, scrm
from Models import EditorModels, EditorHelper
from Preferences import IS

import ProcessProgressDlg, Utils

(wxID_SVNUPDATE, wxID_SVNCOMMIT, wxID_SVNADD, wxID_SVNREMOVE,
 wxID_SVNDIFF, wxID_SVNLOG, wxID_SVNSTATUS, wxID_FSSVNIMPORT, wxID_FSSVNCHECKOUT,
 wxID_FSSVNENV, wxID_SVNTAG, wxID_SVNBRANCH, wxID_SVNLOCK, wxID_SVNUNLOCK, 
 wxID_SVNTEST, wxID_SVNRESOLVED, wxID_SVNINFO) = Utils.wxNewIds(17)

svnFolderImgIdx = 6
maxHelpLines = 30

class SVNEntriesParser:
    def __init__(self, filename):
        self.entries = []
        self.initParser(filename)
    
    def entry(self, name):
        for entryName, entryAttrs in self.entries:
            if name == entryName:
                return entryAttrs
        raise KeyError, name+' not found'
        
    def StartElement(self, name, attrs ):
        name = name.encode()
        if name == 'entry' and 'name' in attrs and attrs['name']:
            self.entries.append( (name, attrs) )

    def initParser(self, filename):
        from xml.parsers import expat
        parser = expat.ParserCreate()
        parser.StartElementHandler = self.StartElement
        return parser.Parse(open(filename,'r').read(), 1)


def isSVN(filename):
    file = os.path.basename(filename)
    return file.lower() == '.svn' and \
                      os.path.exists(os.path.join(filename, 'entries')) and \
                      os.path.exists(os.path.join(filename, 'format')) 

def svnFileLocallyModified(filename, entry):
    """  svnFileLocallyModified -> modified, conflict """

    filets = time.asctime(time.gmtime(os.stat(filename)[stat.ST_MTIME]))
    
    try:
        texttime = entry['text-time'].rsplit('.', 1)[0]
    except KeyError: 
        return False, False
    
    texttime = time.asctime(time.strptime(texttime, '%Y-%m-%dT%H:%M:%S'))
    
    return (texttime != filets, False)
##    
##    
##    ismerge = timestamp.split('+')
##    conflict = ismerge[0] == 'Result of merge'
##    filets = time.asctime(time.gmtime(os.stat(filename)[stat.ST_MTIME]))
##    if conflict and len(ismerge) == 1 or ismerge[0][:15] == 'dummy timestamp':
##        filesegs, svnsegs = 1, 0
##    # convert day to int to avoid zero padded differences
##    else:
##        if conflict:
##            filesegs, svnsegs = filets.split(), ismerge[1].split()
##        else:
##            filesegs, svnsegs = filets.split(), timestamp.split()
##        if svnsegs and svnsegs[0] == 'Initial' or len(svnsegs) < 3:
##            svnsegs.append('0')
##        # use day field of dates for comparison
##        filesegs[2], svnsegs[2] = int(filesegs[2]), int(svnsegs[2])
##
##    return (filesegs != svnsegs, conflict)


class SVNController(ExplorerNodes.Controller):
    updateBmp = 'Images/CvsPics/Update.png'
    commitBmp = 'Images/CvsPics/Commit.png'
    addBmp = 'Images/CvsPics/Add.png'
    removeBmp = 'Images/CvsPics/Remove.png'
    diffBmp = 'Images/CvsPics/Diff.png'
    logBmp = 'Images/CvsPics/Log.png'
    statusBmp = 'Images/CvsPics/Status.png'
    #infoBmp = 'Images/CvsPics/Info.png'
    #resolvedBmp = 'Images/CvsPics/ResolvedConflict.png'
    def __init__(self, editor, list, inspector, controllers):
        ExplorerNodes.Controller.__init__(self, editor)
        self.list = list
        self.menu = wx.Menu()
        self.svnOptions = '-z7'

        self.svnMenuDef = [
              (wxID_SVNUPDATE, 'Update', self.OnUpdateSVNItems, self.updateBmp),
              (wxID_SVNCOMMIT, 'Commit', self.OnCommitSVNItems, self.commitBmp),
              (wxID_SVNRESOLVED, 'Resolved', self.OnResolvedSVNItems, '-'),
              (-1, '-', None, ''),
              (wxID_SVNADD, 'Add', self.OnAddSVNItems, self.addBmp),
              (wxID_SVNREMOVE, 'Remove', self.OnRemoveSVNItems, self.removeBmp),
              (-1, '-', None, ''),
              (wxID_SVNDIFF, 'Diff', self.OnDiffSVNItems, self.diffBmp),
              (wxID_SVNLOG, 'Log', self.OnLogSVNItems, self.logBmp),
              (wxID_SVNSTATUS, 'Status', self.OnStatusSVNItems, self.statusBmp),
              (wxID_SVNINFO, 'Info', self.OnInfoSVNItems, '-'),
              #(-1, '-', None, ''),
              #(wxID_SVNLOCK, 'Lock', self.OnLockSVNItems, '-'),
              #(wxID_SVNUNLOCK, 'Unlock', self.OnUnlockSVNItems, '-'), 
        ]

        self.setupMenu(self.menu, self.list, self.svnMenuDef)

        self.fileSVNMenuDef = [
              #(wxID_FSSVNIMPORT, 'Import', self.OnImportSVNFSItems, '-'),
              (wxID_FSSVNCHECKOUT, 'Checkout', self.OnCheckoutSVNFSItems, '-'),
        ]

        self.fileSVNMenu = wx.Menu()
        self.setupMenu(self.fileSVNMenu, self.list, self.fileSVNMenuDef, False)


        self.images = wx.ImageList(16, 16)
        for svnImg in ( 'Images/CvsPics/File.png',
                        'Images/CvsPics/BinaryFile.png',
                        'Images/CvsPics/ModifiedFile.png',
                        'Images/CvsPics/ModifiedBinaryFile.png',
                        'Images/CvsPics/MissingFile.png',
                        'Images/CvsPics/ConflictingFile.png',
                        'Images/CvsPics/Dir.png',
                        'Images/Modules/FolderUp.png',
                        'Images/CvsPics/UnknownDir.png',
                        'Images/CvsPics/UnknownFile.png'):
            self.images.Add(IS.load(svnImg))

        self.toolbarMenus = [self.svnMenuDef]

        FSSVNFolderNode.images = self.images

    def destroy(self):
        self.svnMenuDef = ()
        self.fileSVNMenuDef = ()
        self.toolbarMenus = ()
        self.images = None
        FSSVNFolderNode.images = None
        self.menu.Destroy()

    def getName(self, item):
        name = ExplorerNodes.Controller.getName(self, item)
        if ' ' in name:
            return '"%s"' % name
        else:
            return name

    def setupListCtrl(self):
        self.list.SetWindowStyleFlag(wx.LC_REPORT)
        self.list.InsertColumn(0, 'Name', wx.LIST_FORMAT_LEFT, 150)
        self.list.InsertColumn(1, 'Rev.', wx.LIST_FORMAT_LEFT, 50)
        self.list.InsertColumn(2, 'Date', wx.LIST_FORMAT_LEFT, 150)
        self.list.InsertColumn(3, 'Status', wx.LIST_FORMAT_LEFT, 150)
        self.list.InsertColumn(4, 'Options', wx.LIST_FORMAT_LEFT, 50)

    def cleanupListCtrl(self):
        cols = range(5)
        cols.reverse()
        for col in cols:
            self.list.DeleteColumn(col)

    def showMessage(self, cmd, msg):
        dlg = ScrolledMessageDialog(self.list, msg, cmd)
        try: dlg.ShowModal()
        finally: dlg.Destroy()

    def svnCmd(self, command, options, files, extraOptions = ''):
        #svnOpts = self.svnOptions
        #if extraOptions:
        #    svnOpts = '%s %s'%(svnOpts, extraOptions)
        return 'svn %s %s %s' % (command, options, ' '.join(files))

    def svnCmdPrompt(self, wholeCommand, inDir, help = ''):
        #if isinstance(self.list.node, FSSVNFolderNode):
        #    svnroot = self.list.node.root
        #else:
        #    svnroot = os.environ.get('SVNROOT', '(not defined)')
        dlg = wx.TextEntryDialog(self.list, '(in dir %s)\n\n%s'%(inDir, help),
              'SVN command line', wholeCommand)
        if wx.Platform == '__WXMSW__':
            te = Utils.getCtrlsFromDialog(dlg, 'TextCtrl')[0]
            try:
                te.SetSelection(wholeCommand.index('['),
                                wholeCommand.index(']')+1)
            except ValueError:
                te.SetInsertionPoint(len(wholeCommand))
        try:
            if dlg.ShowModal() == wx.ID_OK:
                return dlg.GetValue()
            else:
                return ''
        finally:
            dlg.Destroy()

    def getSvnHelp(self, cmd, option = 'help'):
        SVNPD = ProcessProgressDlg.ProcessProgressDlg(self.list,
                  'svn %s %s'% (option, cmd), '', modally=False)
        try:
            return ' '.join(SVNPD.output[:maxHelpLines]).expandtabs(8)
        finally:
            SVNPD.Destroy()

    # svnOutput can be 'output window', 'dialogs' or 'tuple'
    def doSvnCmd(self, cmd, svnDir, stdinput='', svnOutput='output window'):
        # Repaint background
        wx.Yield()

        cwd = os.getcwd()
        try:
            os.chdir(svnDir)
            SVNPD = ProcessProgressDlg.ProcessProgressDlg(self.list, cmd, 'SVN progress...')
            try:
                if SVNPD.ShowModal() == wx.OK:
                    outls = SVNPD.output
                    errls = SVNPD.errors
                else:
                    return
            finally:
                SVNPD.Destroy()

            err = ''.join(errls).strip()

            if svnOutput == 'output window':
                errout = self.editor.erroutFrm
                tbs = errout.updateCtrls((), outls, 'SVN Result', '', err)
                errout.display(tbs)

            elif svnOutput == 'dialogs':
                if err.strip():
                    dlg = wx.MessageDialog(self.list, err,
                      'Server response or Error', wx.OK | wx.ICON_EXCLAMATION)
                    try: dlg.ShowModal()
                    finally: dlg.Destroy()

                if outls and not (len(outls) == 1 and not outls[0].strip()):
                    self.showMessage(cmd, ''.join(outls))
            elif svnOutput == 'tuple':
                return outls, errls

            #msgType = 'warning' if err else 'info' # i wish
            if err: msgType = 'Warning'
            else: msgType = 'Info'
            self.editor.setStatus('SVN command completed: %s'%cmd, msgType)

        finally:
            os.chdir(cwd)

    def doSvnCmdOnSelection(self, cmd, cmdOpts,
              preCmdFunc=None, postCmdFunc=None, svnOutput='output window'):
        if self.list.node:
            names = self.getNamesForSelection(self.list.getMultiSelection())
            svnDir = os.path.dirname(self.list.node.resourcepath)
            if not names: names = ['']
##                names = ['']
##                svnDir, names[0] = os.path.split(svnDir)
            cmdStr = self.svnCmdPrompt(self.svnCmd(cmd, cmdOpts, names), svnDir,
                  self.getSvnHelp(cmd))
            if cmdStr:
                if preCmdFunc: preCmdFunc(names)
                res = self.doSvnCmd(cmdStr, svnDir, svnOutput=svnOutput)
                if postCmdFunc: postCmdFunc(names)
                return res

    def doSvnCmdInDir(self, cmd, cmdOpts, svnDir, items, svnOpts = ''):
        cmdStr = self.svnCmdPrompt(self.svnCmd(cmd, cmdOpts, items, svnOpts),
              svnDir, self.getSvnHelp(cmd))
        if cmdStr:
            self.doSvnCmd(cmdStr, svnDir)
            return True
        else:
            return False

##    def importSVNItems(self):
##        # Imports are called from normal folders not SVN folders
##
##        # XXX Check if SVN folder exists ?
##        svnDir = self.list.node.resourcepath
##        if self.doCvsCmdInDir('import', '', svnDir, ['[MODULE]', 'VENDOR', 'RELEASE']):
##            self.list.refreshCurrent()


    def checkoutSVNItems(self):
        # Checkouts are called from normal folders not SVN folders
        svnDir = self.list.node.resourcepath
        if self.doSvnCmdInDir('checkout', '', svnDir, ['--username [USER]', 
            '--password [PASS]', '[URL]', '[PATH]'], ''):
            self.list.refreshCurrent()

    def updateSVNItems(self):
        self.doSvnCmdOnSelection('update', '')
        self.list.refreshCurrent()

    def OnUpdateSVNItems(self, event):
        self.updateSVNItems()

    def OnCommitSVNItems(self, event):
        self.doSvnCmdOnSelection('commit', '-m "[no message]"')
        self.list.refreshCurrent()

    def OnAddSVNItems(self, event):
        self.doSvnCmdOnSelection('add', '')
        self.list.refreshCurrent()


    quotes = ('"', "'")
    def selPreCmd_remove(self, list):
        dir = os.path.dirname(self.list.node.resourcepath)
        for name in list:
            try:

                if name[0] in self.quotes and name[-1] in self.quotes:
                    name = name[1:-1]
                os.remove(os.path.join(dir, name))
            except OSError, err:
                # Skip files already removed
                print err

    def OnRemoveSVNItems(self, event):
        self.doSvnCmdOnSelection('remove', '', self.selPreCmd_remove)
        self.list.refreshCurrent()

    def OnDiffSVNItems(self, event):
        # a syntax highlighted window is provided for unified diffs
        res = self.doSvnCmdOnSelection('diff', '-r HEAD', svnOutput='tuple')
        if res is not None and len(res)==2:
            outls, errls = res
            errout = self.editor.erroutFrm
            tbs = errout.updateCtrls((), outls, 'SVN Result', '', errls)
            errout.display(tbs)
            errout.displayDiff(''.join(outls))


    def OnLogSVNItems(self, event):
        self.doSvnCmdOnSelection('log', '')

    def OnStatusSVNItems(self, event):
        self.doSvnCmdOnSelection('status', '-u')

    def OnImportSVNFSItems(self, event):
        self.importSVNItems()

    def OnCheckoutSVNFSItems(self, event):
        self.checkoutSVNItems()

    def OnTagSVNItems(self, event):
        self.doSvnCmdOnSelection('tag', '[TAG]')

    def OnBranchSVNItems(self, event):
        self.doSvnCmdOnSelection('tag', '-b')

##    def OnLockSVNItems(self, event):
##        self.doSvnCmdOnSelection('admin', '-l[REV]')
##
##    def OnUnlockSVNItems(self, event):
##        self.doSvnCmdOnSelection('admin', '-u[REV]')

    def OnResolvedSVNItems(self, event):
        self.doSvnCmdOnSelection('resolved', '')
        self.list.refreshCurrent()

    def OnInfoSVNItems(self, event):
        self.doSvnCmdOnSelection('info', '')

class SVNFolderNode(ExplorerNodes.ExplorerNode):
    protocol = 'svn'
    def __init__(self, entry, resourcepath, dirpos, parent):
        if entry:
            name = entry['name']
            try:
                self.revision = entry['committed-rev']
            except KeyError:
                self.revision = ''
            try:
                self.tagdate = entry['committed-date']
            except KeyError:
                self.tagdate = ''
            try:
                self.timestamp = entry['text-time']
            except KeyError:
                self.timestamp = self.tagdate
        else:
            name=self.revision=self.timestamp=self.tagdate = ''

        ExplorerNodes.ExplorerNode.__init__(self, name, resourcepath, None, svnFolderImgIdx, parent)

        self.dirpos = dirpos

    def text(self):
        return '/'.join(('D', self.name, self.revision, self.timestamp, self.options, self.tagdate))

    def isFolderish(self):
        return False

    def notifyBeginLabelEdit(self, event):
        event.Veto()

    def createParentNode(self):
        parent = os.path.abspath(os.path.join(self.resourcepath, '..'))
        return PyFileNode(os.path.basename(parent), parent, self.clipboard,
                  EditorModels.FolderModel.imgIdx, self)

    def open(self, editor):
        tree = editor.explorer.tree
        par = tree.GetItemParent(tree.GetSelection())
        chd = tree.getChildNamed(par, self.name)
        if not tree.IsExpanded(chd):
            tree.Expand(chd)
        svnChd = tree.getChildNamed(chd, '.svn')
        tree.SelectItem(svnChd)
        return None, None

class SVNFileNode(ExplorerNodes.ExplorerNode):
    protocol = 'svn'
    def __init__(self, entry, resourcepath, parent):
        if entry:
            name = entry['name']
            try:
                self.revision = entry['committed-rev']
            except KeyError:
                self.revision = ''
            try:
                self.tagdate = entry['committed-date']
            except KeyError:
                self.tagdate = ''
            try:
                self.timestamp = entry['text-time']
            except KeyError:
                self.timestamp = self.tagdate
        else:
            name=self.revision=self.timestamp=self.tagdate = ''

        ExplorerNodes.ExplorerNode.__init__(self, name, resourcepath, None, -1, parent)

        self.imgIdx = 0#self.missing and self.missing << 2 \
        
        filename = os.path.abspath(os.path.join(self.resourcepath, '..', name))
        if os.path.exists(filename):
            self.modified, self.conflict = svnFileLocallyModified(filename, entry)
            self.missing = False
        else:
            self.modified = self.conflict = False
            self.missing = True

##        self.modified = False
        self.conflict = False
##        self.imgIdx = 0
##        if self.timestamp:
##            filename = os.path.abspath(os.path.join(self.resourcepath, '..', name))
##            if os.path.exists(filename):
##                self.modified, self.conflict = svnFileLocallyModified(filename, self.timestamp)
##            else:
##                self.missing = True
##
        self.imgIdx = self.missing and self.missing << 2 \
                      or self.conflict *5 or self.modified << 1

    def isFolderish(self):
        return False

    def notifyBeginLabelEdit(self, event):
        event.Veto()

    def getDescription(self):
        return '%s, (%s, %s)'%(self.name, self.revision, self.timestamp)#, self.options, self.tagdate), '/')

    def open(self, editor):
        tree = editor.explorer.tree
        node = editor.explorer.list.getSelection()
        timestamp = node.timestamp
        tree.SelectItem(tree.GetItemParent(tree.GetSelection()))
        editor.explorer.list.selectItemNamed(self.name)
        if self.conflict:
            node = editor.explorer.list.getSelection()

            if timestamp.startswith('Result of merge+'):
                model, controller = editor.openOrGotoModule(node.resourcepath,
                      transport=node)

                # XXX inefficient
                conflicts = model.getSVNConflicts()
                if conflicts:
                    from Views.EditorViews import SVNConflictsView
                    if not model.views.has_key(SVNConflictsView.viewName):
                        resultView = editor.addNewView(SVNConflictsView.viewName,
                              SVNConflictsView)
                    else:
                        resultView = model.views[SVNConflictsView.viewName]
                    resultView.refresh()
                    resultView.focus()
                else:
                    editor.setStatus('No SVN conflicts in file', 'Warning', True)

                return model, controller
        return None, None

    def text(self):
        return '/'.join(('', self.name, self.revision, self.timestamp, self.options, self.tagdate))

class SVNUnAddedItem(ExplorerNodes.ExplorerNode):
    def __init__(self, name, resourcepath, parent, isFolder):
        ExplorerNodes.ExplorerNode.__init__(self, name, resourcepath, None, isFolder and 8 or 9, parent)

    def open(self, editor):
        tree = editor.explorer.tree
        tree.SelectItem(tree.GetItemParent(tree.GetSelection()))
        editor.explorer.list.selectItemNamed(self.name)
        return None, None

class FSSVNFolderNode(ExplorerNodes.ExplorerNode):
    protocol = 'svn'
    def __init__(self, name, resourcepath, clipboard, imgIdx, parent, bookmarks=None):
        ExplorerNodes.ExplorerNode.__init__(self, name, resourcepath, clipboard,
              EditorModels.CVSFolderModel.imgIdx, parent)
        self.vetoSort = True
        self.dirpos = 0
        self.upImgIdx = 7

    def destroy(self):
        self.entries = []

    def getDescription(self):
        try:
            return '%s'% (self.root)
        except AttributeError:
            return ExplorerNodes.ExplorerNode.getDescription(self)

    def getTitle(self):
        try:
            return '%s'% (self.repository)
        except AttributeError:
            return ExplorerNodes.ExplorerNode.getTitle(self)

    def isFolderish(self):
        return True

    def notifyBeginLabelEdit(self, event):
        event.Veto()

    def createParentNode(self):
        if self.parent:
            return self.parent
        else:
            parent = os.path.abspath(os.path.join(self.resourcepath, os.path.join('..', '.svn')))
            return FSSVNFolderNode(os.path.basename(parent), parent, self.clipboard,
                      EditorModels.SVNFolderModel.imgIdx, self)

    def createChildNode(self, entry):
        if entry['kind'] == 'dir':
            self.dirpos = self.dirpos + 1
            return SVNFolderNode(entry, self.resourcepath, self.dirpos, self)
        elif entry['kind'] == 'file':
            try:
                return SVNFileNode(entry, self.resourcepath, self)
            except IOError:
                return None

    def openList(self):
        def readFile(self, name):
            return open(os.path.join(self.resourcepath, name)).read().strip()

        #self.root = readFile(self, 'Root')
        #self.repository = readFile(self, 'Repository')
        names = {}
        files = []
        dirs = []
        p = SVNEntriesParser(os.path.join(self.resourcepath, 'entries'))
        for name, entry in p.entries:
            svnNode = self.createChildNode(entry)
            if entry['kind'] == 'file':
                files.append(svnNode)
                names[svnNode.name] = svnNode
            elif entry['kind'] == 'dir':
                dirs.append(svnNode)
                names[svnNode.name] = svnNode

        fileEntries = self.parent.openList()
        for entry in fileEntries:
            if entry.name not in names:
                unaddedNode = SVNUnAddedItem(entry.name, entry.resourcepath, 
                      self, entry.isFolderish())
                if entry.isFolderish():
                    dirs.append(unaddedNode)
                else:
                    files.append(unaddedNode)
        
        self.entries = dirs + files
##            if svnNode:
##                res[svnNode.name] = svnNode
##                if svnNode.name not in filenames:
##                    missingEntries.append(svnNode)



##        res = {}
##        self.dirpos = 0
##        fileEntries = self.parent.openList()
##        txtEntries = open(os.path.join(self.resourcepath, 'Entries')).readlines()
##        filenames = map(lambda x: x.name, fileEntries)
##        missingEntries = []
##
##        for txtEntry in txtEntries:
##            svnNode = self.createChildNode(txtEntry.strip())
##            if svnNode:
##                res[svnNode.name] = svnNode
##                if svnNode.name not in filenames:
##                    missingEntries.append(svnNode)
##
##        lst = []
##        for entry in fileEntries:
##            testSVNDir = os.path.join(entry.resourcepath, 'SVN')
##            if os.path.isdir(entry.resourcepath) and \
##                  os.path.exists(testSVNDir) and isSVN(testSVNDir):
##                node = SVNFolderNode('D/%s////'%entry.name, self.resourcepath,
##                  self.dirpos, self)
##            else:
##                node = res.get(entry.name, SVNUnAddedItem(entry.name, entry.resourcepath, self, entry.isFolderish()))
##            if node:
##                lst.append(node)
##
##        for missing in missingEntries:
##            lst.append(missing)
##
##        self.entries = lst
        return self.entries 

    def open(self, editor):
        return editor.openOrGotoModule(self.resourcepath)

    def openParent(self, editor):
        tree = editor.explorer.tree
        svnParentItemParent = tree.GetItemParent(tree.GetItemParent(tree.GetSelection()))

        svnChd = tree.getChildNamed(svnParentItemParent, '.svn')
        if svnChd.IsOk():
            tree.SelectItem(svnChd)
            return True
        else:
            return False

#---------------------------------------------------------------------------
# Register svn dirs as a subtype of file explorers
from Explorers import FileExplorer
FileExplorer.FileSysNode.subExplorerReg['folder'].append(
      (FSSVNFolderNode, isSVN, EditorHelper.imgCVSFolder)
)

ExplorerNodes.register(FSSVNFolderNode, clipboard=None, confdef=('', ''),
                       controller=SVNController)
