import asyncore, socket
import re
import random
from asyncbase import AsyncBase, MAJOR_VERSION, MINOR_VERSION

# Socket error "Resource temporarily unavailable": try again ;)
EAGAIN = 11

class ClientConnection(object, AsyncBase, asyncore.dispatcher):
    """
    Client connection implementing the asynchronous chat connection according to
    the provided chat protocol.
    """

    def __init__(self):
        AsyncBase.__init__(self)
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)

        self.verified = False
        self.authenticated = False
        self.authentification_sent = False

        self.retrieved_user_list = False
        self.request_user_list_sent = False

        # Generate a random user name
        self.user = 'Anonymous' + str(int(random.random()*10000))

    def connect(self, addr):
        """
        Connect to a chat server, specified by the address tuple (hostname,
        port).
        """
        self.host = addr[0]
        self.port = addr[1]
        super(ClientConnection, self).connect(addr)

    def retrieve_users(self):
        """
        Retreive list of users active at the chat server.
        """
        self.request_user_list_sent = True
        self.retrieved_user_list = False
        self.send_queue.put('NAMES')

    def username(self, name=''):
        """
        Get or set the user name of the client.
        """
        if not name:
            return self.user
        self.user = name
        self.authenticated = False
        self.authentification_sent = True
        self.send_queue.put('USER %s' % name)

    def handle_connect(self):
        """
        Event callback, which is called when a connection is established.
        """

        if 'connect' in self.event_list:
            self.event_list['connect'](self)

    def handle_error(self):
        """
        Event callback, which is triggered when an error occured. This callback
        function will transform the exception's traceback into a condensed
        string and will append that string to the debug log. Also the info bar
        is changed to notify the user of the error occured.
        """
        if hasattr(self, 'main'):
            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]
            info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo])
            self.debug_log('Exception raised in "%s" (%s line %s). Traceback:' \
                    % (function, file, line))
            self.debug_log(info)
            self.main.display_info(str(v))

    def handle_close(self):
        """
        Event callback, which will be called when the connection is about to
        be closed.
        """
        self.close()

    def handle_read(self):
        """
        Event callback, which is triggered when data is available to read.
        """
        buf = ''
        multi_line = False
        response_format = False

        # Receive a (multiline) message.
        while True:
            try:
                chunk = self.recv(1)
            except socket.error, e:
                # Handle "Resource temporarily unavailable" exceptions.
                if e.errno == EAGAIN:
                    import time
                    time.sleep(0.005)
                    continue
                else:
                    raise

            if not chunk:
                raise RuntimeError('socket connection broken')
            if chunk in ['-','+']:
                response_format = True
            elif chunk == ':':
                multi_line = True
            elif chunk == '\n' and buf[-1] == '\r':
                if not multi_line:
                    break
                if buf[-3:] == '\r\n\r':
                    # Remove trailing '\n\r' from multi line response
                    buf = buf[:-2]
                    break
            buf += chunk

        buf = buf[:-1]
        self.debug_log('< %s' % buf.replace('\n','\\n').replace('\r','\\r'))

        # Invoke the proper callback function.
        if not self.verified:
            self.verify_server(buf)
        elif response_format:
            self.parse_response(buf)
        else:
            self.parse_notification(buf)

    def verify_server(self, buf):
        """
        Helper function used to verify the server version according to the
        client major and minor version number. If the server version does not
        match, disconnect immediately.
        """
        match = re.match('^CHAT/(\d\.\d)/([^\x00-\x1F/:]+)$', buf)

        if not match \
                or match.group(1) != '%d.%d' % (MAJOR_VERSION, MINOR_VERSION):
            self.debug_log('Failed to verify connection')
            self.main.execute('disconnect')
            return

        self.debug_log('Server version is %s' % match.group(1))

        if 'verify' in self.event_list:
            self.event_list['verify'](self, buf, match.group(1))

        self.verified = True
        self.send_queue.put('CHAT')

    def parse_response(self, buf):
        """
        Helper function used to parse the server response. This way, message
        parsing is separated from the response logic.
        """
        if 'response' in self.event_list:
            self.event_list['response'](self, buf)

        # First authenticate the user.
        if not self.authenticated:
            if not self.authentification_sent:
                self.send_queue.put('USER %s' % self.user)
                self.authentification_sent = True
            elif buf[0] == '+':
                # TODO: handle 'username is taken'.
                self.authenticated = True
                if 'authenticated' in self.event_list:
                    self.event_list['authenticated'](self, self.user)

        if not self.authenticated:
            return

        # After authentification, fetch the list of users currently logged in.
        if not self.retrieved_user_list:
            if not self.request_user_list_sent:
                self.request_user_list_sent = True
                self.retrieve_users()
            else:
                self.retrieved_user_list = True
                msg = 'Users: [%s]' % '] ['.join(buf.split('\r\n')[1:])
                if 'info' in self.event_list:
                    self.event_list['info'](self, msg)
                else:
                    self.debug_log(msg)

    def parse_notification(self, buf):
        """
        Helper function used to parse the server notifications. This way,
        message parsing is separated from the notification logic.
        """
        match = re.match('^SAY ([^\x00-\x1F:/]+)/([^\x00-\x1F]+)', buf)
        if match:
            if 'message' in self.event_list:
                self.event_list['message'](self, match.group(1), match.group(2))
        elif 'notify' in self.event_list:
            self.event_list['notify'](self, buf)

if __name__ == '__main__':
    client = ClientConnection()
    client.connect(('localhost', 16897))
    client.init_loop()
