#!/usr/bin/env python

from Tkinter import *
import tkMessageBox
from types import *
import re, string, time, os, sys

import pygale.engine
if sys.platform != 'win32':
	pygale.engine.engine = pygale.engine.TkEngine()
from pygale import *
import thumbcache
if sys.platform == 'win32':
	import win32api

if sys.version >= '1.6':
	STRING_TYPES = [StringType, UnicodeType]
else:
	STRING_TYPES = [StringType]

# Set by ui.py
Config = None

# URL matching pattern
URLPAT = 'https?:[\w/\.:;+\-~\%#?=&,()]+|www\.[\w/\.:;+\-~\%#?=&,()]+|' +\
	'ftp:[\w/\.:;+\-~\%#?=&,]+'

# Debug memory leak
DEBUG_MEMLEAK = 0

# Encapsulate information about a single connection to the Gale server
class Connection:
	def __init__(self, name, sub):
		self._name = name		# Name of tab
		self._sub = sub		# Subscription
		self._master = None		# Pointer to a GaleWin
		self._galec = None		# Pointer to Gale client
		self._widget = None		# Pointer to Text widget
		self._socketfd = -1		# Socket descriptor
		self._pufflist = []		# List of puffs
		self._curpuff = None	# Currently selected puff (index into
							# pufflist)
		self._savepuffs = 0		# Save the puffs to disk
		self._logsent = 0		# Log sent puffs (0=no, 1=priv, 2=pub+priv)
		self._ccself = 0		# CC self on private puffs
		self._send_receipts = 1	# Send return receipts to private puffs

		# On new puff:
		self._beep = 0			# Beep
		self._command = None	# Run command
		self._deiconify = 0		# Deiconify window
		self._highlight = 0		# Highlight tab
		self._lift = 0			# Lift tab

		# State vars
		self._temp_hash = {}
	
#	# For pickling
#	def __getstate__(self):
#		state = {}
#		saved_attribs = ['name', 'dosub', 'sub', 'beep',
#			'command', 'deiconify', 'highlight', 'lift', 'savepuffs',
#			'logsent']
#		if self._savepuffs:
#			saved_attribs.append('pufflist')
#		for k in saved_attribs:
#			uname = '_' + k
#			state[uname] = self.__dict__[uname]
#		return state
#
#	def __setstate__(self, dict):
#		self.__init__(dict['_name'], dict['_sub'])
#		for k in dict.keys():
#			# Change old flash settings to new highlight settings
#			# Get rid of this when no one has an old config file anymore
#			if k == '_flash':
#				self.__dict__['_highlight'] = dict['_flash']
#			else:
#				self.__dict__[k] = dict[k]

	# Accessors for config variables
	# Reconnect manually if necessary
	def __setitem__(self, key, val):
		uname = '_' + key
		self.__dict__[uname] = val

	# Getitem does double duty: using an integer index returns the item in
	# the pufflist, using a string index returns the config variable
	def __getitem__(self, key):
		if type(key) is IntType:
			return self._pufflist[key]
		else:
			uname = '_' + key
			return self.__dict__[uname]

	# Get our length as if we were the pufflist
	def __len__(self):
		return len(self._pufflist)

	# Accessors for other variables
	def widget(self):
		return self._widget

	def client(self):
		return self._galec
	
	# Store this master and widget into this connection
	def setwidget(self, master, widget):
		self._master = master
		self._widget = widget
		# Save original background color and color it pink (disconnected)
		self._origbg = self._widget.component('text').configure('bg')[-1]
		self._widget.component('text')['background'] = 'pink'
	
	def disconnect(self):
		"Request disconnection from server"
		self._galec.disconnect()

	# Make connection to server
	def connect(self):
		"Make connection to server"
		self.make_connection()
		if sys.platform == 'win32':
			self.check_socketread()

