# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org>
#
# This file is part of PythonVerse.
#
# PythonVerse is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# PythonVerse is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with PythonVerse; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
import sys, os, asyncore, asynchat, socket, string, struct, stat
import transutil

# Global constants are all caps; global variables start with _

BALLOONXOFFSET = 15
HOME = os.path.expanduser('~/.OpenVerse')
ANIMDIR = os.path.join(HOME, 'anims')
DLDIR = os.path.join(HOME, 'download')
ICONDIR = os.path.join(HOME, 'icons')
IMAGEDIR = os.path.join(HOME, 'images')
OBJDIR = os.path.join(HOME, 'objects')
RIMAGEDIR = os.path.join(HOME, 'rimages')
ROOMDIR = os.path.join(HOME, 'rooms')


def checkcache(filename, size):
    try: s = os.stat(filename)[stat.ST_SIZE]
    except OSError: return None
    else:
        if s == size or size < 0: return filename


class DCC(asyncore.dispatcher):
    def __init__(self, host, port, filename, size, progress_callback,
                 close_callback, sock=()):
        asyncore.dispatcher.__init__(self, sock)
        self.host = host
        self.port = port
        self.filename = filename
        self.size = size
        self.length = 0
        self.outbuf = ''
        self.buffer = ''
        self.progress_callback = progress_callback
        self.close_callback = close_callback
     
    def __repr__(self):
        return '<DCC %s %s:%d %d/%d>' % (self.filename, self.host, self.port,
                                         self.length, self.size)

    def handle_connect(self):
        pass
    
    def handle_write(self):
        sent = self.send(self.outbuf)
        self.outbuf = self.outbuf[sent:]

    def writable(self):
        return len(self.outbuf) > 0

    def handle_close(self):
        print self, 'closing'
        self.close()
        if self.close_callback is not None:
            apply(self.close_callback, (self.tempfilename, self.filename,
                                        self.size))
                    

    
class DCCGet(DCC):
    def __init__(self, host, port, filename, size,
		 progress_callback, close_callback):
        DCC.__init__(self, host, port, filename, size, progress_callback,
                     close_callback)
        self.tempfilename = os.path.join(RIMAGEDIR, filename)
        self.file = open(self.tempfilename, 'wb')
        self.size = size
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((host, port))

    def handle_read(self):
        data = self.recv(4096)
        if data:
            self.file.write(data)
            self.length = self.length + len(data)
            self.outbuf = self.outbuf + struct.pack('>I', self.length)
            if self.progress_callback is not None:
                apply(self.progress_callback, (self.length, self.tempfilename,
                                               self.filename, self.size))

            if self.length == self.size:
                self.file.close()


class DCCSendPassive(DCC):
    def __init__(self, host, port, filename, size, progress_callback,
                 close_callback):
        DCC.__init__(self, host, port, filename, size, progress_callback,
                     close_callback)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((host, port))
        
        self.file = open(filename, 'rb')
        self.outbuf = self.file.read(4096)
        self.sent = 0

    def handle_read(self):
        data = self.recv(4)
        print self, 'received %d bytes' % len(data)
        if data:
            data = self.buffer + data
            # Drop all but one length received
            drop = len(data) / 4 * 4 - 4
            data = data[drop:]
            if len(data) >= 4:
                # Get the number of bytes received by the client
                (self.length,) = struct.unpack('>I', data[:4])
                self.buffer = data[4:]
                print self
                if self.progress_callback is not None:
                    apply(self.progress_callback, (self.length, self.filename,
                                                   self.size))
                    
                if self.length == self.size:
                    self.file.close()
                    self.handle_close()

    def handle_write(self):
        #if self.length < self.sent: return
        sent = self.send(self.outbuf)
        self.outbuf = self.outbuf[sent:]
        self.sent = self.sent + sent
        if len(self.outbuf) < 4096:
            self.outbuf = self.outbuf + self.file.read(4096)


