from os import fstat
from zope.interface import implements
from StringIO import StringIO
from Acquisition import Implicit, aq_base
from AccessControl import ClassSecurityInfo
from ComputedAttribute import ComputedAttribute
try:
    from App.class_init import InitializeClass
    InitializeClass     # keep pyflakes happy...
except ImportError:
    from Globals import InitializeClass
from ZODB.blob import Blob
from persistent import Persistent
from transaction import savepoint
from webdav.common import rfc1123_date
 
from Products.CMFCore.permissions import View
from Products.Archetypes.atapi import ObjectField, FileWidget, ImageWidget
from Products.Archetypes.atapi import PrimaryFieldMarshaller
from Products.Archetypes.Registry import registerField
from Products.Archetypes.utils import contentDispositionHeader
 
from plone.i18n.normalizer.interfaces import IUserPreferredFileNameNormalizer
from plone.app.blob.interfaces import IBlobbable, IWebDavUpload, IBlobField
from plone.app.blob.interfaces import IBlobImageField
from plone.app.blob.interfaces import IBlobWrapper
from plone.app.blob.iterators import BlobStreamIterator
from plone.app.blob.download import handleIfModifiedSince, handleRequestRange
from plone.app.blob.mixins import ImageFieldMixin
from plone.app.blob.utils import getImageSize, getPILResizeAlgo, openBlob
from plone.app.blob.config import blobScalesAttr
 
 
class WebDavUpload(object):
    """ helper class when handling webdav uploads;  the class is needed
        to be able to provide an adapter for this way of creating a blob """
    implements(IWebDavUpload)
 
    def __init__(self, file, filename=None, mimetype=None, context=None, **kwargs):
        self.file = file
        if hasattr(aq_base(context), 'getFilename'):
            filename = context.getFilename() or filename
        self.filename = filename
        self.mimetype = mimetype
        self.kwargs = kwargs
 
 
class BlobMarshaller(PrimaryFieldMarshaller):
 
    def demarshall(self, instance, data, **kwargs):
        p = instance.getPrimaryField()
        mutator = p.getMutator(instance)
        mutator(WebDavUpload(**kwargs))
 
 
