# Copyright 2009-2011 Klas Lindberg <klas.lindberg@gmail.com>

# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.

import sys
import struct
import socket
import json
import math

from tactile import IR

class ID:
	SQUEEZEBOX   = 2
	SOFTSQUEEZE  = 3
	SQUEEZEBOX2  = 4
	TRANSPORTER  = 5
	SOFTSQUEEZE3 = 6
	RECEIVER     = 7
	SQUEEZESLAVE = 8
	CONTROLLER   = 9
	SQUEEZEBOX3  = 104 # not reported by firmware, but infered from HELO msg.

	debug = {
		SQUEEZEBOX   : 'SqueezeBox',
		SOFTSQUEEZE  : 'SoftSqueeze',
		SQUEEZEBOX2  : 'SqueezeBox_2',
		TRANSPORTER  : 'Transporter',
		SOFTSQUEEZE3 : 'SoftSqueeze_3',
		RECEIVER     : 'Receiver',
		SQUEEZESLAVE : 'SqueezeSlave',
		CONTROLLER   : 'Controller',
		SQUEEZEBOX3  : 'SqueezeBox_3'
	}

# there are Messages and Commands. Messages are inbound from the device and
# are used in communication between dwite and the conman. Commands are outbound
# to the hardware device.
# The parser produces Message instances while the Command base class has a
# virtual function serialize() that all subclasses must implement. The
# serialized representation shall be writable on the control connection's
# socket.
# to make things really funky, the SqueezeBox does not use network order to
# describe integers. It also uses different integer sizes to describe message
# and command lengths. On top of this, JSON messages must use a larger size
# field than SB messages to be useful. Joy...
#
# Device message: size field located [4:8], unsigned long, little endian.
# Device command: size field located [0:2], unsigned short, little endian.
# JSON message:   exactly like device message.


class Message(object):
	head = None # string

class Helo(Message):
	head = 'HELO'

	def __init__(self, id, revision, mac_addr, uuid, language):
		self.id       = id       # integer
		self.revision = revision # integer
		self.mac_addr = unicode(mac_addr) # string
		self.uuid     = unicode(uuid)     # string
		self.language = unicode(language) # string

	def __str__(self):
		return 'HELO: %s %d %s %s %s' % (ID.debug[self.id], self.revision,
		                           self.mac_addr, self.uuid, self.language)

class Anic(Message):
	head = 'ANIC'

	def __str__(self):
		return 'ANIC: -'

class Tactile(Message):
	head   = 'IR  '
	code   = 0 # valid values taken from tactile.IR
	stress = 0 # integer

	def __init__(self, code, stress=0):
		self.code   = code
		self.stress = stress

	def __str__(self):
		if self.code > 0:
			return 'IR  : %s %d' % (IR.codes_debug[self.code], self.stress)
		else:
			return 'IR R: %s %d' % (IR.codes_debug[-self.code], self.stress)

class Bye(Message):
	head   = 'BYE '
	reason = 0 # integer

	def __init__(self, reason):
		self.reason = reason
	
	def __str__(self):
		if reason == 1:
			return 'BYE : Player is going out for an upgrade'
		return 'BYE : %d' % self.reason

class Stat(Message):
	head     = 'STAT
