#!/usr/bin/env python
# Copyright 2009 John-Mark Gurney <jmg@funkthat.com>

'''Audio Raw Converter'''

import cdrtoc

from DIDLLite import MusicTrack, AudioItem, MusicAlbum, Resource, ResourceList
from FSStorage import FSObject, registerklassfun

from twisted.web import resource, server, static
from twisted.internet.interfaces import IPullProducer
from zope.interface import implements

decoders = {}
try:
	import flac
	decoders['flac'] = flac.FLACDec
except ImportError:
	pass

mtformat = {
	16: 'audio/l16',	# BE signed
	# 8: 'audio/l8',	unsigned
}

def makeaudiomt(bitsps, rate, nchan):
	try:
		mt = mtformat[bitsps]
	except KeyError:
		raise KeyError('No mime-type for audio format: %s.' %
		    repr(bitsps))

	return '%s;rate=%d;channels=%d' % (mt, rate, nchan)

def makemtfromdec(dec):
	return makeaudiomt(dec.bitspersample, dec.samplerate,
	    dec.channels)

class DecoderProducer:
	implements(IPullProducer)

	def __init__(self, consumer, decoder, tbytes, skipbytes):
		'''skipbytes should always be small.  It is here in case
		someone requests the middle of a sample.'''

		self.decoder = decoder
		self.consumer = consumer
		self.tbytes = tbytes
		self.skipbytes = skipbytes
		#print 'DPregP', `self`, `self.tbytes`, `self.skipbytes`
		consumer.registerProducer(self, False)
		self.resumeProducing()

	def __repr__(self):
		return '<DecoderProducer: decoder: %s, bytes left: %d, skip: %d>' % (repr(self.decoder), self.tbytes, self.skipbytes)

	def pauseProducing(self):
		# XXX - bug in Twisted 8.2.0 on pipelined requests this is
		# called: http://twistedmatrix.com/trac/ticket/3919
		pass

	def resumeProducing(self):
		#print 'DPrP', `self`
		r = self.decoder.read(256*1024)
		if r:
			#print 'DPrP:', len(r)
			if self.skipbytes:
				cnt = min(self.skipbytes, len(r))
				r = r[cnt:]
				self.skipbytes -= cnt

			send = min(len(r), self.tbytes)
			r = r[:send]
			self.tbytes -= len(r)
			self.consumer.write(r)
			#print 'write %d bytes, remaining %d' % (len(r), self.tbytes)
			if self.tbytes:
				return

		#print 'DPurP', `self`
		self.consumer.unregisterProducer()
		self.consumer.finish()

	def stopProducing(self):
		#print 'DPsP', `self`
		self.decoder.close()
		self.decoder = None
		self.consumer = None

class AudioResource(resource.Resource):
	isLeaf = True

	def __init__(self, f, dec, start, cnt):
		resource.Resource.__init__(self)
		self.f = f
		self.dec = dec
		self.start = start
		self.cnt = cnt

	def __repr__(self):
		return '<AudioResource file: %s, dec: %s, start:%d, cnt: %d>' % (repr(self.f), self.dec, self.start, self.cnt)
	def calcrange(self, rng, l):
		rng = rng.strip()
		unit, rangeset = rng.split('=')
		assert unit == 'bytes', repr(unit)
		start, end = rangeset.split('-')
		start = int(start)
		if end:
			end = int(end)
		else:
			end = l

		return start, end - start + 1

	def render(self, request):
		#print 'render:', `request`
		decoder = self.dec(self.f)
		request.setHeader('content-type', makemtfromdec(decoder))
		bytespersample = decoder.channels * decoder.bitspersample / 8
		tbytes = self.cnt * bytespersample
		#print 'tbytes:', `tbytes`, 'cnt:', `self.cnt`
		skipbytes = 0

		request.setHeader('content-length', tbytes)
		request.setHeader('accept-ranges', 'bytes')

		if request.requestHeaders.hasHeader('range'):
			#print 'range req:', `request.requestHeaders.getRawHeaders('range')`
			start, cnt = self.calcrange(
			    request.requestHeaders.getRawHeaders('range')[0],
			    tbytes)
			skipbytes = start % bytespersample

			#print 'going:', start / bytespersample
			decoder.goto(self.start + start / bytespersample)
			#print 'there'

			request.setHeader('content-length', cnt)
			request.setHeader('content-range', 'bytes %s-%s/%s' %
			    (start, start + cnt - 1, tbytes))
			tbytes = cnt
		else:
			decoder.goto(self.start)

		if request.method == 'HEAD':
			return ''

		DecoderProducer(request, decoder, tbytes, skipbytes)
		#print 'producing render', `decoder`, `tbytes`, `skipbytes`

		# and make sure the connection doesn't get closed
		return server.NOT_DONE_YET

