#!/usr/bin/env python

from twisted.internet.protocol import BaseProtocol, Protocol, ConnectedDatagramProtocol
from twisted.internet.serialport import SerialPort
from twisted.internet import defer, reactor, threads
from zope.interface import implements, Interface, Attribute

import array
import cddb
import serial
import sys
import time

class ICDPlayerTransport(Interface):
	power = Attribute('A C{bool}')
	playing = Attribute('A C{bool}')
	paused = Attribute('A C{bool}')
	dooropen = Attribute('A C{bool}')
	discknown = Attribute('A C{bool}')
	curdisc = Attribute('A C{int} of the current disc loaded or None.')
	alldiscs = Attribute('A C{bool}, does playback include all discs.')
	repeateall = Attribute('A C{bool}')
	repeateone = Attribute('A C{bool}')
	shuffle = Attribute('A C{bool}')
	program = Attribute('False or 1, 2 or 3')
	cdnum = Attribute('A number that is the CD player ID')
	disccount = Attribute('A count of the capacity of the player.')
	model = Attribute('The model.')
	
	def play():
		'''Start playing at current possition.'''

	def stop():
		'''Stop playing.'''

	def pause():
		'''Pause.'''

	def togglepause():
		'''Toggle Paused state.  Will trigger the respective paused
		or playing on the protocol.'''

	def nexttrack():
		'''Next Track.'''

	def previoustrack():
		'''Previous Track.'''

	def poweron():
		'''Turn power on.  Returns a defered that will be triggered
		when power is on.'''

	def poweroff():
		'''Turn power off.'''

	def discinfo(d):
		'''Report disc info, will stop playback.  Returns Deferred.'''

	def trackinfo(d, t):
		'''Report track info, will stop playback.  Returns Deferred.'''

	def playdisc(disc, track=1):
		'''Start playing disc.  Returns Deferred.'''

	def cuedisc(disc, track=1):
		'''Cue disc to track.  Returns Deferred.'''

	def loaddisc(disc, track=1, load=True):
		'''Make sure disc is loaded, cue if necessary.  Returns a
		Deferred.  If load is False, will not load the disc.  The
		Deferred will return True if the disc is loaded, otherwise
		False.'''

	def discs():
		'''Return set of discs present, or None if the discs are
		unknown at this time.'''

	def __contains__(k):
		'''Check if a disc is present in the changer.'''

	def __len__(k):
		'''Number of discs present in the changer.  This is not the
		capacitity..'''

	def __iter__():
		'''Iterate over all the discs in the changer.'''

	def setcontinuous(alldiscs=True):
		'''Set continuous playback, if alldiscs is False, one disc.'''

	def setshuffle(alldiscs=True):
		'''Set shuffle playback, if alldiscs is False, one disc.'''

	def settimeout(timeout=300):
		'''After timeout seconds of no commands being issues, and
		the player not playing discs, power off.'''

	def getcddbid(disc):
		'''Return CDDB id of disc.'''

class CDPlayerProtocol(BaseProtocol):
	'''This protocol is a bit different in that the transport implements
	the CDPlayerTransport interface.'''

	def stateChanged(self):
		# one of the attributes changes
		pass

class SLinkException(Exception):
	pass

class DiscError(SLinkException):
	def __init__(self, disc, s):
		SLinkException.__init__(self, s % disc)
		self.disc = disc

class NoDisc(SLinkException):
	pass

