# -*-Python-*-
# 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
#
# vim:syntax=python

import sys, bisect, string, random, webbrowser
import pygame, pygame.font, pygame.image, pygame.time, pygame.draw
from math import *
from pygame.locals import *
from types import *
import transutil, color

def wrap_lines(lines, width, font):
    r = []
    for text in lines:
        r.extend(wrap(text, width, font))

    for t in range(len(r)):
	if t < len(r):		# in case any lines have been removed
	    if r[t] == "": del r[t]

    return r

def wrap(text, width, font):
    """Wrap a line of text, returning a list of lines."""

    lines = []
    while text:
        if font.size(text)[0] <= width: return lines + [text]
        try:
            i = string.rindex(text, ' ')
            while font.size(text[:i])[0] > width:
                i = string.rindex(text, ' ', 0, i)
        except ValueError:
            i = len(text)-1
            while font.size(text[:i])[0] > width and i: i = i - 1
            if not i: raise ValueError, 'width %d too narrow' % width

        lines.append(text[:i])
        text = string.lstrip(text[i:])

    return lines


def progress(fraction, size=(20, 50), fgcolor=(0,255,0),
             bgcolor=(255,255,255)):
    w, h = size
    y = h * fraction
    s = pygame.Surface(size)
    s.fill(bgcolor, (0, 0, w, h-y))
    s.fill(fgcolor, (0, h-y, w, y))
    pygame.draw.rect(s, fgcolor, (0, 0, w-1, h-1), 1)
    s.set_alpha(127)
    return s


def invertrect(initrect, rects, min_width=50, min_height=10):
    left, top = initrect.topleft
    right, bottom = initrect.bottomright
    width, height = initrect.size

    irects = []
    rects = rects + [Rect(left-1, top, 0, height),
                     Rect(left, top-1, width, 0),
                     Rect(right, top, 0, height),
                     Rect(left, bottom+1, width, 0)]

    for a in rects:
        # Pick a left side
        l = a.right+1
        if l < left: continue
        # Pick a top
        for b in rects:
            # Make sure the rect can actually border ours
            if b.right < l or b.bottom > a.bottom: continue
            # Pick a top
            t = b.bottom+1
            # Pick a right side
            for c in rects:
                # Make sure this rect can border ours
                if c.left <= l or c.bottom < t or c.top > bottom or \
                   c.left <= b.left: continue
                r = c.left-1
                w = r - l
                h = bottom - t
                if w < min_width or h < min_height: continue
                # There can be only one rect with these three sides
                rect = Rect(l, t, w, h)
                # Now find the bottom
                for d in rects:
                    if d.colliderect(rect):
                        rect.height = d.top-t-1
                        # Make sure the rect is still sane and still borders
                        # on the original border rects
                        if rect.height < min_height or rect.bottom < a.top or \
                           rect.bottom < c.top: break
                else: irects.append(rect)

    return irects


class Group:
    def __init__(self):
        self.list = []
        self.pollers = []
        self.active = None

    def add(self, sprite):
        self.list.append(sprite)

    def remove(self, sprite):
        self.list.remove(sprite)

    def above(self, sprite):
        """Move to the top of the stacking order"""
        # The way the group is implemented, just reinserting will do it.
        self.list.remove(sprite)
        self.list.append(sprite)

    def mouse(self, mousepos):
        """If a sprite is active, send events only to that sprite until
        it deactivates. Otherwise, send to all sprites in order until
        one activates."""

        dirtyrects = []
        # Send the event to all sprites until one activates
        sprites = self.list
        i = len(sprites)
        while i:
            i = i - 1
            sprite = sprites[i]
            if sprite.mouseme and sprite.rect.collidepoint(mousepos):
                if self.active is sprite: return dirtyrects
                if self.active is not None:
                    dirtyrects.extend(self.active.mouseoff())

                self.active = sprite
                dirtyrects.extend(sprite.mouseon())
                return dirtyrects
 
        if self.active is not None:
            dirtyrects.extend(self.active.mouseoff())
            self.active = None
            return dirtyrects

    def update(self):
        """Update the positions of all the sprites that are moving"""
        # Move any avatars that need moving
        # FIXME: probably needs optimizing
        dirtyrects = []
        ticks = pygame.time.get_ticks()
        pollers = self.list
        i = len(pollers)
        while i:
            i = i - 1
            sprite = pollers[i]
            if sprite.pollme:
                dirtyrects.extend(sprite.poll(ticks))

        return dirtyrects
    
    def draw(self, surface, rect=None):
        if rect is None:
            for sprite in self.list:
                surface.blit(sprite.image, sprite.rect)
        else:
            for sprite in self.list:
                # Redraw sprites whose rects overlap this rect,
                # but only in the overlapping area
                if rect.colliderect(sprite.rect):
                    r = sprite.rect
                    surface.blit(sprite.image, rect,
                                 rect.move((-r[0], -r[1])))



class Sprite:
    mouseme = 0
    pollme = 0
    def __init__(self, position, image=None):
        self.dead = 0
        # self.group = group
        self.image = image
        if len(position) == 4: self.rect = position
        else:
            if image is None: w, h = 0, 0
            else: w, h = image.get_size()
            
            x, y = position
            self.rect = Rect(x-w/2, y-h/2, w, h)

        # group.add(self)

    def __repr__(self):
        return '<%s(%s, %s)>' % (self.__class__, self.image, self.rect)

    def poll(self, t, delta): return []

    def move(self, pos):
        rect = self.rect
        oldrect = Rect(rect)
        rect.center = pos
        if oldrect.colliderect(rect): return [oldrect.union(rect)]
        else: return [oldrect, rect]

