# -*- coding: utf-8 -*-
#######################################################################################
#   Photo is a part of Plinn - http://plinn.org                                       #
#   Copyright © 2004-2008  Benoît PIN <benoit.pin@ensmp.fr>                           #
#                                                                                     #
#   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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
#######################################################################################
""" Photo metadata read / write module



"""

from AccessControl import ClassSecurityInfo
from Acquisition import aq_base
from Globals import InitializeClass
from AccessControl.Permissions import view
from ZODB.interfaces import BlobError
from ZODB.utils import cp
from OFS.Image import File
from xmp import XMP
from logging import getLogger
from cache import memoizedmethod
from libxml2 import parseDoc
from standards.xmp import accessors as xmpAccessors
import xmputils
from types import TupleType
from subprocess import Popen, PIPE
from Products.PortalTransforms.libtransforms.utils import bin_search, \
														  MissingBinary

XPATH_EMPTY_TAGS = "//node()[name()!='' and not(node()) and not(@*)]"
console = getLogger('Photo.metadata')

try :
	XMPDUMP = 'xmpdump'
	XMPLOAD = 'xmpload'
	bin_search(XMPDUMP)
	bin_search(XMPLOAD)
	xmpIO_OK = True
except MissingBinary :
	xmpIO_OK = False
	console.warn("xmpdump or xmpload not available.")

