"""
The server side.
Bagrut rulez!

NOTES:
1) We do not handle all kind of unlikely situations,
as this is for learning purposes only.
For example, if the chat has User1 and User2, and User1 writes "hel" while
User2 sends "hi there", when User1 finishes writing "hello" it will seem bad.

2) The code can be much better coded if we use methods we have not yet learned, such as:
    a) Classes.
    b) Threads.
    
    Keep that in mind!

3) There is much more to implement - like all the other commands. This code shows
you all the basic stuff needed to finish the rest.

Hope you find it useful!

Omerr.
August 2012.
"""

# Imports
import select
import socket
import sys
import cPickle
import struct
import time

marshall = cPickle.dumps
unmarshall = cPickle.loads

# Constants
PORT =                      3344
MAX_LISTEN_CONNECTIONS =    5
SEND_MESSAGE_COMMAND =      1   # The command that represents sending a message
COMMAND_SIZE =            struct.calcsize("L")


def receive_client_message(client_socket):
    '''Receives the client message.
    Receives: client_socket - the socket with the client.
    Returns the message.'''
    message = read_size_and_argument(client_socket)
    return message

def receive_command_from_client(client_socket):
    '''Receives a command from the client.
    Receives: client_socket - the socket with the client.
    Returns a tuple of (user_name, command)'''
    user_name = read_size_and_argument(client_socket)
    command = client_socket.recv(COMMAND_SIZE)
    command = socket.ntohl(struct.unpack("L", command)[0])
    return (user_name, command)
    


def read_size_and_argument(client_socket):
    '''Helper function used to read the size and argument from a client.
    Receives: client_socket - the socket with the client.
    Returns: only the argument, as a string.'''
    # Get the size of the data from the server
    size = struct.calcsize("L")
    size = client_socket.recv(size)

    if len(size) == 0:
        # The connection has been terminated by the user
        raise socket.error

    try:
        # Try to parse the size of the given message.
        size = socket.ntohl(struct.unpack("L", size)[0])
    except struct.error, e:
        # In case of an error, return an empty string.
        return ''
    
    buf = ""

    # Attempt to receive the whole message!
    while len(buf) < size:
        buf += client_socket.recv(size - len(buf))

    return buf

def server_send(active_socket, *arguments):
    '''Provides send functionality.
    Receives:   active_socket - the socket on which the parameters should be sent.
                *arguments - list of arguments to send.
    Returns nothing.'''
    # First - put all the arguments in a buffer
    buf = marshall(arguments)
    # Get the size to send it first
    value = socket.htonl(len(buf))
    # Pack it
    size = struct.pack("L",value)
    active_socket.send(size)
    active_socket.send(buf)
    

def get_current_time():
    '''Retrieves the current time in a known format.
    Receives:   nothing.
    Returnes:   the current time as a string, format of <hour>:<minute>'''
    return time.strftime('%H:%M')

def send_waiting_messages(wlist):
    '''Sends waiting messages to all clients that the message has not yet been
    sent to, and that have a writable socket.
    Receives:   wlist - a list of all currently writable sockets.
    NOTE:       Based on global messages_to_send -  a list of messages that
                needed to be sent, as a tuple of: (message, [sockets])
    Returns nothing.'''
    global messages_to_send     # Declare global list
    # Iterate through all messages to send
    for message in messages_to_send:
        (message_text, clients) = message
        # For every client that still needs to receive this message
        for client in clients:
            # If the socket for the client is writable
            if client in wlist:
                server_send(client, message_text)
                # Now that we sent it, we can clear it off the list
                clients.remove(client)



# AF_INET means IPv4.
# SOCK_STREAM means a TCP connection.
# SOCK_DGRAM would mean an UDP "connection".
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# The parameter is (host, port).
# The host, when empty or when 0.0.0.0, means to accept connections for
# all IP addresses of current machine. Otherwise, the socket will bind
# itself only to one IP.
# The port must greater than 1023 if you plan running this script as a
# normal user. Ports below 1024 require root privileges.
server.bind(('',PORT))

print 'Listening!'
# The parameter defines how many new connections can wait in queue.
# Note that this is NOT the number of open connections (which has no limit).
# Read listen(2) man page for more information.
server.listen(MAX_LISTEN_CONNECTIONS)

# Output socket list
# List of socket objects that are currently open
open_sockets = []
messages_to_send = []

while True:
    rlist, wlist, xlist = select.select( [server] + open_sockets, open_sockets, [] )
    for current_socket in rlist:
        if current_socket == server:
            # A new connection has been received!
            # Handle the socket
            (client, address) = server.accept()
            print 'New connection from %s' % (address,)
            
            open_sockets.append(client)

        else:
            # Data has been received from the client, process it
            try:
                (user_name, client_command) = receive_command_from_client(current_socket)
                if client_command == SEND_MESSAGE_COMMAND:
                    # Receive message from the client
                    client_message = receive_client_message(current_socket)
                    # Format it as <time> <user_name>: <message>
                    client_message = '%s %s: %s' % (get_current_time(), user_name, client_message)

                    # Send the message to other clients
                    clients_to_send = []
                    for sock in open_sockets:
                        if sock == current_socket:
                            continue
                        clients_to_send.append(sock)
                    messages_to_send.append((client_message, clients_to_send))

                # TODO: Add handling for more commands here!
                else:
                    # Unknown command received
                    current_socket.close()
                    open_sockets.remove(current_socket)
                        
            except socket.error, e:
                # Remove
                print 'User disconnected'
                open_sockets.remove(current_socket)
    try:
        send_waiting_messages(wlist)
    except socket.error, e:
        # Remove
        print 'User disconnected'
        open_sockets.remove(current_socket)

# Finish running!
server.close()