##    def die(self):
##        self.group.remove(self)
##        self.dead = 1
##        return [self.rect]

    def set_image(self, image):
        """Change the image without moving the center"""
        self.image = image
        oldrect = self.rect
        x, y = oldrect.center
        w, h = image.get_size()
        self.rect = Rect((x-w/2, y-h/2, w, h))
        # Return the affected area
        return oldrect.union(self.rect)


class ClampingSprite(Sprite):
    def __init__(self, pos, image, clamprect):
        Sprite.__init__(self, pos, image)
        self.pos = pos
        self.clamprect = clamprect
        self.clamp()

    def move(self, pos):
        self.pos = pos
        oldrect = Rect(self.rect)
        self.rect.center = pos
        self.clamp()
        if oldrect.colliderect(self.rect): return [oldrect.union(self.rect)]
        else: return [oldrect, self.rect]

    def clamp(self):
        self.rect = self.rect.clamp(self.clamprect)
        

class Mouseover(Sprite):
    mouseme = 1
    def __init__(self, server, name, pos, image1, image2):
        Sprite.__init__(self, pos, image1)
	self.server = server
	self.name = name
        self.image1 = image1
        self.image2 = image2

    def mouseon(self):
	self.server.whichmouseover(self.name)	# for EXIT_OBJs
        return [self.set_image(self.image2)]

    def mouseoff(self):
	self.server.whichmouseover('')		# for EXIT_OBJs
        return [self.set_image(self.image1)]
    
    def set_image1(self, image):
        self.image1 = image
        return self.set_image(image)

    def set_image2(self, image):
        self.image2 = image
        
        
class Avatar(Sprite):
    mouseme = 1
    def __init__(self, server, group, position, image, nick, noffset, boffset):
        Sprite.__init__(self, position, image)
	self.server = server
        self.rect = self.rect.clamp((0, 0, 640, 480))
        self.group = group
        self.nick = nick
        self.destpos = None
        self.speed = None
        self.balloon = None
        self.label = None
	self.effects = []
	self.effect_count = -1
	self.effect_step = 0
        self.pollme = 0
	self.stoptime = 0
	self.lastpoll = 0
	self.moving = 0
        self.set(noffset, boffset)

    def set(self, noffset, boffset):
        self.boffset = boffset
        nx, ny = noffset
        if nx < -self.rect.width/2-10: nx = -self.rect.width/2-10
        elif nx > self.rect.width/2+10: nx = self.rect.width/2+10
        if ny < -self.rect.height/2-10: ny = -self.rect.height/2-10
        elif ny > self.rect.height/2+10: ny = self.rect.height/2+10
        self.noffset = nx, ny

    def mouseon(self):
	# we want the ORT to behave like an exit
	if self.nick == 'ORT_Number_1':
	    self.server.whichmouseover('ov_tram_exit')
        if self.label is not None:
	    print self.nick, 'on'
	    self.label.die()
	
	# Strip color codes from the nick for display
	nicklist = list(self.nick)
	nick = ''
	avoid = list(string.digits)
	avoid.extend([',', '\x03'])
	found = 0
	for n in nicklist:
	    if n == '\x03':
		found = 1
	    if found == 1:
		try:
		    test = avoid.index(n)
		except:
		    found = 0
	    if found == 0:
		nick = nick + n
	
	sx, sy = _font.size(nick)
	s = _font.render(nick, 0, (255, 255, 255))
	image = pygame.Surface((sx+2, sy+2))
	image.set_colorkey((255,165,0))
	image.fill((255,165,0))
	for x in range(3):
	    for y in range(3):
	        image.blit(s, (x, y))
	image.blit(_font.render(nick, 1, (0,0,0)), (1, 1))
	image.set_alpha(127)
	x, y = self.rect.center
	nx, ny = self.noffset
	self.label = ClampingSprite((x+nx, y+ny), image, Rect(0, 0, 640, 480))
	self.group.add(self.label)
	return [self.label.rect]

    def mouseoff(self):
        if self.label is None:
            print self.nick, 'off'
            return []
        else:
            label = self.label
            self.group.remove(label)
            self.label = None
            return [label.rect]
    
    def move(self, position, speed):
        """Changes the location of the avatar's center to the new position"""
	self.moving = 1
        if position[0] >= 640 or position[1] > 480:
            print >> sys.stderr, 'Attempt to move outside the screen'
            return
	if speed == 0: speed = 1
        self.speed = speed
        pos = self.rect.center
        self.startpos = pos
        self.starttime = pygame.time.get_ticks()
        distance = dist(pos, position)
        if distance == 0.0: return
        self.dx = 3.0 * (position[0]-pos[0]) * self.speed / distance / 10.0
        self.dy = 3.0 * (position[1]-pos[1]) * self.speed / distance / 10.0
        self.stoptime = self.starttime + distance * 10.0 / 3.0 / speed
        self.destpos = position
        self.pollme = 1

    def effect(self, action):
	if action == 'jump' or action == 'shiver':
	    self.effects.append(action)
	    self.pollme = 1

    def poll(self, t):
        """Move the avatar if necessary"""

        dirtyrects = []
        oldrect = Rect(self.rect)
	if self.stoptime != 0:
    	    if t >= self.stoptime:
        	self.rect.center = self.destpos
        	self.pollme = 0
        	if self.balloon is not None and not self.balloon.dead \
					    and self.moving == 1:
            	    bx, by = self.boffset
            	    x, y = self.destpos
            	    dirtyrects.extend(self.balloon.move((x+bx, y+by)))
		    self.moving = 0
    	    else:
        	delta_t = t - self.starttime
        	x, y = self.startpos
        	self.rect.center = x+int(round(self.dx*delta_t)), \
                	           y+int(round(self.dy*delta_t))
      
	# apply effects, if any
	if self.stoptime == 0:
	    pos = (320, 200)
	else:
	    pos = self.rect.center
	if self.effects != []:
	    effect = self.effects[0]
	    self.pollme = 1
	    x, y = pos
	    if self.effect_count == -1:
		if effect == 'shiver':
		    self.effect_step = 3
		    self.effect_count = 100
		if effect == 'jump':
		    self.effect_step = 5
		    self.effect_count = 200
	    if effect == 'shiver' and self.effect_count >= 0:
		if self.effect_step == 3: x = x - 15
		if self.effect_step == 1: x = x + 15
		if self.effect_step == 0:
		    if self.effect_count > 0:
			self.effect_step = 4
	    if effect == 'jump' and self.effect_count > 0:
		if self.effect_step == 5: y = y - 15
		if self.effect_step == 4: y = y - 30
		if self.effect_step == 3: y = y - 45
		if self.effect_step == 2: y = y - 30
		if self.effect_step == 1: y = y - 15
		if self.effect_step == 0:
		    if self.effect_count > 0:
			self.effect_step = 6
	    self.rect.center = (x, y)
	    now = pygame.time.get_ticks()
	    if self.lastpoll == 0:
		self.lastpoll = pygame.time.get_ticks()
	    delay = 30 - (now - self.lastpoll)
	    self.lastpoll = now
	    if delay > 0:
		pygame.time.delay(delay)
	    self.effect_step = self.effect_step -1
	    self.effect_count = self.effect_count -1
	    if self.effect_count == -1:
		self.rect.center = pos
		del self.effects[0]
		if self.effects == []:
		    self.pollme = 0

        return dirtyrects + [oldrect.union(self.rect)]

    def chat(self, group, text):
        x, y = self.rect.center
        xoff, yoff = self.boffset

        if self.balloon is None or self.balloon.dead:
            self.balloon = Balloon(group, self.group, (x+xoff, y+yoff), text,
                                   self)
            group.add(self.balloon)
            return [self.balloon.rect]
        else:
            return self.balloon.add_text(text)


