# synarere -- a trivial Python IRC bot.
# Copyright (C) 2010 Michael Rodriguez.
# Rights to this code are documented in docs/LICENSE.

"""Provide IRC connections, functions, etc."""

# Import required Python modules.
import asyncore, traceback, os, re, socket, time

# Import required source module.
import vars, instance, commands

# A regular expression to match and disect IRC protocol messages.
# This is actually around 60% faster than not using RE.
pattern = r"""
           ^              # beginning of string
           (?:            # non-capturing group
               \:         # if we have a ':' then we have an origin
               ([^\s]+)   # get the origin without the ':'
               \s         # space after the origin
           )?             # close non-capturing group
           (\w+)          # must have a command
           \s             # and a space after it
           (?:            # non-capturing group
               ([^\s\:]+) # a target for the command
               \s         # and a space after it
           )?             # close non-capturing group
           (?:            # non-capturing group
               \:?        # if we have a ':' then we have freeform text
               (.*)       # get the rest as one string without the ':'
           )?             # close non-capturing group
           $              # end of string
           """

# Note that this doesn't match *every* IRC message,
# just the ones we care about. It also doesn't match
# every IRC message in the way we want. We get what
# we need. The rest is ignored.
#
# Here's a compact version if you need it:
#     ^(?:\:([^\s]+)\s)?(\w+)\s(?:([^\s\:]+)\s)?(?:\:?(.*))?$
pattern = re.compile(pattern, re.VERBOSE)

class IRCConnection(asyncore.dispatcher):
    """Provide an event-based IRC connection."""

    def __init__(self, server):
        asyncore.dispatcher.__init__(self)

        self.server = server
        self.holdline = None
        self.last_recv = time.time()
        self.pinged = False

        self.sendq = []
        self.recvq = []

        # Add us to the connections list.
        vars.conns.append(self)

    # We want to write if there's something in our sendq.
    def writable(self):
        return len(self.sendq) > 0

    def handle_read(self):
        data = self.recv(8192)

        self.last_recv = time.time()

        # This means the connection was closed.
        # handle_close() takes care of all of this.
        if not data:
            return

        datalines = data.split('\r\n')

        # Get rid of the empty element at the end.
        if not datalines[-1]:
            datalines.pop()

        # Check to see if we got part of a line previously.
        # If we did, prepend it to the first line this time.
        if self.holdline:
            datalines[0] = self.holdline + datalines[0]
            self.holdline = None

        # Check to make sure we got a full line at the end.
        if not data.endswith('\r\n'):
            self.holdline = datalines[-1]
            datalines.pop()

        # Add this jazz to the recvq.
        self.recvq.extend([line for line in datalines])

        # Send it off to the parser.
        self.parse()

    def handle_write(self):
        """Write the first line in the sendq to the socket."""

        # Grab the first line from the sendq.
        line = self.sendq[-1] + '\r\n'

        # Try to send it.
        num_sent = self.send(line)

        # If it didn't all send we have to work this out.
        if num_sent == len(line):
            instance.logger.log('%s: %s <- %s' % (self.server['id'], self.server['name'], self.sendq.pop()))
        else:
            instance.logger.log('%s: Incomplete write (%d byte%s written instead of %d)' % (self.server['name'], num_sent, 's' if num_sent is not 1 else '', len(line)))
            self.sendq[-1] = self.sendq[-1][num_sent:]

    def handle_connect(self):
        """Log into the IRC server."""

        instance.logger.log('%s: Connection to %s:%d established.' % (self.server['id'], self.server['name'], int(self.server['port'])))

        self.server['connected'] = True

        self.sendq.append('NICK %s' % self.server['nick'])
        self.sendq.append('USER %s 2 3 :%s' % (self.server['ident'], self.server['gecos']))

    def handle_close(self):
        asyncore.dispatcher.close(self)

        instance.logger.log('%s: Connection to %s:%d lost.' % (self.server['id'], self.server['name'], int(self.server['port'])))

        self.server['connected'] = False

        # Remove us from the connections list.
        try:
            vars.conns.remove(self)
        except ValueError:
            instance.logger.error("%s: Couldn't find myself in the connectons list!" % self.server['name'])

    # I absolutely despise `compact_traceback()`.
    def handle_error(self):
        """Record the traceback to var/trace.txt and exit."""

        instance.logger.log('Crash. Writing traceback to %s' % instance.conf.get('options', 'tbfile')[0])

        try:
            tracefile = open(instance.conf.get('options', 'tbfile')[0], 'w')
            traceback.print_exc(file=tracefile)
            tracefile.close()

            # Print one to the screen if we're not forked.
            if not vars.fork:
                traceback.print_exc()
        except:
            raise

        instance.exit('asyncore failure', os.EX_SOFTWARE)

    def parse(self):
        """Parse IRC protocol and call methods based on the results."""

        global pattern

        # Go through every line in the recvq.
        # Ok, so I have no idea how to do this "the right way"
        # with iterators because of the nature of a Queue.
        while len(self.recvq):
            line = self.recvq.pop()

            instance.logger.log('%s: %s -> %s' % (self.server['id'], self.server['name'], line))

            parv = []

            # Split this crap up with the help of RE.
            try:
                origin, command, target, message = pattern.match(line).groups()
            except AttributeError:
                continue

            # Make an IRC parameter argument vector.
            if target:
                parv.append(target)

            parv.append(message)

            if command == 'PING':
                self.sendq.append('PONG :%s' % parv[0])

            if command == '376':
                for i in self.server['chans']:
                    self.sendq.append('JOIN %s' % i)

            if command == 'PRIVMSG':
                n, u, h = dissect_origin(origin)

                # Check to see if it's a channel.
                if parv[0].startswith('#') or parv[0].startswith('&'):
                    # Do the `chan_cmd` related stuff.
                    command = parv[1].split()

                    if not command:
                        return

                    # Chop the command off, as we don't want that.
                    message = command[1:]
                    message = ' '.join(message)
                    command = command[0].upper()

                    # Have we been addressed?
                    # If so, do the `chanme_cmd` related stuff.
                    if parv[1].startswith(self.server['nick']):
                        message = message.split()
                        command = message[0].upper() 
                        del message[0]
                        message = ' '.join(message)

                        # Call the handlers.
                        try:
                            commands.chanme[command]
                        except KeyError:
                            return

                        commands.dispatch(commands.chanme, command, self, (n, u, h), parv[0], message)

                    else:
                         # Call the handlers.
                         try:
                             commands.chan[command]
                         except KeyError:
                             return

                         commands.dispatch(commands.chan, command, self, (n, u, h), parv[0], message)

                else:
                     # CTCP?
                     if parv[1].startswith('\1'):
                         parv[1] = parv[1].strip('\1')
                         command = parv[1].split()

                         if not command:
                             return

                         message = command[1:]
                         message = ' '.join(message)
                         command = command[0].upper()

                         # Call the handlers.
                         try:
                             commands.ctcp[command]
                         except KeyError:
                             return

                         commands.dispatch(commands.ctcp, command, self, (n, u, h), message)

                     else:
                          command = parv[1].split()

                          if not command:
                              return

                          message = command[1:]
                          message = ' '.join(message)
                          command = command[0].upper()

                          # Call the handlers.
                          try:
                             commands.priv[command]
                          except KeyError:
                              try:
                                  command = command[1:]
                                  commands.priv[command]
                              except KeyError:
                                 return

                              commands.dispatch(commands.priv, command, self, (n, u, h), message)

