#!/usr/bin/env python

import AT_GSM
import sys
import StringIO
import serial

# [2008, 05, 01]
def date_hash(value):
	if value is None:
		return 0
	else:
		return sum([hash(x) for x in value])
	
def read_uint_value(input, io):
	field_value = 0
	assert(input in "0123456789")
	while input in "0123456789":
		field_value = field_value * 10 + (ord(input) - ord("0"))
		input = io.read(1)

	return field_value, input



class RemindMode(object):
	no = 0
	minute = 1
	hour = 2
	day = 3
	week = 4

class RepeatMode(RemindMode):
	month = 5

class Item(object):
	def __init__(self):
		self.index = None
		self.subject = None
		self.details = ""
		self.location = ""
		self.start = [None, None, None, None, None] # (year, month, day, hour. minute)
		self.end = [None, None, None, None, None] # (year, month, day, hour. minute)
		self.remind_mode = RemindMode.no
		self.remind_value = 0
		self.repeat_mode = RepeatMode.no
		self.repeat_until = [None, None, None] # [year, month, day]
		# TODO could use "repeat_until" and "ID" for some appointment mtime timestamp IF we don't use it for anything else?
		self.ID = 1 # always between 0 to 2422 (inclusive)!

	def __eq__(self, other):
		# return self.index == other.index and  ???
		# removed since Outlook will update the "ReminderSet" flag after the appointment is over and the handphone does not: self.remind_mode == other.remind_mode and self.remind_value == other.remind_value 
		return self.subject == other.subject and self.details == other.details and self.location == other.location and self.start == other.start and self.end == other.end and self.repeat_mode == other.repeat_mode and self.repeat_until == other.repeat_until # and self.ID == other.ID

	def __hash__(self):
		# removed since Outlook will update the "ReminderSet" flag after the appointment is over and the handphone does not: self.remind_mode == other.remind_mode and self.remind_value == other.remind_value
		# self.ID
		return sum([hash(x) for x in [self.subject, self.details, self.location, date_hash(self.start), date_hash(self.end), self.repeat_mode, date_hash(self.repeat_until)]])

	def __str__(self):
		return "(:start %(start)r :end %(end)r :subject %(subject)r :location %(location)r :details %(details)r)" % self.__dict__
		
	def clone(self):
		result = self.__class__()
		result.index = self.index
		result.subject = self.subject
		result.details = self.details
		result.location = self.location
		result.start = self.start
		result.end = self.end
		result.remind_mode = self.remind_mode
		result.remind_value = self.remind_value
		result.repeat_mode = self.repeat_mode
		result.repeat_until = self.repeat_until
		result.ID = self.ID

		return result
	
	order = [
			("index", "I"),
			("subject", "ls15"),
			("details", "ls"),
			("location", "ls"),
			("start[0]", "I04"),
			("start[1]", "I02"),
			("start[2]", "I02"),
			("end[0]", "I04"),
			("end[1]", "I02"),
			("end[2]", "I02"),
			("start[3]", "I02"),
			("start[4]", "I02"),
			("end[3]", "I02"),
			("end[4]", "I02"),
			("remind_mode", "I"),
			("remind_value", "I"),
			("ID", "I"),
			("repeat_mode", "I"),
			("repeat_until[0]", "I04"),
			("repeat_until[1]", "I02"),
			("repeat_until[2]", "I02"),
	]

	def encode(self):
		io = StringIO.StringIO()

		for field_spec, field_encoding in self.__class__.order:
			items = field_spec.split("[", 1)
			if len(items) > 1:
				field_name, field_index = items
				field_index = int(field_index.replace("]", ""))
			else:
				field_name = field_spec
				field_index = None

			field_value = getattr(self, field_name)

			if field_index is not None:
				field_value = field_value[field_index]

			if field_encoding.startswith("I"):
				#print field_spec, field_value
				try:
					encoded_value = ("%" + field_encoding[1 : ] + "d") % field_value
				except:
					print >>sys.stderr, "error: while processing field (spec %r value %r encoding %r)" % (field_spec, field_name, field_encoding)
					raise
				#print type(encoded_value), encoded_value
				io.write(encoded_value)
			elif field_encoding.startswith("ls"):
				field_value = field_value.replace("\n", "\x0A").replace("\"", "'") # note: while quotes do SOMETIMES work, it's way too semi-broken usually.
				limit = field_encoding[len("ls") : ]
				if limit <> "":
					limit = int(limit)
					field_value = field_value[ : limit]

				if isinstance(field_value, unicode):
					field_value = field_value.encode("utf-8")
					
				encoded_value = "\"%d,%s\"" % (len(field_value), field_value)					
				#print type(encoded_value), encoded_value
				io.write(encoded_value)
			else:
				assert(field_encoding in [ "I", "ls" ])

			io.write(",")

		try:
			return io.getvalue()[ : -1]
		except: # UnicodeDecodeError
			#import pickle
			#f = file("error.dmp", "wb")
			#pickle.dump(io, f)
			#f.close()
			#sys.exit(1)
			raise

	def decode(klass, text):
		io = StringIO.StringIO()
		io.write(text)
		io.write(",")
		io.seek(0)

		self = klass()
		# TODO finish.
		for field_spec, field_encoding in self.__class__.order:
			items = field_spec.split("[", 1)
			if len(items) > 1:
				field_name, field_index = items
				field_index = int(field_index.replace("]", ""))
			else:
				field_name = field_spec
				field_index = None

			input = io.read(1)
			if field_encoding.startswith("I"):
				field_value, input = read_uint_value(input, io)
			elif field_encoding.startswith("ls"):
				assert(input == "\"")
				input = io.read(1)
				field_value_len, input = read_uint_value(input, io)
				assert(input == ",")
				field_value = io.read(field_value_len)
				assert(len(field_value) == field_value_len)
				input = io.read(1)
				assert(input == "\"")
				input = None

				field_value = field_value.replace("\x0A", "\n")
			else:
				assert(field_encoding in [ "I", "ls" ])

			if field_index is not None:
				collection_1 = getattr(self, field_name)
				collection_1[field_index] = field_value
			else:
				setattr(self, field_name, field_value)

			if input is None:
				input = io.read(1)

			assert(input == ",")

		return self

	decode = classmethod(decode)