def arc(radius, center, start_angle, stop_angle, n):
    x, y = center
    step = (stop_angle - start_angle) / n
    points = [0] * (n+1)
    for i in range(n+1):
        angle = start_angle + i*step
        points[i] = (x + int(round(radius*sin(angle))),
                     y - int(round(radius*cos(angle))))

    return tuple(points)


def closest(rect, point):
    """Find the closest point on a rect to a given point"""
    return min(rect.right, max(point[0], rect.left)), \
           min(rect.bottom, max(point[1], rect.top))


def dist(point1, point2):
    x1, y1 = point1
    x2, y2 = point2
    return sqrt((x1-x2)**2 + (y1-y2)**2)


def rectdist(rect, point):
    return dist(closest(rect, point), point)


class Balloon(Sprite):
    """A speech balloon"""
    pollme = 1
    def __init__(self, group, avgroup, pos, text, avatar,
                 timeout=10000, fgcolor=(0, 0, 0), bgcolor=(255, 255, 255)):
        self.timeouts = [pygame.time.get_ticks() + timeout]
        self.group = group
        self.avgroup = avgroup
        Sprite.__init__(self, pos)
        self.pos = pos
        self.timeout = timeout
        self.fgcolor = fgcolor
        self.bgcolor = bgcolor
        self.avatar = avatar
        self.text = [text]
        self.rect = None
        self.render()

    def move(self, pos):
        self.pos = pos
        oldrect = self.rect
        rect = self.render()
        if oldrect.colliderect(rect): return [oldrect.union(rect)]
        else: return [oldrect, rect]

    def nearer(self, rect1, rect2):
        """Compare two rects based on their distance from our position"""
        dist1 = rectdist(rect1, self.pos)
        dist2 = rectdist(rect2, self.pos)
        if dist1 == dist2: return cmp(rect2.width, rect1.width)
        return cmp(dist1, dist2)

    def render(self):
        """Render the text, returning the affected rect."""

	pad = 3
        maxdist = 200
        screen_rect = Rect(0, 0, 640, 480 - (linesize(_font)+2))
        size = linesize(_font)
        
        # Try not overlapping anything
        balloonrects = map(lambda s: s.rect, self.group.list)
        try: balloonrects.remove(self.rect)
        except ValueError: pass

        # First, try to avoid everything
        rects1 = invertrect(screen_rect, balloonrects +
                            map(lambda s: s.rect, self.avgroup.list))
        rects1.sort(self.nearer)

        # Next, try to avoid just balloons and my av
        rects2 = invertrect(screen_rect, balloonrects +
                            [self.avatar.rect])
        rects2.sort(self.nearer)

        # Finally, just avoid my own av
        rects3 = invertrect(screen_rect, [self.avatar.rect])
        rects3.sort(self.nearer)

        rects = filter(lambda r,p=self.pos,m=maxdist: rectdist(r,p) < m,
                       rects1 + rects2 + rects3) + [screen_rect]

        notdone = 1
        while notdone:
            # Loop until we can fit the balloon into *some* rect
            for r in rects:
                try: lines = wrap_lines(self.text, r.width-pad*2, _font)
                except ValueError: continue
                if len(lines) * size + pad*2 < r.height:
                    notdone = 0
                    break
            else:
                # Couldn't render the balloon, delete some lines
                del self.text[0]
                del self.timeouts[0]

        # Count the number of lines in each sequence of text
        surfaces = map(lambda t,f=_font,c=self.fgcolor: f.render(t, 1, c),
                       lines)
        width = max(map(lambda l: _font.size(l)[0], lines)) + (pad * 2)
        height = len(surfaces) * size + (pad * 2)

        x, y = self.pos
        rect = Rect(x-width/2, y-height/2, width, height).clamp(r)

        bigrect = rect.union((self.pos, (1, 1)))
        image = pygame.Surface(bigrect.size)
        image.set_colorkey((255,165,0))
        image.fill((255,165,0))

        cx, cy = closest(rect, self.pos)
        cx = cx - bigrect.left
        cy = cy - bigrect.top
        x = x - bigrect.left
        y = y - bigrect.top
        left = rect.left - bigrect.left
        right = rect.right - bigrect.left - 1
        top = rect.top - bigrect.top
        bottom = rect.bottom - bigrect.top - 1
        # Calculate arcs for corners
        nwarc = arc(pad, (left+pad, top+pad), pi*1.5, pi*2, 5)
        nearc = arc(pad, (right-pad, top+pad), 0, pi*0.5, 5)
        searc = arc(pad, (right-pad, bottom-pad), pi*0.5, pi, 5)
        swarc = arc(pad, (left+pad, bottom-pad), pi, pi*1.5, 5)
        
        # Go in a clockwise direction
        if x > right-pad*2:
            # Drawing to the east
            if y > bottom-pad*2:
                # Draw the arrow to the southeast
                points = ((right, bottom-pad), (x, y),
                          (right-pad, bottom)) + swarc + nwarc + nearc
            elif y < top+pad*2:
                # Draw the arrow to the northeast
                points = ((right-pad, top), (x, y),
                          (right, top+pad)) + searc + swarc + nwarc
            elif x > right:
                # Due east
                points = ((right, cy-pad), (x, y),
                          (right, cy+pad)) + \
                          searc + swarc + nwarc + nearc
            else:
                # Arrow is inside balloon
                points = searc + swarc + nwarc + nearc
        elif x < left+pad*2:
            # Drawing to the west
            if y > bottom-pad*2:
                # Southwest
                points = ((left+pad, bottom), (x, y),
                          (left, bottom-pad)) + \
                          nwarc + nearc + searc
            elif y < top+pad*2:
                # Northwest
                points = ((left, top+pad), (x, y),
                          (left+pad, top)) + \
                          nearc + searc + swarc
            elif x < left:
                # Due west
                points = ((left, cy+pad), (x, y),
                          (left, cy-pad)) + \
                          nwarc + nearc + searc + swarc
            else:
                # Arrow is inside balloon
                points = nwarc + nearc + searc + swarc
        elif y < top:
            # Due north
            points = ((cx-pad, top), (x, y), (cx+pad, top)) + \
                     nearc + searc + swarc + nwarc
        elif y > bottom:
            # Due south
            points = ((cx+pad, bottom), (x, y),
                      (cx-pad, bottom)) + \
                      swarc + nwarc + nearc + searc
        else:
            print >> sys.stderr, 'Oops, arrow point is inside balloon!'
            points = swarc + nwarc + nearc + searc

        pygame.draw.polygon(image, (255,255,255), points, 0)
        pygame.draw.polygon(image, (0,0,0), points, 1)
        y = rect.top - bigrect.top + pad
        x = rect.left - bigrect.left
        for s in surfaces:
            image.blit(s, (((width - s.get_width())/2+x), y))
            y = y + size

        self.image = image

        #rect = rect.clamp((0, 0, 640, 480))
        self.rect = bigrect

        return bigrect

    def add_text(self, text):
        """Add text to the balloon, scrolling it if necessary."""
        self.text.append(text)
        self.timeouts.append(pygame.time.get_ticks() + self.timeout)
        return [self.rect.union(self.render())]

    def poll(self, ticks):
        if ticks >= self.timeouts[0]:
            del self.timeouts[0]
            del self.text[0]
            if self.text: return [self.rect.union(self.render())]
            else:
                self.group.remove(self)
                self.dead = 1
                return [self.rect]
        else: return []


