##############################################################################
#
# Photo for Zope
#
# Copyright (c) 2001 Logic Etc, Inc.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Ron Bickers      rbickers@logicetc.com
#   Logic Etc, Inc.  http://www.logicetc.com/
#
##############################################################################

"""Photo Folder

Photo Folder objects help manage a group of Photo objects by providing
an interface for adding, removing, and modifying the properties and
display sizes of all contained photos, and by providing defaults for
newly created photos.
"""

from Globals import Persistent, InitializeClass, DTMLFile
from OFS.ObjectManager import ObjectManager
from OFS.SimpleItem import Item
from OFS.PropertyManager import PropertyManager
from OFS.FindSupport import FindSupport
from AccessControl.Role import RoleManager
from AccessControl import getSecurityManager, ClassSecurityInfo
from Photo import Photo
from zipfile import *
import tempfile, os, types, string

defaultdisplays = {'thumbnail': (128,128),
                   'xsmall': (200,200),
                   'small': (320,320),
                   'medium': (480,480),
                   'large': (768,768),
                   'xlarge': (1024,1024)
                  }


bad_chars = ' ,+&;()[]{}\xC4\xC5\xC1\xC0\xC2\xC3' \
    '\xE4\xE5\xE1\xE0\xE2\xE3\xC7\xE7\xC9\xC8\xCA\xCB' \
    '\xC6\xE9\xE8\xEA\xEB\xE6\xCD\xCC\xCE\xCF\xED\xEC' \
    '\xEE\xEF\xD1\xF1\xD6\xD3\xD2\xD4\xD5\xD8\xF6\xF3' \
    '\xF2\xF4\xF5\xF8\x8A\x9A\xDF\xDC\xDA\xD9\xDB\xFC' \
    '\xFA\xF9\xFB\xDD\x9F\xFD\xFF\x8E\x9E'

good_chars= '___________AAAAAA' \
    'aaaaaaCcEEEE' \
    'EeeeeeIIIIii' \
    'iiNnOOOOOOoo' \
    'ooooSssUUUUu' \
    'uuuYYyyZz'

TRANSMAP = string.maketrans(bad_chars, good_chars)

def cleanup_id(str):
    """ Cleanup an id
        Should be more thorough and e.g. remove trailing dots/spaces
    """
    return string.translate(str, TRANSMAP)

class PhotoFolder(ObjectManager,
                  PropertyManager,
                  RoleManager,
                  Item,
                  FindSupport):
    """Photo Folder object implementation.

    Photo Folder objects are folders that can contain Photo objects
    and other supporting objects.  They provide properties to all
    Photos and other Photo Folders in them.  They also provide
    support methods for displaying Photos in an album-like manner.
    """

    meta_type = "Photo Folder"

    _properties = (
        {'id':'title', 'type': 'string', 'mode': 'w'},
        {'id':'image', 'type': 'selection', 'mode': 'wd', 'select_variable': 'image_select'},
        )

    manage_options = (
        {'label': 'Contents', 'action': 'manage_main'},
        {'label': 'Photo Properties', 'action': 'manage_editPhotoPropertiesForm'},
        {'label': 'Displays', 'action': 'manage_editDisplaysForm'},
        {'label': 'View', 'action': ''},
        {'label': 'Zipupload', 'action': 'manage_addzipfileform'},
        ) + PropertyManager.manage_options \
          + RoleManager.manage_options \
          + Item.manage_options \
          + FindSupport.manage_options

    security=ClassSecurityInfo()

    def __init__(self, id, title, store='Image', engine='ImageMagick', quality=75,
                 pregen=0, timeout=0):

        self.__version__ = '1.2.3'
        self.id = id
        self.title = title
        self.image = ''

        # Sheet to store containing photo default settings.
        self.propertysheets.manage_addPropertySheet('photoconf', 'photoconf')
        photoconf = self.propertysheets.get('photoconf')
        photoconf.manage_addProperty('store', store, 'string')
        photoconf.manage_addProperty('engine', engine, 'string')
        photoconf.manage_addProperty('quality', quality, 'int')
        photoconf.manage_addProperty('pregen', pregen, 'boolean')
        photoconf.manage_addProperty('timeout', timeout, 'int')

        # Sheet to store containing photo properties.
        self.propertysheets.manage_addPropertySheet('photos', 'photos')

        # Initialize with default hardcoded sizes.
        self._displays = defaultdisplays.copy()

    #
    # Misc. Photo Folder Methods
    #
    security.declareProtected('View', 'default_html')
    default_html = DTMLFile('dtml/sampleFolderView', globals())

