# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php
# Copyright 2005, Tim Potter <tpot@samba.org>
# Copyright 2006-2008 John-Mark Gurney <jmg@funkthat.com>

__version__ = '$Change$'
# $Id$

#
# This module implements the Content Directory Service (CDS) service
# type as documented in the ContentDirectory:1 Service Template
# Version 1.01
#

#
# TODO: Figure out a nicer pattern for debugging soap server calls as
# twisted swallows the tracebacks.  At the moment I'm going:
#
# try:
#	....
# except:
#	traceback.print_exc(file = log.logfile)
#

reqname = 'requests'

from twisted.python import log
from twisted.web import resource, static

from elementtree.ElementTree import Element, SubElement, tostring
from upnp import UPnPPublisher, errorCode
from DIDLLite import DIDLElement, Container, Movie, Resource, MusicTrack

from twisted.internet import defer, threads
from twisted.python import failure

import debug
import traceback
from urllib import quote

class doRecall(defer.Deferred):
	'''A class that will upon any callback from the Deferred object passed
	in, recall fun(*args, **kwargs), just as if a maybeDeferred has been
	processed.

	The idea is to let something deeper called by something sync "abort"
	the call until it's ready, and then reattempt.  This isn't the best
	method as we throw away work, but it can be easier to implement.

	Example:
	def wrapper(five):
		try:
			return doacall(five)
		except defer.Deferred, x:
			return doRecallgen(x, wrapper, five)

	If doacall works, everything is fine, but if a Deferred object is
	raised, we put it in a doRecall class and return the deferred object
	generated by doRecall.'''

	def __init__(self, argdef, fun, *args, **kwargs):
		self.fun = fun
		self.args = args
		self.kwargs = kwargs
		self.defer = defer.Deferred()

		argdef.addCallback(self._done)

	def _done(self, *args, **kwargs):
		ret = self.fun(*self.args, **self.kwargs)
		if isinstance(ret, failure.Failure):
			self.defer.errback(ret)
		elif isinstance(ret, defer.Deferred):
			# We are fruther delayed, continue.
			ret.addCallback(self._done)
		else:
			self.defer.callback(ret)

	@staticmethod
	def wrapper(fun, *args, **kwargs):
		try:
			return fun(*args, **kwargs)
		except defer.Deferred, x:
			return doRecallgen(x, fun, *args, **kwargs)

def doRecallgen(defer, fun, *args, **kwargs):
        i = doRecall(defer, fun, *args, **kwargs)
        return i.defer

