#!/usr/bin/env python

import xmllib, traceback, re, string, os, cPickle, sys
from types import *
import Tkinter
import connection

PYTHON_VERSION = string.split(sys.version)[0]
try:
	import gzip
except:
	HAVE_GZIP = 0
else:
	# Gzip only works correctly with cPickle in Python >= 1.5.2
	if PYTHON_VERSION >= '1.5.2':
		HAVE_GZIP = 1
	else:
		HAVE_GZIP = 0
if PYTHON_VERSION >= '1.6':
	USE_BINARY = 1
else:
	USE_BINARY = 0

# Configuration variable
Config = None

def escapeXML(s):
	try:
		import xml.sax.saxutils
	except ImportError:
		import re
		s = re.sub('&', '&amp;', s)
		s = re.sub('<', '&lt;', s)
		s = re.sub('>', '&gt;', s)
		s = re.sub('"', '&quot;', s)
		s = re.sub("'", '&apos;', s)
	else:
		s = xml.sax.saxutils.escape(s)
	return s

class Element:
	def __init__(self, name='', value=None, attribs=None):
		# We expose the instance variables: name, parent, attribs,
		# children
		self.children = []
		self.name = name
		self.parent = None
		if value is not None:
			self.setvalue(value)
		if attribs is not None:
			self.attribs = attribs
		else:
			self.attribs = {}
	
	def close(self):
		self.parent = None
		for c in self.children:
			c.close()

	def __del__(self):
#		print 'Deleting element', self, self.name
		self.children = []
#		self.parent = None
	
	# Append a child to my list of nodes
	def add_child(self, node):
		node.parent = self
		self.children.append(node)
	
	#------------------------------------------------------------
	# Getting and setting element values
	def value(self):
		s = ''
		for c in self.children:
			if c.__class__ == CData:
				s = s + c.text
		return s

	def setvalue(self, val):
		for c in self.children:
			if c.__class__ != CData:
				raise TypeError,\
					'cannot set value of element with children'
		node = CData(val)
		self.children = [node]
	
	def setattribbool(self, name, val):
		if val:
			self.attribs[name] = 'yes'
		else:
			self.attribs[name] = 'no'

	def setattrib(self, name, val):
		self.attribs[name] = val
	#------------------------------------------------------------
	# Getting the ith element node with this particular name
	# If value is a string, then fetch the first
	# Otherwise, it should be a tuple of (name, index) in which case we
	# return the indexth node with this name
	# Use like: node['nodename', 3] to get the third nodename node
	def __getitem__(self, value):
		if type(value) is TupleType:
			if len(value) < 2:
				raise TypeError, 'Must specify two arguments to getitem'
			name = value[0]
			index = value[1]
		else:
			name = value
			index = 0

		count = 0
		for c in self.children:
			if c.__class__ == Element:
				if c.name == name:
					if count == index:
						return c
					count = count + 1
		return None
	
	#------------------------------------------------------------
	# Getting and setting boolean values as nodes with an attribute
	# uncommented elements
	def getboolean(self, path, default=None):
		if not path:
			if not self.attribs.has_key('on'):
				# For compatibility, default to 'yes' if the node is
				# present but does not have the 'on' attribute
				attrib = 'yes'
			else:
				attrib = self.attribs.get('on')
			return attrib ==  'yes'

		if type(path) is StringType:
			components = string.split(path, '/')
		else:
			components = path
		try:
			node = self[components[0]]
		except:
			traceback.print_exc(1)
			return default
		newpath = components[1:]
		if node is None:
			return default
		else:
			return node.getboolean(newpath, default)

	def setbool(self, value):
		self.setattribbool('on', value)