# TEST: +SSHR:3,"6,Zimmer","15,1636 Stock 16\xAt","0,",2008,4,1,2008,4,1,8,0,8,59,0,0,0,0,2008,4,28\xD\xA

#+SSHR:<index>,<subject>,<details>,"0,",<start-year>,<start-month>,<start-day>,<end-year>,<end-month>,<end-day>,<start-hour>,<start-minute>,<end-hour>,<end-minute>,<remind-flag>,<remind-value>,0,<repeat-flag>,<repeat-until-year>,<repeat-until-month>,<repeat-until-day>\xD\xA

class Connection(AT_GSM.Connection):
	def __init__(self, *args, **kwargs):
		AT_GSM.Connection.__init__(self, *args, **kwargs)
		self.put_workaround_done_P = False
		response = self.execute_and_strip("AT+SSHR=?", strip_response_prefix = "+SSHR:")
		self.max_count = int(response.split(",")[0])
		
	def get(self, index):
		assert(index > 0)
		rest = self.execute_and_strip("AT+SSHR=%d" % index, strip_response_prefix = "+SSHR:")

		return Item.decode(rest) # FIXME make this work (skip "+SSHR:").

	def get_all(self):
		rest = self.execute("AT+SSHR=0")
		
		# this is done in such a weird way because there can be a newline in the payload data in a line. If it is there, it's coded as "\x0A". Therefore, use "\x0D" so we don't split on the wrong position.
		for coded_item in rest.split("\x0D"):
			if coded_item.startswith("\x0A"):
				coded_item = coded_item[1 : ]

			if coded_item == "":
				break
				
			assert(coded_item.startswith("+SSHR:"))
			coded_item = coded_item[len("+SSHR:") : ]
				
			yield Item.decode(coded_item) # FIXME make this work (newlines, multiple entries, ...).
	
	def workaround_put_bug(self):
		return # this doesn't always work so don't bother.
		"""
		This function works around a bug that happens when "AT+SSHW" is used with a string containing a quote as data. In that case you'll get "ERROR" unless you called "AT+INFO" before. Don't ask me why.
		Likewise, there's a bug in "AT+INFO" which breaks both the line terminator on the echo and the response later on.
		Note that this workaround only works on a Bluetooth connection, not on a USB cable connection.
		"""
		text = "AT+INFO\x0D"
		#response = self.execute_and_strip(text, strip_response_prefix = "+INFO:")
		#print ">%r" % text
		
		for c in text:
			self.serial_1.write(c)
			
		response = self.serial_1.readline()
		#print "<%r" % response
			
		assert(response.startswith("AT+INFO\015+INFO:")) # you really don't want to know.
		assert(self.serial_1.readline() == "\015\012") # junk empty line, seems to be misplaced from before.
		status = self.serial_1.readline() # junk empty line, seems to be misplaced from before.
		assert(status.strip() == "OK")

	def append(self, item):
		"""
		item :: <Item>
		"""
		if not self.put_workaround_done_P:
			self.workaround_put_bug()
			self.put_workaround_done_P = True
	
		if item.index is None:
			item.index = 0 # FIXME.

		text = "AT+SSHW=%s" % item.encode().split(",", 1)[1]
		if isinstance(text, unicode):
			text = text.encode("utf-8")

		#text2 = 'AT+SSHW="15,Abteilungs - Jo","0,","0,",2008,05,08,2008,05,08,14,30,15,30,0,0,1,0,2008,05,08'
		#print "text", text
			
		response = self.execute_and_strip(text, strip_response_prefix = "+SSHW:") # skip "index".
		index = int(response)

		item.index = index # evil.

		return index

	def truncate(self):
		""" clears everything. """
		response = self.execute_and_strip("AT+SSHD=0")
				 
	def remove(self, index):
		assert(index > 0)
		response = self.execute_and_strip("AT+SSHD=%d" % index)
		# ??? response

	def full_P(self):
		try:
			self.get(self.max_count)
			return True
		except:
			return False
		