# -*- coding: utf-8 -*-

# Copyright (c) 2007 Stas Zykiewicz <stas.zytkiewicz@gmail.com>
#
#           SpriteUtils.py
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 3 of the GNU General Public License
# as published by the Free Software Foundation.  A copy of this license should
# be included in the file GPL-3.
#
# This program 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

"""
SpriteUtils - Module which provides some high level extensions to the 
basic pygame.sprite classes.

class CPSprite - A class which extends the standard pygame Sprite class.

class CPGroup - A class which extends the standard pygame RenderUpdate group class.

You should let your classes inherit from these classes.
For a detailed explanation you should check the pygame docs as we just extends
that class.
Also look at the docstrings in these classes for more info.
"""

import sys
import pygame
from pygame.constants import *
from utils import trace_error,char2surf
from types import StringType

def CPinit(scr,back,group=pygame.sprite.Group()):
        """ Init(back,scr,group=pygame.sprite.Group()) -> CPGroup instance
        
         This MUST be called BEFORE the SpriteUtils classes are used, and is needed
         for some of the magic.
         It's very straight forward and can probably used for all plugins.
         It must be called with references to the background surface and display surface.
         back = background surface.
         scr = display surface.
         group = pygame.sprite group used as the group which holds all the sprites.
         You probably don't want to change this group.
         
         The group instance returnt is a extended pygame.sprite group, which you should
         use to put your sprites in and then call the refresh method to update all the
         sprites. Be sure to check the reference before using this high level classes.
         It also possible to call this function multiple times to get multiple classes, but be aware
         that they all share the same screen and background surfaces. 
         """
                 
        CPSprite.group = pygame.sprite.Group()# base class to hold sprites
        # All the objects which derives from CPSprite belongs to this group.
        # store a reference, needed for removal and erasing of sprites
        CPSprite.backgr = back
        CPSprite.screen = scr
        
        # CPGroup needs screen and backgr to erase and draw the sprites
        # belonging to this group
        g = CPGroup(scr, back)# special CP group
        return g


