# -*- coding: utf-8 -*-
"""Represents a Quaternion rotation.

The Quaternion class provides a number of convenient functions and
conversions.
::

    import numpy as np
    from pyrr import Quaternion, Matrix33, Matrix44, Vector3, Vector4

    q = Quaternion()

    # explicit creation
    q = Quaternion.from_x_rotation(np.pi / 2.0)
    q = Quaternion.from_matrix(Matrix33.identity())
    q = Quaternion.from_matrix(Matrix44.identity())

    # inferred conversions
    q = Quaternion(Quaternion())
    q = Quaternion(Matrix33.identity())
    q = Quaternion(Matrix44.identity())

    # apply one quaternion to another
    q1 = Quaternion.from_y_rotation(np.pi / 2.0)
    q2 = Quaternion.from_x_rotation(np.pi / 2.0)
    q3 = q1 * q2

    # extract a matrix from the quaternion
    m33 = q3.matrix33
    m44 = q3.matrix44

    # convert from matrix back to quaternion
    q4 = Quaternion(m44)

    # rotate a quaternion by a matrix
    q = Quaternion() * Matrix33.identity()
    q = Quaternion() * Matrix44.identity()

    # apply quaternion to a vector
    v3 = Quaternion() * Vector3()
    v4 = Quaternion() * Vector4()

    # undo a rotation
    q = Quaternion.from_x_rotation(np.pi / 2.0)
    v = q * Vector3([1.,1.,1.])
    # ~q is the same as q.conjugate
    original = ~q * v
    assert np.allclose(original, v)

    # get the dot product of 2 Quaternions
    dot = Quaternion() | Quaternion.from_x_rotation(np.pi / 2.0)
"""

from __future__ import absolute_import
import numpy as np
from multipledispatch import dispatch
from .base import BaseObject, BaseQuaternion, BaseMatrix, BaseVector, NpProxy
from .. import quaternion