class Entry(Sprite):
    def __init__(self, pos, width=640, histlength=100, color=(255,255,255),
                 bgcolor=(0,0,0)):
        Sprite.__init__(self, pos, pygame.Surface((0, 0)))
        self.pos = pos
        self.font = _font
        self.text = u''
        self.color = color
        self.bgcolor = bgcolor
        self.width = width
        self.cursor = 0
        self.buffer = []
        self.index = 0
        self.histlength = histlength

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

    def render(self):
        oldrect = self.rect
	cursorsize = 1
        start = 0
        while self.font.size(self.text[start:self.cursor+5])[0] > self.width:
            start = start + 1
        if self.text:
            image = self.font.render(self.text[start:], 1,
                        	     self.color, self.bgcolor)
	    self.image = pygame.Surface((image.get_width() + cursorsize,
					 image.get_height()))
	    self.image.blit(image, (0, 0))
            # Render the cursor
            cursor_pos = self.font.size(self.text[start:self.cursor])[0]
            cursor_height = self.image.get_height()
            self.image.fill((0,255,0), (cursor_pos, 0, cursorsize,
					cursor_height))
        else: self.image = pygame.Surface((0, 0))
        self.rect = Rect(self.pos, self.image.get_size())
        return oldrect.union(self.rect)

    def insert(self, c):
        self.text = self.text[:self.cursor] + c + self.text[self.cursor:]
        self.cursor = self.cursor + len(c)
        return self.render()

    def backspace(self, n=1):
        """Delete one character at the end"""

        if self.cursor == 0: return
        self.text = self.text[:self.cursor-n] + self.text[self.cursor:]
        self.cursor = self.cursor - n
        return self.render()

    def delete(self, n=1):
        if self.cursor == len(self.text): return
        self.text = self.text[:self.cursor] + self.text[self.cursor+n:]
        return self.render()

    def home(self):
        self.cursor = 0
        return self.render()

    def end(self):
        self.cursor = len(self.text)
        return self.render()

    def move_cursor(self, n):
        self.cursor = self.cursor + n
        if self.cursor > len(self.text): self.cursor = len(self.text)
        if self.cursor < 0: self.cursor = 0
        return self.render()

    def history(self, n):
        if not self.buffer: return
        if self.text and self.index < len(self.buffer) and \
           self.text != self.buffer[self.index]:
            self.add_history()
        self.index = self.index + n
        if self.index >= len(self.buffer): self.index = len(self.buffer) - 1
        if self.index < 0: self.index = 0
        self.text = self.buffer[self.index]
        self.cursor = len(self.text)
        return self.render()
        
    def add_history(self):
        self.buffer.append(self.text)
        if len(self.buffer) > self.histlength:
            del self.buffer[:len(self.buffer) - self.histlength]

    def clear(self):
        if self.text: self.add_history()
        self.index = len(self.buffer)
        self.text = u''
        self.cursor = 0
        return self.render()