class Metadata :
	""" Photo metadata read / write mixin """
	
	security = ClassSecurityInfo()
	
	
	#
	# reading api
	#

	security.declarePrivate('getXMP')
	if xmpIO_OK :
		@memoizedmethod()
		def getXMP(self):
			"""returns xmp metadata packet with xmpdump call
			"""
			if self.size :
				blob_file_path = self.bdata._p_blob_uncommitted or self.bdata._p_blob_committed
				dumpcmd = '%s %s' % (XMPDUMP, blob_file_path)
				p = Popen(dumpcmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=True)
				xmp, err = p.communicate()
				if err :
					raise SystemError, err
				return xmp

	else :
		@memoizedmethod()
		def getXMP(self):
			"""returns xmp metadata packet with XMP object
			"""
			xmp = None
			if self.size :
				try :
					bf = self.open('r')
					x = XMP(bf, content_type=self.content_type)
					xmp = x.getXMP()
				except NotImplementedError :
					pass
		
			return xmp
		
	security.declareProtected(view, 'getXmpFile')
	def getXmpFile(self, REQUEST):
		"""returns the xmp packet over http.
		"""
		xmp = self.getXMP()
		if xmp is not None :
			return File('xmp', 'xmp', xmp, content_type='text/xml').index_html(REQUEST, REQUEST.RESPONSE)
		else :
			return None

	security.declarePrivate('getXmpBag')
	def getXmpBag(self, name, root, index=None) :
		index = self.getXmpPathIndex()
		if index :
			path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
			node = index.get(path)
		
			if node :
				values = xmputils.getBagValues(node.element)
				return values
		return tuple()
	
	security.declarePrivate('getXmpSeq')
	def getXmpSeq(self, name, root) :
		index = self.getXmpPathIndex()
		if index :
			path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
			node = index.get(path)
		
			if node :
				values = xmputils.getSeqValues(node.element)
				return values
		return tuple()

	security.declarePrivate('getXmpAlt')
	def getXmpAlt(self, name, root) :
		index = self.getXmpPathIndex()
		if index :
			path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
			node = index.get(path)

			if node :
				firstLi = node.get('rdf:Alt/rdf:li')
				if firstLi :
					assert firstLi.unique, "More than one rdf:Alt (localisation not yet supported)"
					return firstLi.element.content
		return ''

	security.declarePrivate('getXmpProp')
	def getXmpProp(self, name, root):
		index = self.getXmpPathIndex()
		if index :
			path = '/'.join(filter(None, ['rdf:RDF/rdf:Description', root, name]))
			node = index.get(path)
			if node :
				return node.element.content
		return '' 
		
		
	security.declarePrivate('getXmpPathIndex')
	@memoizedmethod(volatile=True)
	def getXmpPathIndex(self):
		xmp = self.getXMP()
		if xmp :
			d = parseDoc(xmp)
			index = xmputils.getPathIndex(d)
			return index
			
	security.declarePrivate('getXmpValue')
	def getXmpValue(self, name):
		""" returns pythonic version of xmp property """
		info = xmpAccessors[name]
		root = info['root']
		rdfType = info['rdfType'].capitalize()
		methName = 'getXmp%s' % rdfType
		meth = getattr(aq_base(self), methName)
		return meth(name, root)
	
	
	security.declareProtected(view, 'getXmpField')
	def getXmpField(self, name):
		""" returns data formated for a html form field """
		editableValue = self.getXmpValue(name)
		if type(editableValue) == TupleType :
			editableValue = ', '.join(editableValue)
		return {'id' : name.replace(':', '_'),
				'value' : editableValue}
	
	
	#
	# writing api
	#

	security.declarePrivate('setXMP')
	if xmpIO_OK :
		def setXMP(self, xmp):
			"""setXMP with xmpload call
			"""
			if self.size :
				blob = self.bdata
				if blob.readers :
					raise BlobError("Already opened for reading.")
			
				if blob._p_blob_uncommitted is None:
					filename = blob._create_uncommitted_file()
					uncommitted = file(filename, 'w')
					cp(file(blob._p_blob_committed, 'rb'), uncommitted)
					uncommitted.close()
				else :
					filename = blob._p_blob_uncommitted
			
				loadcmd = '%s %s' % (XMPLOAD, filename)
				p = Popen(loadcmd, stdin=PIPE, stderr=PIPE, shell=True)
				p.stdin.write(xmp)
				p.stdin.close()
				p.wait()
				err = p.stderr.read()
				if err :
					raise SystemError, err
			
				f = file(filename)
				f.seek(0,2)
				self.updateSize(size=f.tell())
				f.close()
				self.bdata._p_changed = True
			
			
				# purge caches
				try : del self._methodResultsCache['getXMP']
				except KeyError : pass 
			
				for name in ('getXmpPathIndex',) :
					try :
						del self._v__methodResultsCache[name]
					except (AttributeError, KeyError):
						continue

				self.ZCacheable_invalidate()
				self.ZCacheable_set(None)
				self.http__refreshEtag()
	
	else :
		def setXMP(self, xmp):
			"""setXMP with XMP object
			"""
			if self.size :
				bf = self.open('r+')
				x = XMP(bf, content_type=self.content_type)
				x.setXMP(xmp)
				x.save()
				self.updateSize(size=bf.tell())

				# don't call update_data
				self.ZCacheable_invalidate()
				self.ZCacheable_set(None)
				self.http__refreshEtag()

				# purge caches
				try : del self._methodResultsCache['getXMP']
				except KeyError : pass
				for name in ('getXmpPathIndex', ) :
					try :
						del self._v__methodResultsCache[name]
					except (AttributeError, KeyError):
						continue
		
		
			
	security.declarePrivate('setXmpField')
	def setXmpFields(self, **kw):
		xmp = self.getXMP()
		if xmp :
			doc = parseDoc(xmp)
		else :
			doc = xmputils.createEmptyXmpDoc()
		
		index = xmputils.getPathIndex(doc)
		
		pathPrefix = 'rdf:RDF/rdf:Description'
		preferedNsDeclaration = 'rdf:RDF/rdf:Description'

		for id, value in kw.items() :
			name = id.replace('_', ':')
			info = xmpAccessors.get(name)
			if not info : continue
			root = info['root']
			rdfType = info['rdfType']
			path = '/'.join([p for p in [pathPrefix, root, name] if p])

			Metadata._setXmpField(index
								, path
								, rdfType
								, name
								, value
								, preferedNsDeclaration)
		
		# clean empty tags without attributes
		context = doc.xpathNewContext()
		nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
		while nodeset :
			for n in nodeset :
				n.unlinkNode()
				n.freeNode()
			nodeset = context.xpathEval(XPATH_EMPTY_TAGS)
		
		
		
		xmp = doc.serialize('utf-8')
		# remove <?xml version="1.0" encoding="utf-8"?> header
		xmp = xmp.split('?>', 1)[1].lstrip('\n')
		self.setXMP(xmp)

	@staticmethod
	def _setXmpField(index, path, rdfType, name, value, preferedNsDeclaration) :
		if rdfType in ('Bag', 'Seq') :
			value = value.replace(';', ',')
			value = value.split(',')
			value = [item.strip() for item in value]
			value = filter(None, value)

		if value :
			# edit
			xmpPropIndex = index.getOrCreate(path
									, rdfType
									, preferedNsDeclaration)
			if rdfType == 'prop' :
				xmpPropIndex.element.setContent(value)
			else :
				#rdfPrefix = index.getDocumentNs()['http://www.w3.org/1999/02/22-rdf-syntax-ns#']
				func = getattr(xmputils, 'createRDF%s' % rdfType)
				newNode = func(name, value, index)
				oldNode = xmpPropIndex.element
				oldNode.replaceNode(newNode)
		else :
			# delete
			xmpPropIndex = index.get(path)
			if xmpPropIndex is not None :
				xmpPropIndex.element.unlinkNode()
				xmpPropIndex.element.freeNode()
		

InitializeClass(Metadata)
