# Pyganim
# A sprite animation module for Pygame.
#
# By Al Sweigart al@inventwithpython.com
# http://inventwithpython.com/pyganim
# Released under a "Simplified BSD" license
#
# There's a tutorial (and sample code) on how to use this library at http://inventwithpython.com/pyganim
# NOTE: This module requires Pygame to be installed to use. Download it from http://pygame.org
#
# This should be compatible with both Python 2 and Python 3. Please email any
# bug reports to Al at al@inventwithpython.com
#


# TODO: Feature idea: if the same image file is specified, re-use the Surface object. (Make this optional though.)
# TODO: Dynamically change durations and number of frames.

__version__ = '0.9.2'

import pygame
import time
import os


# setting up constants
PLAYING = 'playing'
PAUSED = 'paused'
STOPPED = 'stopped'

# These values are used in the anchor() method.
NW = NORTHWEST = 'nw'
N = NORTH = 'n'
NE = NORTHEAST = 'ne'
W = WEST = 'w'
C = CENTER = 'c'
E = EAST = 'e'
SW = SOUTHWEST = 'sw'
S = SOUTH = 's'
SE = SOUTHEAST = 'se'

TIME_FUNC = lambda: int(time.time() * 1000)

def getImagesFromSpriteSheet(filename, width=None, height=None, rows=None, cols=None, rects=None):
    """Loads several sprites from a single image file (a "spritesheet").

    One (and only one) of the following parameters should be specified:
        * width & height of each sprite (all must be the same size)
        * number of rows and columns of sprites (all must be the same size)
        * rects, which is a list of tuples formatted as (pygame.Rect, index) or (left, top, width, height)
    """

    argsType = '' # there should be exactly 1 set of arguments passed (i.e. don't pass width/height AND rows/cols)
    if (width is not None or height is not None) and (argsType == ''):
        argsType = 'width/height'
        assert width is not None and height is not None, 'Both width and height must be specified'
        assert type(width) == int and width > 0, 'width arg must be a non-zero positive integer'
        assert type(height) == int and height > 0, 'height arg must be a non-zero positive integer'
    if (rows is not None or cols is not None) and (argsType == ''):
        argsType = 'rows/cols'
        assert rows is not None and cols is not None, 'Both rows and cols must be specified'
        assert type(rows) == int and rows > 0, 'rows arg must be a non-zero positive integer'
        assert type(cols) == int and cols > 0, 'cols arg must be a non-zero positive integer'
    if (rects is not None) and (argsType == ''):
        argsType = 'rects'
        for i, rect in enumerate(rects):
            assert len(rect) == 4, 'rect at index %s is not a sequence of four ints: (left, top, width, height)' % (i)

            assert (type(rect[0]), type(rect[1]), type(rect[2]), type(rect[3])) == (int, int, int, int), 'rect '
    if argsType == '':
        raise ValueError('Only pass one set of args: width & height, rows & cols, *or* rects')

    sheetImage = pygame.image.load(filename)

    if argsType == 'width/height':
        for y in range(0, sheetImage.get_height(), (sheetImage.get_height() // height)):
            if y + height > sheetImage.get_height():
                continue
            for x in range(0, sheetImage.get_width(), (sheetImage.get_width() // width)):
                if x + width > sheetImage.get_width():
                    continue

                rects.append((x, y, width, height))

    if argsType == 'rows/cols':
        spriteWidth = sheetImage.get_width() // cols
        spriteHeight = sheetImage.get_height() // rows

        for y in range(0, sheetImage.get_height(), spriteHeight):
            if y + spriteHeight > sheetImage.get_height():
                continue
            for x in range(0, sheetImage.get_width(), spriteWidth):
                if x + spriteWidth > sheetImage.get_width():
                    continue

                rects.append((x, y, spriteWidth, spriteHeight))

    # create a list of Surface objects from the sprite sheet
    returnedSurfaces = []
    for rect in rects:
        surf = pygame.Surface((rect[2], rect[3]), 0, sheetImage) # create Surface with width/height in rect
        surf.blit(sheetImage, (0, 0), rect, pygame.BLEND_RGBA_ADD)
        returnedSurfaces.append(surf)

    return returnedSurfaces




class PygAnimation():
    def __init__(self, frames, loop=True):
        # Constructor function for the animation object. Starts off in the STOPPED state.
        #
        # @param frames
        #     A list of tuples for each frame of animation, in one of the following format:
        #       (image_of_frame<pygame.Surface>, duration_in_milliseconds<int>)
        #       (filename_of_image<str>, duration_in_milliseconds<int>)
        #     Note that the images and duration cannot be changed. A new PygAnimation object
        #     will have to be created.
        # @param loop Tells the animation object to keep playing in a loop.

        # _images stores the pygame.Surface objects of each frame
        self._images = []
        # _durations stores the durations (in milliseconds) of each frame.
        # e.g. [1000, 1000, 2500] means the first and second frames last one second,
        # and the third frame lasts for two and half seconds.
        self._durations = []
        # _startTimes shows when each frame begins. len(self._startTimes) will
        # always be one more than len(self._images), because the last number
        # will be when the last frame ends, rather than when it starts.
        # The values are in milliseconds.
        # So self._startTimes[-1] tells you the length of the entire animation.
        # e.g. if _durations is [1000, 1000, 2500], then _startTimes will be [0, 1000, 2000, 4500]
        self._startTimes = None

        # if the sprites are transformed, the originals are kept in _images
        # and the transformed sprites are kept in _transformedImages.
        self._transformedImages = []

        self._state = STOPPED # The state is always either PLAYING, PAUSED, or STOPPED
        self._loop = loop # If True, the animation will keep looping. If False, the animation stops after playing once.
        self._rate = 1.0 # 2.0 means play the animation twice as fast, 0.5 means twice as slow
        self._visibility = True # If False, then nothing is drawn when the blit() methods are called

        self._playingStartTime = 0 # the time that the play() function was last called. Epoch timestamp in milliseconds.
        self._pausedStartTime = 0 # the time that the pause() function was last called. Epoch timestamp in milliseconds.

        # NOTE: There is no "self._elapsed" attribute. "Elapsed" is always calculated based on the current time and _playingStartTime.
        if type(frames) == str and frames.lower().endswith('.gif'):
            # frames is an animated gif filename
            gifImages = splitGif(frames)
            # NOTE: I can't understand how PIL and Pygame's tobytes()/tostring()/fromstring() methods work, hence the use of temporary files.
            # NOTE: Named temp files delete themselves on close on Windows, so the temp file has to be created here.
            frames = []
            for im in gifImages:
                im.save('.temp_pyganim.gif')
                frames.append((pygame.image.load('.temp_pyganim.gif'), im.info['duration'])) # gif duration is already in milliseconds
                os.unlink('.temp_pyganim.gif')

        if frames != '_copy': # ('_copy' is passed for frames by the getCopies() method)
            self.numFrames = len(frames)
            assert self.numFrames > 0, 'Must contain at least one frame.'
            for i in range(self.numFrames):
                # load each frame of animation into _images
                frame = (frames[i][0], int(frames[i][1]))
                assert type(frame) in (list, tuple) and len(frame) == 2, 'Frame %s has incorrect format. It should be a tuple of length 2, first item a pygame.Surface or image filename, second item an int/float of duration.' % (i)
                assert type(frame[0]) in (str, pygame.Surface), 'Frame %s image must be a string filename or a pygame.Surface' % (i)
                assert frame[1] > 0, 'Frame %s duration must be greater than zero.' % (i)
                if type(frame[0]) == str:
                    frame = (pygame.image.load(frame[0]), frame[1])
                self._images.append(frame[0])
                self._durations.append(frame[1])

            # calculate start times of each frame
            self._startTimes = [0]
            for i in range(self.numFrames):
                self._startTimes.append(self._startTimes[-1] + self._durations[i])


    def reverse(self):
        # Reverses the order of the frames.
        self.elapsed = (self._durations[-1] + self._startTimes[-1]) - self.elapsed
        self._images.reverse()
        self._transformedImages.reverse()
        self._durations.reverse()


    def getCopy(self):
        # Returns a copy of this PygAnimation object, but one that refers to the
        # Surface objects of the original so it efficiently uses memory.
        #
        # NOTE: Messing around with the original Surface objects will affect all
        # the copies. If you want to modify the Surface objects, then just make
        # copies using constructor function instead.
        return self.getCopies(1)[0]


    def getCopies(self, numCopies=1):
        # Returns a list of copies of this PygAnimation object, but one that refers to the
        # Surface objects of the original so it efficiently uses memory.
        #
        # NOTE: Messing around with the original Surface objects will affect all
        # the copies. If you want to modify the Surface objects, then just make
        # copies using constructor function instead.
        retval = []
        for i in range(numCopies):
            newAnim = PygAnimation('_copy', loop=self.loop)
            newAnim._images = self._images[:]
            newAnim._transformedImages = self._transformedImages[:]
            newAnim._durations = self._durations[:]
            newAnim._startTimes = self._startTimes[:]
            newAnim.numFrames = self.numFrames
            retval.append(newAnim)
        return retval


    def blit(self, destSurface, dest=(0, 0)):
        # Draws the appropriate frame of the animation to the destination Surface
        # at the specified position.
        #
        # NOTE: If the visibility attribute is False, then nothing will be drawn.
        #
        # @param destSurface
        #     The Surface object to draw the frame
        # @param dest
        #     The position to draw the frame. This is passed to Pygame's Surface's
        #     blit() function, so it can be either a (top, left) tuple or a Rect
        #     object.
        if self.isFinished():
            self.state = STOPPED
        if not self.visibility or self.state == STOPPED:
            return
        frameNum = findStartTime(self._startTimes, self.elapsed)
        destSurface.blit(self.getFrame(frameNum), dest)


    def getFrame(self, frameNum):
        # Returns the pygame.Surface object of the frameNum-th frame in this
        # animation object. If there is a transformed version of the frame,
        # it will return that one.
        if self._transformedImages == []:
            return self._images[frameNum]
        else:
            return self._transformedImages[frameNum]


    def getCurrentFrame(self):
        # Returns the pygame.Surface object of the frame that would be drawn
        # if the blit() method were called right now. If there is a transformed
        # version of the frame, it will return that one.
        return self.getFrame(self.currentFrameNum)


    def clearTransforms(self):
        # Deletes all the transformed frames so that the animation object
        # displays the original Surfaces/images as they were before
        # transformation functions were called on them.
        #
        # This is handy to do for multiple transformation, where calling
        # the rotation or scaling functions multiple times results in
        # degraded/noisy images.
        self._transformedImages = []


    def makeTransformsPermanent(self):
        self._images = [pygame.Surface(surfObj.get_size(), 0, surfObj) for surfObj in self._transformedImages]
        for i in range(len(self._transformedImages)):
            self._images[i].blit(self._transformedImages[i], (0,0))


    def blitFrameNum(self, frameNum, destSurface, dest):
        # Draws the specified frame of the animation object. This ignores the
        # current playing state.
        #
        # NOTE: If the visibility attribute is False, then nothing will be drawn.
        #
        # @param frameNum
        #     The frame to draw (the first frame is 0, not 1)
        # @param destSurface
        #     The Surface object to draw the frame
        # @param dest
        #     The position to draw the frame. This is passed to Pygame's Surface's
        #     blit() function, so it can be either a (top, left) tuple or a Rect
        #     object.
        if self.isFinished():
            self.state = STOPPED
        if not self.visibility or self.state == STOPPED:
            return
        destSurface.blit(self.getFrame(frameNum), dest)


    def blitFrameAtTime(self, elapsed, destSurface, dest):
        # Draws the frame the is "elapsed" number of seconds into the animation,
        # rather than the time the animation actually started playing.
        #
        # NOTE: If the visibility attribute is False, then nothing will be drawn.
        #
        # @param elapsed
        #     The amount of time into an animation to use when determining which
        #     frame to draw. blitFrameAtTime() uses this parameter rather than
        #     the actual time that the animation started playing. (In seconds)
        # @param destSurface
        #     The Surface object to draw the frame
        # @param dest
        #     The position to draw the frame. This is passed to Pygame's Surface's
        #     blit() function, so it can be either a (top, left) tuple or a Rect
        #     object.        elapsed = int(elapsed * self.rate)
        if self.isFinished():
            self.state = STOPPED
        if not self.visibility or self.state == STOPPED:
            return
        frameNum = findStartTime(self._startTimes, elapsed)
        destSurface.blit(self.getFrame(frameNum), dest)


    def isFinished(self):
        # Returns True if this animation doesn't loop and has finished playing
        # all the frames it has.
        return not self.loop and self.elapsed >= self._startTimes[-1]


    def play(self, startTime=None): # startTime is epoch timestamp in milliseconds. TODO: Change this?
        # Start playing the animation.

        # play() is essentially a setter function for self._state
        if startTime is None:
            startTime = TIME_FUNC()

        if self._state == PLAYING:
            if self.isFinished():
                # if the animation doesn't loop and has already finished, then
                # calling play() causes it to replay from the beginning.
                self._playingStartTime = startTime
        elif self._state == STOPPED:
            # if animation was stopped, start playing from the beginning
            self._playingStartTime = startTime
        elif self._state == PAUSED:
            # if animation was paused, start playing from where it was paused
            self._playingStartTime = startTime - (self._pausedStartTime - self._playingStartTime)
        else:
            assert False, '_state attribute contains an invalid value: %s' % (str(self._state)[:40])
        self._state = PLAYING


    def pause(self, startTime=None):
        # Stop having the animation progress, and keep it at the current frame.

        # pause() is essentially a setter function for self._state
        if startTime is None:
            startTime = TIME_FUNC()

        if self._state == PAUSED:
            return # do nothing
        elif self._state == PLAYING:
            self._pausedStartTime = startTime
        elif self._state == STOPPED:
            rightNow = TIME_FUNC()
            self._playingStartTime = rightNow
            self._pausedStartTime = rightNow
        else:
            assert False, '_state attribute contains an invalid value: %s' % (str(self._state)[:40])
        self._state = PAUSED


    def stop(self):
        # Reset the animation to the beginning frame, and do not continue playing

        # stop() is essentially a setter function for self._state
        assert self._state in (PLAYING, PAUSED, STOPPED), '_state attribute contains an invalid value: %s' % (str(self._state)[:40])
        if self._state == STOPPED:
            return # do nothing
        self._state = STOPPED


    def togglePause(self):
        # If paused, start playing. If playing, then pause.

        # togglePause() is essentially a setter function for self._state
        if self._state == PLAYING:
            self.pause()
        elif self._state in (PAUSED, STOPPED):
            self.play()
        else:
            assert False, '_state attribute contains an invalid value: %s' % (str(self._state)[:40])


    def framesAreSameSize(self):
        # Returns True if all the Surface objects in this animation object
        # have the same width and height. Otherwise, returns False
        width, height = self.getFrame(0).get_size()
        for i in range(len(self._images)):
            if self.getFrame(i).get_size() != (width, height):
                return False
        return True


    def getMaxSize(self):
        # Goes through all the Surface objects in this animation object
        # and returns the max width and max height that it finds. (These
        # widths and heights may be on different Surface objects.)
        frameWidths = []
        frameHeights = []
        for i in range(len(self._images)):
            frameWidth, frameHeight = self._images[i].get_size()
            frameWidths.append(frameWidth)
            frameHeights.append(frameHeight)
        maxWidth = max(frameWidths)
        maxHeight = max(frameHeights)

        return (maxWidth, maxHeight)


    def getRect(self):
        # Returns a pygame.Rect object for this animation object.
        # The top and left will be set to 0, 0, and the width and height
        # will be set to what is returned by getMaxSize().
        maxWidth, maxHeight = self.getMaxSize()
        return pygame.Rect(0, 0, maxWidth, maxHeight)


    def anchor(self, anchorPoint=NORTHWEST):
        # If the Surface objects are of different sizes, align them all to a
        # specific "anchor point" (one of the NORTH, SOUTH, SOUTHEAST, etc. constants)
        #
        # By default, they are all anchored to the NORTHWEST corner.
        if self.framesAreSameSize():
            return # nothing needs to be anchored
            # This check also prevents additional calls to anchor() from doing
            # anything, since anchor() sets all the image to the same size.
            # The lesson is, you can only effectively call anchor() once.

        self.clearTransforms() # clears transforms since this method anchors the original images.

        maxWidth, maxHeight = self.getMaxSize()
        halfMaxWidth = int(maxWidth / 2)
        halfMaxHeight = int(maxHeight / 2)

        for i in range(len(self._images)):
            # go through and copy all frames to a max-sized Surface object
            # NOTE: This makes changes to the original images in self._images, not the transformed images in self._transformedImages
            newSurf = pygame.Surface((maxWidth, maxHeight)) # TODO: this is probably going to have errors since I'm using the default depth.

            # set the expanded areas to be transparent
            newSurf = newSurf.convert_alpha()
            newSurf.fill((0,0,0,0))

            frameWidth, frameHeight = self._images[i].get_size()
            halfFrameWidth = int(frameWidth / 2)
            halfFrameHeight = int(frameHeight / 2)

            # position the Surface objects to the specified anchor point
            if anchorPoint == NORTHWEST:
                newSurf.blit(self._images[i], (0, 0))
            elif anchorPoint == NORTH:
                newSurf.blit(self._images[i], (halfMaxWidth - halfFrameWidth, 0))
            elif anchorPoint == NORTHEAST:
                newSurf.blit(self._images[i], (maxWidth - frameWidth, 0))
            elif anchorPoint == WEST:
                newSurf.blit(self._images[i], (0, halfMaxHeight - halfFrameHeight))
            elif anchorPoint == CENTER:
                newSurf.blit(self._images[i], (halfMaxWidth - halfFrameWidth, halfMaxHeight - halfFrameHeight))
            elif anchorPoint == EAST:
                newSurf.blit(self._images[i], (maxWidth - frameWidth, halfMaxHeight - halfFrameHeight))
            elif anchorPoint == SOUTHWEST:
                newSurf.blit(self._images[i], (0, maxHeight - frameHeight))
            elif anchorPoint == SOUTH:
                newSurf.blit(self._images[i], (halfMaxWidth - halfFrameWidth, maxHeight - frameHeight))
            elif anchorPoint == SOUTHEAST:
                newSurf.blit(self._images[i], (maxWidth - frameWidth, maxHeight - frameHeight))
            self._images[i] = newSurf


    def nextFrame(self, jump=1):
        # Set the elapsed time to the beginning of the next frame.
        # You can jump ahead by multiple frames by specifying a different
        # argument for jump.
        # Negative values have the same effect as calling prevFrame()
        self.currentFrameNum += int(jump)


    def prevFrame(self, jump=1):
        # Set the elapsed time to the beginning of the previous frame.
        # You can jump ahead by multiple frames by specifying a different
        # argument for jump.
        # Negative values have the same effect as calling nextFrame()
        self.currentFrameNum -= int(jump)


    def rewind(self, milliseconds=None):
        # Set the elapsed time back relative to the current elapsed time.
        if milliseconds is None:
            self.elapsed = 0.0
        else:
            self.elapsed -= milliseconds


    def fastForward(self, milliseconds):
        # Set the elapsed time forward relative to the current elapsed time.
        if milliseconds is None:
            self.elapsed = self._startTimes[-1]
        else:
            self.elapsed += milliseconds

    def _makeTransformedSurfacesIfNeeded(self):
        # Internal-method. Creates the Surface objects for the _transformedImages list.
        # Don't call this method.
        if self._transformedImages == []:
            self._transformedImages = [surf.copy() for surf in self._images]


    # Transformation methods.
    # (These are analogous to the pygame.transform.* functions, except they
    # are applied to all frames of the animation object.
    def flip(self, xbool, ybool):
        # Flips the image horizontally, vertically, or both.
        # See http://pygame.org/docs/ref/transform.html#pygame.transform.flip
        self._makeTransformedSurfacesIfNeeded()
        for i in range(len(self._images)):
            self._transformedImages[i] = pygame.transform.flip(self.getFrame(i), xbool, ybool)


    def scale(self, width_height):
        # NOTE: Does not support the DestSurface parameter
        # Increases or decreases the size of the images.
        # See http://pygame.org/docs/ref/transform.html#pygame.transform.scale
        self._makeTransformedSurfacesIfNeeded()
        for i in range(len(self._images)):
            self._transformedImages[i] = pygame.transform.scale(self.getFrame(i), width_height)


    def rotate(self, angle):
        # Rotates the image.
        # See http://pygame.org/docs/ref/transform.html#pygame.transform.rotate
        self._makeTransformedSurfacesIfNeeded()
        for i in range(len(self._images)):
            self._transformedImages[i] = pygame.transform.rotate(self.getFrame(i), angle)


    def rotozoom(self, angle, scale):
        # Rotates and scales the image simultaneously.
        # See http://pygame.org/docs/ref/transform.html#pygame.transform.rotozoom
        self._makeTransformedSurfacesIfNeeded()
        for i in range(len(self._images)):
            self._transformedImages[i] = pygame.transform.rotozoom(self.getFrame(i), angle, scale)


    def scale2x(self):
        # NOTE: Does not support the DestSurface parameter
        # Double the size of the image using an efficient algorithm.
        # See http://pygame.org/docs/ref/transform.html#pygame.transform.scale2x
        self._makeTransformedSurfacesIfNeeded()
        for i in range(len(self._images)):
            self._transformedImages[i] = pygame.transform.scale2x(self.getFrame(i))


    def smoothscale(self, width_height):
        # NOTE: Does not support the DestSurface parameter
        # Scales the image smoothly. (Computationally more expensive and
        # slower but produces a better scaled image.)
        # See http://pygame.org/docs/ref/transform.html#pygame.transform.smoothscale
        self._makeTransformedSurfacesIfNeeded()
        for i in range(len(self._images)):
            self._transformedImages[i] = pygame.transform.smoothscale(self.getFrame(i), width_height)



    # pygame.Surface method wrappers
    # These wrappers call their analogous pygame.Surface methods on all Surface objects in this animation.
    # They are here for the convenience of the module user. These calls will apply to the transform images,
    # and can have their effects undone by called clearTransforms()
    #
    # It is not advisable to call these methods on the individual Surface objects in self._images.
    def _surfaceMethodWrapper(self, wrappedMethodName, *args, **kwargs):
        self._makeTransformedSurfacesIfNeeded()
        for i in range(len(self._images)):
            methodToCall = getattr(self._transformedImages[i], wrappedMethodName)
            methodToCall(*args, **kwargs)

    # There's probably a more terse way to generate the following methods,
    # but I don't want to make the code even more unreadable.
    def convert(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.convert
        self._surfaceMethodWrapper('convert', *args, **kwargs)


    def convert_alpha(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.convert_alpha
        self._surfaceMethodWrapper('convert_alpha', *args, **kwargs)


    def set_alpha(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.set_alpha
        self._surfaceMethodWrapper('set_alpha', *args, **kwargs)


    def scroll(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.scroll
        self._surfaceMethodWrapper('scroll', *args, **kwargs)


    def set_clip(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.set_clip
        self._surfaceMethodWrapper('set_clip', *args, **kwargs)


    def set_colorkey(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.set_colorkey
        self._surfaceMethodWrapper('set_colorkey', *args, **kwargs)


    def lock(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.unlock
        self._surfaceMethodWrapper('lock', *args, **kwargs)


    def unlock(self, *args, **kwargs):
        # See http://pygame.org/docs/ref/surface.html#Surface.lock
        self._surfaceMethodWrapper('unlock', *args, **kwargs)



    # Getter and setter methods for properties
    def _propGetRate(self):
        return self._rate

    def _propSetRate(self, rate):
        rate = float(rate)
        if rate < 0:
            raise ValueError('rate must be greater than 0.')
        self._rate = rate

    rate = property(_propGetRate, _propSetRate)


    def _propGetLoop(self):
        return self._loop

    def _propSetLoop(self, loop):
        if self.state == PLAYING and self._loop and not loop:
            # if we are turning off looping while the animation is playing,
            # we need to modify the _playingStartTime so that the rest of
            # the animation will play, and then stop. (Otherwise, the
            # animation will immediately stop playing if it has already looped.)
            self._playingStartTime = TIME_FUNC() - self.elapsed
        self._loop = bool(loop)

    loop = property(_propGetLoop, _propSetLoop)


    def _propGetState(self):
        if self.isFinished():
            self._state = STOPPED # if finished playing, then set state to STOPPED.

        return self._state

    def _propSetState(self, state):
        if state not in (PLAYING, PAUSED, STOPPED):
            raise ValueError('state must be one of pyganim.PLAYING, pyganim.PAUSED, or pyganim.STOPPED')
        if state == PLAYING:
            self.play()
        elif state == PAUSED:
            self.pause()
        elif state == STOPPED:
            self.stop()

    state = property(_propGetState, _propSetState)


    def _propGetVisibility(self):
        return self._visibility

    def _propSetVisibility(self, visibility):
        self._visibility = bool(visibility)

    visibility = property(_propGetVisibility, _propSetVisibility)


    def _propSetElapsed(self, elapsed):
        # Set the elapsed time to a specific value.
        # NOTE: elapsed is in milliseconds

        if self.state == STOPPED:
            self.state = PAUSED

        if self._loop:
            elapsed = elapsed % self._startTimes[-1]
        else:
            elapsed = getBoundedValue(0, elapsed, self._startTimes[-1])

        rightNow = TIME_FUNC()
        self._playingStartTime = rightNow - (elapsed * self.rate)

        if self.state in (PAUSED, STOPPED):
            self.state = PAUSED # if stopped, then set to paused
            self._pausedStartTime = rightNow


    def _propGetElapsed(self):
        # To prevent infinite recursion, don't use the self.state property,
        # just read/set self._state directly because the state getter calls
        # this method.

        # NOTE: Elapsed is in milliseconds, not seconds.
        assert self._state in (PLAYING, PAUSED, STOPPED), '_state attribute contains an invalid value: %s' % (str(self._state)[:40])

        # Find out how long ago the play()/pause() functions were called.
        if self._state == STOPPED:
            # if stopped, then just return 0
            return 0
        elif self._state == PLAYING:
            # if playing, then draw the current frame (based on when the animation
            # started playing). If not looping and the animation has gone through
            # all the frames already, then draw the last frame.
            elapsed = (TIME_FUNC() - self._playingStartTime) * self.rate
        elif self._state == PAUSED:
            # if paused, then draw the frame that was playing at the time the
            # PygAnimation object was paused
            elapsed = (self._pausedStartTime - self._playingStartTime) * self.rate

        elapsed = int(elapsed)

        if self._loop:
            elapsed = elapsed % self._startTimes[-1]
        else:
            elapsed = getBoundedValue(0, elapsed, self._startTimes[-1])
        return int(elapsed)

    elapsed = property(_propGetElapsed, _propSetElapsed)


    def _propGetCurrentFrameNum(self):
        # Return the frame number of the frame that will be currently
        # displayed if the animation object were drawn right now.
        return findStartTime(self._startTimes, self.elapsed)


    def _propSetCurrentFrameNum(self, frameNum):
        # Change the elapsed time to the beginning of a specific frame.
        if self._state == STOPPED:
            self._state = PAUSED # setting the frame num automatically puts it as paused.

        if self.loop:
            frameNum = frameNum % len(self._images)
        else:
            frameNum = getBoundedValue(0, frameNum, len(self._images)-1)
        self.elapsed = self._startTimes[frameNum]

    currentFrameNum = property(_propGetCurrentFrameNum, _propSetCurrentFrameNum)



class PygConductor(object):
    def __init__(self, *animations):
        assert len(animations) > 0, 'at least one PygAnimation object is required'

        self._animations = []
        self.add(*animations)


    def add(self, *animations):
        if type(animations[0]) == dict:
            for k in animations[0].keys():
                self._animations.append(animations[0][k])
        elif type(animations[0]) in (tuple, list):
            for i in range(len(animations[0])):
                self._animations.append(animations[0][i])
        else:
            for i in range(len(animations)):
                self._animations.append(animations[i])

    def _propGetAnimations(self):
        return self._animations

    def _propSetAnimations(self, val):
        self._animations = val

    animations = property(_propGetAnimations, _propSetAnimations)

    def play(self, startTime=None):
        if startTime is None:
            startTime = TIME_FUNC()

        for animObj in self._animations:
            animObj.play(startTime)

    def pause(self, startTime=None):
        if startTime is None:
            startTime = TIME_FUNC()

        for animObj in self._animations:
            animObj.pause(startTime)

    def stop(self):
        for animObj in self._animations:
            animObj.stop()

    def reverse(self):
        for animObj in self._animations:
            animObj.reverse()

    def clearTransforms(self):
        for animObj in self._animations:
            animObj.clearTransforms()

    def makeTransformsPermanent(self):
        for animObj in self._animations:
            animObj.makeTransformsPermanent()

    def togglePause(self):
        for animObj in self._animations:
            animObj.togglePause()

    def nextFrame(self, jump=1):
        for animObj in self._animations:
            animObj.nextFrame(jump)

    def prevFrame(self, jump=1):
        for animObj in self._animations:
            animObj.prevFrame(jump)

    def rewind(self, seconds=None):
        for animObj in self._animations:
            animObj.rewind(seconds)

    def fastForward(self, seconds):
        for animObj in self._animations:
            animObj.fastForward(seconds)

    def flip(self, xbool, ybool):
        for animObj in self._animations:
            animObj.flip(xbool, ybool)

    def scale(self, width_height):
        for animObj in self._animations:
            animObj.scale(width_height)

    def rotate(self, angle):
        for animObj in self._animations:
            animObj.rotate(angle)

    def rotozoom(self, angle, scale):
        for animObj in self._animations:
            animObj.rotozoom(angle, scale)

    def scale2x(self):
        for animObj in self._animations:
            animObj.scale2x()

    def smoothscale(self, width_height):
        for animObj in self._animations:
            animObj.smoothscale(width_height)

    def convert(self):
        for animObj in self._animations:
            animObj.convert()

    def convert_alpha(self):
        for animObj in self._animations:
            animObj.convert_alpha()

    def set_alpha(self, *args, **kwargs):
        for animObj in self._animations:
            animObj.set_alpha(*args, **kwargs)

    def scroll(self, dx=0, dy=0):
        for animObj in self._animations:
            animObj.scroll(dx, dy)

    def set_clip(self, *args, **kwargs):
        for animObj in self._animations:
            animObj.set_clip(*args, **kwargs)

    def set_colorkey(self, *args, **kwargs):
        for animObj in self._animations:
            animObj.set_colorkey(*args, **kwargs)

    def lock(self):
        for animObj in self._animations:
            animObj.lock()

    def unlock(self):
        for animObj in self._animations:
            animObj.unlock()


def getBoundedValue(lowerBound, value, upperBound):
    # Returns the value within the bounds of the lower and upper bound parameters.
    # If value is less than lowerBound, then return lowerBound.
    # If value is greater than upperBound, then return upperBound.
    # Otherwise, just return value as it is.
    if upperBound < lowerBound:
        upperBound, lowerBound = lowerBound, upperBound

    if value < lowerBound:
        return lowerBound
    elif value > upperBound:
        return upperBound
    return value


def findStartTime(startTimes, target):
    # With startTimes as a list of sequential numbers and target as a number,
    # returns the index of the number in startTimes that preceeds target.
    #
    # For example, if startTimes was [0, 2000, 4500, 7300, 10000] and target was 6000,
    # then findStartTime() would return 2. If target was 12000, returns 4.
    assert startTimes[0] == 0, 'The first value in the start times list should always be 0.'
    lb = 0 # "lb" is lower bound
    ub = len(startTimes) - 1 # "ub" is upper bound

    # handle special cases:
    if len(startTimes) == 0:
        return 0
    if target >= startTimes[-1]:
        return ub - 1

    # perform binary search:
    while True:
        i = int((ub - lb) / 2) + lb

        if startTimes[i] == target or (startTimes[i] < target and startTimes[i+1] > target):
            if i == len(startTimes):
                return i - 1
            else:
                return i

        if startTimes[i] < target:
            lb = i
        elif startTimes[i] > target:
            ub = i


def splitGif(filename):
    # Takes a filename of an animated GIF and returns a list of Image objects of each frame.
    # Requires PIL or Pillow to be installed
    from PIL import Image

    def iterAnimatedGifFrames(im):
        # Iterator for frames in an animated GIF.
        try:
            i= 0
            while 1:
                im.seek(i)
                imframe = im.copy()
                if i == 0:
                    palette = imframe.getpalette()
                else:
                    imframe.putpalette(palette)
                yield imframe
                i += 1
        except EOFError:
            pass

    gifFile = open(str(filename), 'rb')
    im = Image.open(gifFile) # passing a file-like object so that we can close it and the unit tests won't complain.
    retVal = list(iterAnimatedGifFrames(im))
    gifFile.close()
    return retVal