'
	event    = None # 4 byte string. this is what SBS sources have to say about
	# them: vfdc - vfd received, i2cc - i2c command recevied, STMa - AUTOSTART
	# STMc - CONNECT, STMe - ESTABLISH, STMf - CLOSE, STMh - ENDOFHEADERS,
	# STMp - PAUSE, STMr - UNPAUSE, STMt - TIMER, STMu - UNDERRUN,
	# STMl - FULL (triggers start of synced playback), STMd - DECODE_READY
	# (decoder has no more data
), STMs - TRACK_STARTED (a new track started
	# playing), STMn - NOT_SUPPORTED (decoder does not support the track format)
	# STMz - pseudo-status derived from DSCO meaning end-of-stream.

	# my understanding is that the STMz is not sent by the device but by the
	# SBS to itself when it receives the DiSCOnnect message from the device.
	
	# there are also a couple of undocumented events: STMo - the currently
	# playing track is running out, aude - ACK of aude command, audg - ACK of
	# audg command, strm - ACK of strm command (but which kind?).
	
	# finally there is the undocumented non-event '\0\0\0\0' which is maybe only
	# sent when the device connects to reveal transient state that survived in
	# disconnected mode.
	
	crlfs    = 0    # uint8   number of rc/lf seen during header parsing
	mas_init = 0    # uint8   'm' or 'p'. don't know what it is
	mas_mode = 0    # uint8   SBS code comment only says "serdes mode"
	in_size  = 0    # uint32  size of RX buffer
	in_fill  = 0    # uint32  RX buffer fill
	recv_hi  = 0    # uint64, high bits.  total bytes received
	recv_lo  = 0    # uint64, low bits.   total bytes received
	wifi_pow = 0    # uint16  wifi signal strength
	jiffies  = 0    # uint32  some sort of time slice indication
	out_size = 0    # uint32  output buffer size
	out_fill = 0    # uint32  output buffer fullness
	seconds  = 0    # uint32  elapsed playback seconds
	voltage  = 0    # uint32  analog output voltage. related to preamp value?
	msecs    = 0    # uint32  elapsed playback milliseconds
	stamp    = 0    # uint32  server timestamp used for latency tracking
	error    = 0    # uint16  only set in STAT/STMn? no SBS documentation

	def __str__(self):
		tmp1 = ( 'Event    = "%s"\n' % self.event
		       + 'CRLFs    = %d\n' % self.crlfs
		       + 'MAS init = %d\n' % self.mas_init
	           + 'MAS mode = %d\n' % self.mas_mode
	           + 'In buff  = %d\n' % self.in_size
	           + 'In fill  = %d\n' % self.in_fill
	           + 'Received = %d %d\n' % (self.recv_hi, self.recv_lo) )

		if self.wifi_pow <= 100:
			tmp2 = 'WiFi pow = %d\n' % self.wifi_pow
		else:
			tmp2 = 'Connection = Wired\n'

		tmp3 = ( 'Jiffies  = %d\n' % self.jiffies
		       + 'Out buff = %d\n' % self.out_size
		       + 'Out fill = %d\n' % self.out_fill
		       + 'Elapsed  = %d %d\n' % (self.seconds, self.msecs)
		       + 'Voltage  = %d\n' % self.voltage
		       + 'Stamp    = %d\n' % self.stamp
		       + 'Error    = %d\n' % self.error )

		return '%s%s%s' % (tmp1, tmp2, tmp3)

	def log(self, level):
		if level > 0:
			return (
				'stat event=%s crlf=%d in-fill=%d rx=%d out-fill=%d'
				% (self.event, self.crlfs, self.in_fill,
				   self.recv_hi << 32 | self.recv_lo, self.out_fill)
			)

class Resp(Message):
	head        = 'RESP'
	http_header = None # string
	
	def __init__(self, http_header):
		self.http_header = http_header
	
	def __str__(self):
		return 'RESP: %s' % self.http_header

class Ureq(Message):
	head = 'UREQ'

	def __str__(self):
		return 'UREQ: -'
	
class Dsco(Message):
	head   = 'DSCO'
	reason = 0 # uint8

	def __init__(self, reason):
		self.reason = reason

	def __str__(self):
		if self.reason == 0:
			message = 'Connection closed normally'
		elif self.reason == 1:
			message = 'Connection reset by local host'
		elif self.reason == 2:
			message = 'Connection reset by remote host'
		elif self.reason == 3:
			message = 'Connection is no longer able to work'
		elif self.reason == 4:
			message = 'Connection timed out'
		return 'DSCO: %s' % message



### COMMANDS ###################################################################

class Command(object):
	def serialize(self):
		raise Exception, 'All Command subclasses must implement serialize()'
	
class Strm(Command):
	# the first couple of sections are just named constants to use in the
	# "real" member values.

	# operations
	OP_START   = 's'
	OP_PAUSE   = 'p'
	OP_UNPAUSE = 'u'
	OP_STOP    = 'q'
	OP_FLUSH   = 'f'
	OP_STATUS  = 't'
	OP_SKIP    = 'a' # skip milliseconds in the output buffer
	
	# autostart? ("extern" as in "extern source". e.g. internet radio.)
	AUTOSTART_NO         = '0'
	AUTOSTART_YES        = '1'
	AUTOSTART_EXTERN_NO  = '2' 
	AUTOSTART_EXTERN_YES = '3'

	# formats
	FORMAT_MPEG = 'm'
	FORMAT_WAV  = 'p' # also for AIF
	FORMAT_FLAC = 'f'
	FORMAT_WMA  = 'w' # also for ASX
	FORMAT_OGG  = 'o'
	
	# pcm sample sizes
	PCM_SIZE_8 =  '0'
	PCM_SIZE_16 = '1'
	PCM_SIZE_24 = '2'
	PCM_SIZE_32 = '3'

	# pcm KHz sample rates
	PCM_RATE_8  = '5'
	PCM_RATE_11 = '0'
	PCM_RATE_12 = '6'
	PCM_RATE_16 = '7'
	PCM_RATE_22 = '1'
	PCM_RATE_24 = '8'
	PCM_RATE_32 = '2'
	PCM_RATE_44 = '3' # 44.1, of course
	PCM_RATE_48 = '4'
	PCM_RATE_96 = '9'

	# pcm channels
	PCM_MONO   = '1'
	PCM_STEREO = '2'
	
	# pcm endianness
	PCM_BIG_ENDIAN    = '0'
	PCM_LITTLE_ENDIAN 