# ----------------------------------------------------------------------------
# cocos2d
# Copyright (c) 2008-2011 Daniel Moisset, Ricardo Quesada, Rayentray Tappa,
# Lucio Torre
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in
#     the documentation and/or other materials provided with the
#     distribution.
#   * Neither the name of cocos2d nor the names of its
#     contributors may be used to endorse or promote products
#     derived from this software without specific prior written
#     permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
'''Interval Action

Interval Actions
================

An interval action is an action that takes place within a certain period of time.
It has an start time, and a finish time. The finish time is the parameter
``duration`` plus the start time.

These `IntervalAction` have some interesting properties, like:

  - They can run normally (default)
  - They can run reversed with the `Reverse` action.
  - They can run with the time altered with the `Accelerate`, `AccelDeccel` and
    `Speed` actions.

For example, you can simulate a Ping Pong effect running the action normally and
then running it again in Reverse mode.

Example::

    ping_pong_action = action + Reverse( action )


Available IntervalActions
=========================

  * `MoveTo`
  * `MoveBy`
  * `JumpTo`
  * `JumpBy`
  * `Bezier`
  * `Blink`
  * `RotateTo`
  * `RotateBy`
  * `ScaleTo`
  * `ScaleBy`
  * `FadeOut`
  * `FadeIn`
  * `FadeTo`
  * `Delay`
  * `RandomDelay`


Modifier actions
================

  * `Accelerate`
  * `AccelDeccel`
  * `Speed`


Examples::

    move = MoveBy( (200,0), duration=5 )  # Moves 200 pixels to the right in 5 seconds.

    move = MoveTo( (320,240), duration=5) # Moves to the pixel (320,240) in 5 seconds

    jump = JumpBy( (320,0), 100, 5, duration=5) # Jumps to the right 320 pixels
                                                # doing 5 jumps of 100 pixels
                                                # of height in 5 seconds

    accel_move = Accelerate(move)               # accelerates action move
'''

__docformat__ = 'restructuredtext'

import random
import copy
import math

from base_actions import *
from cocos.euclid import *

__all__ = [  'Lerp',                            # interpolation
            'MoveTo','MoveBy',                  # movement actions
            'Jump', 'JumpTo', 'JumpBy',
            'Bezier',                    # complex movement actions
            'Rotate',"RotateTo", "RotateBy",    # object rotation
            'ScaleTo','ScaleBy',                # object scale
            'Delay','RandomDelay',              # Delays
            'FadeOut','FadeIn','FadeTo',        # Fades in/out action
            'Blink',                            # Blink action

            'Accelerate','AccelDeccel','Speed', # Time alter actions
            ]

class Lerp( IntervalAction ):
    """
    Interpolate between values for some specified attribute 
    
    """
    def init(self, attrib, start, end, duration):
        """Init method.

        :Parameters:
            `attrib` : string
                The name of the attrbiute where the value is stored
            `start`  : float
                The start value
            `end`    : float
                The end value
            `duration` : float
                Duration time in seconds
        """
        self.attrib = attrib
        self.duration = duration
        self.start_p = start
        self.end_p = end
        self.delta = end-start

    def update(self, t):
        setattr(self.target, self.attrib,
                self.start_p + self.delta * t
                )

    def __reversed__(self):
        return Lerp(self.attrib, self.end_p, self.start_p, self.duration)
        
class RotateBy( IntervalAction ):
    """Rotates a `CocosNode` object clockwise a number of degrees
    by modiying it's rotation attribute.

    Example::

        # rotates the sprite 180 degrees in 2 seconds
        action = RotateBy( 180, 2 )
        sprite.do( action )
    """
    def init(self, angle, duration ):
        """Init method.

        :Parameters:
            `angle` : float
                Degrees that the sprite will be rotated.
                Positive degrees rotates the sprite clockwise.
            `duration` : float
                Duration time in seconds
        """
        self.angle = angle          #: Quantity of degrees to rotate
        self.duration = duration    #: Duration in seconds

    def start( self ):
        self.start_angle = self.target.rotation
        
    def update(self, t):
        self.target.rotation = (self.start_angle + self.angle * t ) % 360
            
    def __reversed__(self):
        return RotateBy(-self.angle, self.duration)

Rotate = RotateBy