#   def index_html(self, *args, **kw):
    def index_html(self, client=None, REQUEST=None, RESPONSE=None, **kw):
        """ Show default view """
        # Get the template to use
        template_id = 'photo_folder_index'
        if hasattr(self.aq_base, template_id):      # Look in base
            template = getattr(self.aq_base, template_id)
            template = template.__of__(self)
        elif hasattr(self.aq_parent, template_id):  # Look in parent
            template = getattr(self.aq_parent, template_id)
            template = template.__of__(self)
        else:
            template = self.default_html      # Use default_html
        return apply(template, ((client, self), REQUEST), kw)


    security.declareProtected('Access contents information', 'photoIds')
    def photoIds(self):
        """Return list of Photos in this folder."""
        return self.objectIds(['Photo'])

    def image_select(self):
        return [''] + self.photoIds()

    security.declareProtected('Access contents information', 'numPhotos')
    def numPhotos(self):
        """Return number of Photos in folder tree."""
        photos = len(self.objectIds(['Photo']))
        for folder in self.objectValues(['Photo Folder']):
            photos = photos + folder.numPhotos()
        return photos

    security.declareProtected('Access contents information', 'nextPhotoFolder')
    def nextPhotoFolder(self):
        """Return next Photo Folder."""
        id = self.getId()
        folderIds = self.aq_parent.objectIds(['Photo Folder'])
        folderIds.sort()
        if id == folderIds[-1]:
            return None
        return getattr(self.aq_parent, folderIds[folderIds.index(id)+1])

    security.declareProtected('Access contents information', 'prevPhotoFolder')
    def prevPhotoFolder(self):
        """Return previous Photo Folder."""
        id = self.getId()
        folderIds = self.aq_parent.objectIds(['Photo Folder'])
        folderIds.sort()
        if id == folderIds[0]:
            return None
        return getattr(self.aq_parent, folderIds[folderIds.index(id)-1])

    security.declareProtected('Access contents information', 'displayIds')
    def displayIds(self, exclude=('thumbnail',)):
        """Return list of display Ids."""
        ids = self._displays.keys()
        # Exclude specified displays
        for id in exclude:
            if id in ids:
                ids.remove(id)
        # Sort by size in bytes
        ids.sort(lambda x,y,d=self._displays: cmp(d[x][0]*d[x][1], d[y][0]*d[y][1]))
        return ids

    security.declarePrivate('displayMap')
    def displayMap(self):
        """Return list of displays with size info."""
        displays = []
        for id in self.displayIds([]):
            displays.append({'id': id,
            'width': self._displays[id][0],
            'height': self._displays[id][1],
            })
        return displays

    #
    # Management Interface
    #

    security.declareProtected('Manage properties', 'manage_editPhotoPropertiesForm')
    manage_editPhotoPropertiesForm = DTMLFile('dtml/editPhotoPropertiesForm', globals())

    security.declareProtected('Manage properties', 'manage_editPhotoSettings')
    def manage_editPhotoSettings(self, REQUEST=None):
        """Edit photo settings."""
        photoconf = self.propertysheets.get('photoconf')
        photoconf.manage_editProperties(REQUEST)
        # Update contained Photo objects if requested
        if REQUEST.form.get('changeall', None):
            for photo in self.objectValues(['Photo']):
                REQUEST.set('store', photo.propertysheets.get('photoconf').getProperty('store'))
                photo.propertysheets.get('photoconf').manage_editProperties(REQUEST)
        if REQUEST is not None:
            return self.manage_editPhotoPropertiesForm(REQUEST,
                        manage_tabs_message='Default photo settings updated.')

    security.declareProtected('Manage properties', 'manage_editPhotoProperties')
    def manage_editPhotoProperties(self, REQUEST=None):
        """Edit photo properties."""
        photosheet = self.propertysheets.get('photos')
        photosheet.manage_editProperties(REQUEST)
        if REQUEST is not None:
            return self.manage_editPhotoPropertiesForm(REQUEST,
                        manage_tabs_message='Photo properties updated.')

    security.declareProtected('Manage properties', 'manage_delPhotoProperties')
    def manage_delPhotoProperties(self, ids, REQUEST=None):
        """Delete photo properties."""
        photosheet = self.propertysheets.get('photos')
        photosheet.manage_delProperties(ids)
        # Update contained Photo objects
        for photo in self.objectValues(['Photo']):
            try: photo.manage_delProperties(ids)
            except: pass
        if REQUEST is not None:
            return self.manage_editPhotoPropertiesForm(REQUEST,
                        manage_tabs_message='Photo properties deleted.')

    security.declareProtected('Manage properties', 'manage_addPhotoProperty')
    def manage_addPhotoProperty(self, id, value, type, REQUEST=None):
        """Add photo property."""
        photosheet = self.propertysheets.get('photos')
        photosheet.manage_addProperty(id, value, type)
        # Update contained Photo objects
        for photo in self.objectValues(['Photo']):
            try: photo.manage_addProperty(id, value, type)
            except: pass
        if REQUEST is not None:
            return self.manage_editPhotoPropertiesForm(REQUEST,
                        manage_tabs_message='Photo property added.')

    security.declareProtected('Manage properties', 'manage_editDisplaysForm')
    manage_editDisplaysForm = DTMLFile('dtml/editFolderDisplaysForm', globals())

    security.declareProtected('Manage properties', 'manage_editDisplays')
    def manage_editDisplays(self, displays, REQUEST=None):
        """Edit displays."""
        d = self._displays
        for display in displays:
            if d[display.id] != (display.width, display.height):
                d[display.id] = (display.width, display.height)
        self._displays = d
        if REQUEST is not None:
            return self.manage_editDisplaysForm(REQUEST,
                manage_tabs_message='Displays changed.')

    security.declareProtected('Manage properties', 'manage_delDisplays')
    def manage_delDisplays(self, ids, REQUEST=None):
        """Delete displays."""
        d = self._displays
        for id in ids:
            try: del d[id]
            except: pass
        self._displays = d
        # Update contained Photo objects
        for photo in self.objectValues(['Photo']):
            try: photo.manage_delDisplays(ids)
            except: pass
        if REQUEST is not None:
            return self.manage_editDisplaysForm(REQUEST,
                manage_tabs_message='Displays deleted.')

    security.declareProtected('Manage properties', 'manage_addDisplay')
    def manage_addDisplay(self, id, width, height, REQUEST=None):
        """Add display."""
        d = self._displays
        d[id] = (width, height)
        self._displays = d
        # Update contained Photo objects
        for photo in self.objectValues(['Photo']):
            try: photo.manage_addDisplay(id, width, height)
            except: pass
        if REQUEST is not None:
            return self.manage_editDisplaysForm(REQUEST,
                manage_tabs_message='Display added.')

    security.declareProtected('Manage properties', 'manage_regenDisplays')
    def manage_regenDisplays(self, REQUEST=None):
        """Regenerate all displays of contained photos."""
        for photo in self.objectValues(['Photo']):
            photo.manage_regenDisplays()
        if REQUEST is not None:
            return self.manage_editDisplaysForm(REQUEST,
                manage_tabs_message='Displays regenerated.')

    security.declareProtected('Manage properties', 'manage_purgeDisplays')
    def manage_purgeDisplays(self, exclude=None, REQUEST=None):
        """Purge all generated displays of contained photos."""
        if exclude is None and REQUEST is not None:
            exclude = REQUEST.form.get('ids', [])
        for photo in self.objectValues(['Photo']):
            photo.manage_purgeDisplays(exclude)
        if REQUEST is not None:
            return self.manage_editDisplaysForm(REQUEST,
                manage_tabs_message='Displays purged.')

    security.declareProtected('Manage properties', 'manage_cleanDisplays')
    def manage_cleanDisplays(self, exclude=None, REQUEST=None):
        """Purge all generated displays of contained photos that have expired."""
        if exclude is None and REQUEST is not None:
            exclude = REQUEST.form.get('ids', [])
        for photo in self.objectValues(['Photo']):
            photo.manage_cleanDisplays(exclude)
        if REQUEST is not None:
            return self.manage_editDisplaysForm(REQUEST,
                manage_tabs_message='Expired displays purged.')

    security.declareProtected('View', 'zip')
    def zip(self,REQUEST,RESPONSE):
        """ Go through the photo folder and find all the external images
            then zip them and send the result to the user
            FIXME: Only try it for those that store data on filesystem.
        """
        tmpfile = tempfile.mktemp(".temp")
        request = self.REQUEST
        parents = request.PARENTS
        parent = parents[0]
        try:
            outzd = ZipFile(tmpfile,"w",ZIP_DEFLATED)

            for p in parent.objectValues('Photo'):
                store = p.propertysheets.get('photoconf').getProperty('store')
                if getSecurityManager().checkPermission('View',p) and store == 'ExtImage':
                    outzd.write(p._original._get_filename(), str(p.getId()))

            outzd.close()
            stat = os.stat(tmpfile)

            RESPONSE.setHeader('Content-Type', 'application/x-zip')
            RESPONSE.setHeader('Content-Disposition',
                 'attachment; filename="%s.zip"' % self.id)
            RESPONSE.setHeader('Content-Length', stat[6])
            self._copy(tmpfile,RESPONSE)
        finally:
            os.unlink(tmpfile)
        return ''

    def _copy(self, infile, outfile):
        """ Read binary data from infile and write it to outfile
            infile and outfile may be strings, in which case a file with that
            name is opened, or filehandles, in which case they are accessed
            directly.
        """
        if type(infile) is types.StringType:
            try:
                instream = open(infile, 'rb')
            except IOError:
                raise IOError, ("%s (%s)" %(self.id, infile))
            close_in = 1
        else:
            instream = infile
            close_in = 0

        if type(outfile) is types.StringType:
            try:
                outstream = open(outfile, 'wb')
            except IOError:
                raise IOError, ("%s (%s)" %(self.id, outfile))
            close_out = 1
        else:
            outstream = outfile
            close_out = 0

        try:
            blocksize = 2<<16
            block = instream.read(blocksize)
            outstream.write(block)
            while len(block)==blocksize:
                block = instream.read(blocksize)
                outstream.write(block)
        except IOError:
            raise IOError, ("%s (%s)" %(self.id, filename))
        if close_in: instream.close()
        if close_out: outstream.close()

    #
    # Add Zip file
    #

    security.declareProtected('Change Photo', 'manage_addzipfileform')
    manage_addzipfileform = DTMLFile('dtml/zipfileAdd', globals())

    def _add_file_from_zip(self, zipfile, name):
        """ Generate id from filename and make sure,
            there are no spaces in the id.
        """
        id=name[max(string.rfind(name,'/'),
                  string.rfind(name,'\\'),
                  string.rfind(name,':')
                 )+1:]
        id = cleanup_id(id)
        self.manage_addProduct['Photo'].manage_addPhoto(id, title='', file=zipfile)

    security.declareProtected('Change Photo', 'manage_addzipfile')

    def manage_addzipfile(self, file='', content_type='', REQUEST=None):
        """ Expand a zipfile into a number of Documents.
            Go through the zipfile and for each file in there call
            self.manage_addProduct['Report Document'].manageaddDocument(id,...
        """

        if type(file) is not type('') and hasattr(file,'filename'):
            # According to the zipfile.py ZipFile just needs a file-like object
            zf = ZZipFile(file)
            for name in zf.namelist():
                zf.setcurrentfile(name)
                self._add_file_from_zip(zf,name)

            if REQUEST:
                message = 'The images were successfully created!'
                return self.manage_main(self,REQUEST,manage_tabs_message=message)
        elif REQUEST is not None:
            message = 'You must specify a file!'
            return self.manage_main(self,REQUEST,manage_tabs_message=message)

    #
    # WebDAV/FTP support
    #

    def PUT_factory(self, name, typ, body):
        """Create Photo objects by default for image types."""
        if typ[:6] == 'image/':
            store = self.propertysheets.get('photoconf').getProperty('store')
            photo = Photo(name, '', '', store=store)

            # Init properties with defaults
            props = self.propertysheets.get('photos')
            for propid, value in props.propertyItems():
                try: photo.manage_addProperty(propid, value, props.getPropertyType(propid))
                except: pass

            # Init settings with defaults
            photoconf = self.propertysheets.get('photoconf')
            settings = {}
            for propid, value in photoconf.propertyItems():
                settings[propid] = value
            photo.propertysheets.get('photoconf').manage_changeProperties(settings)

            # Init displays with defaults
            photo._displays = self._displays.copy()
            return photo

        return None

