#!/usr/bin/env python

import sys
import time
import string
import socket
import select
from Tkinter import *
import tkMessageBox
import tkSimpleDialog

import msnlib
import msncb

"""
MSN Tk Client

This is a beta msn client based on msnlib. As you see, it's GUI based on the
Tk bindings, which provide an abstraction to create graphical interfaces; it
works both under linux, windows and probably others too.

For further information refer to the documentation or the source (which is
always preferred).
Please direct any comments to albertito@blitiri.com.ar.
You can find more information, and the package itself, at
http://blitiri.com.ar/p/msnlib/
"""


# main msnlib classes
m = msnlib.msnd()
m.cb = msncb.cb()

# void debug output
#def void(s): pass
#msnlib.debug = msncb.debug = void



#
# useful functions
#

#sys.setdefaultencoding("iso-8859-15")
encoding = 'iso-8859-1'

def encode(s):
	try:
		return s.decode(encoding).encode('utf-8')
	except:
		return s

def decode(s):
	try:
		return s.decode('utf-8').encode(encoding)
	except:
		return s

def nick2email(nick):
	"Returns an email according to the given nick, or None if noone matches"
	for email in m.users.keys():
		if str(m.users[email].nick) == str(nick):
			return email
	if nick in m.users.keys():
		return nick
	return None

def email2nick(email):
	"Returns a nick accoriding to the given email, or None if noone matches"
	if email in m.users.keys():
		return m.users[email].nick
	else:
		return None

def now():
	"Returns the current time in format HH:MM:SSTT"
	return time.strftime('%I:%M:%S%p', time.localtime(time.time()) )

def quit():
	"Cleans up and quits everything"
	try:
		m.disconnect()
	except:
		pass
	root.quit()
	sys.exit(0)



#
# GUI classes
#

class userlist(Frame):
	"The user list"
	def __init__(self, master):
		Frame.__init__(self, master)
		self.scrollbar = Scrollbar(self, orient = VERTICAL)
		self.list = Listbox(self, 
				yscrollcommand = self.scrollbar.set)
		self.list.config(font = "Courier")
		self.scrollbar.config(command = self.list.yview)
		self.scrollbar.pack(side = RIGHT, fill = Y)
		self.list.pack(side = LEFT, fill = BOTH, expand = 1)
		
		self.list.bind("<Double-Button-1>", self.create_chat)
			
	def create_chat(self, evt = None):
		"Creates a chat window"
		if m.status == 'HDN':
			tkMessageBox.showwarning("Warning", 
				"You can't open chats when you're invisible")
			return
		nick = self.list.get(self.list.curselection())[4:]
		email = nick2email(nick)
		if email in emwin.keys():
			emwin[email].lift()
		elif m.users[email].status == 'FLN':
			tkMessageBox.showwarning("Warning",
				"The user is offline")
		else:
			emwin[email] = chatwindow(root, email)
	

class mainmenu(Menu):
	"Main menu used in the main window"
	def __init__(self, master):
		Menu.__init__(self, master)
		self.status_menu = Menu(self, tearoff = 0)
		self.add_cascade(label = "Status", menu = self.status_menu)
		self.status_menu.add_command(label = "Online",
			command = self.chst_online)
		self.status_menu.add_command(label = "Away",
			command = self.chst_away)
		self.status_menu.add_command(label = "Busy",
			command = self.chst_busy)
		self.status_menu.add_command(label = "Be Right Back",
			command = self.chst_brb)
		self.status_menu.add_command(label = "Lunch",
			command = self.chst_lunch)
		self.status_menu.add_command(label = "Phone", 
			command = self.chst_phone)
		self.status_menu.add_command(label = "Invisible", 
			command = self.chst_invisible)
			
		self.add_command(label = 'Info', command = self.show_info)
	
	def show_info(self, evt = None):
		csel = mainlist.list.curselection()
		if not csel:
			return
		nick = mainlist.list.get(csel)[4:]
		email = nick2email(nick)
		infowindow(root, email)

	# status change callbacks
	def clear_heads(self):
		for i in emwin.keys():
			emwin[i].head.config(text = '')
	
	def chst_online(self):
		self.clear_heads()
		m.change_status('online')
	def chst_away(self):
		self.clear_heads()
		m.change_status('away')
	def chst_busy(self):
		self.clear_heads()
		m.change_status('busy')
	def chst_brb(self):
		self.clear_heads()
		m.change_status('brb')
	def chst_lunch(self):
		self.clear_heads()
		m.change_status('lunch')
	def chst_phone(self):
		self.clear_heads()
		m.change_status('phone')
	def chst_invisible(self):
		warn = "Warning: as you are invisible, it is possible that\n"
		warn += "the messages you type here never get to the user."
		for i in emwin.keys():
			emwin[i].head.config(text = warn)
		m.change_status('invisible')