class RotateTo( IntervalAction ):
    """Rotates a `CocosNode` object to a certain angle by modifying it's
    rotation attribute.
    The direction will be decided by the shortest angle.

    Example::

        # rotates the sprite to angle 180 in 2 seconds
        action = RotateTo( 180, 2 )
        sprite.do( action )
    """
    def init(self, angle, duration ):
        """Init method.

        :Parameters:
            `angle` : float
                Destination angle in degrees.
            `duration` : float
                Duration time in seconds
        """
        self.angle = angle%360      #: Destination angle in degrees
        self.duration = duration    #: Duration in seconds

    def start( self ):
        ea = self.angle
        sa = self.start_angle = (self.target.rotation%360)
        self.angle = ((ea%360) - (sa%360))
        if self.angle > 180: 
            self.angle = -360+self.angle
        if self.angle < -180: 
            self.angle = 360+self.angle
        
    def update(self, t):
        self.target.rotation = (self.start_angle + self.angle * t ) % 360

    def __reversed__(self):
        return RotateTo(-self.angle, self.duration)
        
class Speed( IntervalAction ):
    """
    Changes the speed of an action, making it take longer (speed>1)
    or less (speed<1)

    Example::

        # rotates the sprite 180 degrees in 1 secondclockwise
        action = Speed( Rotate( 180, 2 ), 2 )
        sprite.do( action )
    """
    def init(self, other, speed ):
        """Init method.

        :Parameters:
            `other` : IntervalAction
                The action that will be affected
            `speed` : float
                The speed change. 1 is no change.
                2 means twice as fast, takes half the time
                0.5 means half as fast, takes double the time
        """
        self.other = other
        self.speed = speed
        self.duration = other.duration/speed

    def start(self):
        self.other.target = self.target
        self.other.start()

    def update(self, t):
        self.other.update( t )

    def __reversed__(self):
        return Speed( Reverse( self.other ), self.speed )

class Accelerate( IntervalAction ):
    """
    Changes the acceleration of an action

    Example::

        # rotates the sprite 180 degrees in 2 seconds clockwise
        # it starts slow and ends fast
        action = Accelerate( Rotate( 180, 2 ), 4 )
        sprite.do( action )
    """
    def init(self, other, rate = 2):
        """Init method.

        :Parameters:
            `other` : IntervalAction
                The action that will be affected
            `rate` : float
                The acceleration rate. 1 is linear.
                the new t is t**rate
        """
        self.other = other
        self.rate = rate
        self.duration = other.duration

    def start(self):
        self.other.target = self.target
        self.other.start()

    def update(self, t):
        self.other.update( t**self.rate )

    def __reversed__(self):
        return Accelerate(Reverse(self.other), 1.0/self.rate)

class AccelDeccel( IntervalAction ):
    """
    Makes an action change the travel speed but retain near normal
    speed at the beginning and ending.

    Example::

        # rotates the sprite 180 degrees in 2 seconds clockwise
        # it starts slow, gets fast and ends slow
        action = AccelDeccel( RotateBy( 180, 2 ) )
        sprite.do( action )
    """
    def init(self, other):
        """Init method.

        :Parameters:
            `other` : IntervalAction
                The action that will be affected
        """
        self.other = other
        self.duration = other.duration

    def start(self):
        self.other.target = self.target
        self.other.start()

    def update(self, t):
        ft = (t-0.5) * 12
        nt = 1./( 1. + math.exp(-ft) )

        self.other.update( nt )


    def __reversed__(self):
        return AccelDeccel( Reverse(self.other) )



class MoveTo( IntervalAction ):
    """Moves a `CocosNode` object to the position x,y. x and y are absolute coordinates
    by modifying it's position attribute.

    Example::

        # Move the sprite to coords x=50, y=10 in 8 seconds

        action = MoveTo( (50,10), 8 )
        sprite.do( action )
    """
    def init(self, dst_coords, duration=5):
        """Init method.

        :Parameters:
            `dst_coords` : (x,y)
                Coordinates where the sprite will be placed at the end of the action
            `duration` : float
                Duration time in seconds
        """

        self.end_position = Point2( *dst_coords )
        self.duration = duration

    def start( self ):
        self.start_position = self.target.position
        self.delta = self.end_position-self.start_position


    def update(self,t):
        self.target.position = self.start_position + self.delta * t

