#!/usr/bin/python

#~ JollyBOX chat+ client
#~ Copyright (C) 2006 Thomas Jollans
#~
#~ This program is free software; you can redistribute it and/or modify
#~ it under the terms of the GNU General Public License version 2 as 
#~ published by the Free Software Foundation
#~
#~ 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

def printwrapper ( str ):
	print str

# --- IMPORTS
from socket import *
import select
import sys
import time
import os
import re


# --- GLOBAL VARIABLES ---
# list of functions to be called in main loop
loopfuncz = []    

# --- CONSTANTS
TJC_DEFAULT_PORT = 4056

# --- CONSTANTS FOR ChatRoom ---
# passed on to 'info' callback
TJC_INFO_ROOMNAME = 0    # 'data' is the name of the room
TJC_INFO_CLIENTS = 1    # 'data' is a list containing the names of the connected clients ; can be used for ChatRoom.request_info
TJC_INFO_VERSION = 2    # 'data' is a string containing the server's version ; can be used for ChatRoom.request_info

# passed on to 'user_status' callback
TJC_USER_NEW = 0    # 'data' is the name of the user
TJC_USER_GONE = 1    # 'data' is the name of the user


class ChatRoom:
    """A TjChat chat room"""
    
    # --- CONSTRCTOR ---
    
    def __init__ ( self, server, port, name ):
        # default callbacks
        self.__printfn = printwrapper
        self.__msgfn = self.__display_msg
        self.__userstatfn = self.__printuserstat
        self.__infofn = self.__printinfo
        self.__privarrivfn = self.__priv_arrives
        self.__privdepartfn = self.__priv_departs
        self.__shutdownfn = sys.exit
        
        self.__alive = True
        self.__buf = []
        self.__sock = socket(AF_INET, SOCK_STREAM)
        self.__printfn ( "-- socket initialized" )
        self.__sock.connect ( ( gethostbyname ( server ), port ) )
        self.__name = name
        self.__server = getfqdn ( server )
        self.__port = port

        self.__server_caps = []
        
        self.__name = name
        self.__proto = TjChatProtocol(self)
        self.__proto.send_name()
        print "--connected and sent initial info"
    
    # --- CALLBACKS ---
    
    def print_cb ( self, fn ):
        """callback prints text on preferred medium, equivalent to 'print'
        callback signature:
            def printfn ( text )"""
        self.__printfn = fn
        
    def msg_cb ( self, fn ):
        """notifies the arrival or departure of a standard message.
        callback signature:
            def msgfn ( author, message )"""
        self.__msgfn = fn
        
    def user_status_cb ( self, fn ):
        """notifies of a user-related event, e.g. a new user connecting.
        callback signature:
            def userstatfn ( type, data )
        where type is one of the TJC_USER_* constants. the type of data is denoted
        next to the constant definition."""
        self.__userstatfn = fn
    
    def info_cb ( self, fn ):
        """notifies the arrival of informational data.
        callback signature:
            def infofn ( type, data )
        where type is one of the TJC_INFO_* constants. the type of data is denoted
        next to the constant definition."""
        self.__infofn = fn
        
    def private_arriv_cb ( self, fn ):
        """notifies the arrival of a private message.
        callback signature:
            def privmsgfn ( author, message )"""
        self.__privarrivfn = fn
        
    def private_depart_cb ( self, fn ):
        """notifies the departure of a private message.
        callback signature:
            def privdepartfn ( recieptent, message )"""
        self.__privdepartfn = fn
        
    def shutdown_cb ( self, fn ):
        """called when the server closed the connection or the connection is not writable.
        callback signature:
            def privdepartfn ( )"""
        self.__shutdownfn = fn
        
    # --- FUNCTIONS FOR ChatRoom USAGE ---
    
    def main_iteration ( self ):
        if ( self.__alive ):
            r, w, ex = select.select ( [self.__sock], [self.__sock], [],0 )
            if r:
                self.__proto.parse ( r[0].recv(256) )
            if w and self.__buf:
                # workaround around bad protocol design
                # may fail on slow connexions
                time.sleep (0.1)
                
                writethis = self.__buf.pop()
                try:
                    w[0].send( writethis )
                except:
                    # write error. let's commit suicide !
                    self.disconnect()
                    self.__shutdownfn()
    
    def disconnect ( self ):
        self.__alive = False
        self.__sock.close ()
        
    def send_msg ( self, msg ):
        self.__proto.send_msg(msg)
        self.__msgfn ( self.__name, msg )
    
    def send_private_msg ( self, to, msg ):
        self.__proto.send_private_msg(to, msg)
        self.__privdepartfn ( to, msg )
    
    def request_info ( self, type ):
        if type == TJC_INFO_CLIENTS:
            self.__proto.send_special( "#client-list" )
        elif type == TJC_INFO_VERSION:
            self.__proto.send_special( "#server-version" )
    
    def get_name ( self ):
        return self.__name
    
    def get_server ( self ):
        return self.__server
    
    def get_port ( self ):
        return self.__port
    
    # --- INTERNAL FUNCTIONS ---
    
    def __handle_special ( self, code, string ):
        if code == "welcome_to_room":
            self.__infofn ( TJC_INFO_ROOMNAME, string )
        elif code == "new":
            self.__userstatfn ( TJC_USER_NEW, string )
        elif code == "hasleft":
            self.__userstatfn ( TJC_USER_GONE, string )
        elif code == "version":
            if '//' in string:
                ver, meta = string.split('//', 1)
            else:
                ver,meta = string,''
            self.__server_caps = meta.split()
            self.__infofn ( TJC_INFO_VERSION, str(ver) )
            # protocols ?
            if 'EXT:protocol' in self.__server_caps:
                self.__proto.send_special( ".protocol-list" )
                print "protocol list requested"
            else:
                print "protocols extension not supported. (%s)" % self.__server_caps
        elif code == "protocol-list":
            if 'chat+pr0' in string: #something we like and understand.
                self.__proto.send_special( ".protocol-set", "chat+pr0" )
                print "switching to chat+pr0"
            else:
                print string
        elif code == "protocol-ok":
            if string == "chat+pr0":
                self.__proto = ChatPlusProtocol0(self)
                print "switched to chat+pr0"
        elif code == "names":
            if not isinstance(string,list): string = [string]
            self.__infofn ( TJC_INFO_CLIENTS, string )
        elif code == "close":
            # the server is telling me to commit suicide.
            self.disconnect()
            self.__shutdownfn()
        else:
            self.__printfn ( "SPECIAL [ " + code + " ]   " + string )
        
    def __buffer ( self, str ):
        self.__buf.insert ( 0, str )
        
    # --- CALLBACK FALLBACKS
    
    def __display_msg ( self, author, msg ):
        self.__printfn ( "<" + author + ">   " + msg )
        
    def __printuserstat ( self, type, data ):
        if type == TJC_USER_NEW:
            self.__printfn ( "NEW USER: " + data )
        elif type == TJC_USER_GONE:
            self.__printfn ( "USER HAS LEFT: " + data )
        else:
            self.__printfn ( "USER STATUS INFO CODE " + str (type) + ": " + str (data ) )
            
    def __printinfo ( self, type, data ):
        if type == TJC_INFO_ROOMNAME:
            self.__printfn ( "WELCOME TO ROOM " + data )
        elif type == TJC_INFO_CLIENTS:
            self.__printfn ( "CONNECTED CLIENTS:" + ", ".join ( data ) )
        elif type == TJC_INFO_VERSION:
            self.__printfn ( "SERVER VERSION IS: " + data )
        else:
            self.__printfn ( "INFORMATION MSG CODE " + str (type) + ": " + str (data ) )
            
    def __priv_arrives ( self, author, msg ):
        self.__printfn ( "PRIVATE FROM " + author + ": " + msg )
        
    def __priv_departs ( self, recpt, msg ):
        self.__printfn ( "PRIVATE TO " + recpt + ": " + msg )