class chatwindow(Toplevel):
	"Represents a chat window"
	def __init__(self, master, email):
		Toplevel.__init__(self, master)
		self.email = email
		self.protocol("WM_DELETE_WINDOW", self.destroy_window)
		nick = email2nick(email)
		# FIXME: update the title with status change
		status = msnlib.reverse_status[m.users[email].status]
		if nick:
			self.wm_title(nick + ' (' + status + ')')
		else:
			self.wm_title(email + ' (' + status + ')')

		# head label
		self.head = Label(self)
		self.head.pack(side = TOP, fill = X, expand = 0)
		self.head.config(justify = LEFT)
		self.head.config(text = "")

		# text box (with scrollbar), where the message goes
		self.frame = Frame(self)
		self.scrollbar = Scrollbar(self.frame, orient = VERTICAL)
		self.text = Text(self.frame, 
				yscrollcommand = self.scrollbar.set)
		self.scrollbar.config(command = self.text.yview)
		self.scrollbar.pack(side = RIGHT, fill = Y)
		self.text.pack(side = TOP, fill = BOTH, expand = 1)
		self.frame.pack(side = TOP, fill = BOTH, expand = 1)
		
		self.text.config(state = DISABLED)
		self.text.tag_config('from', foreground = 'blue')
		self.text.tag_config('to', foreground = 'red')
		self.text.tag_config('typing', foreground = 'lightblue')
		
		# entry, where the user types
		self.entry = Entry(self)
		self.entry.pack(side = BOTTOM, fill = X, expand = 0)
		self.entry.bind('<Return>', self.send_line)
	
	def append(self, s, direction, scroll = 1):
		"Adds text to the window's text box"
		self.text.config(state = NORMAL)
		self.text.insert(END, s, direction)
		self.text.yview(SCROLL, scroll, UNITS)
		self.text.config(state = DISABLED)
	
	def send_line(self, evt = None):
		"Sends the current entry as a message"
		msg = self.entry.get()
		lines = msg.split('\n')
		if len(lines) == 1:
			s = now() + ' >>> ' + msg + '\n'
		else:
			s = now() + ' >>>\n\t'
			s += string.join(lines, '\n\t')
			s = s[:-1]
		self.append(s, 'to', scroll = len(lines))
		
		# we need to encode it before sending because msg is already
		# an unicode string; so use utf-8
		msg = msg.encode('utf-8')

		m.sendmsg(self.email, msg)
		self.entry.delete(0, END)
	
	def destroy_window(self, evt = None):
		"Clean up when the window is closed"
		del(emwin[self.email])
		self.destroy()


class infowindow(Toplevel):
	"Represents a window with user information"
	def __init__(self, master, email):
		Toplevel.__init__(self, master)
		self.email = email
		self.wm_title('Info on ' + email)
		u = m.users[email]
		out = ''
		out += 'Information for user ' + email + '\n\n'
		out += 'Nick: ' + u.nick + '\n'
		out += 'Status: ' + msnlib.reverse_status[u.status] + '\n'
		if 'B' in u.lists:
			out += 'Mode: ' + 'blocked' + '\n'
		if u.gid != None:
			out += 'Group: ' + m.groups[u.gid] + '\n'
		if u.realnick:
			out += 'Real Nick: ' + u.realnick + '\n'
		if u.homep:
			out += 'Home phone: ' + u.homep + '\n'
		if u.workp:
			out += 'Work phone: ' + u.workp + '\n'
		if u.mobilep:
			out += 'Mobile phone: ' + u.mobilep + '\n'

		self.label = Label(self)
		self.label.pack(side = TOP, fill = BOTH, expand = 1)
		self.label.config(justify = LEFT)
		self.label.config(text = out)