class CPSprite(pygame.sprite.Sprite):
    """ Class to inherited from by regular sprite classes.
    
     This class calls the pygame.sprite.Sprite constructor and adds itself
     to the RenderUpdates group.
     There's also the possibility to connect a callback method to a pygame event
     much like the GTK/libglade way of callbacks.
     Be aware that this class derives from the pygame Sprite class, so
     there are two mandatory attributes: 'image' and 'rect'.
     See the pygame.sprite.Sprite reference for more information.
     
     You MUST call the init function BEFORE you use anything in this module.
    """
    
    def __init__(self):
        pygame.sprite.Sprite.__init__(self,self.group)# This must be set before using this class
        self.DEBUG = 0
        self._name = 'CPSprite'# this can be changed by the user.
        self._args= [self] # default value to pass to update functions
        self._event_type = None # default in case there's no callback while there's a mouseevent
        self._callback = None # same as with self.event_type
        # defaults for movement of the object
        self._start = (0,0)
        self._end = (0,0)
        self._vector = (0,0)
            
    def connect_callback(self,callback=None,event_type=None,*args):
        """ Connect a callback function to a pygame event type with a optional arguments, the *args tuple.
         
         The event_type can be set to a pygame.event, this becomes the event
         to 'trigger' the callback. (see update method)"""
        self._callback = callback
        self._event_type = event_type
        self._args = args
        if self.DEBUG==1: print "connect_callback",callback,event_type,self._args

    def update(self, event=None):
        """ When there's a callback function set it will run the function, then the
        on_update method is called when it exist. 
        
         It is best to loop the event queue and look for a event, then call the group
         refresh method with the event, which updates  all the sprites as well as it 
         calls this method.
         It returns the return values of resp. on_update and the callback, or None 
        """ 
        cb,ou = None, None 
        if self.DEBUG==1 and event != None: print "event,selfevent",event,\
                                        self._event_type,"event is self.event", event is self._event_type 
        if event:
            if self._callback and event.type is self._event_type:
                # self.rect is the rect from the sub class
                if self.DEBUG: print >> sys.stderr,self.__class__, \
                                            "\n### update calls callback ######\n",\
                                            "rect",self.rect,"mousepos",pygame.mouse.get_pos()
                if self._event_type == MOUSEBUTTONDOWN and \
                        self.rect.contains((pygame.mouse.get_pos()+(0,0))) or\
                            self._event_type == KEYDOWN:        
                    try:
                        cb = apply(self._callback,(self,event,self._args))
                    except:
                        print >> sys.stderr,self._name,"\n Callback function",\
                                                      self._callback,"failed"
                        trace_error()
                
        if hasattr(self,"on_update"):
            if cb:
                apply(self.on_update,self._args)
            else:
                ou = apply(self.on_update,self._args)
        if self.DEBUG: print >> sys.stderr,self.__class__,\
                                             "returns cb or ou","cb",cb,"ou",ou 
        return cb or ou
    
    def set_movement(self,start=(0,0),end=(0,0),step=1,retval=0,loop=0,dokill=0):
        """ Define the movement of the object. This movement should only be used
        to move in strait lines.
        
         start = tuple x,y
         end = tuple x,y
         vector = tuple offset x, offset y
         retval = can be anything and is used as return value when the end of the 
         movement is reached.
         loop = times to loop before we signal retval.
         dokill = kill the sprite by calling the kill() method.
        """
        self._stop_moveit = 0# flag used to stop a movement
        self._start, self._end, self._step, self._retval, self._loop = \
                                            start, end, step, retval, loop
        self.rect.move_ip(self._start)# place it in the start position
        if loop:
            # keep a copy of the org vals
            self.org_start, self.org_end, self.org_step, self.org_retval = \
                                                 start, end, step, retval
        self.dokill = dokill        
        
    def moveit(self):
        """ Move this object according to the values in movement.
        
         The set_movement method should be called before this one.
         Returns -1 when the movement continues, and the given return value when
         it stops. (the retval atgument from set_movement)."""
        if self._stop_moveit:#test if the flag is set, see def stop_moveit
            return self._retval
        nv = []# hold the new vector
        step = self._step # keep the org value in self.step
        vector = (self._end[0] - self._start[0], self._end[1] - self._start[1])
        if vector == (0,0):# we reached the end
            if self._loop:# check if we must loop
                self.erase_sprite()
                if self._loop != -1:# do we loop forever?
                    self._loop -= 1
                # reset org values
                self.rect.move_ip(self.org_start)# move this rect to his org position
                self.set_movement(self.org_start, self.org_end, self.org_step, self.org_retval, self._loop)
                return -1# return -1 to signal that the movement is not yet over
            if self.dokill:
                self.kill()
                #print "sprite",self," killed"
                self._loop = 0
                return self._retval
            return self._retval # and return value to signal the end
    
        if self.DEBUG==3: print "vector,start,end,step",vector,self._start,self._end,step
        for i in vector:
            if i == 0: # we are at the same axes already, this doesn't change then.
                nv.append(0)
                continue
            if i > 0:
                step = abs(step)#make sure step is positive
                if i >= step:# we can increase with one step
                    nv.append(step)
                elif i < step: # we are less then a step away (in one axes)
                    nv.append(i) # so we use the calculated distence
            else:
                step = -step # start > end, so the vector must be negative
                             # because we always go from start to end.
                if i <= step:# we can increase with one step
                    nv.append(step)
                elif i > step: # we are less then a step away (in one axes)
                    nv.append(i) # so we use the calculated distence
                
        # Remember that self.rect is a mandatory member of any CPSprite class
        self.rect.move_ip(nv[0],nv[1])
        if self.DEBUG==2: print "self rect",self.rect
        self._start = tuple(self.rect[:2])
        return -1 # we return when the vector is zero
        
    def stop_movement(self,now=None):
        """This stops any running movement, when 'now' is set it will terminate
        
        the movement by changing the return value from 'moveit', else we change the
        loop attribute. This way the movement stops when the current run is done."""
        if now:
            self._stop_moveit = 1
            self.remove_sprite()
            self._loop = 0# to be sure
        else:
            self._loop = 0
    
    def display_sprite(self,pos=None):
        """ Display a sprite without the need to call group methods.
        
        Usefull for just displaying this sprite, nothing more.
        It takes an optional argument 'pos' which will move the sprite to
        that location."""
        if pos:
            self.rect.move_ip(pos)
        r = self.screen.blit(self.image, self.rect)
        pygame.display.update(r)

    def erase_sprite(self):
        """ Erase a sprite without the need to call group methods.
        
        Usefull for just erasing this sprite, nothing more."""
        #print "erase rect",self.rect
        r = self.screen.blit(self.backgr, self.rect.inflate(6,4), self.rect.inflate(6,4))
        pygame.display.update(r)
    
    def remove_sprite(self):
        """ Remove this sprite object from all the groups it belongs
        
          as well as remove it from the screen. This calls pygame.display.update
        """
        #self.screen and self.backgr are set by _Sprite_setup
        self.kill()# removes this object from any group it belongs
        self.erase_sprite()
    
    def get_sprite_height(self):
        """ Returns the height of the surface used in this sprite"""
        return self.image.get_height()
    def get_sprite_width(self):
        """ Returns the width of the surface used in this sprite"""
        return self.image.get_width()
    def get_sprite_rect(self):
        """ Returns the rect of the sprite"""
        return self.rect