class Text:
    def __init__(self, text, font, width, fgcolor, bgcolor=None):
        self.text = text
        self.font = font
        self.fgcolor = fgcolor
        self.bgcolor = bgcolor
        if bgcolor is None:
            self.image = font.render(text, 1, fgcolor)
            self.shadow = font.render(text, 1, (0, 0, 0))
        else:
            self.image = font.render(text, 1, fgcolor, bgcolor)
            self.shadow = None

    def draw(self, pos, rect=None):
        """Pos is the position of the upper left of my rect"""
        myrect = self.image.get_rect().move(pos)
        if rect is None:
            if self.shadow is not None:
                surface.blit(self.shadow, (pos[0]+1,pos[1]+1))
            return surface.blit(self.image, pos)
        
        rect = myrect.clip(rect)
        if self.shadow is not None:
            surface.blit(self.shadow, rect.move(1, 1),
                         ((rect.left-myrect.left-1, rect.top-myrect.top-1),
                          rect.size))
        return surface.blit(self.image, rect,
                            ((rect.left-myrect.left, rect.top-myrect.top),
                             rect.size))

    
class Console(Sprite):
    """Transparent scrollable text widget."""
    def __init__(self, rect, font, scrollback=1000,
                 bg=(255, 165, 0)):
        image = pygame.Surface(rect.size)
        image.set_colorkey(bg)
        image.set_alpha(127)
        image.fill(bg)
        Sprite.__init__(self, rect, image)
        self.scrollback = scrollback
        self.buffer = [''] * scrollback
        self.font = font
        self.bg = bg
        self.linesize = linesize(font)
	self.numlines = int(self.rect.height / self.linesize) + 1
	self.currentcolor = (255, 255, 255)
	self.offset = 0

    def add_lines(self, paragraphs):
        for paragraph in paragraphs:
	    del self.buffer[0]
            self.buffer.append(paragraph)
	    if self.offset > 0:
		self.offset = self.offset + 1
		if self.offset > self.scrollback - self.numlines:
		    self.offset = self.scrollback - self.numlines

        return self.render()

    def scroll_up(self, lines):
	self.offset = self.offset + lines
	if self.offset > self.scrollback - self.numlines:
	    self.offset = self.scrollback - self.numlines
	return self.render()

    def scroll_down(self, lines):
	self.offset = self.offset - lines
	if self.offset < 0: self.offset = 0
	return self.render()

    def render(self):
        """Update my image to match the current buffer"""
        self.image.fill(self.bg)
	lines = self.buffer[(self.scrollback - (self.numlines + self.offset)):
	    (self.scrollback - (self.numlines + self.offset)) + self.numlines]
	newlines = []
	for line in lines:
	    if line != '':
		newlines.extend(wrap(line, self.rect.width - 2, self.font))
	    else:
		newlines.append('')
	lines = newlines[len(newlines) - self.numlines:]
	if self.offset > 0:
	    bgcolor = (127, 0, 0)
	else:
	    bgcolor = (0, 0, 0)
	x = 1
	y = self.rect.height - ((self.numlines * self.linesize) + 1)
	for line in lines:
	    if line != '':
		if line[0:1] == '<':
		    self.currentcolor = (255, 255, 255)
		if line[0:1] == '*':
		    self.currentcolor = (0, 255, 255)
		if line[0:1] == '!':
		    if line[1:14] == 'Whispering to':
			self.currentcolor = (255, 191, 127)
		    else:
			self.currentcolor = (255, 255, 0)
		if line[0:1] == '[':
		    self.currentcolor = (0, 255, 0)
		if line[0:2] == '->':
		    self.currentcolor = (0, 0, 255)
		ss = self.font.render(line, 0, bgcolor)
		for xx in range(3):
		    for yy in range(3):
			self.image.blit(ss, (x + (xx - 1), y + (yy - 1)))
        	self.image.blit(self.font.render(line, 1, self.currentcolor), (x, y))
	    y = y + self.linesize

        return [self.rect]