class TjChatProtocol:
    def __init__(self, room):
        self.room = room

    def __buffer(self, str):
        self.room._ChatRoom__buffer(str.encode('utf-16-le'))

    def send_msg ( self, msg ):
        self.__buffer ( "$" + self.room._ChatRoom__name + ";" + msg + "$" )
    
    def send_private_msg ( self, to, msg ):
        self.__buffer ( "$" + self.room._ChatRoom__name + "~" + to + ";" + msg + "$")

    def send_special ( self, code, content = None ):
        if isinstance(content, basestring): code = code+';'+content
        if isinstance(content, list): code = code+';'+'~'.join(content)
        print repr("$@" + self.room._ChatRoom__name + ";" + code + "$")
        self.__buffer ( "$@" + self.room._ChatRoom__name + ";" + code + "$" )

    def send_name ( self ):
        self.__buffer ( "$" + self.room._ChatRoom__name + "$" )

    def parse ( self, string ):
        #time.sleep(.2)
        s = string.decode( "utf-16-le" ).strip()
        if len ( s ) == 0:
            # probably EOF. let's commit suicide !
            self.room.disconnect()
            self.room._ChatRoom__shutdownfn()
            return
        if s[0] == '$' and s[-1] == '$' : #check if valid
            if re.match(r'^\$@.+;.+\$\$@.+;.+\$$', s):
                ss = s.split('$$', 1)
                self.parse(s[0]+'$')
                self.parse('$'+s[1])
                return
            p1, p2 = s[1:][:-1].split(';', 1)
            if p1[0] == "~":
                self.room._ChatRoom__privarrivfn ( p1[1:], p2 )
                #self.__printfn ( "PRIV [" + p1 [1:]  + "]   " + p2 )
            elif p1[0] == "@":
                if '~' in p2:
                    p2 = p2.split('~')
                self.room._ChatRoom__handle_special (p1[1:], p2)
            else:
                self.room._ChatRoom__msgfn ( p1, p2 )