#		self._widget.configure(text_state = 'normal')
#		sub = string.strip(re.sub('\n|\r|\t', '', self._sub))
#		self._widget.insert('end', 'Subscription is ' + sub + '\n')
#		self._widget.see('end')
#		self._widget.configure(text_state = 'disabled')

	# Our subscription changed, send it to the server
	def resubscribe(self):
		"Send a subscription request to server"
		sub = string.strip(re.sub('\n|\r|\t', ' ', self._sub))
		sublist = string.split(sub, None)
		self._galec.sub_to(sublist, self.resubscribe2)

	def resubscribe2(self, bad_subs, good_subs):
		if bad_subs:
			d = tkMessageBox.showerror(
				title='Subscription error',
				parent=self._master,
				message=
				'Unable to subscribe to: ' + string.join(bad_subs, ', '))

		self._widget.configure(text_state = 'normal')
		if good_subs:
			self._widget.insert('end', 'Subscribed to %s\n' %
				string.join(good_subs, ' '))
		else:
			self._widget.insert('end', 'Not subscribed\n')
		self._widget.see('end')
		self._widget.configure(text_state = 'disabled')
		
	def make_connection(self):
		"Set up connection to Gale server"
		if self._socketfd != -1:
			self.delete_filehandler()
		self._galec = pygale.GaleClient()
		self._galec.set_onconnect(self.connected)
		self._galec.set_ondisconnect(self.disconnected)
		self._galec.connect(lambda h, s=self: s.make_connection2(h))
	
	def make_connection2(self, host):
		"Callback on failed connection"
		if host is None:
			# Couldn't connect to server
			self._master.set_status('Unable to connect to a gale server')
			# Retry after 2 seconds
			self._master.after(2000, self._galec.retry)
			return
	
	def connected(self, host):
		"Callback when connection established"
		self._socketfd = self._galec.socket()
		self.register_filehandler()

		self._widget.configure(text_state='normal')
	
		self._widget.insert('end', 'Connected to server at %s\n' % host)
		self._widget.configure(text_bg=self._origbg)
		self._widget.configure(text_state='disabled')
		self.resubscribe()
	
	def disconnected(self):
		"Callback when connection broken"
		self.delete_filehandler()
		self._widget.configure(text_state='normal')
		self._widget.insert('end', 'Disconnected from server\n')
		self._widget.configure(text_bg='pink')
		self._widget.configure(text_state='disabled')

	def register_filehandler(self):
		self._galec.set_puff_callback(lambda p, s=self:
			s._master.recvpuff(s, p))
		self._galec.set_verify_callback(lambda p, s=self:
			s._master.verify_update(s, p))
#		engine.set_timer_callback(self.timer_callback)

	def delete_filehandler(self):
		self._galec.del_puff_callback()

	# Handle timeout events