class Client:
    """Callbacks for the server connection"""
    def __init__(self):
        global _clients, _active
        self.width, self.height = 640, 480
        self.background = progress(0, (self.width, self.height))
        self.server = None
	self.sobjects = Group()
        self.sprites = Group()
        self.balloons = Group()
        self.text = Group()
        self.avatars = {}
	self.exits = {}
	self.urls = []
	self.whichmouseover = ''
        self.console_height = self.height - (linesize(_font) + 2)
        self.entry = Entry((0, self.console_height + 1))
        self.text.add(self.entry)
        self.console = Console(Rect(0, 0, self.width, self.console_height),
                               _font)
        self.console_active = 0
        self.handler = transutil.InputHandler(
            (('connect', self.cmd_connect, '(\S+) (\d+)', (str, int)),
	     ('reconnect', self.cmd_reconnect, '', ()),
	     ('nick', self.cmd_nick, '(\S+)', (str,)),
	     ('ignore', self.cmd_ignore, '(\S+) (\S+)', (str, str)),
	     ('unignore', self.cmd_unignore, '(\S+) (\S+)', (str, str)),
	     ('msg', self.cmd_msg, '(\S+) (.*)', (str, str)),
	     ('url', self.cmd_url, '(\S+) (\S+)', (str, str)),
             ('quote', self.cmd_quote, '(.*)', (str,)),
             ('avatar', self.cmd_avatar, '(\S+)', (str,)),
	     ('push', self.cmd_push, '', ()),
	     ('effect', self.cmd_effect, '(\S+)', (str,)),
             ('whois', self.cmd_whois, '(\S+)', (str,))))
        _clients.append(self)
        _active = self

    # Utility functions
    
    def set_server(self, server):
        """Set the server connection that the client talks to"""
        self.server = server
        
    def redraw(self, rects=None):
        """Redraw entire screen. Unfortunately required for scrolling text"""
        if self is not _active: return
        if rects is None:
            _display.blit(self.background, (0, 0))
	    self.sobjects.draw(_display)
            self.sprites.draw(_display)
            self.text.draw(_display)
            self.balloons.draw(_display)
            pygame.display.update()
        else:
            if not type(rects) is ListType: rects = [rects]
            for rect in rects:
                _display.blit(self.background, rect, rect)
		self.sobjects.draw(_display, rect)
                self.sprites.draw(_display, rect)
                self.text.draw(_display, rect)
                self.balloons.draw(_display, rect)
                
            pygame.display.update(rects)

    def debug(self, s):
        """Handle debug messages"""
        print >> sys.stderr, "DEBUG: %s" % s

    def write(self, s):
        dirtyrects = self.console.add_lines(string.split(s, '\n'))
        if self.console_active: return dirtyrects
        else: return []

    # Command handlers
    
    def cmd_connect(self, host, port):
	self.write('-> Changing rooms to %s:%d' % (host, port))
	# delete all objects/avatars here
	self.sprites = Group()
        self.avatars = {}
	self.exits = {}
	self.urls = []
	self.set_title('PythonVerse')
	self.redraw()
	self.server.new_connect(host, port)

    def cmd_reconnect(self):
	host, port = self.server.gethostport()
	self.cmd_connect(host, port)

    def cmd_nick(self, nick):
        self.server.set_nick(nick)

    def cmd_ignore(self, nick, what):
	self.server.ignore(what, nick)

    def cmd_unignore(self, nick, what):
	self.server.unignore(what, nick)

    def cmd_quote(self, text):
        self.server.quote(text)

    def cmd_avatar(self, avatar):
        self.server.set_avatar(avatar)

    def cmd_msg(self, nicks, text):
        dirtyrects = self.write('!Whispering to %s! %s' % (nicks, text))
        nicks = string.split(nicks, ',')
        nick = self.server.privmsg(nicks, text)
	avatar = self.avatars[nick]
        dirtyrects.extend(avatar.chat(self.balloons, text))
        dirtyrects.append(avatar.rect)
        self.redraw(dirtyrects)

    def cmd_url(self, nicks, url):
	nicks = string.split(nicks, ',')
	self.server.url(nicks, url)

    def cmd_push(self):
	self.server.push()

    def cmd_effect(self, action):
	self.server.effect(action)

    def cmd_whois(self, nick):
        self.server.whois(nick)

    # UI functions

    def test_rect(self):
        rects = invertrect(Rect(0, 0, 640, 480 - (linesize(_font) + 2)), map(lambda s: s.rect, self.sprites.list + self.balloons.list))
        for r in rects: pygame.draw.rect(_display, (0, 0, 0), r, 1)
        pygame.display.update()

    def poll(self):
        """Update moving stuff, clean up old balloons, etc"""
        dirtyrects = self.sprites.update()
        dirtyrects.extend(self.balloons.update())
	# purge old URLs
	if self.urls != []:
	    timetodie, url = self.urls[0]
	    if pygame.time.get_ticks() >= timetodie:
		del self.urls[0]
		try: urlmo = self.avatars[url]
		except KeyError: self.debug('No URL %s' % url)
		else:
		    dirtyrects.append(urlmo.rect)
		    del self.avatars[url]
		    try: del self.exits['{' + url + '}_link']
		    except: print >> sys.stderr, url, 'is not an exit.'
		    self.sprites.remove(urlmo)
        if dirtyrects: self.redraw(dirtyrects)
        
    def mouse(self, event):
        """Check for mouse movement"""
        
        dirtyrects = self.sprites.mouse(event.pos)
        if dirtyrects: self.redraw(dirtyrects)

    def toggle_console(self):
        if self.console_active:
            self.console_active = 0
            self.text.remove(self.console)
        else:
            self.console_active = 1
            self.text.add(self.console)
            
        self.redraw(self.console.rect)

    def handle_event(self, event):
        if event.type == KEYDOWN:
            if event.mod & KMOD_CTRL:
                if event.key == K_u: self.redraw(self.entry.clear())
		if event.key == K_UP: self.redraw(self.console.scroll_up(1))
		if event.key == K_DOWN: self.redraw(self.console.scroll_down(1))
		if event.key == K_PAGEUP:
		    self.redraw(self.console.scroll_up(self.console.numlines))
		if event.key == K_PAGEDOWN:
		    self.redraw(self.console.scroll_down(self.console.numlines))
            elif event.key == K_BACKSPACE: self.redraw(self.entry.backspace())
            elif event.key == K_DELETE: self.redraw(self.entry.delete())
            elif event.key == K_LEFT: self.redraw(self.entry.move_cursor(-1))
            elif event.key == K_RIGHT: self.redraw(self.entry.move_cursor(1))
            elif event.key == K_UP: self.redraw(self.entry.history(-1))
            elif event.key == K_DOWN: self.redraw(self.entry.history(1))
            elif event.key == K_HOME: self.redraw(self.entry.home())
            elif event.key == K_END: self.redraw(self.entry.end())
            elif event.key == K_RETURN:
                text = self.entry.text
                self.redraw(self.entry.clear())
                if text and text[0] == '/':
                    text = text[1:]
                    if text and text[0] != '/':
                        try: self.handler.handle(text)
                        except transutil.HandlerError, info: self.debug(info)
                        return
                
                self.server.chat(text.encode('utf-8'))
            else: self.redraw(self.entry.insert(event.unicode))
        elif event.type == MOUSEBUTTONDOWN:
	    if self.whichmouseover == '':
		self.server.move(event.pos)
	    else:
		if self.whichmouseover != 'ov_tram_exit':
		    self.whichmouseover = '{' + self.whichmouseover + '}_link'
		try: host, port = self.exits[self.whichmouseover]
		except: self.server.move(event.pos)
		else:
		    if port == -1:
				if os.fork() == -1: webbrowser.open(host)
		    if port == -2:
			realhost, realport, filename, filesize = host
			# TODO: file transfers
			if port > 0:
				self.cmd_connect(host, port)

    # Transport-called functions
    
    def background_image(self, image):
        """Change the background"""
        self.background = image
        self.redraw()

    def background_progress(self, length, filename, size):
        self.background = progress(float(length)/float(size), (640, 480),
                                   (0, 0, 255), (0, 0, 0))
        self.redraw()

    def set_title(self, title):
        """Change the room's name"""
        pygame.display.set_caption(title)

    def raise_object(self, name):
	"""Raise the named object to the top of the stacking order."""
        try: avatar = self.avatars[name]
        except: self.debug('No avatar called %s' % name)
        else:
            self.sprites.above(avatar)
            self.redraw(avatar.rect)

    def mouseover(self, name, pos, image1, image2):
        """Create a mouseover object"""
        mo = Mouseover(self.server, name, pos, image1, image2)
        self.sprites.add(mo)
        self.avatars[name] = mo
        self.redraw(mo.rect)

    def newimage(self, filename=None):
        """Load an image from a file object"""
        #self.debug('newimage %s' % repr(filename))
        if filename is None: return progress(0)
        image = pygame.image.load(filename).convert_alpha()
        return image

    def new_avatar(self, nick, pos, image, noffset, boffset):
        # Fixme: need a real default image
        avatar = Avatar(self.server, self.sprites, pos, image, nick, noffset, boffset)
        self.sprites.add(avatar)
        self.avatars[nick] = avatar
        dirtyrects = self.write('*%s* entered the room.' % nick)