# Not really BaseProtocol
class SLinkCDTransport(object):
	implements(ICDPlayerTransport)

	def __init__(self, sprotocol, cdnum):
		if cdnum not in (1, 2, 3):
			raise ValueError('invalid CD: %d' % cdnum)
		self._proto = sprotocol
		self._cdnum = cdnum
		self._childprotocol = None

		# Internal state variables
		self._discs = None
		self._discpos = None
		self._disccount = None

		self.loadeddisc = None
		self._responses = {}

	def makeConnection(self, protocol):
		print 'mC'
		self._childprotocol = protocol
		# We can now start issuing commands to _proto, our parent.
		# Once we have gathered all the state we need, we will
		# connect to our child protocol.
		l = []
		d = self.runCommand('queryplayercap', 'decksize')
		d.addCallback(lambda x: self.printargs('c'))
		l.append(d)

		d = self.runCommand('querydeckmodel', 'deckmodel')
		d.addCallback(self.processdeckmodel)
		d.addCallback(lambda x: self.printargs('b'))
		l.append(d)

		d = self.runCommand('querystatus', 'status')
		d.addCallback(lambda x: self.printargs('after querystatus done'))
		l.append(d)

		d = defer.DeferredList(l)
		d.addCallback(lambda x: self.printargs('here'))
		d.addCallback(lambda x, p=protocol, s=self: p.makeConnection(s))

	def printargs(self, args):
		print 'mpa:', `args`

	def processdeckmodel(self, args):
		self._model = args[0]

	def processstatus(self, args):
		a = array.array('B', args[0])
		f = a[0]
		r = None
		if f & 0x10:
			self._power = False
		else:
			self._power = True

		if f & 0x2f == 0x2f:
			self._dooropen = True
			self._playing = False
			self._paused = False
		else:
			self._dooropen = False
			if f & 0x1:
				self._playing = True
			else:
				self._playing = False
			if f & 0x2:
				self._paused = True
			else:
				self._paused = False

		f = a[1]
		if f & 0x80:
			self._discknown = True
			if self._discs is None:
				r = self.getdiscs()
		else:
			self._discknown = False
			#self._discs = None

			if self._power is True:
				# We don't know discs yet, try again in a few
				# seconds.
				r = defer.Deferred()
				reactor.callLater(1, lambda:
				    self.runCommand('querystatus',
				    'status').chainDeferred(r))

		if f & 0x40:
			self._alldiscs = True
		else:
			self._alldiscs = False

		if f & 0x10:
			self._repeateall = True
			self._repeateone = False
		elif f & 0x20:
			self._repeateall = False
			self._repeateone = True
		else:
			self._repeateall = False
			self._repeateone = False

		if f & 0xf == 0x1:
			self._shuffle = True
			self._program = False
		elif f & 0xf in (0x4, 0x5, 0x6):
			self._shuffle = False
			self._program = (f & 0xf) - 0x3
		else:
			self._shuffle = False
			self._program = False

		print 'self:', `self`, `r`
		return r

	def __repr__(self):
		return '<SLinkCDTransport: %s>' % ', '.join('%s=%s' % (k, `getattr(self, k)`) for k in ( 'power', 'playing', 'paused', 'dooropen', 'discknown', 'alldiscs', 'repeateall', 'repeateone', 'shuffle', 'program', 'disccount', 'model', ))

	def getdiscs(self):
		print 'gd called'
		self._discsnew = set()
		l = self.runCommand('querydiscs', [ 'disc_list%d' % i for i in xrange((self.disccount + 103) / 104) ])
		for i, d in enumerate(l):
			d.addCallback(lambda x, i=i: self.processdisclist(i, x))

		r = defer.DeferredList(l)
		r.addCallback(self.freezediscs)
		return r

	def freezediscs(self, args):
		print 'fd called'
		self._discs = frozenset(self._discsnew)
		self._discsnew = None

	bits = dict((1 << i, i) for i in xrange(8))

	def processdisclist(self, part, args):
		off = part
		a = array.array('B', args[0])
		for pos, i in enumerate(a):
			while i:
				b = i & -i
				bitpos = self.bits[b]
				i ^= b
				self._discsnew.add(104 * off + pos * 8 +
				    bitpos + 1)

	def insertresponse(self, resp, obj):
		if resp in self._responses:
			self._responses[resp].append(obj)
		else:
			self._responses[resp] = [ obj ]

	def runCommand(self, cmd, response, *args, **kwargs):
		for i in kwargs.pop('ignoreresponses', []):
			d = defer.Deferred()
			self.insertresponse(i, d)

		if isinstance(response, list):
			d = []
			for i in response:
				dd = defer.Deferred()
				self.insertresponse(i, dd)
				d.append(dd)
		else:
			d = defer.Deferred()
			self.insertresponse(response, d)

		self._proto.sendmsg(cmd, self._cdnum, *args)
		return d

	def gotResponse(self, cmd, *args):
		# Got a response from a command
		expected = True

		print 'gR:', `cmd`, `args`
		# A few commands are state change, so handle them here.
		r = None
		if cmd in ('nowat', 'tocread', 'trackinfo', 'track_info', 'discinfo', ):
			self._discpos = args[0]
		elif cmd == 'poweron':
			print 'poweron, doing querystatus'
			r = self.runCommand('querystatus', 'status')
		elif cmd == 'poweroff':
			self._discpos = None
			r = self.runCommand('querystatus', 'status')
		elif cmd == 'decksize':
			if self._disccount is not None and self._disccount != \
			    args[0]:
				# XXX state changed!
				pass
			self._disccount = args[0]
		elif cmd == 'status':
			r = self.processstatus(args)
		#elif cmd == 0x18
		#	self._discs = None
		else:
			expected = False

		if r is not None:
			r.addCallback(lambda x: self.finishResponse(cmd,
			    expected, *args))
			return

		self.finishResponse(cmd, expected, *args)

	def finishResponse(self, cmd, expected, *args):
		if cmd not in self._responses:
			if not expected:
				print 'Unexpected response:', `cmd`, `args`
			return

		# We pop so that a future command will be scheduled properly
		for i in self._responses.pop(cmd):
			i.callback(args)

	power = property(lambda x: x._power)
	playing = property(lambda x: x._playing)
	paused = property(lambda x: x._paused)
	dooropen = property(lambda x: x._dooropen)
	discknown = property(lambda x: x._discknown)
	curdisc = property(lambda x: x._discpos)
	alldiscs = property(lambda x: x._alldiscs)
	repeateall = property(lambda x: x._repeateall)
	repeateone = property(lambda x: x._repeateone)
	shuffle = property(lambda x: x._shuffle)
	program = property(lambda x: x._program)
	cdnum = property(lambda x: x._cdnum)
	disccount = property(lambda x: x._disccount)
	model = property(lambda x: x._model)
	
	def play(self):
		'''Start playing at current possition.'''
		return self.runCommand('play', 'play')

	def stop(self):
		'''Stop playing.'''
		return self.runCommand('stop', 'stop')

	def pause(self):
		'''Pause.'''
		return self.runCommand('pause', 'pause')

	def togglepause(self):
		'''Toggle Paused state.  Will trigger the respective paused
		or playing on the protocol.'''
		l = self.runCommand('togglepause', [ 'play', 'pause', ])
		return defer.DeferList(l, fireOnOneCallback=True,
		    fireOnOneErrback=True)

	def nexttrack(self):
		'''Next Track.'''
		raise NotImplementedError

	def previoustrack(self):
		'''Previous Track.'''
		raise NotImplementedError

	def poweron(self):
		'''Turn power on.'''
		if not self.power:
			return self.runCommand('poweron', 'poweron')

		return defer.succeed(())

	def poweroff(self):
		'''Turn power off.'''
		if self.power:
			return self.runCommand('poweroff', 'poweroff',
			    ignoreresponses=[ 'stop', ])

		return defer.succeed(())

	def discinfo(self, disc):
		'''Report disc info, will stop playback.  Returns Deferred.'''
		print 'di:, scheduling loaddisc'
		return self.loaddisc(disc) \
		    .addCallback(lambda x:
		    self.runCommand('querydiscinfo', 'discinfo', disc))

	def trackinfo(self, disc, track):
		'''Report track info, will stop playback.  Returns Deferred.'''
		print 'ti:, scheduling loaddisc'
		return self.loaddisc(disc).addCallback(lambda x:
		    self.runCommand('querytrackinfo', 'track_length', disc,
		    track))

	def playdisc(self, disc, track=1):
		'''Start playing disc.  Returns Deferred.'''
		return self.loaddisc(disc, track).addCallback(lambda x:
		    self.runCommand('playtrack', 'track_info', disc, track))

	def cuedisc(self, disc, track=1, noload=False):
		'''Cue disc to track.  Returns Deferred.'''
		# XXX - should we wait for the pause response?
		# XXX - we'll recurse, but that should be fine
		if noload:
			print 'z'
			d = defer.succeed(None)
		else:
			print 'w'
			d = self.loaddisc(disc, track)
			# XXX - second cue doesn't return track_info or something
			d.addCallback(lambda x: self.stop())
		return d.addCallback(lambda x:
		    self.runCommand('cuetrack', 'track_info', disc, track,
		    ignoreresponses=[ 'pause', ]))

	def loaddisc(self, disc, track=1, load=True):
		'''Make sure disc is loaded, cue if necessary.  Returns Deferred.'''
		if self._discpos != disc and load:
			print 'x', `self._discpos`, `disc`
			d = self.poweron()
			d.addCallback(lambda x: self.setcontinuous())
			d.addCallback(lambda x: self.cuedisc(disc, noload=True))
		else:
			print 'y'
			d = defer.succeed(None)

		d.addCallback(lambda x: self._discpos == disc)
		return d

	def discs(self):
		'''Return set of discs present.'''
		return self._discs

	def __contains__(self, k):
		'''Check if a disc is present in the changer.'''
		return k in self._discs

	def __len__(self):
		'''Number of discs present in the changer.  This is not the
		capacitity..'''
		return len(self._discs)

	def __iter__(self):
		'''Iterate over all the discs in the changer.'''

		# We use this so that when the discs available changes we
		# automaticly update this.
		for i in xrange(self.disccount):
			if i in self:
				yield i

	def setmode(self, fun):
		print 'sm:', `fun`
		if fun():
			print 'passed!', `self`
			return

		d = defer.Deferred()
		self.insertresponse('status', d)
		d.addCallback(lambda x: self.setmode(fun))
		# send IR code
		print 'sending ir'
		self._proto.sendir('\xeb\x91')
		return d

	def setcontinuous(self, alldiscs=True):
		'''Set continuous playback, if alldiscs is False, one disc.'''
		fun = lambda: not self.program and not self.shuffle and \
		    self.alldiscs == alldiscs
		if not fun() and (self.playing or self.paused):
			d = self.stop()
		else:
			d = defer.succeed(None)

		return d.addCallback(lambda x: self.setmode(fun))

	def setshuffle(self, alldiscs=True):
		'''Set shuffle playback, if alldiscs is False, one disc.'''
		if self.playing or self.paused:
			d = self.stop()
		raise NotImplementedError

	def settimeout(self, timeout=300):
		'''After timeout seconds of no commands being issues, and
		the player not playing discs, power off.'''
		raise NotImplementedError

	# Imported from eslink.py, w/ minor changes
	@defer.deferredGenerator
	def getcddbid(self, disc):
		'''Return CDDB id of disc.'''

		r = defer.waitForDeferred(self.discinfo(disc))
		yield r
		r = r.getResult()
		if r[0] != disc:
			# if the disc isn't present, update the discs
			print 'Disc missing, updating discs'
			r = defer.waitForDeferred(self.getdiscs())
			yield r
			r = r.getResult()
			raise NoDisc(disc, 'disc %d not present')

		r = r[1:]
		def addreducems(i, a):
			ni = (i[0] + a[0], i[1] + a[1])
			m, s = divmod(ni[1], 60)
			i = list(i)
			i[0] = ni[0] + m
			i[1] = s
			return tuple(i)

		tracks = []
		curtot = (0, 2)
		tracklens = []
		for i in range(1, r[0] + 1):
			tracks.append(curtot)
			j = defer.waitForDeferred(self.trackinfo(disc, i))
			yield j
			j = j.getResult()
			assert j[0] == disc
			assert j[1] == i
			j = j[2:]
			tracklens.append(j)
			curtot = addreducems(curtot, tracklens[-1])

		tracks.append(r[1:])
		tracks[-1] = addreducems(tracks[-1], (0, 2))
		id = cddb.discid(tracks)

		yield id

