#-----------------------------------------------------------------------------
# Name:        ZipExplorer.py
# Purpose:
#
# Author:      Riaan Booysen
#
# Created:     2001
# RCS-ID:      $Id: ZipExplorer.py,v 1.28 2007/07/02 15:01:10 riaan Exp $
# Copyright:   (c) 2001 - 2007
# Licence:     GPL
#-----------------------------------------------------------------------------

print 'importing Explorers.ZipExplorer'
import os, zipfile, gzip, time
from cStringIO import StringIO

import wx

from Utils import _

import ExplorerNodes, FileExplorer
from Models import EditorModels, EditorHelper

from ExternalLib import tarfile

def isZip(file):
    return os.path.splitext(file)[1] == '.zip'

wxID_ZIPOPEN = wx.NewId()

class ZipController(ExplorerNodes.Controller, ExplorerNodes.ClipboardControllerMix):
    def __init__(self, editor, list, inspector, controllers):
        ExplorerNodes.ClipboardControllerMix.__init__(self)
        ExplorerNodes.Controller.__init__(self, editor)

        self.list = list
        self.menu = wx.Menu()

        self.setupMenu(self.menu, self.list,
            [ (wxID_ZIPOPEN, _('Open'), self.OnOpenItems, '-'),
              (-1, '-', None, '') ] + self.clipMenuDef)

        self.toolbarMenus = [self.clipMenuDef]

    def destroy(self):
        ExplorerNodes.ClipboardControllerMix.destroy(self)
        self.toolbarMenus = []
        self.menu.Destroy()


class ZipExpClipboard(ExplorerNodes.ExplorerClipboard):
    def clipPaste_FileSysExpClipboard(self, node, nodes, mode):
        for clipnode in nodes:
            if mode == 'cut':
                node.copyFromFS(clipnode)
                self.clipNodes = []
            elif mode == 'copy':
                node.copyFromFS(clipnode)