#        dirtyrects.extend(avatar.chat(self.balloons,
#                                      '*%s* entered the room.' % nick))
        dirtyrects.append(avatar.rect)
        self.redraw(dirtyrects)

    def del_avatar(self, nick):
        try:
            avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else:
            dirtyrects = self.write('*%s* left the room.' % nick)
#            dirtyrects.extend(avatar.chat(self.balloons,
#                                          '*%s* left the room' % nick))
            dirtyrects.append(avatar.rect)
            del self.avatars[nick]
            self.sprites.remove(avatar)
            self.redraw(dirtyrects)
    
    def exit_obj(self, name, host, port):
	if host != 'dummyhost':
	    self.exits[name] = (host, port)
	else:
	    del self.exits[name]

    def avatar(self, nick, image, noffset, boffset):
        """Change the avatar for a nick"""
        # FIXME: need to handle other parameters (bubble position, nametag)
        try: avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else:
            self.redraw(avatar.set_image(image))
            avatar.set(noffset, boffset)

    def mouseover_image1(self, name, image):
        """Set the unactivated image for a mouseover"""
        self.redraw(self.avatars[name].set_image1(image))

    def mouseover_image2(self, name, image):
        """Set the activated image for a mouseover"""
        self.avatars[name].set_image2(image)
        
    def avatar_image(self, nick, image):
        """Set the image for an avatar"""
        try: avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else:
            self.redraw(avatar.set_image(image))

    def avatar_progress(self, nick, length, filename, size):
        """Set an avatar's image to a progress bar"""
        try: avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else:
            rect = avatar.set_image(progress(float(length)/float(size)))
            self.redraw(rect)

    def move_avatar(self, nick, x, y, speed):
        try: avatar = self.avatars[nick]
        except: self.debug('No avatar called %s' % nick)
        else:
            avatar.move((x, y), speed)

    def effect(self, nick, action):
	action.lower()
	try: avatar = self.avatars[nick]
	except: self.debug('No avatar called %s' % nick)
	else: avatar.effect(action)

    def privmsg(self, nick, s):
        dirtyrects = self.write('!%s whispers! %s' % (nick, s))
        try: avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else: self.redraw(dirtyrects + avatar.chat(self.balloons, s))

    def url(self, nick, url):
	try: test = self.avatars[url]
	except:
	    dirtyrects = self.write('[URL from %s] %s' % (nick, url))
	    # TODO: the 30000 should be replaced with URL timeout from Setup
	    self.urls.append((pygame.time.get_ticks() + 30000, url))
	    # parse URL to find out what we want to do with it
	    text = 'Bad'
	    tagcolor = (255, 0, 0)
	    head = url[0:3]
	    head.lower()
	    if head == 'ope':
		offset = url.find('://') + 3
		offset2 = url.find(':', offset)
		host = url[offset:offset2]
		port = int(url[offset2+1:])
		self.exit_obj('{' + url + '}_link', host, port)
		text = 'OV:'
		tagcolor = (255, 255, 0)
	    if head == 'fil':
		offset = url.find('://') + 3
		offset2 = url.find(':', offset)
		offset3 = url.find('/', offset2)
		offset4 = url.find(':', offset3)
		host = url[offset:offset2]
		port = url[offset2+1:offset3]
		filename = url[offset3+1:offset4]
		filesize = url[offset4:]
		text = 'File'
		tagcolor = (0, 255, 255)
		self.exit_obj('{' + url + '}_link' ,
			     (host, port, filename, filesize), -2)
	    if head == 'htt' or head == 'ftp' or head == 'mai':
		text = 'URL'
		tagcolor = (0, 255, 0)
		self.exit_obj('{' + url + '}_link', url, -1)
	    # generate images for URL links
	    image1 = pygame.Surface((40, 16))
	    image2 = pygame.Surface((40, 16))
	    image1.set_colorkey((255,165,0))
	    image1.fill((255,165,0))
	    image2.set_colorkey((255,165,0))
	    image2.fill((255,165,0))
	    pygame.draw.polygon(image2, (0, 0, 0),
	        ((1,1), (1, 15), (39, 15), (39, 6), (34, 1), (1,1)),
	        0)
	    pygame.draw.polygon(image2, tagcolor,
	        ((0,0), (0, 14), (38, 14), (38, 5), (33, 0), (0,0)),
	        0)
	    pygame.draw.polygon(image2, (0, 0, 255),
	        ((0,0), (0, 14), (28, 14), (28, 5), (23, 0), (0,0)),
	        0)
	    image2.blit(_font.render(text, 1, (255, 255, 255)), (2, 0))
	    image1.blit(image2, (0, 0))
	    image1.set_alpha(127)
	    pos = (int((random.random()*600)+20), int((random.random()*445)+10))
	    urlmo = Mouseover(self.server, url, pos, image1, image2)
	    self.sprites.add(urlmo)
	    self.avatars[url] = urlmo
	    dirtyrects.append(urlmo.rect)
	    self.redraw(dirtyrects)

    def chat(self, nick, s):
        dirtyrects = self.write('<%s> %s' % (nick, s))
        try: avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else: dirtyrects.extend(avatar.chat(self.balloons, s))
        self.redraw(dirtyrects)

    def setmouseover(self, name):
	if name == 'ov_tram_exit':
	    try: test = self.exits[name]	# is this really the ORT?
	    except: self.whichmouseover = ''
	    else: self.whichmouseover = name
	else:
	    self.whichmouseover = name