class Quaternion(BaseQuaternion):
    _module = quaternion
    _shape = (4,)

    #: The X value of this Quaternion.
    x = NpProxy(0)
    #: The Y value of this Quaternion.
    y = NpProxy(1)
    #: The Z value of this Quaternion.
    z = NpProxy(2)
    #: The W value of this Quaternion.
    w = NpProxy(3)
    #: The X,Y value of this Quaternion as a numpy.ndarray.
    xy = NpProxy([0,1])
    #: The X,Y,Z value of this Quaternion as a numpy.ndarray.
    xyz = NpProxy([0,1,2])
    #: The X,Y,Z,W value of this Quaternion as a numpy.ndarray.
    xyzw = NpProxy([0,1,2,3])
    #: The X,Z value of this Quaternion as a numpy.ndarray.
    xz = NpProxy([0,2])
    #: The X,Z,W value of this Quaternion as a numpy.ndarray.
    xzw = NpProxy([0,2,3])
    #: The X,Y,W value of this Quaternion as a numpy.ndarray.
    xyw = NpProxy([0,1,3])
    #: The X,W value of this Quaternion as a numpy.ndarray.
    xw = NpProxy([0,3])

    ########################
    # Creation
    @classmethod
    def from_x_rotation(cls, theta, dtype=None):
        """Creates a new Quaternion with a rotation around the X-axis.
        """
        return cls(quaternion.create_from_x_rotation(theta, dtype))

    @classmethod
    def from_y_rotation(cls, theta, dtype=None):
        """Creates a new Quaternion with a rotation around the Y-axis.
        """
        return cls(quaternion.create_from_y_rotation(theta, dtype))

    @classmethod
    def from_z_rotation(cls, theta, dtype=None):
        """Creates a new Quaternion with a rotation around the Z-axis.
        """
        return cls(quaternion.create_from_z_rotation(theta, dtype))

    @classmethod
    def from_axis_rotation(cls, axis, theta, dtype=None):
        """Creates a new Quaternion with a rotation around the specified axis.
        """
        return cls(quaternion.create_from_axis_rotation(axis, theta, dtype))

    @classmethod
    def from_axis(cls, axis, dtype=None):
        """Creates a new Quaternion from an axis with angle magnitude.
        """
        return cls(quaternion.create_from_axis(axis, dtype))

    @classmethod
    def from_matrix(cls, matrix, dtype=None):
        """Creates a Quaternion from the specified Matrix (Matrix33 or Matrix44).
        """
        return cls(quaternion.create_from_matrix(matrix, dtype))

    @classmethod
    def from_eulers(cls, eulers, dtype=None):
        """Creates a Quaternion from the specified Euler angles.
        """
        return cls(quaternion.create_from_eulers(eulers, dtype))

    @classmethod
    def from_inverse_of_eulers(cls, eulers, dtype=None):
        """Creates a Quaternion from the inverse of the specified Euler angles.
        """
        return cls(quaternion.create_from_inverse_of_eulers(eulers, dtype))

    def __new__(cls, value=None, dtype=None):
        if value is not None:
            obj = value
            if not isinstance(value, np.ndarray):
                obj = np.array(value, dtype=dtype)

            # matrix33, matrix44
            if obj.shape in ((4,4,), (3,3,)) or isinstance(obj, (Matrix33, Matrix44)):
                obj = quaternion.create_from_matrix(obj, dtype=dtype)
        else:
            obj = quaternion.create(dtype=dtype)
        obj = obj.view(cls)
        return super(Quaternion, cls).__new__(cls, obj)

    ########################
    # Basic Operators
    @dispatch(BaseObject)
    def __add__(self, other):
        self._unsupported_type('add', other)

    @dispatch(BaseObject)
    def __sub__(self, other):
        self._unsupported_type('subtract', other)

    @dispatch(BaseObject)
    def __mul__(self, other):
        self._unsupported_type('multiply', other)

    @dispatch(BaseObject)
    def __truediv__(self, other):
        self._unsupported_type('divide', other)

    @dispatch(BaseObject)
    def __div__(self, other):
        self._unsupported_type('divide', other)

    ########################
    # Quaternions
    @dispatch((BaseQuaternion, np.ndarray, list))
    def __sub__(self, other):
        return Quaternion(super(Quaternion, self).__sub__(other))

    @dispatch((BaseQuaternion, list))
    def __mul__(self, other):
        return self.cross(other)

    @dispatch((BaseQuaternion, list))
    def __or__(self, other):
        return self.dot(other)

    def __invert__(self):
        return self.conjugate

    @dispatch((BaseQuaternion, np.ndarray, list))
    def __ne__(self, other):
        # For quaternions q and -q represent the same rotation
        return bool(np.any(super(Quaternion, self).__ne__(other)))\
               or bool(np.all(super(Quaternion, self).__eq__(-other)))

    @dispatch((BaseQuaternion, np.ndarray, list))
    def __eq__(self, other):
        # For quaternions q and -q represent the same rotation
        return bool(np.all(super(Quaternion, self).__eq__(other))) \
               or bool(np.all(super(Quaternion, self).__eq__(-other)))

    ########################
    # Matrices
    @dispatch(BaseMatrix)
    def __mul__(self, other):
        return self * Quaternion(other)

    ########################
    # Vectors
    @dispatch(BaseVector)
    def __mul__(self, other):
        return type(other)(quaternion.apply_to_vector(self, other))

    ########################
    # Methods and Properties
    @property
    def length(self):
        """Returns the length of this Quaternion.
        """
        return quaternion.length(self)

    def normalize(self):
        """normalizes this Quaternion in-place.
        """
        self[:] = quaternion.normalize(self)

    @property
    def normalized(self):
        """Returns a normalized version of this Quaternion as a new Quaternion.
        """
        return Quaternion(quaternion.normalize(self))

    def normalise(self):    # TODO: mark as deprecated
        """normalizes this Quaternion in-place.
        """
        self[:] = quaternion.normalize(self)

    @property
    def normalised(self):    # TODO: mark as deprecated
        """Returns a normalized version of this Quaternion as a new Quaternion.
        """
        return Quaternion(quaternion.normalize(self))

    @property
    def angle(self):
        """Returns the angle around the axis of rotation of this Quaternion as a float.
        """
        return quaternion.rotation_angle(self)

    @property
    def axis(self):
        """Returns the axis of rotation of this Quaternion as a Vector3.
        """
        return Vector3(quaternion.rotation_axis(self))

    def cross(self, other):
        """Returns the cross of this Quaternion and another.

        This is the equivalent of combining Quaternion rotations (like Matrix multiplication).
        """
        return Quaternion(quaternion.cross(self, other))

    def lerp(self, other, t):
        """Interpolates between quat1 and quat2 by t.
        The parameter t is clamped to the range [0, 1]
        """
        return Quaternion(quaternion.lerp(self, other, t))

    def slerp(self, other, t):
        """Spherically interpolates between quat1 and quat2 by t.
        The parameter t is clamped to the range [0, 1]
        """
        return Quaternion(quaternion.slerp(self, other, t))

    def dot(self, other):
        """Returns the dot of this Quaternion and another.
        """
        return quaternion.dot(self, other)

    @property
    def conjugate(self):
        """Returns the conjugate of this Quaternion.

        This is a Quaternion with the opposite rotation.
        """
        return Quaternion(quaternion.conjugate(self))

    @property
    def inverse(self):
        """Returns the inverse of this quaternion.
        """
        return Quaternion(quaternion.inverse(self))

    def exp(self):
        """Returns a new Quaternion representing the exponentional of this Quaternion
        """
        return Quaternion(quaternion.exp(self))

    def power(self, exponent):
        """Returns a new Quaternion representing this Quaternion to the power of the exponent.
        """
        return Quaternion(quaternion.power(self, exponent))

    @property
    def negative(self):
        """Returns the negative of the Quaternion.
        """
        return Quaternion(quaternion.negate(self))

    @property
    def is_identity(self):
        """Returns True if the Quaternion has no rotation (0.,0.,0.,1.).
        """
        return quaternion.is_identity(self)

    @property
    def matrix44(self):
        """Returns a Matrix44 representation of this Quaternion.
        """
        return Matrix44.from_quaternion(self)

    @property
    def matrix33(self):
        """Returns a Matrix33 representation of this Quaternion.
        """
        return Matrix33.from_quaternion(self)

from .vector3 import Vector3
from .matrix33 import Matrix33
from .matrix44 import Matrix44