class ZipItemNode(ExplorerNodes.ExplorerNode):
    protocol = 'zip'
    ArchiveClass = zipfile.ZipFile
    InfoClass = zipfile.ZipInfo

    def __init__(self, name, resourcepath, clipboard, isFolder, imgIdx, parent,
          zipFileNode, ChildClass):
        ExplorerNodes.ExplorerNode.__init__(self, name, resourcepath, clipboard,
              imgIdx, parent)
        self.isFolder = isFolder
        self.zipFileNode = zipFileNode
        ##self.lineSep = None # meaning binary
        self.ChildClass = ChildClass
        self.compression = zipfile.ZIP_DEFLATED

    def isFolderish(self):
        return self.isFolder

    def createChildNode(self, name, resourcepath, isFolder):

        imgIdx = isFolder and EditorHelper.imgFolder or \
              EditorHelper.imgTextModel
        if not isFolder:
            from Models import Controllers
            imgIdx = Controllers.identifyFile(name, localfs=False)[0].imgIdx
        zin = self.ChildClass(name, resourcepath and resourcepath+'/'+name or name,
              self.clipboard, isFolder, imgIdx, self, self.zipFileNode, self.ChildClass)
        zin.category = self.category
        return zin

    def newInfoClass(self, name):
        info = self.InfoClass(name)
        info.compress_type = self.compression
        return info

    def splitBaseDir(self, file):
        if not file:
            return '', '', True
        segs = file.split('/')
        # ends on /
        if segs[-1] == '':
            base = segs[-2]
            dir = '/'.join(segs[:-2])
            isdir = 1
        else:
            base = segs[-1]
            dir = '/'.join(segs[:-1])
            isdir = 0
        return base, dir, isdir

    def openList(self, resourcepath=None):
        if resourcepath is None: resourcepath = self.resourcepath
        res = []
        files = self.zipFileNode.getFiles(resourcepath)
        for file in files:
            base, dir, isdir = self.splitBaseDir(file)

            res.append(self.createChildNode(base, dir, self.zipFileNode.isDir(file)) )
        return res

    def getArcDir(self):
        if self.isFolderish():
            return self.resourcepath
        else:
            return os.path.dirname(self.resourcepath)

    def walkFS(self, (files, excludes), dirname, names):
        for name in names:
            if name not in excludes:
                filename = os.path.join(dirname, name)
                if os.path.isfile(filename):
                    files.append(filename)

    def copyFromFS(self, fsNode, fn=''):
        if not fn:
            fn = os.path.basename(fsNode.resourcepath)

        if fsNode.isFolderish():
            fsFiles = []
            fsDir = fsNode.resourcepath
            fsDirDir = os.path.dirname(fsNode.resourcepath)
            os.path.walk(fsDir, self.walkFS, (fsFiles, ()))
            new = []
            replace = []
            for fn in fsFiles:
                if os.path.isabs(self.resourcepath):
                    pref = ''
                else:
                    pref = self.resourcepath + '/'
                destName = pref+fn[len(fsDirDir)+1:]
                if destName in self.zipFileNode.allFileNames:
                    replace.append( (destName, fn) )
                else:
                    new.append( (destName, fn) )
            # replace existing
            filesData = [ (arcname, '', open(filename, 'rb').read())
                          for arcname, filename in replace ]
            self.replaceFilesInArchive(filesData)

            # append new
            if new:
                zf = self.ArchiveClass(self.zipFileNode.resourcepath, 'a', self.compression)
                try:
                    for arcname, filename in new:
                        zi = self.newInfoClass(arcname)
                        zf.writestr(zi, open(filename, 'rb').read())
                finally:
                    zf.close()
                self.zipFileNode.allFiles = None
        else:
            data = fsNode.load(mode='rb')
            # if already in archive, must be replaced, else can be appended
            #arcDir = os.path.dirname(self.resourcepath)
            destName = '/'.join([self.getArcDir(), fn])
            if destName in self.zipFileNode.allFileNames:
                # XXX prompt?
                self.replaceFilesInArchive([(destName, '', data)])
            else:
                zf = self.ArchiveClass(self.zipFileNode.resourcepath, 'a', self.compression)
                try:
                    zi = self.newInfoClass(destName)
                    zi.compress_type = zipfile.ZIP_DEFLATED
                    zi.file_size = len(data)
                    zi.date_time = time.localtime(fsNode.stdAttrs['modify-date'])[:6]
                    zf.writestr(zi, data)
                finally:
                    zf.close()
                self.zipFileNode.allFiles = None

    def copyToFS(self, fsFolderNode):
        fn = os.path.join(fsFolderNode.resourcepath, self.name)

        zf = self.ArchiveClass(self.zipFileNode.resourcepath,
                               compression=self.compression)
        try:
            if self.isFolderish():
                try: os.mkdir(fn)
                except OSError: pass
                files = self.zipFileNode.getFiles(self.resourcepath, True)
                for arcName in files:
                    fn = os.path.join(fsFolderNode.resourcepath, arcName)
                    try: os.makedirs(os.path.dirname(fn))
                    except OSError: pass
                    open(fn, 'wb').write(zf.read(arcName))
            else:
                open(fn, 'wb').write(zf.read(self.resourcepath))
        finally:
            zf.close()

    def newFolder(self, name):
        zf = self.ArchiveClass(self.zipFileNode.resourcepath, 'a', self.compression)
        try:
            ad = self.getArcDir()
            if ad: ad +='/'
            newArcName = '%s%s/'%(ad, name)
            zi = self.newInfoClass(newArcName)
            zi.file_size = 0
            zi.flag_bits = 0x02
            zi.date_time = time.localtime()[:6]
            zf.writestr(zi, '')
            self.zipFileNode.allFiles = None
        finally:
            zf.close()
        return name
        #raise Exception, 'The zipfile module does not suppport adding empty folders'

    def newBlankDocument(self, name=''):
        zf = self.ArchiveClass(self.zipFileNode.resourcepath, 'a', self.compression)
        try:
            ad = self.getArcDir()
            if ad: ad +='/'
            newArcName = '%s%s'%(ad, name)
            zi = self.newInfoClass(newArcName)
            zi.file_size = 0
            zi.date_time = time.localtime()[:6]
            zf.writestr(zi, '')
            self.zipFileNode.allFiles = None
        finally:
            zf.close()
        return name

    def load(self, mode='rb'):
        zf = self.ArchiveClass(self.zipFileNode.resourcepath)
        try:
            return zf.read(self.resourcepath)
        finally:
            zf.close()

    def save(self, filename, data, mode='wb', overwriteNewer=True):
        self.replaceFilesInArchive([(self.resourcepath, filename, data)])

    def renameItem(self, name, newName):
        arcDir = self.getArcDir()
        oldfile = '/'.join([arcDir, name])
        newfile = '/'.join([arcDir, newName])
        self.replaceFilesInArchive([(oldfile, newfile, None)])

    def replaceFilesInArchive(self, filesData):
        # There seems to be no way to replace a zip entry.
        # Here the whole archive is recreated
        zipStream = StringIO(open(self.zipFileNode.resourcepath, 'rb').read())
        zfSrc = self.ArchiveClass(zipStream, 'r')
        try:
            changed = False
            zfDst = self.ArchiveClass(self.zipFileNode.resourcepath, 'w', self.compression)
            try:
                for zi in zfSrc.infolist():
                    for fn, nfn, data in filesData:
                        if zi.filename == fn:
                            changed = True
                            if nfn:
                                zi.filename = nfn
                            if data is None:
                                zipData = data = zfSrc.read(fn)
                            else:
                                zipData = data
                            break
                    else:
                        zipData = zfSrc.read(zi.filename)
                    zi.size = zi.file_size = len(zipData)
                    zfDst.writestr(zi, zipData)
            finally:
                zfDst.close()
        finally:
            zfSrc.close()

        if changed:
            self.zipFileNode.allFiles = None


    def getNodeFromPath(self, respath):
        base, dir, isdir = self.splitBaseDir(respath)
        return self.createChildNode(base, dir, self.zipFileNode.isDir(respath))