class BlobWrapper(Implicit, Persistent):
    """ persistent wrapper for a zodb blob, also holding some metadata """
    implements(IBlobWrapper)
 
    security = ClassSecurityInfo()
 
    def __init__(self, content_type):
        self.blob = Blob()
        self.content_type = content_type
        self.filename = None
 
    security.declareProtected(View, 'index_html')
    def index_html(self, REQUEST=None, RESPONSE=None, charset='utf-8', disposition='inline'):
        """ make it directly viewable when entering the objects URL """
 
        if REQUEST is None:
            REQUEST = self.REQUEST
 
        if RESPONSE is None:
            RESPONSE = REQUEST.RESPONSE
 
        RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime))
        RESPONSE.setHeader('Content-Type', self.getContentType())
        RESPONSE.setHeader('Accept-Ranges', 'bytes')
 
        if handleIfModifiedSince(self, REQUEST, RESPONSE):
            return ''
 
        length = self.get_size()
        RESPONSE.setHeader('Content-Length', length)
 
        filename = self.getFilename()
        if filename is not None:
            if not isinstance(filename, unicode):
                filename = unicode(filename, charset, errors="ignore")
            filename = IUserPreferredFileNameNormalizer(REQUEST).normalize(
                filename)
            header_value = contentDispositionHeader(
                disposition=disposition,
                filename=filename)
            RESPONSE.setHeader("Content-disposition", header_value)
 
        request_range = handleRequestRange(self, length, REQUEST, RESPONSE)
        return self.getIterator(**request_range)
 
    security.declarePrivate('setBlob')
    def setBlob(self, blob):
        """ set the contained blob object """
        self.blob = blob
 
    security.declarePrivate('getBlob')
    def getBlob(self):
        """ return the contained blob object """
        return self.blob
 
    security.declarePrivate('getIterator')
    def getIterator(self, **kw):
        """ return a filestream iterator object from the blob """
        return BlobStreamIterator(self.blob, **kw)
 
    security.declareProtected(View, 'get_size')
    def get_size(self):
        """ return the size of the blob """
        blob = openBlob(self.blob)
        size = fstat(blob.fileno()).st_size
        blob.close()
        return size
 
    __len__ = get_size
 
    def __nonzero__(self):
        # count as having a value unless we lack both data and a filename
        return bool(self.filename or len(self))
 
    security.declareProtected(View, 'getSize')
    def getSize(self):
        """ return image dimensions of the blob """
        # TODO: this should probably be cached...
        blob = openBlob(self.blob)
        size = getImageSize(blob)
        blob.close()
        return size
 
    security.declareProtected(View, 'width')
    @property
    def width(self):
        """ provide the image width as an attribute """
        width, height = self.getSize()
        return width
 
    security.declareProtected(View, 'height')
    @property
    def height(self):
        """ provide the image height as an attribute """
        width, height = self.getSize()
        return height
 
    security.declarePrivate('setContentType')
    def setContentType(self, value):
        """ set mimetype for this blob """
        value = str(value).split(';')[0].strip()    # might be like: text/plain; charset='utf-8'
        self.content_type = value
 
    security.declarePublic('getContentType')
    def getContentType(self):
        """ return mimetype for this blob """
        return self.content_type
 
    security.declarePrivate('setFilename')
    def setFilename(self, value):
        """ set filename for this blob """
        if isinstance(value, basestring):
            value = value[max(value.rfind('/'),
                              value.rfind('\\'),
                              value.rfind(':')) + 1:]
        self.filename = value
 
    security.declarePrivate('getFilename')
    def getFilename(self):
        """ return filename for this blob """
        return self.filename
 
    # compatibility methods
 
    def __str__(self):
        """ return data as a string;  this is highly inefficient as it
            loads the complete blob content into memory, but the method
            is unfortunately still used here and there... """
        return openBlob(self.blob).read()
 
    data = ComputedAttribute(__str__, 0)
 
 
InitializeClass(BlobWrapper)
 
 
class ReuseBlob(Exception):
    """ exception indicating that a blob should be reused """
 
 
