# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------
"""Implementation of Grid3DAction actions
"""
from __future__ import division, print_function, unicode_literals

__docformat__ = 'restructuredtext'

import math
import random

from cocos.director import director
from cocos.euclid import *
from .basegrid_actions import *


rr = random.randrange

__all__ = ['Waves3D',  # 3d actions that modifies the z-coordinate
           'FlipX3D',
           'FlipY3D',
           'Lens3D',
           'Shaky3D',
           'Ripple3D',
           'Liquid',  # 3d actions that don't modify the z-coordinate
           'Waves',
           'Twirl', ]


class Waves3D(Grid3DAction):
    """Simulates waves using the math.sin() function in the z-axis
    The x and y coordinates remains unmodified.

    Example::

       scene.do(Waves3D(waves=5, amplitude=40, grid=(16,16), duration=10))
    """

    def init(self, waves=4, amplitude=20, *args, **kw):
        """
        :Parameters:
            `waves` : int
                Number of waves (2 * pi) that the action will perform. Default is 4
            `amplitude` : int
                Wave amplitude (height). Default is 20
        """
        super(Waves3D, self).init(*args, **kw)

        #: Total number of waves to perform
        self.waves = waves

        #: amplitude rate. Default: 1.0.
        #: This value is modified by other actions like `AccelAmplitude`.
        self.amplitude_rate = 1.0
        self.amplitude = amplitude

    def update(self, t):
        for i in range(0, self.grid.x + 1):
            for j in range(0, self.grid.y + 1):
                x, y, z = self.get_original_vertex(i, j)

                z += (math.sin(t*math.pi*self.waves*2 + (y + x)*.01) * self.amplitude * self.amplitude_rate)

                self.set_vertex(i, j, (x, y, z))