def nulltrim(buf):
	s = buf.find('\x00')
	if s == -1:
		return buf

	return buf[:s]

class SLinkProtocol(ConnectedDatagramProtocol):
	'''Interface for an S-Link Port.  To be passed into something that
	passes messages to/from an S-Link Port.'''

	simpcmds = {
		'play':	'\x00',
		'stop':	'\x01',
		'pause':	'\x02',
		'togglepause':	'\x03',
		'nexttrack':	'\x08',
		'queryplayercap': '\x22',
		'poweron':	'\x2e',
		'poweroff':	'\x2f',
	}
	_bysimpcmds = dict((v, k) for k, v in simpcmds.iteritems())
	#_bysimpcmds['\x0e'] = 'poweron'		# Disc loaded?

	simpcmds['querystatus'] = '\x0f'
	simpcmds['querydiscinfo'] = '\x44'
	simpcmds['querytrackinfo'] = '\x45'
	simpcmds['playtrack'] = '\x50'
	simpcmds['cuetrack'] = '\x51'
	simpcmds['querydeckmodel'] = '\x6a'
	simpcmds['querydiscs'] = '\x72'

	# Bytes returned by cmd 0x61
	decksizes = {
		'\x05\x63':     5,
		'\xfe\x0b':     200,
		'\x64\x6b':     300,
		'\x64\x0b':     300,
		'\xfe\x6b':     300,
		'\xc8\x6b':     400,
	}

	def discdecode(hidisc, x):
		disc = array.array('B', x)[0]
		if hidisc:
			return disc + 200
		elif disc == 0x00:
			return 100	# XXX?
		elif disc >= 0x9a:
			return 100 + disc - 0x9a
		else:
			return bcdtoint(disc)

	def discinfo(hidisc, x, dd=discdecode):
		a = array.array('B', x)
		return dd(hidisc, x), bcdtoint(a[2]), \
		    bcdtoint(a[3]), bcdtoint(a[4]), bcdtoint(a[5])

	def trackinfo(hidisc, x, dd=discdecode):
		a = array.array('B', x)
		return dd(hidisc, x), bcdtoint(a[1]), \
		    bcdtoint(a[2]), bcdtoint(a[3])

	responses = {
		'\x0e': lambda hidisc, x: ('invalidmode', ()),
		'\x14': lambda hidisc, x: ('discinfo', (None, )),
		'\x15': lambda hidisc, x: ('trackinfo', (None, )),
		'\x45': lambda hidisc, x: ('trackinfo', (x, )),
		'\x50': lambda hidisc, x, ti=trackinfo: ('track_info',
		    ti(hidisc, x)),
		'\x52': lambda hidisc, x, dd=discdecode: ('tocread',
		    (dd(hidisc, x),)),
		'\x58': lambda hidisc, x, dd=discdecode: ('nowat',
		    (dd(hidisc, x),)),
		'\x60': lambda hidisc, x, di=discinfo: ('discinfo',
		    di(hidisc, x)),
		'\x61':	lambda hidisc, x, y=decksizes: ('decksize',
		    (y[x],)),
		'\x62': lambda hidisc, x, ti=trackinfo: ('track_length',
		    ti(hidisc, x)),
		'\x6a': lambda hidisc, x: ('deckmodel',
		    (nulltrim(x).decode('ascii'),)),
		'\x70': lambda hidisc, x: ('status', (x, )),
		'\x72': lambda hidisc, x: ('disc_list0', (x, )),
		'\x73': lambda hidisc, x: ('disc_list1', (x, )),
		'\x74': lambda hidisc, x: ('disc_list2', (x, )),
		'\x75': lambda hidisc, x: ('disc_list3', (x, )),
	}

	def __init__(self):
		self._queue = []
		self._cdplayers = {}
		self._pendingmsgs = {}
		self._started = False
		# Should be set by class

	def startProtocol(self):
		self._started = True
		for fun, arg in self._queue:
			fun(arg)
		self._queue = None

	def stopProtocol(self):
		self._started = False

	def connectCDPlayer(self, num, protocol):
		if num < 1 or num > 3:
			raise ValueError('invalid CD number: %d' % num)

		if not isinstance(protocol, CDPlayerProtocol):
			raise ValueError('protocol must be CDPlayerProtocol')

		if num in self._cdplayers:
			raise ValueError('CD player already attached')

		transport = SLinkCDTransport(self, num)
		if not self._started:
			self._queue.append((transport.makeConnection,
			    protocol))
		else:
			transport.makeConnection(protocol)

		self._cdplayers[num] = transport

	def datagramReceived(self, msg, port):
		'''Decode message and dispatch (if possible).'''

		mar = array.array('B', msg)

		if (mar[0] & 0xf0) != 0x90:
			print 'unknown msg:', msg.encode('hex')

		if msg in self._pendingmsgs:
			self._pendingmsgs[msg].cancel()
			del self._pendingmsgs[msg]
			return

		if (mar[0] & 0x8) != 0x8:
			# command from another device, ignore
			#print 'unkn command:', msg.encode('hex')
			return

		# From here all msgs are responses
		cdnum = (mar[0] & 0x3) + 1
		hidisc = False
		if cdnum > 3:
			hidisc = True
			cdnum -= 3

		if cdnum not in self._cdplayers:
			# We don't have this CD Player registered.
			print 'unkn responses %d:' % cdnum, msg.encode('hex')
			return

		if len(msg) == 2 and msg[1] in self._bysimpcmds:
			attr = self._bysimpcmds[msg[1]]
			args = ()
		elif msg[1] in self.responses:
			attr, args = self.responses[msg[1]](hidisc, msg[2:])
		else:
			print 'foo:', msg.encode('hex')
			return
		try:
			self._cdplayers[cdnum].gotResponse(attr, *args)
		except TypeError, e:
			if str(e) == 'gotResponse() argument after * must be a sequence':
				raise TypeError('args for %s not sequence' % `msg[1].encode('hex')`)
			else:
				print 'te:', `str(e)`
				raise

	# Imported from eslink.py
	def makeaddrcmd(self, msgto, cmd, cdnum, disc=None, track=None,
	    hibit=False):
		addr = 0x90 + (cdnum - 1)
		if not msgto:
			addr |= 0x8

		if disc != None:
			if disc > 200:
				addr += 3
				disc -= 200
			elif disc < 100:
				disc = inttobcd(disc)
			else:
				disc = 0x9a + disc - 100
			if track is None:
				return '%c%c%c' % (addr, cmd, disc)
			else:
				track = inttobcd(track)
				return '%c%c%c%c' % (addr, cmd, disc, track)
		else:
			if hibit:
				addr += 3
			return '%c%c' % (addr, cmd)

	def schedulemsg(self, msg):
		print 'sending msg:', `msg`
		self.transport.write(msg)
		# XXX - should probably recall sendmsg to reschedule
		if msg in self._pendingmsgs:
			print 'old:', self._pendingmsgs[msg]
		self._pendingmsgs[msg] = reactor.callLater(1,
		    lambda m=msg: self.schedulemsg(msg))

	def sendir(self, code):
		# XXX make it properly addressable
		self.transport.write(code)

	def sendmsg(self, type, cdpnum, disc=None, track=None):
		msg = self.makeaddrcmd(True, self.simpcmds[type], cdpnum,
		    disc, track)
		self.schedulemsg(msg)

