#!/usr/bin/python

######################################################################
#     XTalk - A BSD talk client written in Python.
#     (C) Adam P. Jenkins <adampjenkins@yahoo.com>
#     
#     This program is free software; you can redistribute it and/or
#     modify it under the terms of the GNU General Public License
#     as published by the Free Software Foundation; either version 2
#     of the License, or (at your option) any later version.
#     
#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
#     
#     You should have received a copy of the GNU General Public License
#     along with this program; if not, write to the Free Software
#     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
######################################################################


import sys
sys.path.append('@LIBDIR@')

from socket import *
SocketError = error

from Tkinter import *
from ScrolledText import ScrolledText
import TalkdInter, string, regex, errno

versionMajor = 1
versionMinor = 3

# set to 1 to swap BackSpace and Delete keybindings in edit widgets.
swapBsDel = 0

class Talk(Frame):
    # used to raise exceptions
    error = 'TalkError'
    
    def __init__(self, parent=None, addr):
	self.sock = None
	self.afterId = None
	self.servSock = None
	self.talkd = None
	
	Frame.__init__(self, parent)
	self.pack(fill=BOTH, expand=TRUE)

	self.makeControls()
	self.makeEntry()
	self.makeEdits()
	self.entry.var.set(addr)
	self.status = Label(self, relief=SUNKEN)
	self.status.pack(side=TOP, fill=X, expand=FALSE)
	self.entry.address.focus()
    
    ##### callback functions
    def quit(self):
	self.disconnect()
	Frame.quit(self)

    def connect(self, event=None):
	try:
	    address = self.parseAddress(self.entry.var.get())
	except Talk.error, msg:
	    self.status['text'] = msg
	    return

	# Disable "connect" event handler, enable disconnect
	self.entry.address.unbind('<Return>')
	self.buttons.connect.config(state=DISABLED)
	self.buttons.disconnect.config(state=NORMAL)
	
	self.status['text'] = "Making connection to '%s@%s %s'" % \
			      (address[1], address[0], address[2])
        self.update_idletasks()
	apply(self.makeConnection, address)

	
    def disconnect(self):
	self.cleanup()
	self.status['text'] = "Disconnected"
		

    ##### Functions to create the GUI
    def makeControls(self):
	self.buttons = Frame(self, relief=RAISED)
	self.buttons.pack(side=TOP, fill=X)
	self.buttons.quit = Button(self.buttons, text='Quit',
				   command=self.quit)
	self.buttons.quit.pack(side=LEFT, padx=2, pady=2)

	self.buttons.connect = Button(self.buttons, text='Connect',
				      command=self.connect)

	self.buttons.connect.pack(side=LEFT, padx=2, pady=2)

	self.buttons.disconnect = Button(self.buttons, text='Disconnect',
					 command=self.disconnect,
					 state=DISABLED)
	self.buttons.disconnect.pack(side=LEFT, padx=2, pady=2)

    def makeEntry(self):
	self.entry = Frame(self, relief=RAISED)
	self.entry.pack(side=TOP, fill=X)
	self.entry.lab = Label(self.entry, text='Address')
	self.entry.lab.pack(side=LEFT)
	self.entry.var = StringVar()

	self.entry.address = Entry(self.entry, textvariable=self.entry.var)
	self.entry.address.pack(side=LEFT, fill=X, expand=TRUE)
	self.entry.address.bind('<Return>', self.connect)

    def makeEdits(self):
	self.edit = Frame(self, relief=RAISED)
	self.edit.pack(side=TOP, fill=BOTH, expand=TRUE)

	self.edit.local = ScrolledText(self.edit, wrap=WORD, width=80,
				       height=12, state=DISABLED)
	self.edit.remote = ScrolledText(self.edit, wrap=WORD, width=80,
					height=12, state=DISABLED)
	self.edit.local.pack(side=TOP, fill=BOTH, expand=TRUE, pady=2)
	self.edit.remote.pack(side=TOP, fill=BOTH, expand=TRUE, pady=2)
	if swapBsDel:
	    bscmd = self.tk.call('bind', 'Text', '<Key-BackSpace>')
	    dlcmd = self.tk.call('bind', 'Text', '<Key-Delete>')
	    self.tk.call('bind', 'Text', '<Key-BackSpace>', dlcmd)
	    self.tk.call('bind', 'Text', '<Key-Delete>', bscmd)

	
    ##### Implementation

    def parseAddress(self, addr):
 	# parses a an address as entered by a user, and returns a
 	# tuple (remote-host, remote-user, remote-tty) or raises a
 	# Talk.error exception if there's an error in address.	
	rx = regex.compile("\(^[^ \t@]+\)\(@\([^ \t@]+\)\)?\([ \t]+\(\w+\)\)?$")
	if rx.match(string.strip(addr)) > 0:
	    ruser, rhost, rtty = rx.group(1, 3, 5)
	    if not rhost:
		rhost = 'localhost'
	    if not rtty:
		rtty = ''
	    return (rhost, ruser, rtty)
	else:
	    raise Talk.error, "Bad address format given."

    def cleanup(self):
	if self.afterId:
	    self.after_cancel(self.afterId)
	    self.afterId = None
	if self.talkd:
	    try:
		self.talkd.deleteInvite('mine')
		self.talkd.deleteInvite('his')
	    except TalkdInter.error, msg:
		pass
	    self.talkd = None
	if self.servSock:
	    tkinter.deletefilehandler(self.servSock)
	    self.servSock.close()
	    self.servSock = None
	if self.sock:
	    tkinter.deletefilehandler(self.sock)
	    self.edit.local.config(state=DISABLED)
	    self.edit.local.unbind('<Key>')
	    
	    # unset paste handling in local window
	    for e in ['<Button-2>', '<Control-y>']:
		self.edit.local.unbind(e)

	    self.sock.close()
	    self.sock = None
	    
	# enable "connect" event handler, disable disconnect
	self.entry.address.bind('<Return>', self.connect)
	self.buttons.connect.config(state=NORMAL)
	self.buttons.disconnect.config(state=DISABLED)

	
    def makeConnection(self, rhost, ruser, rtty):
	try:
	    self.talkd = TalkdInter.TalkdInter(rhost, ruser, rtty)
	    
	    self.status['text'] = "Checking for invite..."
	    self.update_idletasks()
	
	    try:
		raddr = self.talkd.lookUp()
	    except TalkdInter.error, msg:
		raddr = None

	    if raddr:
		self.status['text'] = "Found invitation. connecting..."
		self.update_idletasks()
		self.sock = socket(AF_INET, SOCK_STREAM)
		self.sock.connect(raddr)
		self.status['text'] = "Connected to " + `raddr`
		self.update_idletasks()
		self.setupIO()
		return
	    self.servSock = socket(AF_INET, SOCK_STREAM)
	    self.servSock.bind((gethostname(), 0))
	    myaddr = self.servSock.getsockname()
	    self.servSock.listen(1)

	    self.talkd.leaveInvite(myaddr)

	    self.talkd.announce()
	except (TalkdInter.error, SocketError), err:
	    self.status['text'] = err
	    self.cleanup()
	    return
	self.status['text'] = "Ringing remote party..."
	self.numTries = 1

	tkinter.createfilehandler(self.servSock, tkinter.READABLE,
				  self.acceptConnection)
	
	# announce again after 30 seconds
	self.afterId = self.after(30000, self.announceAgain)
	    
    def announceAgain(self):
	try:
	    self.talkd.announce()
	    self.talkd.leaveInvite(self.servSock.getsockname())
	except TalkdInter.error, msg:
	    self.status['text'] = msg
	    self.cleanup()
	    return
	self.numTries = self.numTries + 1
	self.status['text'] = "Ringing remote party, try " + `self.numTries`
	self.afterId = self.after(30000, self.announceAgain)

    def acceptConnection(self, file, mask):
	acc = self.servSock.accept()
	self.status['text'] = "Accepted connection from " + `acc[1]`
	tkinter.deletefilehandler(self.servSock)
	self.sock = acc[0]
	self.servSock.close()
	self.servSock = None
	try:
	    self.talkd.deleteInvite('mine')
	    self.talkd.deleteInvite('his')
	except TalkdInter.error, msg:
	    pass
	self.talkd = None
	self.setupIO()

    def setupIO(self):
	self.bell()

	if self.afterId:
	    self.after_cancel(self.afterId)
	    self.afterId = None

	# exchange edit chars.  The edit chars are supposed to be
	# 1) erase, i.e. backspace, Text widget uses \010
	# 2) kill, not sure, think it's delete current line
	# 3) wkill, not sure, think it's delete to beginning of word.

	if swapBsDel:
	    self.sock.send('\177\0\0')
	else:
	    self.sock.send('\010\0\0')
	    
	self.editChars = self.sock.recv(3)
	
	tkinter.createfilehandler(self.sock, tkinter.READABLE,
				  self.handleRemoteInput)
	self.sock.setblocking(0)

	self.edit.local.config(state=NORMAL)
	self.edit.remote.config(state=NORMAL)
	# clear both edit windows
	self.edit.local.delete('1.0', END)
	self.edit.remote.delete('1.0', END)
	self.edit.remote.config(state=DISABLED)
	self.edit.local.bind('<Key>', self.handleLocalInput)
	
	# set up paste handling in local window
	for e in ['<Button-2>', '<Control-y>']:
	    self.edit.local.bind(e, self.handlePaste)
	    
	self.edit.local.focus()

    def handleRemoteInput(self, file, mask):
	try:
	    inp = self.sock.recv(80)
	except SocketError, err:
	    if err[0] != errno.EWOULDBLOCK:
		print err[1]
		self.disconnect()
		return
	if not inp:
	    self.disconnect()
	    return
	self.edit.remote.config(state=NORMAL)
	for c in inp:
	    if c == self.editChars[0]:
		self.edit.remote.delete("end - 2 char")
	    else:
		self.edit.remote.insert(END, c)
	self.edit.remote.see(END)
	self.edit.remote.config(state=DISABLED)
	
    def handleLocalInput(self, event):
	c = event.char
	if c == '':
	    return
	if c == '\015':
	    c = '\012'
	try:
	    self.sock.send(c)
	except SocketError, err:
	    if err[0] != errno.EWOULDBLOCK:
		print err[1]
		self.disconnect()

    def handlePaste(self, event):
	try:
	    p = self.selection_get()
	except TclError:
	    return
	if len(p) == 0:
	    return
	self.edit.local.see(END)
	try:
	    self.sock.send(p)
	except SocketError, err:
	    if err[0] != errno.EWOULDBLOCK:
		print err[1]
		self.disconnect()
		
def main():
    if len(sys.argv) > 1:
	if len(sys.argv) >= 3:
	    addr = "%s %s" % (sys.argv[1], sys.argv[2])
	else:
	    addr = sys.argv[1]
    else:
	addr = ''
	
    root = Tk(className='XTalk')
    
    opt = string.lower(root.option_get('swapBsDel',''))
    if opt == 'true':
	swapBsDel = 1
    elif opt == 'false':
	swapBsDel = 0
    elif opt != '':
	print 'Warning: unrecognized value for XTalk.swapBsDel: %s' % opt
    
    talk = Talk(None, addr)
	
    talk.winfo_toplevel().title("XTalk %d.%d" % (versionMajor, versionMinor))
    talk.mainloop()

if __name__ == '__main__':
    main()