class CPGroup(pygame.sprite.RenderUpdates):
    """ Group  wich extends the standard RenderUpdates group.
    
    This is meant to use together with the CPSprite class.
    You MUST call the init function BEFORE you use anything in this module.
    
    Look at the examples that came with this module for possible usage.
    If you want to grok this magic, look at the pygame docs (sprite classes).
    The best you can do is just use the magic from the example ;-)
     """
    def __init__(self,scr,bck):
        """__init__(scr,bck)
           scr = reference to the display screen.
           bck = reference to a background screen."""
        self.DEBUG = 0
        pygame.sprite.RenderUpdates.__init__(self)
        self.scr = scr
        self.bck = bck
        
    def refresh(self,*args):
        """ Clear the sprites, calls the update method on the sprites,
        redraw the sprites and update the display.
        It returns the return values of any on_update or callback function.
        Look at the update method for a example of the stuff returnt."""
        self.clear(self.scr,self.bck)
        if args:
            v = self.update(args[0])
        else:
            v = self.update()
        if self.DEBUG: print >> sys.stderr,self.__class__,"returnt from self.update",v
        rects = self.draw(self.scr)    
        pygame.display.update(rects)
        return v

    def get_stack_ids(self):
        return self.sprites()

    def update(self, *args):
        """update(...), this overrides the pygame.sprite.group.update
           call update for all member sprites

           calls the update method for all sprites in the group.
           Passes all arguments on to the Sprite update and callback function.
           It returns all the return values of these functions in a list.
           
           Example: [(sprite name,return val),(sprite name,return val)]
           sprite name is sprite.__class__, so you can change it to make the sprite name unique.
           return val is the val returnt by the CPSprite.update method and it's the value from the callback
           function if it exist otherwise it's from the on_update."""
        l = []
        l_append = l.append
        if args:
            a=apply
            for s in self.spritedict.keys():
                v = a(s.update, args)
                if self.DEBUG: print >> sys.stderr,self.__class__,"returnt from CPGroup update",v
                if v:
                    l_append((s,v))
        else:
            for s in self.spritedict.keys():
                s.update()
        return l
        