class ChatPlusProtocol0:
    def __init__(self, room):
        self.room = room
        self.old = ''

    def __buffer(self, str):
        self.room._ChatRoom__buffer(str.encode('utf-8'))

    def send_msg ( self, msg ):
        self.__buffer ( "MESSAGE;%s\x1e" % msg )
    
    def send_private_msg ( self, to, msg ):
        self.__buffer ( "PRIVATE;%s;%s\x1e" % (to,msg) )

    def send_special ( self, code, content = None ):
        if isinstance(content, basestring): code = code+';'+content
        if isinstance(content, list): code = code+';'+'\x1f'.join(content)
        self.__buffer ( "SPECIAL;%s\x1e" % code )

    def send_name ( self ):
        pass #not implemented in protocol

    def parse ( self, string ):
        s = string.decode( "utf-8" )
        if len ( s ) == 0:
            # probably EOF. let's commit suicide !
            self.room.disconnect()
            self.room._ChatRoom__shutdownfn()
            return

        strs = s.split('\x1e')
        strs[0] = self.old + strs[0] # get possible left-over chars from last time
        self.old = strs.pop()        # the last is empty if there was a terminating RS, otherwise it's incomplete.

        for s in strs:
            prts = s.split(';')
            if prts[0] == 'MESSAGE':
                self.room._ChatRoom__msgfn ( prts[1], ';'.join(prts[2:]) )
            elif prts[0] == 'PRIVATE':
                self.room._ChatRoom__privarrivfn ( prts[1], ';'.join(prts[2:]) )
            elif prts[0] == 'SPECIAL':
                data = ';'.join(prts[2:])
                if '\x1f' in data: data = data.split('\x1f')
                self.room._ChatRoom__handle_special (prts[1], data)


if __name__ == "__main__":
    print "to start the GTK+ GUI, run gtkchat.py; for other UIs, wait."
    sys.exit(1)