#
# Factory methods
#

manage_addPhotoFolderForm = DTMLFile('dtml/addPhotoFolderForm', globals())

def manage_addPhotoFolder(self, id, title, store='Image',
                          engine='ImageMagick', quality=75, pregen=0, timeout=0,
                          createsamples=0, REQUEST=None):
    """Add Photo Folder object"""
    self._setObject(id, PhotoFolder(id, title, store, engine, quality, pregen, timeout))
    if createsamples:
        ob = self._getOb(id)

        sampleView = DTMLFile('dtml/sampleView', globals())
        ob.manage_addDTMLMethod('photo_view', '')
        ob._getOb('photo_view').manage_edit(sampleView, 'Sample View')

        sampleFolderView = DTMLFile('dtml/sampleFolderView', globals())
        ob.manage_addDTMLMethod('photo_folder_index', '')
        ob._getOb('photo_folder_index').manage_edit(sampleFolderView, 'Sample Folder View')

    if REQUEST is not None:
        return self.manage_main(self, REQUEST, update_menu=1)

InitializeClass(PhotoFolder)

#
# Support class to provide more similarity with Zope upload files.
# Sometimes the calling program will make a read() with a maxsize.
# This class will simply deliver what is available even if it is bigger.
# On some occasions though, the size of the content will be exactly the
# amount request and the calling function will assume that there is more
# to read. Therefore we allow only one read to work.
#
class ZZipFile(ZipFile):

    def read(self,size=-1):
        if(self.hasbeenread == 0):
            self.hasbeenread = 1
            return ZipFile.read(self,self.filename)
        else:
            return ""

    def seek(self):
        "Ignore since it is only used to figure out size."
        self.hasbeenread = 0
        return 0

    def tell(self):
        return self.getinfo(filename).file_size

    def setcurrentfile(self,filename):
        self.hasbeenread = 0
        self.filename=filename