class MoveBy( MoveTo ):
    """Moves a `CocosNode` object x,y pixels by modifying it's 
    position attribute.
    x and y are relative to the position of the object.
    Duration is is seconds.

    Example::

        # Move the sprite 50 pixels to the left in 8 seconds
        action = MoveBy( (-50,0), 8 )
        sprite.do( action )
    """
    def init(self, delta, duration=5):
        """Init method.

        :Parameters:
            `delta` : (x,y)
                Delta coordinates
            `duration` : float
                Duration time in seconds
        """
        self.delta = Point2( *delta )
        self.duration = duration

    def start( self ):
        self.start_position = self.target.position
        self.end_position = self.start_position + self.delta

    def __reversed__(self):
        return MoveBy(-self.delta, self.duration)

class FadeOut( IntervalAction ):
    """Fades out a `CocosNode` object by modifying it's opacity attribute.

    Example::

        action = FadeOut( 2 )
        sprite.do( action )
    """
    def init( self, duration ):
        """Init method.

        :Parameters:
            `duration` : float
                Seconds that it will take to fade
        """
        self.duration = duration

    def update( self, t ):
        self.target.opacity = 255 * (1-t)

    def __reversed__(self):
        return FadeIn( self.duration )

class FadeTo( IntervalAction ):
    """Fades a `CocosNode` object to a specific alpha value by modifying it's opacity attribute.

    Example::

        action = FadeTo( 128, 2 )
        sprite.do( action )
    """
    def init( self, alpha, duration ):
        """Init method.

        :Parameters:
            `alpha` : float
                0-255 value of opacity
            `duration` : float
                Seconds that it will take to fade
        """
        self.alpha = alpha
        self.duration = duration

    def start(self):
        self.start_alpha = self.target.opacity

    def update( self, t ):
        self.target.opacity = self.start_alpha + (
                    self.alpha - self.start_alpha
                    ) * t


class FadeIn( FadeOut):
    """Fades in a `CocosNode` object by modifying it's opacity attribute.

    Example::

        action = FadeIn( 2 )
        sprite.do( action )
    """
    def update( self, t ):
        self.target.opacity = 255 * t

    def __reversed__(self):
        return FadeOut( self.duration )

class ScaleTo(IntervalAction):
    """Scales a `CocosNode` object to a zoom factor by modifying it's scale attribute.

    Example::

        # scales the sprite to 5x in 2 seconds
        action = ScaleTo( 5, 2 )
        sprite.do( action )
    """
    def init(self, scale, duration=5 ):
        """Init method.

        :Parameters:
            `scale` : float
                scale factor
            `duration` : float
                Duration time in seconds
        """
        self.end_scale = scale
        self.duration = duration

    def start( self ):
        self.start_scale = self.target.scale
        self.delta = self.end_scale-self.start_scale

    def update(self, t):
        self.target.scale = self.start_scale + self.delta * t


class ScaleBy(ScaleTo):
    """Scales a `CocosNode` object a zoom factor by modifying it's scale attribute.

    Example::

        # scales the sprite by 5x in 2 seconds
        action = ScaleBy( 5, 2 )
        sprite.do( action )
    """

    def start( self ):
        self.start_scale = self.target.scale
        self.delta =  self.start_scale*self.end_scale - self.start_scale

    def __reversed__(self):
        return ScaleBy( 1.0/self.end_scale, self.duration )


class Blink( IntervalAction ):
    """Blinks a `CocosNode` object by modifying it's visible attribute
    
    Example::

        # Blinks 10 times in 2 seconds
        action = Blink( 10, 2 )
        sprite.do( action )
    """


    def init(self, times, duration):
        """Init method.

        :Parameters:
            `times` : integer
                Number of times to blink
            `duration` : float
                Duration time in seconds
        """
        self.times = times
        self.duration = duration

    def update(self, t):
        slice = 1 / float( self.times )
        m =  t % slice
        self.target.visible = (m  >  slice / 2.0)

    def __reversed__(self):
        return self


class Bezier( IntervalAction ):
    """Moves a `CocosNode` object through a bezier path by modifying it's position attribute.

    Example::

        action = Bezier( bezier_conf.path1, 5 )   # Moves the sprite using the
        sprite.do( action )                       # bezier path 'bezier_conf.path1'
                                                  # in 5 seconds
    """
    def init(self, bezier, duration=5, forward=True):
        """Init method

        :Parameters:
            `bezier` : bezier_configuration instance
                A bezier configuration
            `duration` : float
                Duration time in seconds
        """
        self.duration = duration
        self.bezier = bezier
        self.forward = forward

    def start( self ):
        self.start_position = self.target.position

    def update(self,t):
        if self.forward:
            p = self.bezier.at( t )
        else:
            p = self.bezier.at( 1-t )
        self.target.position = ( self.start_position +Point2( *p ) )

    def __reversed__(self):
        return Bezier(self.bezier, self.duration, not self.forward)