#	def timer_callback(self, *args):
#		next_timeout = engine.next_timeout()
#		if next_timeout is None:
#			return
#		time_til_timeout = next_timeout - time.time()
#		if (time_til_timeout <= 0):
#			engine.process()
#			return
#		self._master.after(int(time_til_timeout * 1000),
#			self.timer_callback)

	# ------------------------------------------------------------
	# Icky workaround for Win32
	def check_socketread(self, *args):
		engine.engine.process(0)
		self._master.after(500, self.check_socketread)

	# ------------------------------------------------------------

	def pufflist(self):
		return self._pufflist

	def set_curpuff(self, index):
		if self._curpuff is not None:
			self._widget.tag_remove('selected-puff', '1.0', 'end')
		puff = self._pufflist[index]
		tag = 'pufftag-%i' % hash(puff)
		self._curpuff = index
		# Fix due to MKF: the tag ranges might be discontiguous
		ranges = self._widget.tag_ranges(tag)
		start = ranges[0]
		end = ranges[-1]
		self._widget.see(end)
		self._widget.see(start)
		self._widget.tag_add('selected-puff', start, end)
	
	def set_curpuff_frompuff(self, puff):
		self._master.set_loc_focus()
		index = self._pufflist.index(puff)
		self.set_curpuff(index)
	
	def curpuff(self):
		if self._curpuff is not None:
			return self._pufflist[self._curpuff]
		else:
			return None
	
	def curindex(self):
		return self._curpuff

	def addpuff(self, msg, new=1):
		self._widget.configure(text_state = 'normal')
		self._temp_hash[msg] = 1
		self._pufflist.append(msg)
		tag = 'pufftag-%i' % hash(msg)
		signer_mark_begin = 'signer-mark-%i-begin' % hash(msg)

		# Indent margins
		self._widget.tag_config(tag, lmargin1=4, lmargin2=4, rmargin=4)

		# The previous line created the latest tag
		# Now raise the sel tag so that selections are always drawn on
		# top of any other coloration
		self._widget.tag_raise('quotation')
		self._widget.tag_raise('sel')

		# Check for this condition before adding all this text below
		if self._widget.dlineinfo('end - 2 lines'):
			scroll_bottom = 1
		else:
			scroll_bottom = 0

		list = msg.get_text('message/body')
		if not list:
			bodytext = ''
		else:
			bodytext = string.join(list, '|')
		bodytext = re.sub('\r\n', '\n', bodytext)
		if bodytext and bodytext[-1] != '\n':
			bodytext = bodytext + '\n'

		# Format return receipts specially
		if msg.get_text('answer.receipt') or\
			msg.get_text('answer/receipt') or\
			msg.get_text('notice/presence'):
			# Return receipts are never "new"
			if msg.get_text('answer.receipt') or\
				msg.get_text('answer/receipt'):
				new = 0

			now = msg.get_time_first('id/time', time.time())
			self._widget.insert('end',
				'%s ' %
				time.strftime('%m-%d %H:%M:%S',
				time.localtime(now)), tag)

			if msg.get_text('answer.receipt'):
				self._widget.insert('end', 'received: ', tag)
			if msg.get_text('answer/receipt'):
				self._widget.insert('end', 'received: ', tag)
			if msg.get_text('notice/presence'):
				self._widget.insert('end',
					msg.get_text_first('notice/presence')
					+ ': ', tag)
			self._widget.mark_set(signer_mark_begin, 'end - 1 chars')
			self._widget.mark_gravity(signer_mark_begin, 'left')
			self._widget.insert('end',
				msg.get_signer('*verifying*'), tag)
			if msg.get_text('message/sender'):
				self._widget.insert('end', " (%s)" %
					msg.get_text_first('message/sender'), tag)
			if msg.get_text('id/class'):
				self._widget.insert('end',
					'  %s' % msg.get_text_first('id/class'),
					(tag, 'small'))
			self._widget.insert('end', '\n', tag)
		# Format regular puff
		else:
			self._widget.insert('end', '\n', ('hrspacing', tag))
			self._widget.insert('end', '\n', ('hr', tag))
			self._widget.insert('end', '\n', ('hrspacing', tag))

			sender_tags = (tag,)
			signer = msg.get_signer(None)
			if signer is None or (signer != '*unverified*' and\
				signer != '*unsigned*'):
				# Display a thumbnail pic
				if Config.SHOW_THUMBS:
					thumb_mark = 'thumb-mark-%i' % hash(msg)
					self._widget.mark_set(thumb_mark, 'end -1 chars')
					self._widget.mark_gravity(thumb_mark, 'left')
					if signer:
						url = msg.get_text_first('message/image', None)
						if type(url) not in STRING_TYPES:
							url = None
						if url is None:
							url = thumbcache.id_to_url(signer)
						thumbcache.getthumb(url,
							lambda img, h=hash(msg), s=self:
							s.display_thumb(img, h),
							fromcache=not new)

				# Colorize sender attribute
				if signer:
					for key in Config.COLORS.keys():
						if re.search(key, signer):
							sender_tags = sender_tags + ('%s-fg' %
								Config.COLORS[key],)
							break

			locs = string.split(msg.get_loc(), None)
			first = 1
			if locs:
				self._widget.insert('end', 'To ', (tag, 'header'))
				for loc in locs:
					if first:
						first = 0
					else:
						self._widget.insert('end', ', ', (tag,
							'header'))
					domain, cats = pygale.parse_location_upper(loc)
					self._widget.insert('end', string.join(cats, '.'),
						(tag, 'bold'))
					self._widget.insert('end', '@' + domain, (tag,
						'header'))

			if msg.get_text('message.keyword'):
				self._widget.insert('end',
					' [%s]' %
					string.join(msg.get_text('message.keyword'), ', '),
					(tag, 'bold'))
			if msg.get_text('message/subject'):
				self._widget.insert('end',
					' "%s"' % msg.get_text_first('message/subject'),
					(tag, 'bold'))
			if msg.get_text('id/class'):
				self._widget.insert('end',
					'  %s' % msg.get_text_first('id/class'),
					(tag, 'small'))
			msgstart = self._widget.index('end')
			self._widget.insert('end', '\n', tag)
			# Check for WQP in bodytext
			bodylines = string.split(bodytext, '\n')
			for i in range(len(bodylines)):
				line = bodylines[i]
				# Add the newline back to every line but the last
				if i != len(bodylines) - 1:
					line = line + '\n'
				if line[:2] == '> ':
					self._widget.insert('end', line,
						(tag, 'quotation'))
				else:
					self._widget.insert('end', line, tag)
			# Last line: -- sender, signer at date
			self._widget.insert('end', '-- ', (tag, 'right'))
			sender = msg.get_text_first('message/sender', None)
			if sender:
				self._widget.insert('end', sender, sender_tags +
					('right',))
					
			self._widget.insert('end', ' <', (tag, 'right'))
			self._widget.mark_set(signer_mark_begin, 'end -1 chars')
			self._widget.mark_gravity(signer_mark_begin, 'left')
			self._widget.insert('end', msg.get_signer('*verifying*'), 
				(tag, 'right'))
			self._widget.insert('end', '>', (tag, 'right'))
			sentstr = ''
			if msg.get_time('id/time'):
				sentstr = ' at %s\n' %\
					time.strftime("%m-%d %H:%M:%S",
					time.localtime(msg.get_time_first('id/time')))
			else:
				sentstr = '\n'
			self._widget.insert('end', sentstr, (tag, 'right'))

			# Check for URLs (in non-receipt puffs only)
			urlpat = re.compile(URLPAT)
			rowoffset = 0
			for line in string.split(bodytext, '\n'):
				coloffset = 0
				while 1:
					match = urlpat.search(line, coloffset)
					if match:
						(patstart, patend) = match.span()
						while line[patend-1] in ['.', ',']:
							patend = patend - 1
						coloffset = patend
						self._widget.tag_add('URL',
							'%s + %i lines + %i chars' %
							(msgstart, rowoffset, patstart),
							'%s + %i lines + %i chars' %
							(msgstart, rowoffset, patend))
					else:
						break
				rowoffset = rowoffset + 1


		# Set this to be the current puff on mouse click
		# This causes loads of problems with the other bindings
		self._widget.tag_bind(tag, '<Button-1>',
			lambda e, s=self, p=msg: s.set_curpuff_frompuff(p))
		self._widget.tag_bind(tag, '<Button-3>',
			lambda e, s=self, p=msg: s.set_curpuff_frompuff(p))

		# Delete old puffs from buffer
		if len(self._pufflist) > Config.SCROLLBACK:
			numdel = len(self._pufflist) - Config.SCROLLBACK
			to_be_deleted = self._pufflist[:numdel]
			# Compute start using the pufftag here, because the pufftags
			# will be deleted in the loop below
			first_remaining = self._pufflist[numdel]
			start = self._widget.tag_ranges(
				'pufftag-%i' % hash(first_remaining))[0]
			self._pufflist = self._pufflist[numdel:]
			for p in to_be_deleted:
				signer_mark_begin = 'signer-mark-%i-begin' % hash(p)
				thumb_mark = 'thumb-mark-%i' % hash(p)
				self._widget.mark_unset(signer_mark_begin)
				self._widget.mark_unset(thumb_mark)
				pufftag = 'pufftag-%i' % hash(p)
				self._widget.tag_delete(pufftag)
				# also delete URL tags
				self._widget.tag_remove('URL', 1.0, start)
			self._widget.delete(1.0, start)
			if self._curpuff and self._curpuff >= numdel:
				self._curpuff = self._curpuff - numdel

		# Only scroll at end if end is visible
		# Warning: do this after setting the current puff, above, since
		# set_curpuff will cause the text widget to scroll there
		if scroll_bottom:
			self._widget.see('end')