#		tagtext = '<%s/>' % name
#		for pos in range(len(self.children)):
#			c = self.children[pos]
#			if c.__class__ == Element and c.name == name:
#				# It's already on
#				if not value:
#					# If we have to turn it off
#					node = Comment(' ' + tagtext + ' ', self)
#					self.children[pos] = node
#				return
#			elif c.__class__ == Comment and\
#				string.strip(c.text) == tagtext:
#				# It's already commented out
#				if value:
#					node = Element(name)
#					node.parent = self
#					self.children[pos] = node
	
	#------------------------------------------------------------
	# Getting and setting list structures
	def getlist(self, itemname):
		out = []
		for i in self.children:
			if i.__class__ == Element and i.name == itemname:
				out.append(i.value())
		return out
	# TODO: fix this
	def setlist(self, itemname, values):
		node = self[listname]
		if node is None:
			node = Element(listname)
			node.parent = self
			first_whitespace = []
			last_whitespace = []
		else:
			# Attempt to preserve whitespace
			textnodes = filter(lambda x: x.__class__ == CData,
				node.children)
			first_whitespace = [textnodes[0]]
			last_whitespace = [textnodes[-1]]
		newkids = []
		for val in values:
			k = Element(itemname)
			k.parent = node
			k.setvalue(val)
			newkids = newkids + first_whitespace + [k]
		self.children = newkids + last_whitespace
	
	#------------------------------------------------------------
	# Counting the number of nodes with this tag name
	def getcount(self, name):
		count = 0
		for i in self.children:
			if i.__class__ == Element and i.name == name:
				count = count + 1
		return count

	#------------------------------------------------------------
	# Return an XML representation of myself
	def toXML(self, indent=0):
		attribstr = ''
		myindentstr = '\n' + (' ' * indent)
		indent = indent + 2
		for key in self.attribs.keys():
			attribstr = attribstr + ' %s="%s"' % (key,
				self.attribs[key])
		if attribstr:
			opentag = '<%s%s>' % (self.name, attribstr)
		else:
			opentag = '<%s>' % self.name
		closetag = ('</%s>' % self.name)
		if self.children:
			value = myindentstr + opentag
			for c in self.children:
				value = value + c.toXML(indent)
			if filter(lambda x: x.__class__ == Element, self.children):
				# Subelemnts; format myself with embedded newlines
				value = value + myindentstr + closetag
			else:
				value = value + closetag
		else:
			if attribstr:
				value = myindentstr + '<%s%s/>' % (self.name, attribstr)
			else:
				value = myindentstr + '<%s/>' % self.name
		return value

class CData:
	def __init__(self, text):
		if type(text) is not StringType:
			print 'Constructing CData with non-string argument', text
		self.text = text
		self.parent = None
	
	def toXML(self, indent=0):
		return escapeXML(self.text)
	
	def close(self):
		self.parent = None
	
class Comment:
	def __init__(self, text, value=None):
		self.text = text
		self.parent = None
	
	def toXML(self, indent):
		indentstr = '\n' + (' ' * indent)
		return indentstr + '<!--  %s  -->' % self.text

	def close(self):
		self.parent = None

class Boolean:
	def __init__(self, name, value):
		self.name = name
		self.value = value
	
	def toXML(self, indent):
		indentstr = '\n' + (' ' * indent)
		tag = '<%s on="%s"/>' % (self.name, ['no', 'yes'][self.value])
		return indentstr + tag

	def close(self):
		self.parent = None

class List:
	def __init__(self, name, valuelist):
		self.root = Element('%s-list' % name)
		for val in valuelist:
			node = Element(name)
			node.setvalue(val)
			self.root.add_child(node)

	def toXML(self, indent):
		return self.root.toXML(indent)

	def close(self):
		self.parent = None
		self.root.close()
	
