##
## clirc -- An IRC client in Python.
## ircnet.py - IRC network object.
##
## Copyleft 1997-2000 Teemu Kalvas <chery@s2.org>
##

import re
import string
import time

import config
import crypt
import dns
from event import event
import reloadable
import rnd
import runloop

import main

rx_ping = re.compile('PING')
rx_error = re.compile('ERROR')
rx_line = re.compile(':([^ !]*)(!([^ ]*))? ([a-zA-Z0-9]*) (.*)')
rx_ctcp = re.compile('\001([^ ]+)( (.*))?\001')

low_table = string.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]',
			     'abcdefghijklmnopqrstuvwxyz{|}')

def low(str):
    return string.translate(str, low_table)

def low_equal(a, b):
    return low(a) == low(b)

def is_channel(name):
    return len(name) and name[0] in '#&+!'

def key_xform(key):
    if type(key) == type(''):
	return low(key)
    elif type(key) == type(()):
	return tuple(map(key_xform, key))
    else:
	return key

class Hashtable(reloadable.Reloadable):
    def __init__(self):
        self.hash = {}

    def __len__(self):
        return len(self.hash)

    def __getitem__(self, key):
        return self.hash[key_xform(key)][1]

    def __setitem__(self, key, value):
        self.hash[key_xform(key)] = key, value

    def __delitem__(self, key):
        del self.hash[key_xform(key)]

    def get(self, key, fallback = None):
        if self.has_key(key):
            return self[key]
        else:
            return fallback

    def has_key(self, key):
        return self.hash.has_key(key_xform(key))

    def keys(self):
        return map(lambda (key, value): key, self.hash.values())

    def values(self):
        return map(lambda (key, value): value, self.hash.values())

    def items(self):
        return self.hash.values()

reloadable.register_for_reload(Hashtable)

## The channel module has to be imported after the Hashtable class has
## been defined because channel.Channel subclasses from ircserv.Hashtable.
import channel

ignore_commands = {'002': (), '003': (), '004': (), '219': (), '318': (),
                   '366': (), '368': (), '376': ()}

class AutoopTimer(reloadable.Reloadable, runloop.SingleTimer):
    def __init__(self, person, chan, serv):
	foo = (rnd.rnd() % 17000) / 1000.0 + 5.0
	runloop.SingleTimer.__init__(self, foo)
	self.person = person
	self.chan = chan
	self.serv = serv

    def run(self):
	channels = self.serv.channels
	if channels.has_key(self.chan):
	    chan = channels[self.chan]
	    if chan.has_key(self.person) and '@' not in chan[self.person].mode \
	       and '@' in chan[self.serv.people[self.serv.nick]].mode:
		self.serv.out_line('MODE ' + self.chan + ' +o '
				   + self.person.nick)

reloadable.register_for_reload(AutoopTimer)

class DNSAnswer:
    def __init__(self, serv):
	self.serv = serv

    def answer(self, host):
	self.serv.init_connect(host)

    def error(self, err):
	event.error(error = err)