class ZipFileNode(ZipItemNode):
    protocol = 'zip'
    ChildClass = ZipItemNode

    def __init__(self, name, resourcepath, clipboard, imgIdx, parent,
          bookmarks=None):
        if clipboard:
            zipClip = ZipExpClipboard(clipboard.globClip)
        else:
            zipClip = None
        ZipItemNode.__init__(self, name, resourcepath, zipClip, True,
            imgIdx, parent, self, self.ChildClass)
        self.allFiles = None
        self.allFileNames = []
        self.category = self.getTitle()+'://'

    def getURI(self):
        return '%s://%s' % (self.protocol, self.getTitle())

    def isFolderish(self):
        return True

    def isDir(self, path = ''):
        if path:
            return path[-1] == '/'
        else:
            return False

    def getArcDir(self):
        return ''

    def openList(self):
        self.updateFilelists()
        # Add implicit folder entries
        for filename in self.allFileNames[:]:
            if filename[-1] == '/':
                # don't build if zip has folder entries
                break
            while 1:
                filename = os.path.dirname(filename)
                path = filename +'/'
                if path == '/':
                    break
                if path not in self.allFileNames:
                    self.allFileNames.insert(0, path)
                    self.allFiles.insert(0, self.newInfoClass(path))

        return ZipItemNode.openList(self, '')

    def getFiles(self, base, nested=False):
        files = []
        if self.allFiles is None:
            self.updateFilelists()

        for file in self.allFiles:
            if file.filename[-1] == '/':
                fn = file.filename[:-1]
            else:
                fn = file.filename

            if nested:
                if fn.startswith(base):
                    files.append(file.filename)
            else:
                dir = os.path.dirname(fn)
                dirdir = os.path.dirname(dir)
                if dir and dirdir == base:
                    try: idx = files.index(dir+'/')
                    except ValueError: files.append(dir+'/')

                if dir == base:
                    try: files.index(file.filename)
                    except ValueError: files.append(file.filename)
        return files

    def updateFilelists(self):
        zf = self.ArchiveClass(self.resourcepath, 'r')
        try:
            self.compression = zf.compression
            self.allFiles = zf.infolist()
            self.allFileNames = [fl.filename for fl in self.allFiles]
        finally:
            zf.close()


EditorHelper.imgZipFileModel = \
      EditorHelper.addPluginImgs('Images/Modules/ZipFile.png')
class ZipFileModel(EditorModels.EditorModel):
    modelIdentifier = 'ZipFile'
    defaultName = 'zip'
    bitmap = 'ZipFile.png'
    imgIdx = EditorHelper.imgZipFileModel
    ext = '.zip'