class Tree:
	def __init__(self):
		self.root = None
		self.curnode = None
	
	def close(self):
		if self.root is not None:
			self.root.close()

	def addtag(self, name, attribs):
		if self.curnode is None:
			self.root = Element(name, None, attribs)
			self.curnode = self.root
		else:
			node = Element(name, None, attribs)
			node.parent = self.curnode
			self.curnode.add_child(node)
			self.curnode = node

	def addcontent(self, content):
		if self.curnode is None:
			return
		node = CData(content)
		self.curnode.add_child(node)

	def endtag(self):
		self.curnode = self.curnode.parent
	
	def addcomment(self, comment):
		node = Comment(comment, self)
		self.curnode.add_child(node)
	
	def toXML(self):
		return '<?xml version="1.0"?>\n' + self.root.toXML(0)
	
	# Returns default if the path does not exist or there is otherwise
	# some problem
	def getvalue(self, path, default=None):
		components = string.split(path, '/')
		node = self.root
		while components:
			try:
				node = node[components[0]]
			except:
				traceback.print_exc(1)
				return default
			components = components[1:]
		if node is None:
			return default
		else:
			return node.value()

	def getboolean(self, path, default=None):
		return self.root.getboolean(path, default)
	
	def getlist(self, path, nodename):
		components = string.split(path, '/')
		node = self.root
		while components:
			try:
				node = node[components[0]]
			except:
				traceback.print_exc(1)
				return []
			components = components[1:]
		return node.getlist(nodename)

def settag(*args):
	print 'Starting configuration:', args
#	global Config
#	Config = ConfigClass()

def endtag(*args):
	print 'End configuration:', args

class FuguConfParser(xmllib.XMLParser):
	def __init__(self):
		xmllib.XMLParser.__init__(self)
		self.tree = Tree()
		self.elements = {}
		for i in ['fuguconfig', 'parameters']:
			self.elements[i] = (self.tree.addtag, self.tree.endtag)
	
	def __del__(self):
		self.tree.close()

	def handle_starttag(self, tag, method, attribs):
		method(tag, attribs)
	
	def unknown_starttag(self, tag, attribs):
		self.tree.addtag(tag, attribs)
	def unknown_endtag(self, tag):
		self.tree.endtag()

	def handle_data(self, data):
		self.tree.addcontent(data)

	def handle_cdata(self, data):
		self.tree.addcontent(data)
	
	def handle_comment(self, comment):
		self.tree.addcomment(comment)
	
	def toXML(self):
		return self.tree.toXML()