class BlobField(ObjectField):
    """ file field implementation based on zodb blobs """
    implements(IBlobField)
 
    _properties = ObjectField._properties.copy()
    _properties.update({
        'type': 'blob',
        'default': None,
        'primary': False,
        'widget': FileWidget,
        'default_content_type': 'application/octet-stream',
    })
 
    security = ClassSecurityInfo()
 
    security.declarePrivate('getUnwrapped')
    def getUnwrapped(self, instance, **kwargs):
        return super(BlobField, self).get(instance, **kwargs)
 
    security.declarePrivate('get')
    def get(self, instance, **kwargs):
        value = super(BlobField, self).get(instance, **kwargs)
        if getattr(value, '__of__', None) is not None:
            return value.__of__(instance)
        else:
            return value
 
    security.declarePrivate('set')
    def set(self, instance, value, **kwargs):
        """ use input value to populate the blob and set the associated
            file name and mimetype.  the latter can be overridden by an
            option "mimetype" keyword argument """
        if value in ('DELETE_FILE', 'DELETE_IMAGE'):
            super(BlobField, self).unset(instance, **kwargs)
            return
        # create a new blob instead of modifying the old one to
        # achieve copy-on-write semantics
        blob = BlobWrapper(self.default_content_type)
        if isinstance(value, basestring):
            value = StringIO(value)     # simple strings cannot be adapted...
            setattr(value, 'filename', kwargs.get('filename', None))
        if value is not None:
            blobbable = IBlobbable(value)
            try:
                blobbable.feed(blob.getBlob())
            except ReuseBlob, exception:
                blob.setBlob(exception.args[0])     # reuse the given blob
            mimetype = kwargs.get('mimetype', None)
            if not mimetype:
                mimetype = blobbable.mimetype()
            if mimetype and mimetype != 'None':
                blob.setContentType(mimetype)
            blob.setFilename(kwargs.get('filename', blobbable.filename()))
        super(BlobField, self).set(instance, blob, **kwargs)
        # a transaction savepoint is created after setting the blob's value
        # in order to make it available at its temporary path (e.g. to index
        # pdfs etc using solr & tika within the same transaction)
        savepoint(optimistic=True)
 
    security.declarePrivate('fixAutoId')
    def fixAutoId(self, instance):
        """ if an explicit id was given and the instance still has the
            auto-generated one it should be renamed;  also see
            `_setATCTFileContent` in ATCT's `ATCTFileContent` class """
        if not self.primary:
            return
        filename = self.getFilename(instance)
        if filename is not None and instance._isIDAutoGenerated(instance.getId()):
            request = instance.REQUEST
            req_id = request.form.get('id')
            if req_id and not instance._isIDAutoGenerated(req_id):
                return      # don't rename if an explicit id was given
            if hasattr(aq_base(instance), '_should_set_id_to_filename'):
                # ^^ BBB for ATContentTypes <2.0
                if not instance._should_set_id_to_filename(filename, request.form.get('title')):
                    return      # don't rename now if AT should do it from title
            if not isinstance(filename, unicode):
                filename = unicode(filename, instance.getCharset())
            filename = IUserPreferredFileNameNormalizer(request).normalize(filename)
            if filename and not filename == instance.getId():
                # a file name was given, so the instance needs to be renamed...
                instance.setId(filename)
 
    security.declareProtected(View, 'download')
    def download(self, instance, REQUEST=None, RESPONSE=None):
        """ download the file (use default index_html) """
        return self.index_html(instance, REQUEST, RESPONSE, disposition='attachment')
 
    security.declareProtected(View, 'index_html')
    def index_html(self, instance, REQUEST=None, RESPONSE=None, **kwargs):
        """ make it directly viewable when entering the objects URL """
        blob = self.get(instance, raw=True)    # TODO: why 'raw'?
        charset = instance.getCharset()
        return blob.index_html(
            REQUEST=REQUEST, RESPONSE=RESPONSE,
            charset=charset, **kwargs
            )
 
    security.declarePublic('get_size')
    def get_size(self, instance):
        """ return the size of the blob used for get_size in BaseObject """
        blob = self.getUnwrapped(instance)
        if blob is not None:
            return blob.get_size()
        else:
            return 0
 
    security.declarePublic('getContentType')
    def getContentType(self, instance, fromBaseUnit=True):
        """ return the mimetype associated with the blob data """
        blob = self.getUnwrapped(instance)
        if blob is not None:
            return blob.getContentType()
        else:
            return 'application/octet-stream'
 
    security.declarePrivate('getFilename')
    def getFilename(self, instance, fromBaseUnit=True):
        """ return the file name associated with the blob data """
        blob = self.getUnwrapped(instance)
        if blob is not None:
            return blob.getFilename()
        else:
            return None
 
 
registerField(BlobField, title='Blob',
              description='Used for storing files in blobs')
 
 
 
# convenience base classes for blob-aware file & image fields
 
class FileField(BlobField):
    """ base class for a blob-based file field """
 
    _properties = BlobField._properties.copy()
    _properties.update({
        'type': 'file',
    })
 
 
registerField(FileField, title='Blob-aware FileField',
              description='Used for storing files in blobs')
 
 
 
class ImageField(BlobField, ImageFieldMixin):
    """ base class for a blob-based image field """
    implements(IBlobImageField)
 
    _properties = BlobField._properties.copy()
    _properties.update({
        'type': 'image',
        'original_size': None,
        'max_size': None,
        'sizes': None,
        'swallowResizeExceptions': False,
        'pil_quality': 88,
        'pil_resize_algo': getPILResizeAlgo(),
        'default_content_type': 'image/png',
        'allowable_content_types': ('image/gif', 'image/jpeg', 'image/png'),
        'widget': ImageWidget,
    })
 
    def set(self, instance, value, **kwargs):
        super(ImageField, self).set(instance, value, **kwargs)
        if hasattr(aq_base(instance), blobScalesAttr):
            delattr(aq_base(instance), blobScalesAttr)
 
 
registerField(ImageField, title='Blob-aware ImageField',
              description='Used for storing image in blobs')