class Jump(IntervalAction):
    """Moves a `CocosNode` object simulating a jump movement by modifying it's position attribute.

    Example::

        action = Jump(50,200, 5, 6)    # Move the sprite 200 pixels to the right
        sprite.do( action )            # in 6 seconds, doing 5 jumps
                                       # of 50 pixels of height
    """

    def init(self, y=150, x=120, jumps=1, duration=5):
        """Init method

        :Parameters:
            `y` : integer
                Height of jumps
            `x` : integer
                horizontal movement relative to the startin position
            `jumps` : integer
                quantity of jumps
            `duration` : float
                Duration time in seconds
        """

        import warnings
        warnings.warn('Deprecated "Jump" action. Consider using JumpBy instead', DeprecationWarning)
        

        self.y = y
        self.x = x
        self.duration = duration
        self.jumps = jumps

    def start( self ):
        self.start_position = self.target.position

    def update(self, t):
        y = int( self.y * abs( math.sin( t * math.pi * self.jumps ) ) )

        x = self.x * t
        self.target.position = self.start_position + Point2(x,y)

    def __reversed__(self):
        return Jump(self.y, -self.x, self.jumps, self.duration)

class JumpBy(IntervalAction):
    """Moves a `CocosNode` object simulating a jump movement by modifying it's position attribute.

    Example::

        # Move the sprite 200 pixels to the right and up
        action = JumpBy((100,100),200, 5, 6)    
        sprite.do( action )            # in 6 seconds, doing 5 jumps
                                       # of 200 pixels of height
    """

    def init(self, position=(0,0), height=100, jumps=1, duration=5):
        """Init method

        :Parameters:
            `position` : integer x integer tuple
                horizontal and vertical movement relative to the 
                starting position
            `height` : integer
                Height of jumps
            `jumps` : integer
                quantity of jumps
            `duration` : float
                Duration time in seconds
        """
        self.position = position
        self.height = height
        self.duration = duration
        self.jumps = jumps

    def start( self ):
        self.start_position = self.target.position
        self.delta = Vector2(*self.position)
        
    def update(self, t):
        y = self.height * abs( math.sin( t * math.pi * self.jumps ) )
        y = int(y+self.delta[1] * t)
        x = self.delta[0] * t
        self.target.position = self.start_position + Point2(x,y)
        
    def __reversed__(self):
        return JumpBy( (-self.position[0],-self.position[1]), self.height, self.jumps, self.duration)

class JumpTo(JumpBy):
    """Moves a `CocosNode` object to a position simulating a jump movement by modifying
    it's position attribute.

    Example::

        action = JumpTo(50,200, 5, 6)  # Move the sprite 200 pixels to the right
        sprite.do( action )            # in 6 seconds, doing 5 jumps
                                       # of 50 pixels of height
    """


    def start( self ):
        self.start_position = self.target.position
        self.delta = Vector2(*self.position)-self.start_position
          

class Delay(IntervalAction):
    """Delays the action a certain amount of seconds

   Example::

        action = Delay(2.5)
        sprite.do( action )
    """
    def init(self, delay):
        """Init method

        :Parameters:
            `delay` : float
                Seconds of delay
        """
        self.duration = delay

    def __reversed__(self):
        return self
        

class RandomDelay(Delay):
    """Delays the actions between *min* and *max* seconds

   Example::

        action = RandomDelay(2.5, 4.5)      # delays the action between 2.5 and 4.5 seconds
        sprite.do( action )
    """
    def init(self, low, hi):
        """Init method

        :Parameters:
            `low` : float
                Minimun seconds of delay
            `hi` : float
                Maximun seconds of delay
        """
        self.low = low
        self.hi = hi

    def __deepcopy__(self, memo):
        new = copy.copy(self)
        new.duration = self.low + (random.random() * (self.hi - self.low))
        return new

    def __mul__(self, other):
        if not isinstance(other, int):
            raise TypeError("Can only multiply actions by ints")
        if other <= 1:
            return self
        return RandomDelay(low*other, hi*other)