class ConfigClass:
	def toInt(self, str):
		try:
			return int(str)
		except:
			print 'Unable to convert value %s to integer' % `str`
			return -1
	
	# To add a new config variable, edit this function as well as
	# load_vars.
	def toXML(self):
		"Create an XML tree, and return its string representation"
		root = Element('fuguconfig')
		root.add_child(
			Comment('Warning: this file is automatically generated'))
		prefs = Element('preferences')
		root.add_child(prefs)

		for (nodetype, name, value) in [
			(Element, 'buffersize', `self.SCROLLBACK`),
			(Element, 'signer', self.SIGNER),
			(Element, 'sender', self.SENDER),
			(Comment, 'Window sizes', ''),
			(Element, 'pane-height', `self.PANE_HEIGHT`),
			(Element, 'pane-width', `self.PANE_WIDTH`),
			(Element, 'pane-top', `self.PANE_TOP`),
			(Element, 'pane-bottom', `self.PANE_BOTTOM`),
			(Boolean, 'save-pane-width', self.SAVE_PANE_WIDTH),
			(Boolean, 'buttonbar', self.MAP_PUFFBUTTONS),
			(Boolean, 'thumbnails', self.SHOW_THUMBS),
			(Boolean, 'autocomplete', self.AUTOCOMPLETE),
			(Boolean, 'ccself', self.CC_SELF),
			(Boolean, 'broadcast-presence', self.BROADCAST_PRESENCE),
			(Boolean, 'keyword-prompt', self.KEYWORD_PROMPT),
			(Element, 'urlcommand', self.URLLOAD),
			(Element, 'thumbnail-url', self.THUMBNAIL_URL),
			(Element, 'cursorcolor', self.HIGHLIGHT),
			(List, 'presence', self.PRESENCE_STRINGS),
			(Element, 'default-presence', self.DEFAULT_PRESENCE)]:
			prefs.add_child(nodetype(name, value))
		node = Element('persist-pufflog')
		node.setattribbool('on', self.PERSIST_INTERVAL > 0)
		node.setattrib('interval', self.PERSIST_INTERVAL)
		prefs.add_child(node)

		# Screen nodes
		for conn in self.SUBLIST:
			screennode = Element('screen')
			root.add_child(screennode)

			screennode.add_child(Element('name', value=conn['name']))
			screennode.add_child(Element('subscription',
				value=conn['sub']))
			screennode.add_child(Boolean('savehistory',
				conn['savepuffs']))
			# Workaround because send_receipts has been in the wrong
			# place in some versions of the config file, so it may have
			# the value None
			screennode.add_child(Boolean('send-receipts',
				conn['send_receipts']))
			screennode.add_child(Element('logsent', str(conn['logsent'])))
			onpuff = Element('onpuff')
			screennode.add_child(onpuff)
			onpuff.add_child(Boolean('beep', conn['beep']))
			onpuff.add_child(Boolean('highlight-button', conn['highlight']))
			onpuff.add_child(Boolean('lift-screen', conn['lift']))
			onpuff.add_child(Boolean('deiconify', conn['deiconify']))
			if conn['command'] is None:
				attribs = {'on': 'no'}
			else:
				attribs = {'on': 'yes'}
			cmdnode = Element('command', value=conn['command'],
				attribs=attribs)
			onpuff.add_child(cmdnode)

		ret = '<?xml version="1.0"?>\n' + root.toXML()
		root.close()
		return ret

	def load_vars(self, p):
		val = p.getvalue('preferences/buffersize')
		if val is not None:
			self.SCROLLBACK = self.toInt(val)
		val = p.getvalue('preferences/signer')
		if val is not None:
			self.SIGNER = val
		val = p.getvalue('preferences/sender')
		if val is not None:
			self.SENDER = val
		val = p.getvalue('preferences/pane-height')
		if val is not None:
			self.PANE_HEIGHT = self.toInt(val)
		val = p.getvalue('preferences/pane-width')
		if val is not None:
			self.PANE_WIDTH = self.toInt(val)
		val = p.getvalue('preferences/pane-top')
		if val is not None:
			self.PANE_TOP = self.toInt(val)
		val = p.getvalue('preferences/pane-bottom')
		if val is not None:
			self.PANE_BOTTOM = self.toInt(val)
		val = p.getboolean('preferences/save-pane-width')
		if val is not None:
			self.SAVE_PANE_WIDTH = val
		val = p.getboolean('preferences/buttonbar')
		if val is not None:
			self.MAP_PUFFBUTTONS = val
		val = p.getboolean('preferences/thumbnails')
		if val is not None:
			self.SHOW_THUMBS = val
		val = p.getvalue('preferences/thumbnail-url')
		if val is not None:
			self.THUMBNAIL_URL = val
		val = p.getboolean('preferences/autocomplete')
		if val is not None:
			self.AUTOCOMPLETE = val
		val = p.getboolean('preferences/ccself')
		if val is not None:
			self.CC_SELF = val
		val = p.getboolean('preferences/keyword-prompt')
		if val is not None:
			self.KEYWORD_PROMPT = val
		val = p.getboolean(
			'preferences/broadcast-presence')
		if val is not None:
			self.BROADCAST_PRESENCE = val
		val = p.getvalue('preferences/default-presence')
		if val is not None:
			self.DEFAULT_PRESENCE = val
		val = p.getvalue('preferences/cursorcolor', '#00ffff')
		if val is not None:
			self.HIGHLIGHT = val
		val = p.getvalue('preferences/urlcommand')
		if val is not None:
			self.URLLOAD = val
		self.PRESENCE_STRINGS = p.getlist('preferences/presence-list',
			'presence')
		if self.DEFAULT_PRESENCE not in self.PRESENCE_STRINGS:
			self.PRESENCE_STRINGS.insert(0, self.DEFAULT_PRESENCE)

		self.PERSIST_INTERVAL = 300
		try:
			persist_node = p.root['preferences']['persist-pufflog']
		except AttributeError:
			pass
		else:
			if persist_node is not None:
				if persist_node.attribs['on'] == 'no':
					self.PERSIST_INTERVAL = 0
				else:
					self.PERSIST_INTERVAL =\
						int(persist_node.attribs['interval'])

		# Rather than relying on global defaults for each screen, we have
		# to specify them here, because the user may have any number of
		# screens, but those screens may not have the new variables
		# configured yet.
		self.SUBLIST = []
		for i in range(p.root.getcount('screen')):
			screennode = p.root['screen', i]
			if screennode is None:
				print 'Weird error: screen node #%i is NULL' % i
				break
			namenode = screennode['name']
			if namenode is None:
				name = ('Screen %i' % i)
			else:
				name = namenode.value()
			sub = screennode['subscription']
			if sub is None:
				subscription = 'Unconfigured subscription'
			else:
				subscription = sub.value()
			conn = connection.Connection(name, subscription)
			conn['savepuffs'] = screennode.getboolean('savehistory', 0)
			try:
				conn['logsent'] = self.toInt(screennode['logsent'].value()
					or '0')
			except: traceback.print_exc(1)
			conn['send_receipts'] = screennode.getboolean('send-receipts',
				1)
			onpuff = screennode['onpuff']
			if onpuff is not None:
				conn['beep'] = onpuff.getboolean('beep', 0)
				conn['highlight'] = onpuff.getboolean('highlight-button',
					1)
				conn['lift'] = onpuff.getboolean('lift-screen', 0)
				conn['deiconify'] = onpuff.getboolean('deiconify', 0)
				cmdnode = onpuff['command']
				if cmdnode and cmdnode.attribs['on'] == 'yes':
					conn['command'] = cmdnode.value()
				else:
					conn['command'] = None
			self.SUBLIST.append(conn)

	def load_config(self, fname):
		try:
			data = open(fname, 'r').read()
			parser = FuguConfParser()
			parser.feed(data)
			if parser.tree.root is None:
				print 'Error loading empty or ill-formed config file',\
					fname
				return
			self.load_vars(parser.tree)
		except KeyboardInterrupt:
			print 'Keyboard interrupt caught; exiting...'
			sys.exit(0)
		except:
			traceback.print_exc()
			print 'Unable to load configuration from', fname

	def load_default_config(self, data):
		try:
			parser = FuguConfParser()
			parser.feed(data)
			self.load_vars(parser.tree)
		except KeyboardInterrupt:
			print 'Keyboard interrupt caught; exiting...'
			sys.exit(0)
		except:
			traceback.print_exc()
			print 'Unable to load default configuration'
	
	def load_oldconf(self, fname):
		import cPickle
		try:
			f = open(fname, 'r')
			newconf = cPickle.load(f)
			f.close()
		except:
			traceback.print_exc()
			print 'Error reading old-style config file.'
			return

		# Workarounds for wonky config files
		for key in dir(newconf):
			if key == 'PRESENCE_STRINGS':
				self.__dict__[key] = list(newconf.__dict__[key])
			elif key == 'SUBLIST':
				# Convert from old-style Connection to new-style
				self.SUBLIST = []
				for c in newconf.SUBLIST:
					conn = connection.Connection(
						c['name'], c['sub'])
					conn['savepuffs'] = c['savepuffs']
					conn['logsent'] = c['logsent']
					conn['beep'] = c['beep']
					conn['highlight'] = c['highlight']
					conn['lift'] = c['lift']
					conn['deiconify'] = c['deiconify']
					conn['command'] = c['command']
					try:
						conn['pufflist'] = c['pufflist']
					except: pass
					self.SUBLIST.append(conn)
			else:
				self.__dict__[key] = newconf.__dict__[key]
		if hasattr(self, 'PRESENCE') and\
			self.PRESENCE not in self.PRESENCE_STRINGS:
			self.PRESENCE_STRINGS.insert(0, self.PRESENCE)

	def load(self, fugudir):
		# Load default config first for defaults
		import DefaultConfig
		self.load_default_config(DefaultConfig.getXML())

		# Load user-specific config file
		fname = os.path.join(fugudir, 'fuguconfig.xml')
		if not os.path.exists(fname):
			# If the config file doesn't exist
			fname = os.path.join(fugudir, 'fugu.conf')
			# Check for old-style config
			if os.path.exists(fname):
				print 'Converting old-style config file to new format.'
				self.load_oldconf(fname)
				self.save(fugudir)
			else:
				print 'No Fugu configuration found.  Using defaults.'
		else:
			self.load_config(fname)

		# Load sender coloring info
		self.COLORS = {}
		colorfn = os.path.join(fugudir, 'fugu-conf.py')
		if not os.path.exists(colorfn):
			return
		d = {}
		try:
			execfile(colorfn, {}, d)
		except:
			print 'ERROR parsing', colorfn
			traceback.print_exc()
		else:
			if d.has_key('sender') and type(d['sender']) is DictType:
				self.COLORS = d['sender']

	def save(self, fugudir):
		fname = os.path.join(fugudir, 'fuguconfig.xml')
		try:
			f = open(fname, 'w')
			f.write(self.toXML())
			f.close()
		except:
			traceback.print_exc()
			print 'Unable to save configuration to', fname
	
	def load_pufflog(self, fugudir):
		# Load puff history, assuming SUBLIST has already been
		# initialized
		fname = os.path.join(fugudir, 'fugustate.pck')
		gzfname = fname + '.gz'
		if not (os.path.exists(fname) or os.path.exists(gzfname)):
			# No file found
			return
		if os.path.exists(gzfname) and not HAVE_GZIP and not\
			os.path.exists(fname):
			print 'Compressed puff history exists, but can\'t be decoded!'
			return

		try:
			if HAVE_GZIP and os.path.exists(gzfname):
				f = gzip.GzipFile(gzfname, 'rb')
			else:
				f = open(fname, 'rb')
			pufflog = cPickle.load(f)
			f.close()
		except KeyboardInterrupt:
			print 'Keyboard interrupt caught; exiting...'
			sys.exit(0)
		except:
			traceback.print_exc()
			print 'Puff history exists, but can\'t be loaded!'
			return
		for conn in self.SUBLIST:
			if pufflog.has_key(conn['name']):
				conn['pufflist'] = pufflog[conn['name']]
	
	def persist_pufflog(self, fugudir):
		# Save puff history to disk
		pufflog = {}
		for c in self.SUBLIST:
			if c['savepuffs']:
				pufflog[c['name']] = c['pufflist']
		if not pufflog:
			return
		fname = os.path.join(fugudir, 'fugustate.pck')
		gzfname = fname + '.gz'
		try:
			if HAVE_GZIP:
				f = gzip.GzipFile(gzfname, 'wb')
				os.chmod(gzfname, 0600)
			else:
				f = open(fname, 'wb')
				os.chmod(fname, 0600)
			cPickle.dump(pufflog, f, USE_BINARY)
			f.close()
		except:
			traceback.print_exc(1)
			print 'Unable to save puff histories to', fname

def main():
	c = ConfigClass()
	c.load('.')
	print c.toXML()

if __name__ == '__main__':
	from pygale import *
	import sys
	pygale.init()
	sys.exitfunc = pygale.shutdown
	main()
