# -*-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, thread, asyncore, time
import OpenVerse, transutil
from wxPython.wx import *
from math import *
from types import *

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

    return r

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

    lines = []
    while text:
        if dc.GetTextExtent(text)[0] <= width: return lines + [text]
        try:
            i = string.rindex(text, ' ')
            while dc.GetTextExtent(text[:i])[0] > width:
                i = string.rindex(text, ' ', 0, i)
        except ValueError:
            i = len(text)-1
            while dc.GetTextExtent(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)):
    w, h = size
    bitmap = wxEmptyBitmap(w, h)
    y = h * fraction
    dc = wxMemoryDC()
    dc.SelectObject(bitmap)
    dc.SetPen(wxTRANSPARENT_PEN)
    dc.SetBrush(wxBLACK_BRUSH)
    dc.DrawRectangle(0, 0, w, h-y)
    dc.SetBrush(wxGREEN_BRUSH)
    dc.DrawRectangle(0, h-y+1, w, y)
    return bitmap


def invertrect(left, top, width, height, rects, min_width=50, min_height=10):
    irects = []
    rects = rects + [(left-1, top, 0, height),
                     (left, top-1, width, 0),
                     (left+width, top, 0, height),
                     (left, top+height, width, 0)]

    for ax, ay, aw, ah in rects:
        # Pick a left side
        l = ax+aw
        if l < left: continue
        # Pick a top
        for bx, by, bw, bh in rects:
            # Make sure the rect can actually border ours
            if bx+bw <= l or by+bh > ay+ah: continue
            # Pick a top
            t = by+bh
            # Pick a right side
            for cx, cy, cw, ch in rects:
                # Make sure this rect can border ours
                if cx-1 <= l or cy+ch <= t or \
                   cy >= top+height or \
                   cx-1 <= bx: continue
                r = cx-1
                #w = r - l
                #h = bottom - t
                if r-l < min_width or top+height-t < min_height: continue
                bot = top+height-1
                # There can be only one rect with these three sides
                #rect = wxRect(l, t, w, h)
                # Now find the bottom
                for dx, dy, dw, dh in rects:
                    # Check if this rect overlaps ours
                    if r >= dx and l < dx+dw and \
                       bot >= dy and t < dy+dh:
                        bot = dy-1
                        # Make sure the rect is still sane and still borders
                        # on the original border rects
                        if bot-t < min_height or \
                           bot < ay or bot < cy: break
                else: irects.append((l, t, r-l+1, bot-t+1))
                
    return irects


class Sprite(wxEvtHandler):
    mouseme = 0
    pollme = 0
    def __init__(self, parent, x, y):
        wxEvtHandler.__init__(self)
        self.dead = 0
        self.parent = parent
        self.x = x
        self.y = y
        self.rect = None
        self.parent.AddSprite(self)

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

    def move(self, x, y):
        self.x = x
        self.y = y
        self.CalcRect()

    def SetRect(self, x, y, w, h):
        dc = wxClientDC(self.parent)
        if self.rect is not None:
            ox, oy, ow, oh = self.rect
            self.rect = x, y, w, h
            if ox+oh > x and ox <= x+w and oy+oh > y and oy <= y+h:
                # The rectangles overlap
                nx = min(x, ox)
                ny = min(y, oy)
                w = max(x+w, ox+ow) - nx
                h = max(y+h, oy+oh) - ny
                x = nx
                y = ny
            else: self.parent.DoDrawing(dc, ox, oy, ow, oh)
        else: self.rect = x, y, w, h
        self.parent.DoDrawing(dc, x, y, w, h)
        