def bcdtoints(i):
	t, o = divmod (i, 16)
	assert t < 10 and o < 10

	return t, o

def inttobcd(i):
	assert i < 100, 'i is invalid: %s' % `i`
	t, o = divmod(i, 10)

	return t << 4 | o

def bcdtoint(i):
	t, o = divmod (i, 16)
	assert t < 10 and o < 10

	return t * 10 + o

class SLinkEPort(object):
	'''Implement one port transport for SLink-E.'''

	def __init__(self, slinke, port, protocol):
		self.slinke = slinke
		self.port = port
		self.protocol = protocol
		self.openned = True

	def write(self, msg):
		if not self.openned:
			raise RuntimeError('port has been closed')

		self.slinke.sendmsg(self.port, [ msg ])

	def writeSequence(self, data):
		self.slinke.sendmsg(self.port, data)

	def loseConnection(self):
		'''Close the port.  Will return a Deferred object that is
		called when the port has finished closing.'''

		self.openned = False
		self.slinke.closePort(self.port)

	def getPeer(self):
		return self.port

class SLinkE(Protocol):
	'''Interface for talking to the SLink-E.  It sends commands, and
	dispatched commands recevied for the port.'''

	commands = {
		'reset':	'\xff\xff',
		'getversion':	'\xff\x0b',
		'getserial':	'\xff\x0c',
		'disableport':	lambda x: chr((x << 5) | 0x1f) + '\x02',
		'enableport':	lambda x: chr((x << 5) | 0x1f) + '\x03',
	}

	# XXX - It's assumed all responses are two bytes.
	receivedresp = {
		'versionis':	('\xff\x0b', 1),
		'portenabled':	(lambda x: (ord(x[0]) & 0x1f) == 0x1f and
		    x[1] == '\x03', 0),
		'portdisabled':	(lambda x: (ord(x[0]) & 0x1f) == 0x1f and
		    x[1] == '\x02', 0),
		'slinkerror':	(lambda x: (ord(x[0]) & 0x1f) == 0x1f and
		    x[1] == '\x80', 0),
	}

	version = property(lambda x: x._version)
	#serial = property(lambda x: x._serial)

	def __init__(self, *args, **kwargs):
		self._ports = {}
		self._portqueue = {}
		self._portpending = {}
		self._buffer = []
		self._bufferlen = 0
		self._currentresp = None
		self._bytesneeded = None
		_version = (None, None)

	def sendcmd(self, cmd):
		#print 'sending:', `cmd`
		self.transport.write(cmd)

	def openPort(self, port, protocol):
		if port in self._ports:
			raise ValueError('port already opened')
		if port in self._portpending:
			raise ValueError('port already pending')

		self.sendcmd(self.commands['enableport'](port))
		self._portpending[port] = SLinkEPort(self, port, protocol)

	def closePort(self, port):
		self.sendcmd(self.commands['disableport'](port))

	def sendmsg(self, port, msg):
		'''Send the sequence of strings in msg out the port.'''
		pos = 0
		fragpos = 0
		msgbuf = []
		while pos != len(msg) and fragpos != len(msg[-1]):
			fraglen = 0
			msgfrag = []
			while fraglen < 30 and pos != len(msg) and \
			    fragpos != len(msg[-1]):
				msgfrag.append(msg[pos][fragpos:fragpos + (30 - fraglen)])
				fragpos += len(msgfrag[-1])
				fraglen += len(msgfrag[-1])
				if fragpos == len(msg[pos]):
					pos += 1
					fragpos = 0

			#print 'h:', `fraglen`, `msgfrag`
			msgbuf.append(chr((port << 5) | fraglen))
			msgbuf.extend(msgfrag)

		# Port Send End
		msgbuf.append(chr(port << 5))

		#print 'sending:', `msgbuf`
		self.transport.writeSequence(msgbuf)

	# Protocol Methods
	def connectionMade(self):
		Protocol.connectionMade(self)
		#self.sendcmd(self.commands['reset'])
		#time.sleep(1)
		self.sendcmd(self.commands['getversion'])
		#self.sendcmd(self.commands['disableport'](7))
		#for i in xrange(6):
		#	self.sendcmd(self.commands['enableport'](i))

	def dataReceived(self, data):
		#print 'dR:', `data`
		self._buffer.append(data)
		self._bufferlen += len(data)
		i = True
		while i and self._buffer:
			i = self.checkBuffer()

	# Internal Buffer routines
	def peakBuffer(self, cnt):
		if cnt > self._bufferlen:
			return

		r = []
		rem = cnt
		for buf in self._buffer:
			r.append(buf[:rem])
			rem -= len(r[-1])
			if rem == 0:
				break

		r = ''.join(r)
		assert len(r) == cnt, 'failed: cnt=%d, buffer=%s' % (cnt, self._buffer)
		return r

	def consumeBuffer(self, cnt):
		assert self._bufferlen >= cnt

		rem = cnt
		while rem:
			buf = self._buffer.pop(0)
			self._bufferlen -= len(buf)
			if len(buf) > rem:
				self._buffer.insert(0, buf[rem:])
				self._bufferlen += len(buf) - rem
				return

			rem -= len(buf)

	def getBuffer(self, cnt):
		buf = self.peakBuffer(cnt)
		if buf is None:
			return

		self.consumeBuffer(cnt)
		return buf

	def lenBuffer(self):
		return self._bufferlen

	def checkBuffer(self):
		'''Dispatch messages if available in buffer.  Returns True if
		a message was dispatched.  Returns False if we need more data
		to dispatch.'''

		if self._currentresp is not None:
			raise NotImplementedError

		firstbyte = ord(self._buffer[0][0])
		portnum = firstbyte >> 5
		bytecnt = (firstbyte & 0x1f)
		#print 'portnum:', portnum, 'bytecnt:', bytecnt, 'buf:', self._buffer
		if bytecnt == 0:
			# port receive end
			self.consumeBuffer(1)
			if portnum not in self._portqueue:
				# XXX - stale data
				return
			msg = ''.join(self._portqueue[portnum])
			self._portqueue[portnum] = []
			#print 'delivering msg %d:' % portnum, `msg`
			self._ports[portnum].protocol.datagramReceived(msg, portnum)
		elif bytecnt == 0x1f:
			# modify command
			buf = self.peakBuffer(2)
			if buf is None:
				return False
			for k, (resp, extracnt) in self.receivedresp.iteritems():
				if (callable(resp) and resp(buf)) or buf == resp:
					if self.lenBuffer() < len(buf) + extracnt:
						return False
					self.consumeBuffer(len(buf))
					extra = self.getBuffer(extracnt)
					self.dispatch(k, portnum, extra)
					return True
			else:
				raise RuntimeError('unhandled msg: %s' % `''.join(self._buffer)`)
		else:
			# port receive data
			buf = self.peakBuffer(bytecnt + 1)
			if buf is None:
				return False

			#print 'f:', bytecnt, `buf`
			self.consumeBuffer(bytecnt + 1)
			if portnum in self._portqueue:
				self._portqueue[portnum].append(buf[1:])

		return True

	def dispatch(self, k, portnum, extra):
		'''Dispatch a message.'''
		#print 'dispatching: %s(%d, %s)' % (`k`, portnum, `extra`)
		getattr(self, 'R_%s' % k)(portnum, extra)

	# Messages
	def R_versionis(self, portnum, data):
		assert len(data) == 1
		self._version = bcdtoints(ord(data[0]))
		#print 'version:', `self._version`

	def R_portdisabled(self, portnum, data):
		if portnum == 7:
			assert not self._ports, \
			    'disabled, but ports present: %s' % `self._ports`
			return

		pobj = self._ports[portnum]

		del self._ports[portnum]
		del self._portqueue[portnum]

		# Signal that we are done
		pobj.protocol.doStop()

	def R_portenabled(self, portnum, data):
		if portnum not in self._portpending:
			#print 'skipping:', portnum
			return

		self._ports[portnum] = self._portpending[portnum]
		self._portqueue[portnum] = []
		del self._portpending[portnum]
		self._ports[portnum].protocol.makeConnection(self._ports[portnum])

	def R_slinkerror(self, portnum, data):
		pass
		#print 'got slinkerror on port:', `portnum`

