from AccessControl import ClassSecurityInfo
from Globals import InitializeClass

from ExtensionClass import Base
from Acquisition import Implicit, aq_parent
from OFS.Traversable import Traversable

from OFS.Cache import ChangeCacheSettingsPermission
from Products.CMFCore import CMFCorePermissions

from Products.CMFDefault.Image import Image
import OFS.Image
from BTrees.OOBTree import OOBTree

from cgi import escape
from cStringIO import StringIO
import sys
from zLOG import LOG, ERROR, INFO

from imageengine import isPilAvailable, isConvertAvailable

DEBUG=1
DEFAULT_QUALITY=100

if isPilAvailable:
    import PIL.Image
    from PIL.Image import NEAREST, BILINEAR, BICUBIC, ANTIALIAS
    # NEAREST (use nearest neighbour)
    # BILINEAR (linear interpolation in a 2x2 environment)
    # BICUBIC (cubic spline interpolation in a 4x4 environment)
    # ANTIALIAS (a high-quality downsampling filter)
    # antialiasing is the best algorithm for shrinking pictures
    RESIZING_ALGO = ANTIALIAS
    # PIL doesn't support antialiasing for rotating!
    ROTATING_ALGO = BICUBIC

# transpose constants, taken from PIL.Image to maintain compatibilty
FLIP_LEFT_RIGHT = 0
FLIP_TOP_BOTTOM = 1
ROTATE_90 = 2
ROTATE_180 = 3
ROTATE_270 = 4

TRANSPOSE_MAP = (
    (FLIP_LEFT_RIGHT, "Flip around vertical axis"),
    (FLIP_TOP_BOTTOM, "Flip around horizontal axis"),
    (ROTATE_270,      "Rotate 90 clockwise"),
    (ROTATE_180,      "Rotate 180"),
    (ROTATE_90,       "Rotate 90 counterclockwise"),
)

factory_type_information = {
    'id'             : 'Photo',
    'meta_type'      : 'Photo',
    'description'    : 'Photos objects can be embedded in Portal documents.',
    'icon'           : 'photo_icon.gif',
    'product'        : 'CMFPhoto',
    'factory'        : 'addPhoto',
    'immediate_view' : 'image_edit_form',
    'actions'        :
    ( { 'id'            : 'view',
        'name'          : 'View',
        'action'        : 'photo_view',
        'permissions'   : (CMFCorePermissions.View, )
        },
      { 'id'            : 'edit',
        'name'          : 'Properties',
        'action'        : 'portal_form/image_edit_form',
        'permissions'   : (CMFCorePermissions.ModifyPortalContent, )
        },
      { 'id'            : 'transform',
        'name'          : 'Transform Image',
        'action'        : 'photo_transform',
        'permissions'   : (CMFCorePermissions.ModifyPortalContent, )
        },
      { 'id'            : 'metadata',
        'name'          : 'Metadata',
        'action'        : 'portal_form/metadata_edit_form',
        'permissions'   : (CMFCorePermissions.ModifyPortalContent, )
        }
      )
    }

def addPhoto( self
              , id
              , title=''
              , file=''
              , content_type=''
              , precondition=''
              , subject=()
              , description=''
              , contributors=()
              , effective_date=None
              , expiration_date=None
              , format='image/png'
              , language=''
              , rights=''
              ):
    """
    Add an Photo
    """

    # cookId sets the id and title if they are not explicity specified
    id, title = OFS.Image.cookId(id, title, file)

    self=self.this()

    # Instantiate the object and set its description.
    iobj = Photo( id, title, '', content_type, precondition, subject
                  , description, contributors, effective_date, expiration_date
                  , format, language, rights
                  )

    # Add the Photo instance to self
    self._setObject(id, iobj)

    # 'Upload' the photo.  This is done now rather than in the
    # constructor because it's faster (see File.py.)
    self._getOb(id).manage_upload(file)