def connect(server):
    """Connect to an IRC server."""

    instance.logger.log('Connecting to %s (%s:%d)' % (server['id'], server['name'], int(server['port'])))

    conn = IRCConnection(server)

    # This step is low-level to permit IPv6.
    af, type, proto, canon, sa = socket.getaddrinfo(server['name'], int(server['port']), 0, 1)[0]
    conn.create_socket(af, type)

    # Now connect to the IRC server.
    conn.connect(sa)

def new_server():
    """Create a server."""

    server = { 'id'        : None,
               'name'      : None,
               'port'      : 0,
               'nick'      : None,
               'ident'     : None,
               'gecos'     : None,
               'chans'     : [],
               'connected' : False }

    return server

def connect_to_all():
    """Connect to all servers in the configuration."""

    for i in instance.conf.get('network'):
        serv = new_server()

        serv['id'] = i.get('id')
        serv['name'] = i.get('address')
        serv['port'] = i.get('port')
        serv['nick'] = i.get('nick')
        serv['ident'] = i.get('ident')
        serv['gecos'] = i.get('gecos')
        serv['chans'].append(i.get('chans'))

        try:
            connect(serv)
        except socket.error, e:
            instance.logger.log('%s: Unable to connect to %s:%d - (%s)' % (serv['id'], serv['name'], int(serv['port']), os.strerror(e.args[0])))

def dissect_origin(origin):
    """Split nick!user@host into nick, user, host."""

    n, uh = origin.split('!')
    u, h = uh.split('@')

    return n, u, h

def quit_all(reason):
    """Quit all IRC networks."""

    instance.logger.log('Quitting all IRC networks.')

    for i in vars.conns:
        i.sendq.append('QUIT :%s' % reason)