# Test Code

class MyCDPlayer(CDPlayerProtocol):
	@staticmethod
	def printarg(args):
		print 'pa:', `args`

	def connectionMade(self):
		print 'power:', `self.transport.power`
		if not self.transport.power:
			print 'pon:', `self.transport.poweron()`
		print self.transport.discs()
		self.transport.trackinfo(32, 5).addCallback(
		    self.printarg).addCallback(lambda x: self.transport.discinfo(32)).addCallback(self.printarg).addCallback(lambda x: self.transport.poweroff()).addCallback(lambda x: reactor.stop())
		#reactor.callLater(20, self.transport.poweroff)

		#print 'powered on, playing...'
		#self.transport.querydeckmodel()
		#self.transport.querystatus()
		#self.transport.querydiscinfo(5)
		#self.transport.querytrackinfo(5, 3)
		#self.transport.playtrack(10, 3)

		#print 'powered off...'
		#reactor.stop()

	def stateChanged(self):
		print 'sC'

if __name__ == '__main__':
	if False:
		import serial
		s = serial.Serial('/dev/ttyU0', baudrate=38400, rtscts=True, timeout=5)
		s.write('\xff\xff')
		print 'reset'
		time.sleep(1)

		s.write(''.join(['\x02', '\x90"', '\x00']))
		print 'cmd'
		while True:
			print 'resp:', `s.read(1)`
		sys.exit(0)

	slinke = SLinkE()
	s = SerialPort(slinke, '/dev/ttyU0', reactor, baudrate=38400, rtscts=True)
	th = SLinkProtocol()
	slinke.openPort(0, th)
	th.connectCDPlayer(1, MyCDPlayer())
	#reactor.callLater(5, lambda: slinke.closePort(0))
	reactor.run()