# ----------------------------------------------------------------------------
# cocos2d
# Copyright (c) 2008-2012 Daniel Moisset, Ricardo Quesada, Rayentray Tappa,
# Lucio Torre
# Copyright (c) 2009-2017  Richard Jones, Claudio Canepa
# 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
"""

from __future__ import division, print_function, unicode_literals


__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):
        if t != 1.0:
            ft = (t - 0.5) * 12
            t = 1./(1. + math.exp(-ft))
        self.other.update(t)

    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

    The action ends with the same visible state than at the start time.

    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 start(self):
        self.end_invisible = not self.target.visible

    def update(self, t):
        slice = 1.0 / self.times
        m = t % slice
        self.target.visible = self.end_invisible ^ (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(self.low * other, self.hi * other)