# Exported functions

def linesize(font):
    # Bug in pygame or SDL_ttf?
    size = font.get_linesize()
    if size == 0:
        size = font.get_height()

    return size


_clients = []
_display = None
_active = None

def init():
    global _font, _display, _lastpoll
    
    pygame.init()
    _display = pygame.display.set_mode((640, 480))
    # display startup image
    s = pygame.display.get_surface()
    i = pygame.image.load('pvstart.jpg')
    s.blit(i, (0, 0))
    # other settings
    pygame.display.set_caption('PythonVerse')
    _font = pygame.font.Font('geneva.ttf', 12)
    pygame.key.set_repeat(300, 30)
    pygame.event.set_blocked((ACTIVEEVENT, KEYUP, MOUSEBUTTONUP,
                              JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION,
                              JOYBUTTONUP, JOYBUTTONDOWN, VIDEORESIZE,
                              VIDEOEXPOSE))
    _lastpoll = pygame.time.get_ticks()


def poll():
    global _lastpoll
    # Handle mouseevents
    events = pygame.event.get(MOUSEMOTION)
    if events:
        event = events[-1]
        _active.mouse(event)
        
    events = pygame.event.get((KEYDOWN, MOUSEBUTTONDOWN, QUIT))
    pygame.event.pump()
    for event in events:
        if event.type == KEYDOWN:
            if event.key == K_ESCAPE:
                pygame.event.post(pygame.event.Event(QUIT))
            elif event.mod & KMOD_ALT:
                if event.key == K_f: pygame.display.toggle_fullscreen()
                elif event.key == K_c: _active.toggle_console()
                elif event.key == K_t: _active.test_rect()
            else: _active.handle_event(event)

        elif event.type == QUIT: return -1
        else: _active.handle_event(event)

    i = len(_clients)
    while i:
        i = i - 1
        done = _clients[i].poll()
        if done: del _clients[i]

    now = pygame.time.get_ticks()
    delay = 15 - (now - _lastpoll)
    _lastpoll = now
    if delay < 0: return 0
    return delay/1000.0
    
                