class Mouseover(Sprite):
    def __init__(self, parent, x, y, image1, image2):
        self.image1 = image1
        self.image2 = image2
        self.on = 0
        Sprite.__init__(self, parent, x, y)
        self.CalcRect()

    def mouseon(self):
        self.on = 1
        self.CalcRect()
        
    def mouseoff(self):
        self.on = 0
        self.CalcRect()
    
    def set_image1(self, image):
        self.image1 = image
        self.CalcRect()

    def set_image2(self, image):
        self.image2 = image
        self.CalcRect()

    def CalcRect(self):
        if self.on: image = self.image2
        else: image = self.image1
        w = image.GetWidth()
        h = image.GetHeight()
        x = self.x-w/2
        y = self.y-h/2
        self.SetRect(x, y, w, h)

    def draw(self, dc):
        wxLogVerbose('draw')
        if self.on: image = self.image2
        else: image = self.image1
        w = self.image2.GetWidth()
        h = self.image2.GetHeight()
        dc.DrawBitmap(image, self.x-w/2, self.y-h/2, 1)


class MoveTimer(wxTimer):
    """Call the update function for avatars"""
    def __init__(self):
        wxTimer.__init__(self)
        self.sprites = []
        
    def Notify(self):
        t = time.time()
        sprites = self.sprites
        for i in range(len(sprites)-1, -1, -1):
            sprite = sprites[i]
            r = sprite.update(t)
            if r: del sprites[i]

        if not sprites: self.Stop()

    def add(self, sprite):
        if sprite in self.sprites: return
        self.sprites.append(sprite)
        if len(self.sprites) == 1:
            wxLogVerbose('starting timer')
            self.Start(40)

_timer = MoveTimer()

        
class Avatar(Sprite):
    mouseme = 1
    def __init__(self, parent, x, y, bitmap, nick, noffset, boffset):
        Sprite.__init__(self, parent, x, y)
        self.bitmap = bitmap
        self.nick = nick
        self.destpos = None
        self.speed = None
        self.balloon = None
        self.label_on = 1
        self.set(noffset, boffset)

    def SaneNametagOffset(self):
        w = self.bitmap.GetWidth()
        h = self.bitmap.GetHeight()
        nx = max(-w/2-10, min(self.nx, w/2+10))
        ny = max(-h/2-10, min(self.ny, h/2+10))
        return nx, ny

    def SaneBalloonOffset(self):
        w = self.bitmap.GetWidth()
        h = self.bitmap.GetHeight()
        bx = max(-w/2-10, min(self.bx, w/2+10))
        by = max(-h/2-10, min(self.by, h/2+10))
        return bx, by

    def Balloon(self):
        w = self.bitmap.GetWidth()
        h = self.bitmap.GetHeight()
        bx, by = self.SaneBalloonOffset()
        if self.balloon is None or self.balloon.dead:
            self.balloon = Balloon(self.parent, self, self.x+bx, self.y+by)
            
        return self.balloon

    def SetImage(self, bitmap):
        self.bitmap = bitmap
        self.CalcRect()

    def draw(self, dc):
        nx, ny = self.SaneNametagOffset()
        dc.DrawBitmap(self.bitmap, self.x-self.bitmap.GetWidth()/2,
                      self.y-self.bitmap.GetHeight()/2, 1)
        if self.label_on:
            dc.SetFont(wxSMALL_FONT)
            w, h = dc.GetTextExtent(self.nick)
            dc.SetTextForeground(wxCYAN)
            dc.SetLogicalFunction(wxXOR)
            dc.DrawText(self.nick, self.x+nx-w/2, self.y+ny-h/2)
            dc.SetTextForeground(wxBLACK)
            dc.SetLogicalFunction(wxCOPY)

    def CalcRect(self):
        w = self.bitmap.GetWidth()
        h = self.bitmap.GetHeight()
        x = self.x-w/2
        y = self.y-h/2
        if self.label_on:
            dc = wxClientDC(self.parent)
            dc.SetFont(wxSMALL_FONT)
            tw, th = dc.GetTextExtent(self.nick)
            nx, ny = self.SaneNametagOffset()
            tx, ty = self.x+nx-tw/2, self.y+ny-th/2
            x, y, w, h = RectUnion((x, y, w, h),
                                   (tx, ty, tw, th))
        self.SetRect(x, y, w, h)

    def set(self, noffset, boffset):
        self.bx, self.by = boffset
        self.nx, self.ny = noffset
        self.CalcRect()
        
    def AnimateMove(self, position, speed):
        """Changes the location of the avatar's center to the new position"""
        wxLogVerbose('AnimateMove')
        if position[0] >= 640 or position[1] > 480:
            wxLogWarning('Attempt to move outside the screen')
            return
        self.speed = speed
        pos = self.x, self.y
        self.startpos = pos
        self.starttime = time.time()
        distance = dist(pos, position)
        if distance == 0.0: return
        self.dx = 300.0 * (position[0]-pos[0]) * self.speed / distance 
        self.dy = 300.0 * (position[1]-pos[1]) * self.speed / distance 
        self.stoptime = self.starttime + distance / 300.0 / speed
        self.destpos = position
        _timer.add(self)

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

        if t >= self.stoptime:
            self.move(self.destpos[0], self.destpos[1])
            if self.balloon is not None and not self.balloon.dead:
                bx, by = self.SaneBalloonOffset()
                x, y = self.destpos
                self.balloon.move(x+bx, y+by)
                
            return 1
        else:
            delta_t = t - self.starttime
            x, y = self.startpos
            x = x+int(round(self.dx*delta_t))
            y = y+int(round(self.dy*delta_t))
            self.move(x, y)
      

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, x, y):
    """Find the closest point on a rect to a given point"""
    rx, ry, rw, rh = rect
    return min(rx+rw-1, max(x, rx)), min(ry+rh-1, max(y, ry))


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