class CPStackGroup:
    """ Group which mimics the pygame.sprite groups, but keeps the sprites
    it contains on a stack so that you have control over the way the sprites are handled.
    
    Besides typical stack like stuff like pop and push this group keeps a reference
    to each sprite that gets added, which is also returned when the sprite is added.
    Use this group when you want to manage the way and moment each sprite is updated.
    The way the sprites are cleared is by getting a run-time background rect of the area
    of the sprite just before it gets drawn.
    The stack makes also possible to raise or lower sprites.
    See the actual methods on usages.
    Be aware that this group sprite updating methods involves a lot of overhead, so use
    it only if you need some of the features it provides.
    
    You MUST call the init function BEFORE you use anything in this module.
    """
    _spritegroup = 1 #dummy val to identify groups
    
    def __init__(self, scr, bck):
        """__init__(scr,bck)
           scr = reference to the display screen.
           bck = reference to a background screen.
        """
        self.DEBUG=1
        self.stack = []# becomes [(ID,sprite),(ID,sprite),...]
        self.ID = 0 # becomes self.ID += 1 : str(self.ID)
        self.scr = scr
        self.bck = bck

    def add(self, sprite):
        """add(sprite)
           add sprite to the group and push it/them on the stack."""
        try: 
            len(sprite) #see if its a sequence
        except (TypeError, AttributeError):
            id = self.push(sprite)
            sprite.add_internal(self)
            return id
        else:
            t = ()
            for s in sprite:
                t = t+self.push(s)
                s.add_internal(self)
            return t

    def get_sprite(self,id):
        """get(id)
           Get a sprite from the stack.
           
           This returns a sprite but does NOT remove it.
           """
        found = self._find_in_stack(id)
        if found != -1:
            s = self.stack[found][1]
            return s
            
    
    def push(self,sprite):
        """push(sprite)
           Push a sprite on the stack."""
        self.ID += 1
        id = str(self.ID)
        self.stack.append((id,sprite))
        return id
    
    def pop(self, index=-1):
        """pop(index=-1)
           Pop a sprite from the stack, remove and return the last one added.
           
           Optional you can give a index which item to pop.
           It defaults to -1, the last item."""
        try:
            s = self.stack.pop(index)[1]
        except IndexError:
            return None
        else:
            r = self.scr.blit(self.bck,s.rect,s.rect)
            #print "pop clear",s.rect,"rect",r
            pygame.display.update(r)
        return s
        
                     
    def _find_in_stack(self,sprite):
        # search the stack for a sprite, sprite can be a sprite object or a ID string
        index = 0
        for item in self.stack:
            if sprite in item:
                return index
            else:
                index += 1
        return -1# not found      
        
    def add_internal(self, sprite):
        self.push(sprite)


    def remove(self, sprite):
        """remove(sprite)
           remove sprite from the stack and group

           Remove a sprite or sequence of sprites from the stack and group."""
        try:
            len(sprite) #see if its a sequence
        except (TypeError, AttributeError):
            found = self._find_in_stack(sprite)#returns a index
            if self.DEBUG==1: print "found",found
            if found != -1:
                self.remove_internal(found)
                sprite.remove_internal(self)
        else:
            for s in sprite:
                found = self._find_in_stack(s)
                if found != -1:
                    self.remove_internal(found)
                    sprite.remove_internal(self)
                    
    def remove_internal(self, index):
        found = self._find_in_stack(index)
        if found != -1:
            s = self.pop(found)
            

    def draw(self,*args):
        """draw(surface)
           draw all sprites onto the surface

           Draws all the sprites onto the given surface. It
           creates a list of rectangles, which which are passed
           to pygame.display.update()
           Sprites are first erased and then drawn "FILD",
           First In Last Drawn."""
        bck = self.bck
        scr_blit = self.scr.blit
        
        stack = self.stack[:]
        rects = []
        rects_append = rects.append
        for id,sprite in stack:
            # first we erase the sprite by blitting the old backgr
            rects_append(scr_blit(bck,sprite.rect, sprite.rect))
            if sprite.update(*args):# callback function returns true/false
                break# then we considered to stop 
            # now we draw them 
            rects_append(scr_blit(sprite.image,sprite.rect))
        pygame.display.update(rects)
        
            
    def refresh(self,event):
        """ refresh(*args)
             Calls the update and draw methods on all the sprites belonging to this group.
        """
        #self.update(*args)
        self.draw(event)
        #print "Stack",self.stack

    def update(self, *args):
        """update(...)
           call update for all member sprites

           calls the update method for all sprites in the group.
           passes all arguments are to the Sprite update function."""
        if args:
            a=apply
            stack = self.stack
            for i,s in stack:
                a(s.update, args)
        else:
            for i,s in stack:
                s.update()


    def get_stack_ids(self):
        """ get_stack_ids()
            Get a list with the current IDs from the stack.
            
            Use this to get a overview of the contents of the stack but use pop, push etc
            to handle the stack.
            """
        ids = []
        ids_append = ids.append
        for i in self.stack:
            ids_append(i[0])
        return ids
    
    def up(self,id):
        """ up(ID)
            Raise the sprite to the top of the stack.
            """
        index = self._find_in_stack(id)
        if index != -1:
            self.stack.append(self.stack.pop(index))
                
    def down(self):
        """ down() 
            Get the sprite on top of the stack and put it on the bottom of the stack.
            """
        self.stack.insert(0,self.stack.pop())

class MySprite(CPSprite):
    """Container to turn a surface into a CPSprite object"""
    def __init__(self,img):
        CPSprite.__init__(self)
        self.image = img
        self.rect = img.get_rect()