class IRCServ(reloadable.Reloadable, runloop.DataSocket):
    def __init__(self, addr, port, net, nicks):
        runloop.DataSocket.__init__(self)
        self.addr = addr
        self.port = port
        self.net = net
        self.nicks = nicks
	self.channels = Hashtable()
	self.people = Hashtable()
	dns.gethostbyname(addr, DNSAnswer(self))
	self.post_reload()

    def post_reload(self):
	self.ensure_defaults([('in_data', ''),
			      ('ping_time', 0.0),
			      ('quitting', 0),
			      ('active', 0),
			      ('initializing', 1),
                              ('awaymsgs', {})])

    def init_connect(self, host):
	self.addr_numeric = host
	self.connect((host, self.port))

    def connected(self, sock):
        self.read()
	self.active = 1
        self.out_line('USER ' + config.login + ' * * :' + config.realname)
	init_nick = self.nicks[0][:6] + repr(rnd.rnd() % 1000)
        self.out_line('NICK ' + init_nick)

    def received(self, data):
        self.in_data = self.in_data + data
        while 1:
            i = string.find(self.in_data, '\r\n')
            if i == -1:
                break
	    line = self.in_data[:i]
	    self.in_data = self.in_data[i + 2:]
            self.process_line(line)

    def disconnected_reason(self, reason):
	self.active = 0
        if self.quitting:
            self.terminate()
	    self.net.delete_buffers()
        else:
            if runloop.reading.has_key(self.sock):
                del runloop.reading[self.sock]
            if runloop.writing.has_key(self.sock):
                del runloop.writing[self.sock]
            event.disconnected(net = self.net, reason = reason)

    def connection_refused(self):
	self.disconnected_reason('connection refused')

    def connection_reset(self):
	self.disconnected_reason('connection reset by peer')

    def disconnected(self):
        self.disconnected_reason('by remote host')

    def broken_pipe(self):
        self.disconnected_reason('broken pipe')

    def process_line(self, line):
        if rx_ping.match(line):
            self.out_line('PONG' + line[4:])
        elif rx_error.match(line):
            self.disconnected_reason(line[6:])
        else:
            match = rx_line.match(line)
            if match:
                nick, addr, command, tail = match.group(1, 3, 4, 5)
                command = string.lower(command)
                args = []
		while 1:
		    if len(tail) == 0:
			break
		    if tail[0] == ':':
			args.append(tail[1:])
			break
		    i = string.find(tail, ' ')
		    if i == -1:
			args.append(tail)
			break
		    args.append(tail[:i])
		    tail = tail[i + 1:]
                if not ignore_commands.has_key(command):
                    handler = 'handle_' + command
                    if hasattr(self, handler):
                        getattr(self, handler)(nick, addr, args)
		    elif config.verbose:
			event.unknown(command = command, nick = nick,
				      addr = addr, args = args)

    def out_line(self, line):
        self.write(line + '\r\n')

    def terminate(self):
        if runloop.reading.has_key(self.sock):
            del runloop.reading[self.sock]
        if runloop.writing.has_key(self.sock):
            del runloop.writing[self.sock]
        self.sock.close()

    def set_nick(self):
	self.out_line('NICK ' + self.nicks[0])

    def handle_001(self, nick, addr, args):
	self.name = nick
	self.nick = args[0]
	self.people[args[0]] = channel.Person(args[0])
        self.net.connected(self)

    def handle_215(self, nick, addr, args):
        event.status(net = self.net, message = nick + ' stats: '
                     + reduce(lambda x, y: x + ' ' + y, args[1:]))

    def handle_218(self, nick, addr, args):
        event.status(net = self.net, message = nick + ' stats: '
                     + reduce(lambda x, y: x + ' ' + y, args[1:]))

    def handle_251(self, nick, addr, args):
        event.status(net = self.net, message = args[1])

    def handle_252(self, nick, addr, args):
        event.status(net = self.net, message = args[1] + ' ' + args[2])

    def handle_253(self, nick, addr, args):
        event.status(net = self.net, message = args[1] + ' ' + args[2])

    def handle_254(self, nick, addr, args):
        event.status(net = self.net, message = args[1] + ' ' + args[2])

    def handle_255(self, nick, addr, args):
        event.status(net = self.net, message = args[1])

    def handle_301(self, nick, addr, args):
        ## Handling of away message display is maximally complicated, as we
        ## want to properly handle the people whose nick changes we
        ## get and somehow handle the others.
        display = 0
        person = self.people.get(args[1])
        if person:
            if person.awaymsg <> args[2]:
                display = 1
                person.awaymsg = args[2]
        else:
            if self.awaymsgs.get(args[1]) <> args[2]:
                display = 1
                self.awaymsgs[args[1]] = args[2]
        if display:
            event.status(net = self.net, message = args[1] + ' is away: '
                         + args[2])

    def handle_311(self, nick, addr, args):
        event.status(net = self.net, message = args[1] + ' (' + args[2] + '@'
                     + args[3] + ') is ' + args[5])

    def handle_312(self, nick, addr, args):
        event.status(net = self.net, message = args[1] + ' is on irc via '
                     + args[2] + ' (' + args[3] + ')')

    def handle_313(self, nick, addr, args):
        event.status(net = self.net, message = args[1] + ' ' + args[2])

    def handle_317(self, nick, addr, args):
        event.status(net = self.net, message = args[1] + ' has been ' + args[2]
                     + ' ' + args[3])

    def handle_319(self, nick, addr, args):
        event.status(net = self.net,
                     message = args[1] + ' is on channels: ' + args[2])

    def handle_332(self, nick, addr, args):
        event.status_topic(net = self.net, chan = args[1], topic = args[2])

    def handle_353(self, nick, addr, args):
        if self.channels.has_key(args[2]):
            for n in string.split(args[3]):
                if n[0] == '@' or n[0] == '+':
		    mode = n[0]
                    nn = n[1:]
                else:
		    mode = ''
                    nn = n
		if not self.people.has_key(nn):
		    self.people[nn] = channel.Person(nn)
		self.people[nn].common[args[2]] = ()
		if not self.channels[args[2]].has_key(self.people[nn]):
		    self.channels[args[2]][
			self.people[nn]] = channel.Member(mode)
        event.status_names(net = self.net, chan = args[2], names = args[3])

    def handle_367(self, nick, addr, args):
        event.status(net = self.net, message = 'ban: ' + args[2])

    def handle_372(self, nick, addr, args):
        event.motd(net = self.net, message = args[1])

    def handle_375(self, nick, addr, args):
        event.motd(net = self.net, message = args[1])

    def handle_433(self, nick, addr, args):
        self.nicks = self.nicks[1:] + self.nicks[:1]
        self.out_line('NICK ' + self.nicks[0])

    def handle_437(self, nick, addr, args):
	event.status(net = self.net, message = args[1]
		     + ' temporarily unavailable')
        if not is_channel(args[1]):
            self.nicks = self.nicks[1:] + self.nicks[:1]
            self.out_line('NICK ' + self.nicks[0])

    def handle_475(self, nick, addr, args):
	# Cannot join channel (+k)
	event.status(net = self.net, message = args[2] + ' ' + args[1])

    def handle_invite(self, nick, addr, args):
	event.invite(net = self.net, nick = nick, addr = addr, chan = args[1])

    def handle_join(self, nick, addr, args):
	i = string.find(args[0], '\007')
	mode = ''
        modestr = ''
        chan = args[0]
	if i <> -1:
            modestr = chan[i + 1:]
	    chan = chan[:i]
	    if 'o' in modestr:
		mode = mode + '@'
	    if 'v' in modestr:
		mode = mode + '+'
        if low_equal(nick, self.nick):
            event.own_join(net = self.net, chan = chan)
	    self.channels[chan] = channel.Channel()
        event.join(net = self.net, nick = nick, addr = addr, chan = chan,
                   modestr = modestr)
	if not self.people.has_key(nick):
	    self.people[nick] = channel.Person(nick, addr)
	self.people[nick].common[chan] = ()
	self.channels[chan][self.people[nick]] = channel.Member(mode)
	if config.autoop(addr, chan):
	    AutoopTimer(self.people[nick], chan, self)

    def handle_kick(self, nick, addr, args):
        event.kick(net = self.net, nick = nick, addr = addr, chan = args[0],
                   victim = args[1], reason = args[2])
	person = self.people[args[1]]
	del self.channels[args[0]][person]
	del person.common[args[0]]
	if not person.common:
	    del self.people[args[1]]
        if low_equal(args[1], self.nick):
	    self.own_leave(args[0])

    def own_leave(self, chan):
	for person in self.channels[chan].keys():
	    del person.common[chan]
	    if not person.common:
		del self.people[person.nick]
	del self.channels[chan]

    def handle_mode(self, nick, addr, args):
	if self.channels.has_key(args[0]):
            event.mode(net = self.net, nick = nick, addr = addr,
                       chan = args[0],
                       modes = reduce(lambda x, y: x + ' ' + y, args[1:]))
	    chs, as = args[1], args[2:]
	    for ch in chs:
		if ch == '+':
		    sign = 1
		elif ch == '-':
		    sign = 0
		## someone ought to explain to me the +o with no argument modes
		## from servers after splits
		elif ch in 'ov' and len(as) > 0:
		    dict = self.channels[args[0]]
                    if sign:
                        if ch == 'o':
                            dict[self.people[as[0]]].set_mode('@')
                        elif ch == 'v':
                            dict[self.people[as[0]]].set_mode('+')
                    elif ch == 'o':
                        dict[self.people[as[0]]].unset_mode('@')
                    else:
                        dict[self.people[as[0]]].unset_mode('+')
                    del as[0]
		elif ch in 'bk' or (ch == 'l' and sign):
		    del as[0]
	else:
            event.own_mode(net = self.net, nick = nick,
                           modes = reduce(lambda x, y: x + ' ' + y, args))

    def handle_nick(self, nick, addr, args):
        event.nick(net = self.net, nick = nick, addr = addr, nnick = args[0])
	person = self.people[nick]
	del self.people[nick]
	person.nick = args[0]
	self.people[args[0]] = person
        if low_equal(nick, self.nick):
            event.own_nick(net = self.net, nick = args[0])
            self.nick = args[0]
	    self.net.status_change()
	    if self.initializing:
		for chan in self.net.channel_names:
		    if type(chan) == type(''):
			self.out_line('JOIN ' + chan)
		    else:
			self.out_line('JOIN ' + chan[0] + ' :' + chan[1])
		self.initializing = 0

    def handle_notice(self, nick, addr, args):
	match = rx_ctcp.match(args[1])
	if match:
	    command, tail = match.group(1, 3)
	    command = 'handle_ctcp_answer_' + string.lower(command)
	    if hasattr(self, command):
		getattr(self, command)(nick, addr, args[0], tail)
	else:
	    event.notice(net = self.net, nick = nick, addr = addr,
			 target = args[0], private = not is_channel(args[0]),
			 message = args[1])

    def handle_part(self, nick, addr, args):
	if len(args) > 1:
            message = args[1]
	else:
            message = None
        event.part(net = self.net, nick = nick, addr = addr, chan = args[0],
                   message = message)
	person = self.people[nick]
	del self.channels[args[0]][person]
	del person.common[args[0]]
	if not person.common:
	    del self.people[nick]
        if low_equal(nick, self.nick):
            event.own_part(net = self.net, chan = args[0], message = message)
	    self.own_leave(args[0])

    def handle_pong(self, nick, addr, args):
        event.pong(net = self.net, nick = nick,
                   time = time.time() - self.ping_time)

    def handle_privmsg(self, nick, addr, args):
        match = rx_ctcp.match(args[1])
        if match:
            command, tail = match.group(1, 3)
            command = 'handle_ctcp_' + string.lower(command)
            if hasattr(self, command):
                getattr(self, command)(nick, addr, args[0], tail)
        else:
            message = crypt.decrypt_message(args[1])
            n = 1 + (message[0] > 0)
            if message[0]:
                event.privmsg(net = self.net, nick = nick, addr = addr,
                              target = args[0], message = message[1],
                              crpt = message[0] > 0,
                              private = is_channel(args[0]))
                if not is_channel(args[0]):
		    self.net.last_msg_from = nick
                    i = 0
                    while i < len(self.net.msg_nicklist):
                        if low_equal(nick, self.net.msg_nicklist[i]):
                            del self.net.msg_nicklist[i]
                            break
                        i = i + 1
                    self.net.msg_nicklist.insert(0, nick)
            else:
                event.decrypt_fail(net = self.net, nick = nick, addr = addr,
                                   target = args[0], message = args[1],
                                   failcode = message[1])

    def handle_quit(self, nick, addr, args):
        event.quit(net = self.net, nick = nick, addr = addr, reason = args[0])
	person = self.people[nick]
	for chan in self.channels.values():
	    if chan.has_key(person):
		del chan[person]
	person.common = ()
	del self.people[nick]

    def handle_topic(self, nick, addr, args):
        event.topic(net = self.net, nick = nick, addr = addr, chan = args[0],
                    topic = args[1])

    def handle_ctcp_action(self, nick, addr, chan, args):
        event.ctcp_action(net = self.net, nick = nick, addr = addr,
                          target = chan, message = args)

    def handle_ctcp_dcc(self, nick, addr, chan, args):
	cmd, filename, host, port, size = tuple(string.split(args, ' '))
	if string.lower(cmd) == 'send':
	    main.dcc_count = main.dcc_count + 1
	    main.dcc_hash[main.dcc_count] = filename, host, port, size, nick, \
                                            addr, self.net
            event.dcc_send_request(net = self.net, nick = nick, addr = addr,
                                   id = main.dcc_count, filename = filename,
                                   size = size)

    def handle_ctcp_ping(self, nick, addr, chan, args):
	if args == None:
	    args = ''
        self.ctcp_answer(nick, 'PING ' + args)

    def handle_ctcp_version(self, nick, addr, chan, args):
        self.ctcp_answer(nick, 'VERSION clirc ' + main.version)

    def handle_ctcp_answer_version(self, nick, addr, chan, args):
	event.version(net = self.net, nick = nick, addr = addr, version = args)

    def ctcp_answer(self, nick, data):
        self.out_line('NOTICE ' + nick + ' :\001' + data + '\001')

reloadable.register_for_reload(IRCServ)

## End. ##