def rectdist(rect, x, y):
    return dist(closest(rect, x, y), (x, y))


def ClampRect(r1, r2):
    x1, y1, w1, h1 = r1
    x2, y2, w2, h2 = r2
    x = max(x1, x2)
    y = max(y1, y2)
    if x+w1 > x2+w2: x = x2+w2-w1
    if y+h1 > y2+h2: y = y2+h2-h1
    return x, y, w1, h1


def RectUnion(r1, r2):
    x1, y1, w1, h1 = r1
    x2, y2, w2, h2 = r2
    x = min(x1, x2)
    y = min(y1, y2)
    w = max(x1+w1, x2+w2)-x
    h = max(y1+h1, y2+h2)-y
    return x, y, w, h


class Balloon(Sprite):
    """A speech balloon"""
    # Padding for balloon rectangles, also radius of corner arcs
    pad = 3
    
    def __init__(self, parent, avatar, x, y):
        Sprite.__init__(self, parent, x, y)
        self.avatar = avatar
        self.text = []
        #EVT_TIMER(self, -1, self.OnTimer)

    def OnTimer(self):
        if self.dead: return
        del self.text[0]
        if self.text: self.CalcRect()
        else:
            self.dead = 1
            self.parent.RemoveSprite(self)
        
    def move(self, x, y):
        self.x = x
        self.y = y
        self.CalcRect()

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

    def CalcRect(self):
        rects = self.parent.BalloonRects(self)
        dc = wxClientDC(self.parent)
        dc.SetFont(wxSMALL_FONT)
        notdone = 1
        while notdone:
            # Loop until we can fit the balloon into *some* rect
            for r in rects:
                try: lines = wrap_lines(dc, self.text, r[2]-self.pad*2)
                except ValueError: continue
                height = 0
                for l in lines: height = height + dc.GetTextExtent(l)[1]
                if height < r[3]:
                    notdone = 0
                    break
            else:
                # Couldn't render the balloon, delete some lines
                del self.text[0]

        # Count the number of lines in each sequence of text
        width = max(map(lambda l, dc=dc: dc.GetTextExtent(l)[0], lines)) +\
                (self.pad * 2)

        self.lines = lines
        self.textrect = ClampRect((self.x-width/2, self.y-height/2, width,
                                   height), r)
        bigrect = RectUnion(self.textrect,
                            (self.x, self.y, 1, 1))
        self.SetRect(bigrect[0], bigrect[1], bigrect[2], bigrect[3])

    def draw(self, dc):
        # The rectangles had better already be calculated.
        pad = self.pad
        left, top, w, h = self.textrect
        right = left+w-1
        bottom = top+h-1
        dc.SetPen(wxBLACK_PEN)
        dc.DrawRoundedRectangle(left, top, w, h, pad)
        x = self.x
        y = self.y
        if x < left or x > right or y < top or y > bottom:
            cx, cy = closest(self.textrect, x, y)
            if x > right-pad*2:
                # Drawing to the east
                if y > bottom-pad*2:
                    # Draw the arrow to the southeast
                    x1 = right
                    y1 = bottom-pad
                    x2 = right-pad
                    y2 = bottom
                elif y < top+pad*2:
                    # Draw the arrow to the northeast
                    x1 = right
                    x2 = right-pad
                    y2 = top+1
                    y1 = top+pad
                elif x > right:
                    # Due east
                    x1 = right
                    y1 = cy-pad
                    x2 = right
                    y2 = cy+pad
            elif x < left+pad*2:
                # Drawing to the west
                if y > bottom-pad*2:
                    # Southwest
                    x1 = left
                    y1 = bottom-pad
                    x2 = left+pad
                    y2 = bottom
                elif y < top+pad*2:
                    # Northwest
                    x1 = left
                    y1 = top+pad
                    x2 = left+pad
                    y2 = top+1
                elif x < left:
                    # Due west
                    x1 = left+1
                    y1 = cy+pad
                    x2 = left+1
                    y2 = cy-pad
            else:
                # Top or bottom
                x1 = cx-pad
                x2 = cx+pad
                if y < top:
                    # Due north
                    y1 = top+1
                    y2 = top+1
                elif y > bottom:
                    # Due south
                    y1 = bottom
                    y2 = bottom
            points = [wxPoint(x1, y1), wxPoint(x, y), wxPoint(x2, y2)]
            dc.SetPen(wxTRANSPARENT_PEN)
            dc.DrawPolygon(points)
            dc.SetPen(wxBLACK_PEN)
            dc.DrawLines(points)
        
        ty = top
        tx = left + w/2
        dc.SetFont(wxSMALL_FONT)
        for line in self.lines:
            lw, lh = dc.GetTextExtent(line)
            dc.DrawText(line, tx-lw/2, ty)
            ty = ty + lh

        dc.SetPen(wxNullPen)
        

    def add_text(self, text, timeout=10):
        """Add text to the balloon, scrolling it if necessary."""
        self.text.append(text)
        self.CalcRect()
        _scheduler.ScheduleRel(10, self.OnTimer, ())
    