class DynVariantWrapper(Base):
    """
    provide a transparent wrapper from photo to dynvariant
    call it with url ${photo_url}/variant/${variant}
    """

    def __of__(self, parent):
        return parent.Variants()


class DynVariant(Implicit, Traversable):
    """
    provide access to the variants
    """
    def __init__(self):
	pass

    def __getitem__(self, name):
	if self.checkForVariant(name):
            return self.getPhoto(name).__of__(aq_parent(self))
	else:
	    return self


class Photo(Image):
    """
    Implements a Photo, a scalable image
    """


    __implements__ = ( Image.__implements__ ,)


    meta_type = 'Photo'


    def __init__( self
                , id
                , title=''
                , file=''
                , content_type=''
                , precondition=''
                , subject=()
                , description=''
                , contributors=()
                , effective_date=None
                , expiration_date=None
                , format='image/png'
                , language='en-US'
                , rights=''
                ):
        Image.__init__(self, id, title, file, content_type, precondition,
                       subject, description, contributors, effective_date,
                       expiration_date, format, language, rights)
        self._photos = OOBTree()


    security = ClassSecurityInfo()

    # make image variants accesable via url
    variant=DynVariantWrapper()
    security.declareProtected(CMFCorePermissions.View, 'Variants')
    def Variants(self):
        # Returns a variants wrapper instance
        return DynVariant().__of__(self) 

    security.declareProtected(CMFCorePermissions.View, 'getPhoto')
    def getPhoto(self,size):
        '''returns the Photo of the specified size'''
        return self._photos[size]

    security.declareProtected(CMFCorePermissions.View, 'getDisplays')
    def getDisplays(self):
        result = []

        for name, size in self.photo_display_sizes().items():
            if len(size) == 3:
                quality = size[2]
            else:
                quality = DEFAULT_QUALITY
            result.append({
                'name':name, 'label':'%s (%dx%d)' % (name, size[0], size[1]),
                'size':(size[0],size[1]),
                'quality' : quality
                })

        result.sort(lambda d1,d2: cmp(d1['size'][0]*d1['size'][0],d2['size'][1]*d2['size'][1])) #sort ascending by size
        return result

    security.declareProtected(CMFCorePermissions.ModifyPortalContent, 'getTransforms')
    def getTransforms(self):
        return [{'name': method, 'label': name} for method, name in TRANSPOSE_MAP ]

    security.declarePrivate('checkForVariant')
    def checkForVariant(self, size):
	"""Create variant if not there."""
        if size in self.photo_display_sizes().keys():
	    # Create resized copy, if it doesnt already exist
            if not self._photos.has_key(size):
                self._photos[size] = OFS.Image.Image(size, size,
                                                     self._resize(self.photo_display_sizes().get(size, (0,0))))
            # a copy with a content type other than image/* exists, this
            # probably means that the last resize process failed. retry
            elif not self._photos[size].getContentType().startswith('image'):
                self._photos[size] = OFS.Image.Image(size, size,
                                                     self._resize(self.photo_display_sizes().get(size, (0,0))))

            return 1

        else: return 0

    security.declareProtected(CMFCorePermissions.View, 'index_html')
    def index_html(self, REQUEST, RESPONSE, size=None):
        """Return the image data."""
        if self.checkForVariant(size):
            return self.getPhoto(size).index_html(REQUEST, RESPONSE)

        return Photo.inheritedAttribute('index_html')(self, REQUEST, RESPONSE)

    security.declareProtected(CMFCorePermissions.View, 'tag')
    def tag(self, height=None, width=None, alt=None,
            scale=0, xscale=0, yscale=0, css_class=None, title=None, size='original', **args):
        """ Return an HTML img tag (See OFS.Image)"""

        # Default values
        w=self.width
        h=self.height

        if height is None or width is None:

            if size in self.photo_display_sizes().keys():
                if not self._photos.has_key(size):
                    # This resized image isn't created yet.
                    # Calculate a size for it
                    x,y = self.photo_display_sizes().get(size)
                    tmpw, tmph = self.width, self.height
                    
                    try:
                        mirror, rotation = self.exif_orientation()
                        if rotation == 90 or rotation == 270:
                            tmpw, tmph = tmph, tmpw
                        
                        if tmpw > tmph:
                            w = x
                            h = int(round(1.0/(float(tmpw)/w/tmph)))
                        else:
                            h = y
                            w = int(round(1.0/(float(tmph)/x/tmpw)))
                    
                    except ValueError:
                        # OFS.Image only knows about png, jpeg and gif.
                        # Other images like bmp will not have height and
                        # width set, and will generate a ValueError here.
                        # Everything will work, but the image-tag will render
                        # with height and width attributes.
                        w=None
                        h=None
                else:
                    # The resized image exist, get it's size
                    photo = self._photos.get(size)
                    w=photo.width
                    h=photo.height

        if height is None: height=h
        if width is None:  width=w

        # Auto-scaling support
        xdelta = xscale or scale
        ydelta = yscale or scale

        if xdelta and width:
            width =  str(int(round(int(width) * xdelta)))
        if ydelta and height:
            height = str(int(round(int(height) * ydelta)))

        result='<img src="%s/variant/%s"' % (self.absolute_url(), escape(size))

        if alt is None:
            alt=getattr(self, 'title', '')
        result = '%s alt="%s"' % (result, escape(alt, 1))

        if title is None:
            title=getattr(self, 'title', '')
        result = '%s title="%s"' % (result, escape(title, 1))

        if height:
            result = '%s height="%s"' % (result, height)

        if width:
            result = '%s width="%s"' % (result, width)

        if not 'border' in [ x.lower() for x in  args.keys()]:
            result = '%s border="0"' % result

        if css_class is not None:
            result = '%s class="%s"' % (result, css_class)

        for key in args.keys():
            value = args.get(key)
            result = '%s %s="%s"' % (result, key, value)

        return '%s />' % result

    security.declareProtected(CMFCorePermissions.ModifyPortalContent, 'doTransform')
    def doTransform(self, method, REQUEST=None):
        """
        Transform an Image:
            FLIP_LEFT_RIGHT
            FLIP_TOP_BOTTOM
            ROTATE_90 (rotate counterclockwise)
            ROTATE_180
            ROTATE_270 (rotate clockwise)
        """ 
        image = StringIO()

        method = int(method)
        if isPilAvailable:
            img = PIL.Image.open(StringIO(str(self.data)))
            fmt = img.format
            img = img.transpose(method)
            img.save(image, fmt, quality=DEFAULT_QUALITY)
        elif isConvertAvailable: # fall back to convert
            if method in [ROTATE_90, ROTATE_180, ROTATE_270]:
                deg = 90
                if method == ROTATE_180:
                    deg = 180
                elif method == ROTATE_270:
                    deg = 270
                image = self.callConvert(image, rotate=deg)
                
            elif method == FLIP_LEFT_RIGHT:
                image = self.callConvert(image, 'flop')
            elif method == FLIP_TOP_BOTTOM:
                image = self.callConvert(image, 'flip')
            else:
                raise ValueError, "Unknown method '%s'" % (method,)
        else:
            if DEBUG:
                raise Exception('Error in doTransform')

        self.update_data(image.getvalue())

        if REQUEST:
             REQUEST.RESPONSE.redirect(self.absolute_url() + '/photo_transform')

    security.declarePrivate('callConvert')
    def callConvert(self, img_file_obj, *args, **kwargs):
        """
        Convert an image using the 'convert' program
        img_file_obj is a StringIO instance
        """

        command = "convert -quality %s" % DEFAULT_QUALITY
        # TODO check convert manual for argument precedence
        for arg in args:
            command += " -%s " % (arg,)
        for key, val in kwargs.items():
            command += " -%s %s " % (key, val)

        command += " - -" # stdin & stdout as input & output

        if sys.platform == 'win32':
            from win32pipe import popen2
            imgin, imgout = popen2(command, 'b')
        else:
            from popen2 import Popen3
            convert=Popen3(command)
            imgout=convert.fromchild
            imgin=convert.tochild

        imgin.write(str(self.data))
        imgin.close()
        img_file_obj.write(imgout.read())
        imgout.close()

        #Wait for process to close if unix. Should check returnvalue for wait
        if sys.platform !='win32':
            convert.wait()

        img_file_obj.seek(0)
        return img_file_obj

    security.declarePrivate('update_data')
    def update_data(self, data, content_type=None, size=None):
        """
        Update/upload image -> remove all copies
        """
        self.clearCache()
        Image.update_data(self, data, content_type, size)
        
    def _resize(self, size, quality=DEFAULT_QUALITY):
        """Resize and resample photo."""
        image = StringIO()

        width = size[0]
        height = size[1]
        if len(size) == 3 and quality == DEFAULT_QUALITY:
            quality = size[2] 

        
        # check if picture needs to be rotated
        mirror, rotation = self.exif_orientation()
        if isPilAvailable:
            img = PIL.Image.open(StringIO(str(self.data)))
            fmt = img.format
            # Resize photo
            img.thumbnail((width, height), RESIZING_ALGO)
            if rotation:
                rotation = 360 - rotation
                img = img.rotate(rotation, ROTATING_ALGO)
            # Store copy in image buffer
            img.save(image, fmt, quality=quality)
        elif isConvertAvailable:
            geometry = "%sx%s" % (width, height)
            image = self.callConvert(image, rotate=rotation, geometry=geometry)
        else:
            if DEBUG:
                raise RuntimeError('Error in _resize: No image manipulation engine found! Pleas read the readme')

        return image

    security.declareProtected(CMFCorePermissions.View, 'getEXIF')
    def getEXIF(self):
        """
        Extracts the exif metadata from the image and returns
        it as a hashtable
        """
        import EXIF

        try:
            data = EXIF.process_file(StringIO(str(self.data)))
        except:
            data = {}
        if not data:
            data = {}

        keys = data.keys()
        keys.sort()

        result = {}

        for key in keys:
            if key in ('JPEGThumbnail', 'TIFFThumbnail'):
                continue
            try:
                result[key] = str(data[key].printable)
            except:
                pass
        return result

    security.declareProtected(CMFCorePermissions.View, 'exif_orientation')
    def exif_orientation(self):
        """XXX
        """
        exif = self.getEXIF()
        mirror = 0;
        rotation = 0;
        
        if not exif.has_key('Image Orientation'):
            return (mirror, rotation)
        
        code = exif.get('Image Orientation')

        try:
            code = int(code)
        except ValueError:
            return (mirror, rotation)
            
        if code in (2, 4, 5, 7):
            mirror = 1
        if code in (1, 2):
            rotation = 0
        elif code in (3, 4):
            rotation = 180
        elif code in (5, 6):
            rotation = 90
        elif code in (7, 8):
            rotation = 270
       
        return (mirror, rotation)
 
    security.declareProtected(ChangeCacheSettingsPermission, 'ZCacheable_setManagerId')
    def ZCacheable_setManagerId(self, manager_id, REQUEST=None):
        '''Changes the manager_id for this object.
           overridden because we must propagate the change to all variants'''
        for size in self._photos.keys():
            variant = self.getPhoto(size).__of__(self)
            variant.ZCacheable_setManagerId(manager_id)
        return Photo.inheritedAttribute('ZCacheable_setManagerId')(self, manager_id, REQUEST)

    security.declareProtected(CMFCorePermissions.View, 'SearchableText')
    def SearchableText(self):
        """ Used by the catalog for basic full text indexing """
        return "%s %s" % ( self.title_or_id(), self.description ) 

    security.declareProtected(CMFCorePermissions.ManagePortal , 'clearCache')        
    def clearCache(self):
        """Clears the internal cache and the zope cache and removes all resized variants
        """
        self.ZCacheable_invalidate() 
        self._photos = OOBTree()

InitializeClass(Photo)
