#!/usr/bin/env python

import asyncore
import logging
import logging.config
import re
import socket
import sys

from asyncbase import AsyncBase, MAJOR_VERSION, MINOR_VERSION

# Greeting message sent to a connected client.
GREETING_MSG = 'Hi there!'

class SocketError(RuntimeError):
    pass

class ClientData(object):
    pass

class Server(asyncore.dispatcher):
    """
    Basic server which will listen on an host address and port. The given
    handler is used to handle the accepted incoming connections.
    """

    def __init__(self, ip, port, handler):
        asyncore.dispatcher.__init__(self)

        self.ip = ip
        self.port = port
        self.handler = handler

        # Dictonary which maps ip/port tuple to client data object.
        self.clients = {}

        logging.config.fileConfig('logging.conf')
        self.log = logging.getLogger('Server')

        # Listen on given ip-address and port
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((ip, port))
        self.log.info('waiting for incoming connections on port %s.' % port)
        self.listen(5)

    def handle_accept(self):
        """
        Handle the incoming connection's request.
        """
        # TODO: use threads to handle the incoming connections.
        try:
            conn, addr = self.accept()
            self.log.info('accepted client %s:%d' % addr)
            self.connect_client(conn, addr)
        except socket.error:
            self.log.warning('warning: server accept() threw an exception.')
            return
        except TypeError:
            self.log.warning('warning: server accept() threw EWOULDBLOCK.')
            return

    def connect_client(self, conn, addr):
        """
        Initialise a client connection after the server accepted an incoming
        connection. This will set the connection handler.
        """
        client = ClientData()
        # creates an instance of the handler class to handle the
        # request/response on the incoming connection.
        client.handler = self.handler(conn, addr, client, self)
        client.username = ''
        self.clients['%s:%d' % addr] = client
        return client

    def disconnect_client(self, addr):
        """
        Client leaves the chat server. This function is called when the socket
        is broken or the client closes the connection gracefully.
        """
        # Suppress error if a client is already disconnected.
        try:
            client = self.clients['%s:%d' % addr]
            self.send_all('LEAVE %s' % client.username, True)
            del self.clients['%s:%d' % addr]
        except KeyError:
            pass

    def change_username(self, addr, username):
        """
        Update the username of the client.
        """
        self.clients['%s:%d' % addr].username = username

    def send_all(self, msg, sender=None):
        """
        Send a message or notification from a client to all connected clients
        (optionally including to the sender).
        """
        for c in self.clients:
            if self.clients[c].handler != sender:
                self.clients[c].handler.send_raw(msg)

class RequestHandler(AsyncBase, asyncore.dispatcher):
    def __init__(self, conn, address, client, server):
        AsyncBase.__init__(self)
        asyncore.dispatcher.__init__(self, conn)

        self.address = address
        self.client = client
        self.server = server
        self.username = ''

        self.log = self.server.log
        self.send_welcome_message()

    def send_welcome_message(self):
        """
        Welcome our new client by sending a welcome message. This message
        contains the server version number and a greetings message.
        """
        self.send_raw("CHAT/%d.%d/%s" \
                % (MAJOR_VERSION, MINOR_VERSION, GREETING_MSG))

    def handle_read(self):
        """
        Receive a message from the client. If the connection is somehow broken,
        disconnect the client and clean the corresponding data.
        """
        buf = ''
        try:
            while True:
                chunk = self.recv(1)
                if not chunk:
                    raise SocketError('socket connection broken')
                elif chunk == '\n' and buf[-1] == '\r':
                    break
                buf += chunk
        except SocketError, socket.error:
            self.log.info('client %s:%d disconnected or socket is broken.' \
                    % self.address)
            self.server.disconnect_client(self.address)
            return

        # Received a message, so it's time to parse the message.
        buf = buf[:-1]
        self.debug_log('< %s' % buf)
        self.parse_response(buf)

    def handle_error(self):
        import sys
        t, v, tb = sys.exc_info()
        tbinfo = []
        while tb:
            tbinfo.append((
                tb.tb_frame.f_code.co_filename,
                tb.tb_frame.f_code.co_name,
                str(tb.tb_lineno)
            ))
            tb = tb.tb_next
        file, function, line = tbinfo[-1]

        self.log.error('%s raised "%s" in %s:%s' \
                % (str(v), function, file, line))
        self.log.error(' '.join(['[%s|%s|%s]' % x for x in tbinfo]))
        self.server.disconnect_client(self.address)

    def parse_response(self, buf):
        """
        >>> class DummyServer(object):
        ...     def __init__(self): self.log = None; self.clients = {}
        ...     def change_username(self, addr, username): pass
        ...     def send_all(self, msg, sender): pass
        >>> req = RequestHandler(None, None, None, DummyServer())
        >>> assert req.parse_response('CHAT')
        >>> assert req.parse_response('USER foo')
        >>> # Some error handling
        >>> assert not req.parse_response('CHAT ')    # Client must send CHAT
        >>> assert not req.parse_response('USER')     # No username given.
        >>> assert not req.parse_response('USER j/k') # username has a /
        """
        cmd = buf.split(' ')[0]

        if buf == 'CHAT':
            # Client wants to chat.
            return self.send_positive('Ok')
        if cmd == 'USER':
            # User changes/sets its username. Make sure the username is not
            # already taken. If the name is valid, notify the other clients.
            name = buf[5:]
            for c in self.server.clients:
                if self.server.clients[c].username == name:
                    return self.send_negative('Username already taken.')
            if re.match(ur'^[^\u0000-\u001f\u007f-\u009f/:]+$', name):
                if not self.username:
                    self.send_all('JOIN %s' % name, True)
                else:
                    self.send_all('RENAME %s/%s' \
                            % (self.username, name), True)
                self.set_username(name)
                return self.send_positive('Ok')
            return self.send_negative('Invalid username.')

        # TODO: user can't send a message if he didn't send his username first.
        if cmd == 'SAY':
            return self.send_all('SAY %s/%s' % (self.username, buf[4:]))
        if cmd == 'NAMES':
            clients = self.server.clients
            self.send_raw('+Ok:\r\n' \
                    + ''.join([clients[c].username + '\r\n' for c in clients]))
            return True
        return self.send_negative('Unsupported command.')

    def send_positive(self, msg):
        self.send_raw('+%s' % msg)
        return True

    def send_negative(self, msg):
        self.send_raw('-%s' % msg)
        return False

    def send_all(self, msg, except_sender=False):
        self.server.send_all(msg, except_sender and self)
        return True

    def set_username(self, username):
        self.username = username
        self.server.change_username(self.address, username)

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print 'usage: %s HOSTNAME PORT' % sys.argv[0]
        sys.exit(-1)

    # launch the server on the specified hostname and port.
    host = sys.argv[1]
    port = int(sys.argv[2])
    server = Server(host, port, RequestHandler)
    try:
        asyncore.loop(timeout=.1, use_poll=True)
    except KeyboardInterrupt:
        print 'Shutting down the server.'