class ClientCanvas(wxScrollingWindow):
    def __init__(self, parent):
        wxPanel.__init__(self, parent, -1)
        self.parent = parent
        self.sprites = []
        self.spriterects = {}
        #self.background_dc = wxMemoryDC()
        self.dc = wxMemoryDC()
        self.SetBackground(wxEmptyBitmap(640, 480))
        EVT_PAINT(self, self.OnPaint)
        EVT_LEFT_DOWN(self, self.OnLeftButtonEvent)

    def AddSprite(self, sprite):
        self.sprites.append(sprite)
        if sprite.rect is None: return
        x, y, w, h = sprite.rect
        self.DoDrawing(wxClientDC(self), x, y, w, h)

    def RemoveSprite(self, sprite):
        self.sprites.remove(sprite)
        if sprite.rect is None: return
        x, y, w, h = sprite.rect
        self.DoDrawing(wxClientDC(self), x, y, w, h)

    def BalloonRects(self, balloon):
        w = self.background.GetWidth()
        h = self.background.GetHeight()
        x = balloon.x
        y = balloon.y
        rects = map(lambda s: s.rect,
                    filter(lambda s,n=balloon: s!=n,
                           self.sprites))
        irects1 = invertrect(0, 0, w, h, rects)
        irects1 = filter(lambda r,x=x,y=y: rectdist(r, x, y) <= 200, irects1)
        irects1.sort(balloon.nearer)
        rects = map(lambda s: s.rect,
                    filter(lambda s,n=balloon:
                           s!=n and s.__class__ is Balloon, self.sprites))
        rects.append(balloon.avatar.rect)
        irects2 = invertrect(0, 0, w, h, rects)
        irects2.sort(balloon.nearer)

        return irects1 + irects2

    def Balloon(self, nick, text):
        try: avatar = self.parent.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else:
            dc = wxClientDC(self)
            w = self.background.GetWidth()
            h = self.background.GetHeight()
            balloon = avatar.Balloon()
            if balloon not in self.sprites: self.sprites.append(balloon)
            balloon.add_text(text)

    def SetBackground(self, bitmap):
        #self.background_dc.SelectObject(bitmap)
        self.background = bitmap
        self.width = bitmap.GetWidth()
        self.height = bitmap.GetHeight()
        self.SetClientSizeWH(self.width, self.height)
        self.SetSizeHints(self.width, self.height, self.width, self.height)
        self.bitmap = wxEmptyBitmap(self.width, self.height)
        self.dc.SelectObject(self.bitmap)
        self.SetScrollbars(20, 20, self.width/20, self.height/20)
        self.DoDrawing(wxClientDC(self), 0, 0, self.width, self.height)

    def OnLeftButtonEvent(self, event):
        x = event.GetX()
        y = event.GetY()
        self.parent.server.move((x, y))
        self.parent.entry.SetFocus()

    def OnPaint(self, event):
        dc = wxPaintDC(self)
        #self.PrepareDC(dc)
        upd = wxRegionIterator(self.GetUpdateRegion())
        while upd.HaveRects():
            x = upd.GetX()
            y = upd.GetY()
            w = upd.GetW()
            h = upd.GetH()
            self.DoDrawing(dc, x, y, w, h)
            upd.Next()

    def DoDrawing(self, dc, x, y, w, h):
        wxLogVerbose('%d %d %d %d' % (x, y, w, h)) 
        dc.SetClippingRegion(x, y, w, h)
        r = x+w-1
        b = y+h-1
        dc.BeginDrawing()
        dc.DrawBitmap(self.background, 0, 0)
        #dc.Blit(x, y, w, h, self.background_dc, x, y)
        for sprite in self.sprites:
            # Draw the sprite if its rect overlaps
            if sprite.rect is None:
                wxLogVerbose('Null rect for sprite')
                continue
            sx, sy, sw, sh = sprite.rect
            if sx+sw > x and sx <= x+w and sy+sh > y and sy <= y+h:
                sprite.draw(dc)

        dc.EndDrawing()
        dc.DestroyClippingRegion()
        #dc.Blit(x, y, w, h, mdc, x, y)