class AudioDisc(FSObject, MusicAlbum):
	def __init__(self, *args, **kwargs):
		self.cuesheet = kwargs.pop('cuesheet')
		self.cdtoc = kwargs.pop('toc', {})
		self.kwargs = kwargs.copy()
		# XXX - need to convert to cdtoc?
		self.tags = tags = kwargs.pop('tags', {})

		self.file = kwargs.pop('file')
		nchan = kwargs.pop('channels')
		samprate = kwargs.pop('samplerate')
		bitsps = kwargs.pop('bitspersample')
		samples = kwargs.pop('samples')
		picts = kwargs.pop('pictures', {})
		totalbytes = nchan * samples * bitsps / 8

		FSObject.__init__(self, kwargs.pop('path'))

		# XXX - exclude track 1 pre-gap?
		kwargs['content'] = cont = resource.Resource()
		cont.putChild('audio', AudioResource(file,
		    kwargs.pop('decoder'), 0, samples))
		#print 'doing construction'
		MusicAlbum.__init__(self, *args, **kwargs)

		#print 'adding resource'
		self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
		if 'cover' in picts:
			pict = picts['cover'][0]
			#print 'p:', `pict`
			cont.putChild('cover', static.Data(pict[7], pict[1]))
			self.albumArtURI = '%s/%s/cover' % (self.cd.urlbase,
			    self.id)

		self.res = ResourceList()
		if 'DYEAR' in tags:
			self.year = tags['DYEAR']
		if 'DGENRE' in tags:
			self.genre = tags['DGENRE']
		#if 'DTITLE' in tags:
		#	self.artist, self.album = tags['DTITLE'][0].split(' / ', 1)
		self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)

		r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
		    samprate, nchan))
		r.size = totalbytes
		r.duration = float(samples) / samprate
		r.bitrate = nchan * samprate * bitsps / 8
		r.sampleFrequency = samprate
		r.bitsPerSample = bitsps
		r.nrAudioChannels = nchan
		self.res.append(r)
		#print 'completed'

	def sort(self, fun=lambda x, y: cmp(int(x.originalTrackNumber), int(y.originalTrackNumber))):
		return list.sort(self, fun)

	def genChildren(self):
		r = [ str(x['number']) for x in
		    self.cuesheet['tracks_array'] if x['number'] not in
		    (170, 255) ]
		#print 'gC:', `r`
		return r

	def findtrackidx(self, trk):
		for idx, i in enumerate(self.cuesheet['tracks_array']):
			if i['number'] == trk:
				return idx

		raise ValueError('track %d not found' % trk)

	@staticmethod
	def findindexintrack(trk, idx):
		for i in trk['indices_array']:
			if i['number'] == idx:
				return i

		raise ValueError('index %d not found in: %s' % (idx, trk))

	def gettrackstart(self, i):
		idx = self.findtrackidx(i)
		track = self.cuesheet['tracks_array'][idx]
		index = self.findindexintrack(track, 1)
		return track['offset'] + index['offset']

	def createObject(self, i, arg=None):
		'''This function returns the (class, name, *args, **kwargs)
		that will be passed to the addItem method of the
		ContentDirectory.  arg will be passed the value of the dict
		keyed by i if genChildren is a dict.'''

		oi = i
		i = int(i)
		trkidx = self.findtrackidx(i)
		trkarray = self.cuesheet['tracks_array']
		kwargs = self.kwargs.copy()
		start = self.gettrackstart(i)
		tags = self.tags
		#print 'tags:', `tags`
		kwargs['start'] = start
		kwargs['samples'] = trkarray[trkidx + 1]['offset'] - start
		#print 'track: %d, kwargs: %s' % (i, `kwargs`)
		kwargs['originalTrackNumber'] = i
		try:
			tinfo = self.cdtoc['tracks'][i]
			#print 'tinfo:', `tinfo`
		except KeyError:
			tinfo = {}

		if 'dtitle' in tags and ' / ' in tags['dtitle'][0]:
			kwargs['artist'], kwargs['album'] = tags['dtitle'][0].split(' / ')
		else:
			if 'TITLE' in self.cdtoc:
				kwargs['album'] = self.cdtoc['TITLE']
			if 'PERFORMER' in tinfo:
				kwargs['artist'] = tinfo['PERFORMER']

		tt = 'ttitle%d' % (i - 1)
		if tt in tags:
			if len(tags[tt]) != 1:
				# XXX - track this?
				print('hun? ttitle:', repr(tags[tt]))

			ttitle = tags[tt][0]
			if ' / ' in ttitle:
				kwargs['artist'], oi = ttitle.split(' / ')
			else:
				oi = ttitle
		else:
			if 'TITLE' in tinfo:
				oi = tinfo['TITLE']

		#print 'title:', `oi`
		#print 'kwargs:', `kwargs`
		#print 'artist:', `kwargs['artist']`
		#import traceback
		#traceback.print_stack()
		return AudioRawTrack, oi, (), kwargs