class ServerConnection(transutil.Connection):
    def __init__(self, host, port, client, nick, avatar):
        transutil.Connection.__init__(self, transutil.InputHandler(
            (('ABOVE', self.cmd_ABOVE, '(\S+)', (str,)),
	     ('CHAT', self.cmd_CHAT, '(\S+) (.*)', (str, str)),
             ('SCHAT', self.cmd_SCHAT, '(\S+) (\S+) (.*)', (str, str, str)),
             ('MOVE', self.cmd_MOVE, '(\S+) (\d+) (\d+) (\d+)',
              (str, int, int, int)),
	     ('EFFECT', self.cmd_EFFECT, '(\S+) (\S+)', (str, str)),
             ('PRIVMSG', self.cmd_PRIVMSG, '(\S+) (.*)',
              (str, str)),
             ('AVATAR', self.cmd_AVATAR,
              '(\S+) (\S+) (-?\d+) (-?\d+) (\d+) (-?\d+) (-?\d+)',
              (str, str, int, int, int, int, int)),
             ('URL', self.cmd_URL, '(\S+) (.*)', (str, str)),
             ('NEW', self.cmd_NEW,
              '(\S+) (\d+) (\d+) (\S+) (-?\d+) (-?\d+) (\d+) (-?\d+) (-?\d+)',
              (str, int, int, str, int, int, int, int, int)),
             ('NOMORE', self.cmd_NOMORE, '(\S+)', (str,)),
	     ('EXIT_OBJ', self.cmd_EXIT_OBJ,
	      '(\S+) (-?\d+) (-?\d+) (-?\d+) (-?\d+) (\d+) (\S+) (\d+)',
	      (str, int, int, int, int, int, str, int)),
             ('DCCGETAV', self.cmd_DCCGET, '(\d+) (\S+) (\d+)',
              (int, str, int)),
             ('DCCGETROOM', self.cmd_DCCGET, '(\d+) (\S+) (\d+)',
              (int, str, int)),
             ('DCCGETOB', self.cmd_DCCGET, '(\d+) (\S+) (\d+)',
              (int, str, int)),
             ('PING', self.cmd_PING, '', ()),
             ('ROOM', self.cmd_ROOM, '(\S+) (\d+)', (str, int)),
             ('ROOMNAME', self.cmd_ROOMNAME, '(.*)', (str,)),
             ('MOUSEOVER', self.cmd_MOUSEOVER,
              '(\S+) (\d+) (\d+) (\S+) (\d+) (\S+) (\d+) (\d+)',
              (str, int, int, str, int, str, int, int)),
             ('DCCSENDAV', self.cmd_DCCSENDAV, '(\d+) (\S+)', (int, str)),
	     ('SUB', self.cmd_SUB, '(\S+) (\S+) (.*)', (str, str, str)),
             ('WHOIS', self.cmd_WHOIS, '(\S+) (.*)', (str, str)))))
        self.host = host
        self.port = port
        self.pending_images = {}
        self.client = client
        self.images = {}
        self.nick = nick
	avdata = self.parse_anim(avatar)
        self.avatar_filename = avdata[0]
        self.nx = avdata[1]
        self.ny = avdata[2]
        self.bx = avdata[3]
        self.by = avdata[4]
        # Connect last
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((host, port))
                                      
    def handle_connect(self):
        size = os.stat(os.path.join(IMAGEDIR,
                                    self.avatar_filename))[stat.ST_SIZE]
        self.write("AUTH %s %d %d %s %d %d %d %d %d\r\n" %
                  (self.nick, 320, 200, self.avatar_filename, self.nx, self.ny,
                   size, self.bx, self.by))

    def handle_close(self):
        transutil.Connection.handle_close(self)
        self.client.close()

    def debug(self, info):
        self.client.debug(info)
        
    # Utility functions

    def get_image(self, filename, size, command, pcallback, callback, args=()):
	# Prevent possible embedded '/' attacks
	filename = os.path.basename(filename)
        if filename == 'default.gif': size = -1
        try: image = self.images[filename, size]
        except KeyError:
            # Check in locally cached images
            file = checkcache(os.path.join(RIMAGEDIR, filename), size)
            if file is None:
                # Check my own avatars as well
                file = checkcache(os.path.join(IMAGEDIR, filename), size)

            if file is not None:
                image = self.client.newimage(file)
                self.images[filename, size] = image
                return image
                        
            blob = (pcallback, callback, args)
            # Take the callback out if it's already in there
            for pending in self.pending_images.values():
                if blob in pending: pending.remove(blob)
                
            try: self.pending_images[filename, size].append(blob)
            except KeyError: self.pending_images[filename, size] = [blob]
            else: pending.append(blob)
                
            self.write('%s %s\r\n' % (command, filename))
            
        else: return image

    def progress_callback(self, length, tempfilename, filename, size):
        for pcallback, callback, args in self.pending_images[(filename, size)]:
            if pcallback: apply(pcallback, args + (length,tempfilename,size))

    def image_callback(self, tempfilename, filename, size):
        image = self.client.newimage(tempfilename)
        self.images[(filename, size)] = image
        for pcallback, callback, args in self.pending_images[(filename, size)]:
            apply(callback, args + (image,))

    # Client functions

    def getnick(self):
	return self.nick

    def gethostport(self):
	return (self.host, self.port)

    def new_connect(self, host, port):
	self.close()
	self.host = host
	self.port = port
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
	self.connect((host, port))

    def move(self, pos):
        x, y = pos
        self.write('MOVE %s %d %d 1\r\n' % (self.nick, x, y))

    def push(self):
	self.write('PUSH 100\r\n')

    def effect(self, action):
	self.write('EFFECT %s\r\n' % action.lower())

    def privmsg(self, nicks, text):
	for n in nicks:
	    self.write('PRIVMSG %s %s\r\n' % (n, text))
	return self.nick

    def url(self, nicks, url):
	for n in nicks:
	    self.write('URL %s %s\r\n' % (n, url))

    def chat(self, text):
        self.write('CHAT %s\r\n' % text)

    def set_nick(self, nick):
        self.nick = nick
        self.write('NICK %s\r\n' % nick)

    def ignore(self, what, nick):
	self.write('IGNORE %s %s\r\n' % (what.upper(), nick))

    def unignore(self, what, nick):
	self.write('UNIGNORE %s %s\r\n' % (what.upper(), nick))

    def quote(self, text):
        """Send raw commands to the server"""
        self.write('%s\r\n' % text)
        
    def set_avatar(self, avatar):
	av = self.parse_anim(avatar)
	try: size = os.stat(os.path.join(IMAGEDIR, av[0]))[stat.ST_SIZE]
	except OSError, info: self.client.debug(info)
	else:
            self.avatar_filename = av[0]
            self.nx = av[1]
            self.ny = av[2]
            self.bx = av[3]
            self.by = av[4]
	    self.write('AVATAR %s %d %d %d %d %d\r\n' %
		      (av[0], av[1], av[2], size, av[3], av[4]))

    def parse_anim(self, avatar):
	"""Parse the avatar definition file for information"""
	try: avfile = open(os.path.join(ANIMDIR, avatar), 'r')
	except:
	    try: avfile = open(os.path.join(ANIMDIR, avatar + '.av'), 'r')
	    except:
		print "Avatar", avatar, "not found."
		if self.avatar_filename is not None:
		    return [self.avatar_filename,
			    self.nx, self.ny, self.bx, self.by]
		return ["default.gif", 0, 36, 24, 6]
	animdata = avfile.readlines()
    	avfile.close()
	for animitem in animdata:
	    # parse the whole file for future addition of animation
	    splitanimitem = animitem.split()
	    if splitanimitem[1] == "MV(anim.x_off)":
	        nx = int(splitanimitem[2])
	    if splitanimitem[1] == "MV(anim.y_off)":
	        ny = int(splitanimitem[2])
	    if splitanimitem[1] == "MV(anim.baloon_x)":
	        bx = int(splitanimitem[2])
	    if splitanimitem[1] == "MV(anim.baloon_y)":
	        by = int(splitanimitem[2])
	    if splitanimitem[1] == "MV(anim.0)":
	        avimage = splitanimitem[2].strip()
	return [avimage, nx, ny, bx, by]

    def whois(self, nick):
        self.write('WHOIS %s\r\n' % nick)

    def whichmouseover(self, name):
	self.client.setmouseover(name)

    # Command handlers
        
    def cmd_ABOVE(self, name):
	"""Raise the named object to the top of the stacking order"""
	self.client.raise_object(name)

    def cmd_CHAT(self, nick, text):
        self.client.chat(nick, text)

    def cmd_SCHAT(self, emote, nick, text):
        self.client.chat(nick, '*%s* %s' % (emote, text))

    def cmd_MOVE(self, nick, x, y, speed):
        # FIXME: Need to use speed too
        self.client.move_avatar(nick, x, y, speed)

    def cmd_EFFECT(self, nick, action):
	self.client.effect(nick, action)

    def cmd_PRIVMSG(self, nick, text):
        #PRIVMSG nick text / nick text
        self.client.privmsg(nick, text)

    def cmd_ROOM(self, filename, filesize):
        """Set a new background image"""
        image = self.get_image(filename, filesize, 'DCCSENDROOM',
                               self.client.background_progress,
                               self.client.background_image)
        if image is not None: self.client.background_image(image)

    def cmd_AVATAR(self, nick, filename, nx, ny, size, bx, by):
        image = self.get_image(filename, size, 'DCCSENDAV',
                               self.client.avatar_progress,
                               self.client.avatar_image, (nick,))
        if image is None: image = self.client.newimage()
        # Need to shift 15 pixels to the left because OV uses the edge of
        # the balloon rather than the arrow as the offset point. I do this
        # here because it's OV specific.
        self.client.avatar(nick, image, (nx, ny), (bx-BALLOONXOFFSET, by))

    def cmd_PING(self):
        self.write('PONG\r\n')
        
    def cmd_URL(self, nick, text):
        self.client.url(nick, text)

    def cmd_NEW(self, nick, x, y, filename, nx, ny, size, bx, by):
        image = self.get_image(filename, size, 'DCCSENDAV',
                               self.client.avatar_progress,
                               self.client.avatar_image, (nick,))
        if image is None: image = self.client.newimage()
        self.client.new_avatar(nick, (x, y), image, (nx, ny),
                               (bx-BALLOONXOFFSET, by))

    def cmd_NOMORE(self, nick):
        self.client.del_avatar(nick)

    def cmd_EXIT_OBJ(self, name, x1, y1, x2, y2, duration, host, port):
	self.client.exit_obj(name, host, port)

    def cmd_DCCGET(self, port, filename, size):
        DCCGet(self.host, port, filename, size,
	       self.progress_callback, self.image_callback)

    def cmd_DCCSENDAV(self, port, filename):
        filename = os.path.join(IMAGEDIR, os.path.basename(filename))
        try:
            size = os.stat(filename)[stat.ST_SIZE]
            DCCSendPassive(self.host, port, filename, size, None, None)
        except IOError, info: self.debug(info)
        
    def cmd_ROOMNAME(self, name):
        self.client.set_title(name)

    def cmd_MOUSEOVER(self, name, x, y, image1, size1, image2, size2, flag):
        image1 = self.get_image(image1, size1, 'DCCSENDOB', None,
                                self.client.mouseover_image1, (name,))
        image2 = self.get_image(image2, size2, 'DCCSENDOB', None,
                                self.client.mouseover_image2, (name,))
        if image1 is None: image1 = self.client.newimage()
        if image2 is None: image2 = self.client.newimage()
        self.client.mouseover(name, (x, y), image1, image2)

    def cmd_SUB(self, nick, command, cmdargs):
	print 'TODO: Implement SUB'

    def cmd_WHOIS(self, nick, text):
        self.client.chat(nick, '*%s* is %s' % (nick, text))
        

def poll():
    asyncore.poll()