class ContentDirectoryControl(UPnPPublisher, dict):
	"""This class implements the CDS actions over SOAP."""

	namespace = 'urn:schemas-upnp-org:service:ContentDirectory:1'
	updateID = property(lambda x: x['0'].updateID)
	urlbase = property(lambda x: x._urlbase)

	def getnextID(self):
		ret = str(self.nextID)
		self.nextID += 1
		return ret

	def addContainer(self, parent, title, klass = Container, *args, **kwargs):
		ret = self.addObject(parent, klass, title, *args, **kwargs)
		if ret is None:
			return
		self.children[ret] = self[ret]
		return ret

	def addItem(self, parent, klass, title, *args, **kwargs):
		if issubclass(klass, Container):
			return self.addContainer(parent, title, klass, *args, **kwargs)
		else:
			return self.addObject(parent, klass, title, *args, **kwargs)

	def addObject(self, parent, klass, title, *args, **kwargs):
		'''If the generated object (by klass) has an attribute content, it is installed into the web server.'''
		assert isinstance(self[parent], Container)
		nid = self.getnextID()
		try:
			i = klass(self, nid, parent, title, *args, **kwargs)
		except:
			import traceback
			traceback.print_exc()
			return

		if hasattr(i, 'content'):
			self.webbase.putChild(nid, i.content)
		#log.msg('children:', `self.children[parent]`, `i`)
		self.children[parent].append(i)
		self[i.id] = i
		return i.id

	def has_key(self, key):
		return dict.has_key(self, key)

	def delItem(self, id):
		if not self.has_key(id):
			log.msg('already removed:', id)
			return
		#log.msg('removing:', id)
		if isinstance(self[id], Container):
			#log.msg('children:', Container.__repr__(self.children[id]), map(None, self.children[id]))
			while self.children[id]:
				self.delItem(self.children[id][0].id)
			assert len(self.children[id]) == 0
			del self.children[id]
		# Remove from parent
		self.children[self[id].parentID].remove(self[id])
		# Remove content
		if hasattr(self[id], 'content'):
			self.webbase.delEntity(id)
		del self[id]

	def getchildren(self, item):
		assert isinstance(self[item], Container)
		return self.children[item][:]

	def __init__(self, title, *args, **kwargs):
		debug.insertringbuf(reqname)
		super(ContentDirectoryControl, self).__init__(*args)
		self.webbase = kwargs['webbase']
		self._urlbase = kwargs['urlbase']
		del kwargs['webbase'], kwargs['urlbase']
		fakeparent = '-1'
		self.nextID = 0
		self.children = { fakeparent: []}
		self[fakeparent] = Container(None, None, '-1', 'fake')
		root = self.addContainer(fakeparent, title, **kwargs)
		assert root == '0'
		del self[fakeparent]
		del self.children[fakeparent]

	# Required actions

	def soap_GetSearchCapabilities(self, *args, **kwargs):
		"""Required: Return the searching capabilities supported by the device."""

		log.msg('GetSearchCapabilities()')
		return { 'SearchCaps': '' }

	def soap_GetSortCapabilities(self, *args, **kwargs):
		"""Required: Return the CSV list of meta-data tags that can be used in
		sortCriteria."""

		log.msg('GetSortCapabilities()')
		return { 'SortCaps': '' }

	def soap_GetSystemUpdateID(self, *args, **kwargs):
		"""Required: Return the current value of state variable SystemUpdateID."""

		return { 'Id': self.updateID }

	BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren')

	def soap_Browse(self, *args):
		l = {}
		debug.appendnamespace(reqname, l)
		if self.has_key(args[0]):
			l['object'] = self[args[0]]
		l['query'] = 'Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, ' \
		    'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' % \
		    tuple(map(repr, args))

		def setresp(r):
			l['response'] = r
			return r

		return threads.deferToThread(self.thereal_soap_Browse,
		    *args).addCallback(setresp)

	def thereal_soap_Browse(self, *args):
		"""Required: Incrementally browse the native heirachy of the Content
		Directory objects exposed by the Content Directory Service."""

		(ObjectID, BrowseFlag, Filter, StartingIndex, RequestedCount,
		    SortCriteria) = args
		StartingIndex = int(StartingIndex)
		RequestedCount = int(RequestedCount)

		didl = DIDLElement()

		# return error code if we don't exist anymore
		if ObjectID not in self:
			raise errorCode(701)

		# check to see if object needs to be updated
		self[ObjectID].checkUpdate()

		# make sure we still exist, we could of deleted ourself
		if ObjectID not in self:
			raise errorCode(701)

		if BrowseFlag == 'BrowseDirectChildren':
			ch = self.getchildren(ObjectID)[StartingIndex: StartingIndex + RequestedCount]
			for i in ch:
				if i.needupdate:
					i.checkUpdate()
				didl.addItem(i)
			total = len(self.getchildren(ObjectID))
		else:
			didl.addItem(self[ObjectID])
			total = 1

		r = { 'Result': didl.toString(), 'TotalMatches': total,
		    'NumberReturned': didl.numItems(), }

		if hasattr(self[ObjectID], 'updateID'):
			r['UpdateID'] = self[ObjectID].updateID
		else:
			r['UpdateID'] = self.updateID

		return r

	# Optional actions

	def soap_Search(self, *args, **kwargs):
		"""Search for objects that match some search criteria."""

		(ContainerID, SearchCriteria, Filter, StartingIndex,
		 RequestedCount, SortCriteria) = args

		log.msg('Search(ContainerID=%s, SearchCriteria=%s, Filter=%s, ' \
		    'StartingIndex=%s, RequestedCount=%s, SortCriteria=%s)' %
		    (`ContainerID`, `SearchCriteria`, `Filter`,
		    `StartingIndex`, `RequestedCount`, `SortCriteria`))

	def soap_CreateObject(self, *args, **kwargs):
		"""Create a new object."""

		(ContainerID, Elements) = args

		log.msg('CreateObject(ContainerID=%s, Elements=%s)' %
		    (`ContainerID`, `Elements`))

	def soap_DestroyObject(self, *args, **kwargs):
		"""Destroy the specified object."""

		(ObjectID) = args

		log.msg('DestroyObject(ObjectID=%s)' % `ObjectID`)

	def soap_UpdateObject(self, *args, **kwargs):
		"""Modify, delete or insert object metadata."""

		(ObjectID, CurrentTagValue, NewTagValue) = args

		log.msg('UpdateObject(ObjectID=%s, CurrentTagValue=%s, ' \
		    'NewTagValue=%s)' % (`ObjectID`, `CurrentTagValue`,
		    `NewTagValue`))

	def soap_ImportResource(self, *args, **kwargs):
		"""Transfer a file from a remote source to a local
		destination in the Content Directory Service."""

		(SourceURI, DestinationURI) = args

		log.msg('ImportResource(SourceURI=%s, DestinationURI=%s)' %
		    (`SourceURI`, `DestinationURI`))

	def soap_ExportResource(self, *args, **kwargs):
		"""Transfer a file from a local source to a remote
		destination."""

		(SourceURI, DestinationURI) = args

		log.msg('ExportResource(SourceURI=%s, DestinationURI=%s)' %
		    (`SourceURI`, `DestinationURI`))

	def soap_StopTransferResource(self, *args, **kwargs):
		"""Stop a file transfer initiated by ImportResource or
		ExportResource."""

		(TransferID) = args

		log.msg('StopTransferResource(TransferID=%s)' % TransferID)

	def soap_GetTransferProgress(self, *args, **kwargs):
		"""Query the progress of a file transfer initiated by
		an ImportResource or ExportResource action."""

		(TransferID, TransferStatus, TransferLength, TransferTotal) = args

		log.msg('GetTransferProgress(TransferID=%s, TransferStatus=%s, ' \
		    'TransferLength=%s, TransferTotal=%s)' %
		    (`TransferId`, `TransferStatus`, `TransferLength`,
		    `TransferTotal`))

	def soap_DeleteResource(self, *args, **kwargs):
		"""Delete a specified resource."""

		(ResourceURI) = args

		log.msg('DeleteResource(ResourceURI=%s)' % `ResourceURI`)

	def soap_CreateReference(self, *args, **kwargs):
		"""Create a reference to an existing object."""

		(ContainerID, ObjectID) = args

		log.msg('CreateReference(ContainerID=%s, ObjectID=%s)' %
		    (`ContainerID`, `ObjectID`))

	def __repr__(self):
		return '<ContentDirectoryControl: cnt: %d, urlbase: %s, nextID: %d>' % (len(self), `self.urlbase`, self.nextID)

class ContentDirectoryServer(resource.Resource):
	def __init__(self, title, *args, **kwargs):
		resource.Resource.__init__(self)
		self.putChild('scpd.xml', static.File('content-directory-scpd.xml'))
		self.control = ContentDirectoryControl(title, *args, **kwargs)
		self.putChild('control', self.control)