# XXX - figure out how to make custom mix-ins w/ other than AudioItem
# metaclass?
class AudioRawBase(FSObject):
	def __init__(self, *args, **kwargs):
		file = kwargs.pop('file')
		nchan = kwargs.pop('channels')
		samprate = kwargs.pop('samplerate')
		bitsps = kwargs.pop('bitspersample')
		samples = kwargs.pop('samples')
		startsamp = kwargs.pop('start', 0)
		tags = kwargs.pop('tags', {})
		picts = kwargs.pop('pictures', {})
		totalbytes = nchan * samples * bitsps / 8

		FSObject.__init__(self, kwargs.pop('path'))

		#print 'AudioRaw:', `startsamp`, `samples`
		kwargs['content'] = cont = resource.Resource()
		cont.putChild('audio', AudioResource(file,
		    kwargs.pop('decoder'), startsamp, samples))
		self.baseObject.__init__(self, *args, **kwargs)

		if 'DYEAR' in tags:
			self.year = tags['DYEAR'][0]
		if 'DGENRE' in tags:
			self.genre = tags['DGENRE'][0]
		if 'DTITLE' in tags and ' / ' in tags['DTITLE'][0]:
			self.artist, self.album = tags['DTITLE'][0].split(' / ', 1)
		self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
		if 'cover' in picts:
			pict = picts['cover'][0]
			cont.putChild('cover', static.Data(pict[7], pict[1]))
			self.albumArtURI = '%s/%s/cover' % (self.cd.urlbase,
			    self.id)

		self.res = ResourceList()
		r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
		    samprate, nchan))
		r.size = totalbytes
		r.duration = float(samples) / samprate
		r.bitrate = nchan * samprate * bitsps / 8
		r.sampleFrequency = samprate
		r.bitsPerSample = bitsps
		r.nrAudioChannels = nchan
		self.res.append(r)

	def doUpdate(self):
		print('dU:', repr(self), self.baseObject.doUpdate)
		print(self.__class__.__bases__)
		#import traceback
		#traceback.print_stack()
		self.baseObject.doUpdate(self)

class AudioRaw(AudioRawBase, AudioItem):
	baseObject = AudioItem

class AudioRawTrack(AudioRawBase, MusicTrack):
	baseObject = MusicTrack

def detectaudioraw(origpath, fobj):
	for i in decoders.values():
		try:
			obj = i(origpath)
			# XXX - don't support down sampling yet
			if obj.bitspersample not in (8, 16):
				continue

			args = {
			    'path': origpath,
			    'decoder': i,
			    'file': origpath,
			    'channels': obj.channels,
			    'samplerate': obj.samplerate,
			    'bitspersample': obj.bitspersample,
			    'samples': obj.totalsamples,
			    'tags': obj.tags,
			    'pictures': obj.pictures,
			}

			if obj.cuesheet is not None:
				try:
					args['toc'] = cdrtoc.parsetoc(
					    obj.tags['cd.toc'][0])
				except KeyError:
					pass
				except:
					import traceback
					print('WARNING: failed to parse toc:')
					traceback.print_exc()

				args['cuesheet'] = obj.cuesheet
				return AudioDisc, args

			return AudioRaw, args
		except:
			#import traceback
			#traceback.print_exc()
			pass

	return None, None

registerklassfun(detectaudioraw, True)