def redraw_main():
	"Redraws the main screen"
	# sync the user list - FIXME: instead of redrawing, use the callbacks
	# for status change notifications
	nicks = []
	for i in m.users.keys():
		if m.users[i].status == 'FLN':
			s = '[X] '
		elif m.users[i].status in ('NLN', 'IDL'):
			s = '[ ] '
		else:
			s = '[-] '
		if 'B' in m.users[i].lists:
			s = '[!] '
		
		s += m.users[i].nick
		nicks.append(s)
	nicks.sort()
	mainlist.list.delete(0, END)
	for i in nicks:
		mainlist.list.insert(END, i)
	
	# update status
	s = msnlib.reverse_status[m.status]
	status.config(text = s)



#
# callbacks
#

def cb_msg(md, type, tid, params, sbd):
	"Gets a message"
	t = tid.split(' ')
	email = t[0]

	# parse
	lines = params.split('\n')
	headers = {} 
	eoh = 0
	for i in lines:
		# end of headers
		if i == '\r':
			break
		tv = i.split(':', 1)
		type = tv[0]
		value = tv[1].strip()
		headers[type] = value
		eoh += 1
	eoh +=1

	# ignore hotmail messages
	if email == 'Hotmail':
		return
	
	if email not in emwin.keys():
		emwin[email] = chatwindow(root, email)
		
	# typing notifications
	if (headers.has_key('Content-Type') and 
			headers['Content-Type'] == 'text/x-msmsgscontrol'):
		if not m.users[email].priv.has_key('typing'):
			m.users[email].priv['typing'] = 1
			msg = now() + ' --- is typing\n'
			emwin[email].append(msg, 'typing')
			
	# normal message
	else:
		if len(lines[eoh:]) > 1:
			msg = now() + ' <<<\n\t'
			msg += string.join(lines[eoh:], '\n\t')
			msg = msg.replace('\r', '')
		else:
			msg = now() + ' <<< ' + lines[eoh] + '\n'
			
		if m.users[email].priv.has_key('typing'):
			del(m.users[email].priv['typing'])
			
		emwin[email].append(msg, 'from')
		root.bell()

	msncb.cb_msg(md, type, tid, params, sbd)
m.cb.msg = cb_msg



#
# main
#

# email - chatwindow dictionary
emwin = {}

# gui init
root = Tk()
root.wm_title('msnlib')

mainlist = userlist(root)
mainlist.pack(side = TOP, fill = BOTH, expand = 1)

status = Label(root, text = "logging in...", bd=1, relief = SUNKEN, anchor = W)
status.pack(side = BOTTOM, fill = X, expand = 0)

menu = mainmenu(root)
root.config(menu = menu)

# initial update, to display at least something while we log in
root.update()

# ask for username and password if not given in the command line
if len(sys.argv) < 3:
	m.email = tkSimpleDialog.askstring("Username",
		"Please insert your email")
	if not m.email:
		quit()
	
	m.pwd = tkSimpleDialog.askstring("Password",
		"Please insert your password")
	if not m.pwd:
		quit()
else:
	m.email = sys.argv[1]
	m.pwd = sys.argv[2]

m.email = m.email.strip()
m.pwd = m.pwd.strip()

# the encoding is utf-8 because the text class uses unicode directly
m.encoding = 'utf-8'

root.update()

# login
try:
	m.login()
	m.sync()
except msnlib.AuthError:
	tkMessageBox.showerror("Login", "Error logging in: wrong password")
	quit()

# start as invisible
m.change_status('invisible')


# main loop
while 1:
	fds = m.pollable()
	infd = fds[0]
	outfd = fds[1]
	
	try:
		# both network and gui checks
		fds = select.select(infd, outfd, [], 0)
		root.update()
	except KeyboardInterrupt:
		quit()
	except TclError:
		quit()

	for i in fds[0] + fds[1]:
		try:
			m.read(i)
		except (msnlib.SocketError, socket.error), err:
			if i != m:
				m.close(i)
			else:
				tkMessageBox.showwarning("Warning",
					"Server disconnected us - you " +
					"probably logged in somewhere else")
				quit()
		
		# always redraw after a network event
		redraw_main()
	
	# sleep a bit so we don't take over the cpu
	time.sleep(0.05)