def uriSplitZip(filename, zipfile, zipentry):
    return 'zip', zipfile, zipentry, filename

def findZipExplorerNode(category, respath, transports):
    zf = ZipFileNode(os.path.basename(category), category, None, -1, None, None)
    zf.openList()
    return zf.getNodeFromPath(respath)

#-------------------------------------------------------------------------------
# Register zip files as a subtype of file explorers
FileExplorer.FileSysNode.subExplorerReg['file'].append(
      (ZipFileNode, isZip, EditorHelper.imgZipFileModel))
ExplorerNodes.register(ZipItemNode, clipboard=ZipExpClipboard,
      controller=ZipController)
ExplorerNodes.uriSplitReg[('zip', 3)] = uriSplitZip
ExplorerNodes.transportFindReg['zip'] = findZipExplorerNode
ExplorerNodes.fileOpenDlgProtReg.append('zip')
EditorHelper.modelReg[ZipFileModel.modelIdentifier] = ZipFileModel
EditorHelper.binaryFilesReg.append('.zip')

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

def isTarGzip(file):
    name, ext = os.path.splitext(file)
    return ext == '.tgz' or ext == '.gz' and os.path.splitext(name)[1] == '.tar'

class TarGzipController(ZipController): pass

class TarGzipInfoMixin:
    InfoClass = tarfile.TarInfo
    def newInfoClass(self, name):
        info = ZipItemNode.newInfoClass(self, name)
        info.filename = name
        if name[-1] == '/':
            info.type = tarfile.DIRTYPE
        return info

class TarGzipItemNode(TarGzipInfoMixin, ZipItemNode):
    protocol = 'tar.gz'
    ArchiveClass = tarfile.TarFileCompat


class TarGzipFileNode(TarGzipInfoMixin, ZipFileNode):
    protocol = 'tar.gz'
    ArchiveClass = tarfile.TarFileCompat
    ChildClass = TarGzipItemNode

    def isDir(self, path=''):
        if path:
            try: idx = self.allFileNames.index(path)
            except ValueError: return path[-1] == '/'
            return self.allFiles[idx].isdir()
        else:
            return False

EditorHelper.imgTarGzipFileModel = \
      EditorHelper.addPluginImgs('Images/Modules/TarGzipFile.png')
class TarGzipFileModel(EditorModels.EditorModel):
    modelIdentifier = 'TarGzipFile'
    defaultName = 'tar'
    bitmap = 'TarGzipFile.png'
    imgIdx = EditorHelper.imgTarGzipFileModel
    ext = '.gz'

def uriSplitTarGzip(filename, gzipfile, gzipentry):
    return 'tar.gz', gzipfile, gzipentry, filename

def uriSplitTGZ(filename, gzipfile, gzipentry):
    return 'tgz', gzipfile, gzipentry, filename

def findTarGzipExplorerNode(category, respath, transports):
    gzf = TarGzipFileNode(os.path.basename(category), category, None, -1, None, None)
    gzf.openList()
    return gzf.getNodeFromPath(respath)

#-------------------------------------------------------------------------------
# Register gzip files as a subtype of file explorers
FileExplorer.FileSysNode.subExplorerReg['file'].append(
      (TarGzipFileNode, isTarGzip, EditorHelper.imgZipFileModel))
ExplorerNodes.register(TarGzipItemNode, clipboard=ZipExpClipboard,
      controller=TarGzipController)
ExplorerNodes.uriSplitReg[('tar.gz', 3)] = uriSplitTarGzip
ExplorerNodes.uriSplitReg[('tgz', 3)] = uriSplitTGZ
ExplorerNodes.transportFindReg['tar.gz'] = findTarGzipExplorerNode
ExplorerNodes.transportFindReg['tgz'] = findTarGzipExplorerNode
ExplorerNodes.fileOpenDlgProtReg.extend(['tar.gz', 'tgz'])
EditorHelper.modelReg[TarGzipFileModel.modelIdentifier] = TarGzipFileModel
EditorHelper.binaryFilesReg.extend(['.gz', '.tgz'])
EditorHelper.extMap['.tgz'] = TarGzipFileModel