#			retval = self._widget.tag_nextrange(tag, 1.0)
#			if retval:
#				self._widget.see(retval[1])
#				self._widget.see(retval[0])

		self._widget.configure(text_state = 'disabled')
	
		if not new: return
		# Beep if it's encrypted
		if self._beep:
			self._master.bell()
		if self._command is not None:
			# TODO: set envvars like for gsubrc
			# TODO: figure out how to fork() on Win32
			if sys.platform != 'win32':
				args = string.split(self._command)
				args = [args[0]] + args
				pid = os.fork()
				if pid:
					# parent
					pass
				else:
					try:
						apply(os.execlp, tuple(args))
					except:
						sys.exit(0)
			else:
				# This is rumored to work
#				win32api.WinExec(self._command, win32api.SW_SHOW)
				pass
		if self._lift:
			self._master.switch_screen(Config.SUBLIST.index(self))
		if self._deiconify:
			self._master.master.deiconify()
		# Flash interacts weirdly with lifting the screen
		if self._highlight and not self._master.is_curscreen(self):
			index = Config.SUBLIST.index(self)
			self._master._buttonlist[index]['background'] = 'maroon'

	def display_thumb(self, thumb, hash):
		if thumb is not None:
			if not hasattr(self._widget, 'image_create'):
				self._master.gale_error('Unable to display thumbnail; '+
					'old version of Python/Tkinter?')
				return
			mark = 'thumb-mark-%i' % hash
			tag = 'pufftag-%i' % hash
			curstate = self._widget.component('text').configure('state')[-1]
			self._widget.component('text').configure(state='normal')
			try:
				index = self._widget.index(mark)
				self._widget.insert(index, ' ', tag)
				self._widget.image_create(index, image=thumb,
					align='bottom')
			except TclError, e:
				self._master.gale_error('Error displaying thumbnail: %s'
					% `e`)
			self._widget.component('text').configure(state=curstate)

			# Try to make the end of this puff visible if it's not
			# This may have weird effects
			ranges = self._widget.tag_ranges(tag)
			start = ranges[0]
			end = ranges[-1]
			if self._widget.dlineinfo(start):
				self._widget.see(end)
		# clear out unused thumbs in cache
		imagelist = []
		for c in Config.SUBLIST:
			image_str = c.widget().image_names()
			l = string.split(image_str)
			l = map(lambda x: string.split(x, '#')[0], l)
			imagelist = imagelist + l
		thumbcache.GC(imagelist)

	def puff_verified(self, puff):
		signer = puff.get_signer(None)
		if signer is None:
			return
		signer_mark_begin = 'signer-mark-%i-begin' % hash(puff)
		self._widget.configure(text_state = 'normal')

		# Display a thumbnail pic
		thumb_mark = 'thumb-mark-%i' % hash(puff)
		if Config.SHOW_THUMBS and thumb_mark in self._widget.mark_names()\
			and signer != '*unverified*' and signer != '*unsigned*':
			# Eeek, I don't like that *unverified* test
			url = thumbcache.id_to_url(signer)
			thumbcache.getthumb(url, lambda img, s=self,
				h=hash(puff): s.display_thumb(img, h))
		# Redraw signer
		try:
			index = self._widget.index(signer_mark_begin)
			self._widget.delete(signer_mark_begin, 
				'%s + 2 chars wordend wordend' % index)
			self._widget.insert(signer_mark_begin, signer)
		except TclError:
			pass
		self._widget.configure(text_state = 'disabled')

	def cancel_highlight(self):
		index = Config.SUBLIST.index(self)
		self._master._buttonlist[index]['background'] = self._origbg
	
	def clear_puffs(self):
		self._pufflist = []
		self._widget.configure(text_state = 'normal')
		self._widget.delete(1.0, 'end')
		marks = self._widget.mark_names()
		for name in marks:
			self._widget.mark_unset(name)
		pufftags = self._widget.tag_names()
		prefixlen = len('pufftag')
		for name in pufftags:
			if name[:prefixlen] == 'pufftag':
				self._widget.tag_delete(name)
		self._widget.configure(text_state = 'disabled')
		self._curpuff = None