class FlipX3D(Grid3DAction):
    """FlipX3D flips the screen using the Y-axis as a pivot."""

    def init(self, grid=(1, 1), *args, **kw):
        if grid != (1, 1):
            raise GridException("Invalid grid size.")
        super(FlipX3D, self).init(grid=grid, *args, **kw)

    def update(self, t):
        angle = math.pi * t  # 180 degrees
        mz = math.sin(angle)
        angle = angle / 2.0  # x calculates degrees from 0 to 90
        mx = math.cos(angle)

        x0, y, z = self.get_original_vertex(1, 1)
        x1, y, z = self.get_original_vertex(0, 0)

        if x0 > x1:
            # Normal Grid
            a = (0, 0)
            b = (0, 1)
            c = (1, 0)
            d = (1, 1)
            x = x0
        else:
            # Reversed Grid
            c = (0, 0)
            d = (0, 1)
            a = (1, 0)
            b = (1, 1)
            x = x1

        diff_x = x - x * mx
        diff_z = abs((x * mz) // 4.0)

        # bottom-left
        x, y, z = self.get_original_vertex(*a)
        self.set_vertex(a[0], a[1], (diff_x, y, z + diff_z))

        # upper-left
        x, y, z = self.get_original_vertex(*b)
        self.set_vertex(b[0], b[1], (diff_x, y, z + diff_z))

        # bottom-right
        x, y, z = self.get_original_vertex(*c)
        self.set_vertex(c[0], c[1], (x - diff_x, y, z - diff_z))

        # upper-right
        x, y, z = self.get_original_vertex(*d)
        self.set_vertex(d[0], d[1], (x - diff_x, y, z - diff_z))


class FlipY3D(Grid3DAction):
    """FlipY3D flips the screen using the X-axis as a pivot."""

    def init(self, grid=(1, 1), *args, **kw):
        if grid != (1, 1):
            raise GridException("Invalid grid size.")
        super(FlipY3D, self).init(grid=grid, *args, **kw)

    def update(self, t):
        angle = math.pi * t  # 180 degrees
        mz = math.sin(angle)
        angle = angle / 2.0  # x calculates degrees from 0 to 90
        my = math.cos(angle)

        x, y0, z = self.get_original_vertex(1, 1)
        x, y1, z = self.get_original_vertex(0, 0)

        if y0 > y1:
            # Normal Grid
            a = (0, 0)
            b = (0, 1)
            c = (1, 0)
            d = (1, 1)
            y = y0
        else:
            # Reversed Grid
            b = (0, 0)
            a = (0, 1)
            d = (1, 0)
            c = (1, 1)
            y = y1

        diff_y = y - y*my
        diff_z = abs((y * mz) // 4.0)

        # bottom-left
        x, y, z = self.get_original_vertex(*a)
        self.set_vertex(a[0], a[1], (x, diff_y, z + diff_z))

        # upper-left
        x, y, z = self.get_original_vertex(*b)
        self.set_vertex(b[0], b[1], (x, y - diff_y, z - diff_z))

        # bottom-right
        x, y, z = self.get_original_vertex(*c)
        self.set_vertex(c[0], c[1], (x, diff_y, z + diff_z))

        # upper-right
        x, y, z = self.get_original_vertex(*d)
        self.set_vertex(d[0], d[1], (x, y - diff_y, z - diff_z))


class Lens3D(Grid3DAction):
    """Lens simulates a Lens / Magnifying glass effect.
    It modifies the z-coordinate while the x and y remains unmodified.

    Example::

       scene.do(Lens3D(center=(320,240), radius=150, grid=(16,16), duration=10))
    """

    def init(self, center=(-1, -1), radius=160, lens_effect=0.7, *args, **kw):
        """
        :Parameters:
            `center` : (int,int)
                Center of the lens. Default: (win_size_width /2, win_size_height /2 )
            `radius` : int
                Radius of the lens.
            `lens_effect` : float
                How strong is the lens effect. Default: 0.7. 0 is no effect at all, 1 is a very strong lens effect.
        """
        super(Lens3D, self).init(*args, **kw)

        x, y = director.get_window_size()
        if center == (-1, -1):
            center = (x // 2, y // 2)

        #: position of the center of the len. Type: (int,int).
        #: This value can be modified by other actions, like `JumpBy` to simulate a jumping lens
        self.position = Point2(center[0] + 1, center[1] + 1)

        #: radius of the lens. Type: float
        self.radius = radius

        #: lens effect factor. Type: float
        self.lens_effect = lens_effect

        self._last_position = (-1000, -1000)  # dirty vrbl

    def update(self, t):
        if self.position != self._last_position:
            for i in range(0, self.grid.x + 1):
                for j in range(0, self.grid.y + 1):

                    x, y, z = self.get_original_vertex(i, j)

                    p = Point2(x, y)
                    vect = self.position - p
                    r = abs(vect)

                    if r < self.radius:

                        r = self.radius - r
                        pre_log = r / self.radius
                        if pre_log == 0:
                            pre_log = 0.001
                        l = math.log(pre_log) * self.lens_effect
                        new_r = math.exp(l) * self.radius

                        vect.normalize()
                        new_vect = vect * new_r

                        z += abs(new_vect) * self.lens_effect  # magic vrbl

                    # set all vertex, not only the on the changed
                    # since we want to 'fix' possible moved vertex
                    self.set_vertex(i, j, (x, y, z))
            self._last_position = self.position


class Ripple3D(Grid3DAction):
    """Simulates a ripple (radial wave) effect.
    The amplitude of the wave will decrease when it goes away from the center of the ripple.
    It modifies the z-coordinate while the x and y remains unmodified.

    Example::

       scene.do(Ripple3D(center=(320,240), radius=240, waves=15, amplitude=60, duration=20, grid=(32,24)))
    """

    def init(self, center=(-1, -1), radius=240, waves=15, amplitude=60, *args, **kw):
        """
        :Parameters:
            `center` : (int,int)
                Center of the ripple. Default: (win_size_width /2, win_size_height /2)
            `radius` : int
                Radius of the ripple. Default: 240
            `waves` : int
                Number of waves (2 * pi) that the action will perform. Default: 15
            `amplitude` : int
                Wave amplitude (height). Default is 60
        """
        super(Ripple3D, self).init(*args, **kw)

        x, y = director.get_window_size()
        if center == (-1, -1):
            center = (x // 2, y // 2)

        #: Center of the ripple. Type: (int,int).
        #: This value can be modified by other actions, like `JumpBy` to simulate a jumping ripple effect
        self.position = Point2(center[0] + 1, center[1] + 1)

        #: radius of the ripple. Type: float
        self.radius = radius

        #: number of waves. Type: int
        self.waves = waves

        #: amplitude rate. Default: 1.0.
        #: This value is modified by other actions like `AccelAmplitude`.
        self.amplitude_rate = 1.0
        self.amplitude = amplitude

    def update(self, t):
        for i in range(0, self.grid.x + 1):
            for j in range(0, self.grid.y + 1):

                x, y, z = self.get_original_vertex(i, j)

                p = Point2(x, y)
                vect = self.position - p
                r = abs(vect)

                if r < self.radius:
                    r = self.radius - r
                    rate = pow(r / self.radius, 2)
                    z += (
                        math.sin(t*math.pi*self.waves*2 + r*0.1) * self.amplitude * self.amplitude_rate * rate)

                self.set_vertex(i, j, (x, y, z))


class Shaky3D(Grid3DAction):
    """Shaky simulates an earthquake by modifying randomly the x, y and z coordinates of each vertex.

    Example::

       scene.do(Shaky3D(randrange=6, grid=(4,4), duration=10))
    """

    def init(self, randrange=6, *args, **kw):
        """
        :Parameters:
            `randrange` : int
                Number that will be used in random.randrange(-randrange, randrange) to do the effect
        """
        super(Shaky3D, self).init(*args, **kw)

        #: random range of the shaky effect
        self.randrange = randrange

    def update(self, t):
        for i in range(0, self.grid.x + 1):
            for j in range(0, self.grid.y + 1):
                x, y, z = self.get_original_vertex(i, j)
                x += rr(-self.randrange, self.randrange + 1)
                y += rr(-self.randrange, self.randrange + 1)
                z += rr(-self.randrange, self.randrange + 1)

                self.set_vertex(i, j, (x, y, z))


class Liquid(Grid3DAction):
    """Simulates a liquid effect using the math.sin() function modifying the x and y coordinates.
    The z coordinate remains unmodified.

    Example::

       scene.do(Liquid(waves=5, amplitude=40, grid=(16,16), duration=10))
    """

    def init(self, waves=4, amplitude=20, *args, **kw):
        """
        :Parameters:
            `waves` : int
                Number of waves (2 * pi) that the action will perform. Default is 4
            `amplitude` : int
                Wave amplitude (height). Default is 20
        """
        super(Liquid, self).init(*args, **kw)

        #: total number of waves
        self.waves = waves

        #: amplitude of the waves
        self.amplitude = amplitude

        #: amplitude rate. Default: 1.0.
        #: This value is modified by other actions like `AccelAmplitude`.
        self.amplitude_rate = 1.0

    def update(self, t):

        for i in range(1, self.grid.x):
            for j in range(1, self.grid.y):
                x, y, z = self.get_original_vertex(i, j)
                xpos = (x + (math.sin(t*math.pi*self.waves*2 + x*.01) * self.amplitude * self.amplitude_rate))
                ypos = (y + (math.sin(t*math.pi*self.waves*2 + y*.01) * self.amplitude * self.amplitude_rate))
                self.set_vertex(i, j, (xpos, ypos, z))


class Waves(Grid3DAction):
    """Simulates waves using the math.sin() function both in the vertical and horizontal axis.
    The z coordinate is not modified.

    Example::

        scene.do(Waves(waves=4, amplitude=20, hsin=False, vsin=True, grid=(16,16), duration=10))
    """

    def init(self, waves=4, amplitude=20, hsin=True, vsin=True, *args, **kw):
        """Initializes the Waves actions

        :Parameters:
            `waves` : int
                Number of waves (2 * pi) that the action will perform. Default is 4
            `amplitude` : int
                Wave amplitude (height). Default is 20
            `hsin` : bool
                whether or not in will perform horizontal waves. Default is True
            `vsin` : bool
                whether or not in will perform vertical waves. Default is True
        """
        super(Waves, self).init(*args, **kw)

        #: whether or not it will do horizontal waves
        self.hsin = hsin

        #: whether or not it will do vertical waves
        self.vsin = vsin

        #: total number of wave
        self.waves = waves

        #: amplitude of the waves
        self.amplitude = amplitude

        #: amplitude rate. Default: 1.0
        #: This value is modified by other actions like `AccelAmplitude`.
        self.amplitude_rate = 1.0

    def update(self, t):
        for i in range(0, self.grid.x + 1):
            for j in range(0, self.grid.y + 1):
                x, y, z = self.get_original_vertex(i, j)
                if self.vsin:
                    xpos = (
                        x + (math.sin(t*math.pi*self.waves*2 + y*.01) * self.amplitude * self.amplitude_rate))
                else:
                    xpos = x

                if self.hsin:
                    ypos = (
                        y + (math.sin(t*math.pi*self.waves*2 + x*.01) * self.amplitude * self.amplitude_rate))
                else:
                    ypos = y

                self.set_vertex(i, j, (xpos, ypos, z))


class Twirl(Grid3DAction):
    """Simulates a twirl effect modifying the x and y coordinates.
    The z coordinate is not modified.

    Example::

       scene.do(Twirl(center=(320,240), twirls=5, amplitude=1, grid=(16,12), duration=10))
    """

    def init(self, center=(-1, -1), twirls=4, amplitude=1, *args, **kw):
        """
        :Parameters:
            `twirls` : int
                Number of twirls (2 * pi) that the action will perform. Default is 4
            `amplitude` : flaot
                Twirl amplitude. Default is 1
            `center` : (int,int)
                Center of the twirl in x,y coordinates. Default: center of the screen
        """
        super(Twirl, self).init(*args, **kw)

        x, y = director.get_window_size()
        if center == (-1, -1):
            center = (x // 2, y // 2)

        #: position of the center of the Twril. Type: (int,int).
        #: This value can be modified by other actions, like `JumpBy` to simulate a jumping Twirl
        self.position = Point2(center[0] + 1, center[1] + 1)

        #: total number of twirls
        self.twirls = twirls

        #: amplitude of the twirls
        self.amplitude = amplitude

        #: amplitude rate. Default: 1.0.
        #: This value is modified by other actions like `AccelAmplitude`.
        self.amplitude_rate = 1.0

    def update(self, t):

        cx = self.position.x
        cy = self.position.y

        for i in range(0, self.grid.x + 1):
            for j in range(0, self.grid.y + 1):
                x, y, z = self.get_original_vertex(i, j)

                r = math.sqrt((i - self.grid.x/2.0)**2 + (j - self.grid.y/2.0)**2)

                amplitude = 0.1 * self.amplitude * self.amplitude_rate

                a = r * math.cos(math.pi/2.0 + t*math.pi*self.twirls*2) * amplitude

                dx = math.sin(a)*(y - cy) + math.cos(a)*(x - cx)
                dy = math.cos(a)*(y - cy) - math.sin(a)*(x - cx)

                self.set_vertex(i, j, (cx + dx, cy + dy, z))