class Client(wxPanel):
    """Callbacks for the server connection"""
    def __init__(self, frame, parent, id, host, port, nick, avatar):
        wxPanel.__init__(self, parent, id)
        self.frame = frame
        self.parent = parent
        self.nick = nick
        self.title = '%s:%s' % (host, port)
        self.canvas = ClientCanvas(self)
        hbox = wxBoxSizer(wxHORIZONTAL)
        hbox.Add(self.canvas, 0, 0)
        self.listbox = wxListBox(self, -1,
                                 style=wxLB_EXTENDED|wxLB_NEEDED_SB|wxLB_SORT) 
        hbox.Add(self.listbox, 1, wxEXPAND)
        
        self.sizer = wxBoxSizer(wxVERTICAL)
        self.sizer.Add(hbox, 0, wxEXPAND)
        self.textchat = wxTextCtrl(self, -1, style=wxTE_MULTILINE|\
                                   wxTE_READONLY|wxTE_RICH)
        self.sizer.Add(self.textchat, 1, wxEXPAND)
        self.entry = wxTextCtrl(self, -1, style=wxTE_PROCESS_ENTER)
        self.sizer.Add(self.entry, 0, wxEXPAND)
        self.SetAutoLayout(true)
        self.SetSizer(self.sizer)
        self.sizer.SetSizeHints(self)
        self.server = OpenVerse.ServerConnection(host, port, self, nick,
                                                 avatar)
        self.avatars = {}
        self.handler = transutil.InputHandler(
            (('nick', self.cmd_nick, '(\S+)', (str,)),
             ('quote', self.cmd_quote, '(.*)', (str,)),
             ('avatar', self.cmd_avatar, '(\S+)', (str,)),
             ('whois', self.cmd_whois, '(\S+)', (str,))))
        EVT_TEXT_ENTER(self, self.entry.GetId(), self.OnSendText)

    # Event handlers

    def OnSendText(self, event):
        text = self.entry.GetValue()
        self.entry.SetValue('')
        self.entry.SetFocus()
        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)
        
    # Utility functions

    def debug(self, s):
        """Handle debug messages"""
        wxLogVerbose(s)

    # Command handlers
    
    def cmd_nick(self, nick):
        self.nick = nick
        self.frame.rename(self, '%s - %s' % (self.title, nick))
        self.server.set_nick(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):
        nicks = string.split(nicks, ',')
        self.server.privmsg(nicks, text)

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

    # Transport-called functions

    def close(self):
        self.frame.close(self)

    def background_image(self, image):
        """Change the background"""
        self.canvas.SetBackground(image)

    def background_progress(self, length, filename, size): pass
        #self.background_image(progress(float(length)/float(size)))

    def set_title(self, title):
        """Change the room's name"""
        self.title = title
        self.frame.rename(self, '%s - %s' % (title, self.nick))

    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.canvas.sprites.above(avatar)

    def mouseover(self, name, pos, image1, image2):
        """Create a mouseover object"""
        x, y = pos
        mo = Mouseover(self.canvas, x, y, image1, image2)
        self.avatars[name] = mo

    def newimage(self, filename=None):
        """Load an image from a file object"""
        self.debug('newimage %s' % repr(filename))
        if filename is None: return wxNullBitmap
        return wxImage(filename).ConvertToBitmap()

    def new_avatar(self, nick, pos, image, noffset, boffset):
        x, y = pos
        avatar = Avatar(self.canvas, x, y, image, nick, noffset, boffset)
        self.avatars[nick] = avatar
        self.listbox.Append(nick)
        self.canvas.Balloon(nick, '*%s* has entered the room.' % nick)

    def del_avatar(self, nick):
        self.textchat.AppendText('*%s* left the room.\n' % nick)
        self.canvas.Balloon(nick, '*%s* has left the room.' % nick)
        self.listbox.Delete(self.listbox.FindString(nick))
        try: avatar = self.avatars[nick]
        except KeyError: pass
        else:
            self.canvas.RemoveSprite(avatar)
            del self.avatars[nick]
    
    def avatar(self, nick, image, noffset, boffset):
        """Change the avatar for a nick"""
        try: avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else:
            avatar.SetImage(image)
            avatar.set(noffset, boffset)

    def mouseover_image1(self, name, image):
        """Set the unactivated image for a mouseover"""
        mo = self.avatars[name]
        mo.set_image1(image)
        #self.canvas.DoDrawing(mo.rect.x, mo.rect.y, mo.rect.width,
        #                      mo.rect.height)

    def mouseover_image2(self, name, image):
        """Set the activated image for a mouseover"""
        mo = self.avatars[name]
        mo.set_image2(image)
        #self.canvas.DoDrawing(mo.rect.x, mo.rect.y, mo.rect.width,
        #                      mo.rect.height)

    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:
            avatar.SetImage(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: avatar.SetImage(progress(float(length)/float(size)))

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

    def privmsg(self, nick, s):
        self.textchat.AppendText('[%s] %s\n' % (nick, s))
        try: avatar = self.avatars[nick]
        except KeyError: self.debug('No avatar called %s' % nick)
        else: self.canvas.Balloon(nick, s)

    def chat(self, nick, s):
        self.textchat.AppendText('<%s> %s\n' % (nick, s))
        self.canvas.Balloon(nick, s)


class Frame(wxFrame):
    """The main frame of the application"""
    def __init__(self, parent, ID, title):
        wxFrame.__init__(self, parent, ID, title,
                         wxDefaultPosition)
        self.CreateStatusBar()
        self.SetStatusText("This is the statusbar")

        ID_ABOUT = wxNewId()
        ID_EXIT = wxNewId()
        ID_CONNECT = wxNewId()
        
        menu = wxMenu()
        menu.Append(ID_CONNECT, "&Connect", "Connect to a new room")
        menu.AppendSeparator()
        menu.Append(ID_EXIT, "E&xit", "Terminate the program")
        
        menuBar = wxMenuBar()
        menuBar.Append(menu, "&File")

        menu = wxMenu()
        menu.Append(ID_ABOUT, "&About",
                    "More information about this program")
        menuBar.Append(menu, "&Help")
        
        self.SetMenuBar(menuBar)
        EVT_MENU(self, ID_ABOUT, self.OnAbout)
        EVT_MENU(self, ID_EXIT,  self.TimeToQuit)
        EVT_MENU(self, ID_CONNECT, self.OnConnection)

        self.client_nb = wxNotebook(self, -1)
        self.client_nb.AddPage(LogWindow(self.client_nb), "Log")
        self.sizer = wxNotebookSizer(self.client_nb)
        self.SetAutoLayout(true)
        self.SetSizer(self.sizer)

    def GetClientPage(self, client):
        """Return the page number for a given client"""

        ID = client.GetId()
        for i in range(self.client_nb.GetPageCount()):
            page = self.client_nb.GetPage(i)
            if page.GetId() == ID:
                return i
        else: raise ValueError, 'Nonexistent page'

    def close(self, client):
        self.client_nb.DeletePage(self.GetClientPage(client))

    def rename(self, client, name):
        self.client_nb.SetPageText(self.GetClientPage(client), name)
            
    def resize(self):
        self.Fit()
        self.sizer.SetSizeHints(self)
       
    def OnAbout(self, event):
        dlg = wxMessageDialog(self, "PythonVerse\n"
                              "Copyright 2001 Christine McIntyre\n"
                              "All rights reserved.\n",
                              "About PythonVerse", wxOK | wxICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()

    def OnConnection(self, event):
        window = ConnectionDialog(self, -1)
        window.Show(true)

    def TimeToQuit(self, event):
        self.Close(true)

    def connect(self, server, port, nick, avatar):
        self.client_nb.AddPage(Client(self, self.client_nb, -1, server, port,
                                      nick, avatar),
                               "%s:%d" % (server, port))


class LogWindow(wxPanel):
    def __init__(self, parent, ID=-1):
        wxPanel.__init__(self, parent, ID)
        self.tc = wxTextCtrl(self, ID,
                             style=wxTE_MULTILINE|wxTE_READONLY|wxTE_RICH)
        self.logger = wxLogTextCtrl(self.tc)
        self.logger.SetVerbose(true)
        wxLog_SetActiveTarget(self.logger)
        self.sizer = wxBoxSizer(wxVERTICAL)
        self.sizer.Add(self.tc, 1, wxEXPAND)
        self.SetSizer(self.sizer)
        self.SetAutoLayout(true)


class ConnectionDialog(wxDialog):
    """Dialog for connecting to a server"""
    def __init__(self, parent, ID):
        wxDialog.__init__(self, parent, ID, "Connect to server")
        self.parent = parent
        gs = wxFlexGridSizer(4,2,5,5)
        gs.AddGrowableCol(1)
        gs.Add(wxStaticText(self, -1, "Server"), 0, wxEXPAND) 
        self.editserver = wxTextCtrl(self, 255, "openverse.com")
        gs.Add(self.editserver, 1, wxEXPAND) 
        #EVT_TEXT(self, 20, self.EvtText) 
        #EVT_CHAR(self.editname, self.EvtChar) 
        gs.Add(wxStaticText(self, -1, "Port"), 0, wxEXPAND) 
        self.editport = wxTextCtrl(self, 5, "6900")
        gs.Add(self.editport, 1, wxEXPAND)
        gs.Add(wxStaticText(self, -1, "Nick"), 0, wxEXPAND)
        self.editnick = wxTextCtrl(self, 9, "Ryoko")
        gs.Add(self.editnick, 1, wxEXPAND)
        gs.Add(wxStaticText(self, -1, "Avatar"), 0, wxEXPAND)
        self.editavatar = wxTextCtrl(self, 9, "ryoko")
        gs.Add(self.editavatar, 1, wxEXPAND)
        vbox = wxBoxSizer(wxVERTICAL)
        vbox.Add(gs)
        hbox = wxBoxSizer(wxHORIZONTAL)
        button = wxButton(self, wxID_OK, "Connect")
        EVT_BUTTON(self, wxID_OK, self.OnOK)
        hbox.Add(button)
        button = wxButton(self, wxID_CANCEL, "Cancel")
        hbox.Add(button)
        vbox.Add(hbox)
        self.sizer = vbox
        self.SetAutoLayout(true) 
        self.SetSizer(self.sizer)
        self.sizer.Fit(self)

    def OnOK(self, event):
        server = self.editserver.GetValue()
        port = self.editport.GetValue()
        nick = self.editnick.GetValue()
        avatar = self.editavatar.GetValue()
        self.parent.connect(server, int(port), nick, avatar)
        #wxDialog.OnOK(self, event)
        return event.Skip()


class Application(wxApp):
    """The main application object"""
    def OnInit(self):
        """Initialization function called by wxPython"""
        # Create the main frame
        frame = Frame(NULL, -1, 'PythonVerse')
        frame.Show(true)
        self.SetTopWindow(frame)
        return true


class Scheduler(wxTimer):
    def __init__(self):
        wxTimer.__init__(self)
        self.events = []

    def Schedule(self, t, func, args):
        event = t, func, args
        bisect.insort(self.events, event)
        # Restart the timer
        self.Start(min(0, t-time.time())*1000, true)
        return event

    def ScheduleRel(self, delay, func, args):
        now = time.time()
        return self.Schedule(now+delay, func, args)

    def Cancel(self, event):
        self.events.remove(event)

    def Notify(self):
        now = time.time()
        while self.events:
            t, func, args = self.events[0]
            if t > now: break
            apply(func, args)
            del self.events[0]

        # If there are still events left over, then the last
        # setting of t will be for the first event that's not yet
        # scheduled. Make sure it's a single shot :)
        if self.events: self.Start((t-now)*1000, true)

_scheduler = Scheduler()
            

class IOTimer(wxTimer):
    """Poll asyncore, would be better to use threads"""
    def Notify(self):
        asyncore.poll(0.0)
        

def main(argv):
    app = Application(0)
    wxInitAllImageHandlers()
    t = IOTimer()
    t.Start(500)
    app.MainLoop()


if __name__ == '__main__': main(sys.argv)
                

