#!/usr/bin/env python3


"""
Quaternion implementation for use in pymavlink
"""

from __future__ import absolute_import, division, print_function

from builtins import object
import numpy as np
from .rotmat import Vector3, Matrix3

__author__ = "Thomas Gubler"
__copyright__ = "Copyright (C) 2014 Thomas Gubler"
__license__ = "GNU Lesser General Public License v3"
__email__ = "thomasgubler@gmail.com"


class QuaternionBase(object):

    """
    Quaternion class, this is the version which supports numpy arrays
    If you need support for Matrix3 look at the Quaternion class

    Usage:
        >>> from quaternion import QuaternionBase
        >>> import numpy as np
        >>> q = QuaternionBase([np.radians(20), np.radians(20), np.radians(20)])
        >>> print(q)
        [ 0.9603483   0.13871646  0.19810763  0.13871646]
        >>> print(q.dcm)
        [[ 0.88302222 -0.21147065  0.41898917]
         [ 0.3213938   0.92303098 -0.21147065]
         [-0.34202014  0.3213938   0.88302222]]
        >>> q = QuaternionBase([1, 0, 0, 0])
        >>> print(q.euler)
        [ 0. -0.  0.]
        >>> m = [[1, 0, 0], [0, 0, -1], [0, 1, 0]]
        >>> q = QuaternionBase(m)
        >>> vector = [0, 1, 0]
        >>> vector2 = q.transform(vector)
    """

    def __init__(self, attitude=[1, 0, 0, 0]):
        """
        Construct a quaternion from an attitude

        :param attitude: another QuaternionBase,
            3 element list [roll, pitch, yaw],
            4 element list [w, x, y ,z], DCM (3x3 array)
        """
        if isinstance(attitude, QuaternionBase):
            self.q = attitude.q
        elif np.array(attitude).shape == (3, 3):
            self.dcm = attitude
        elif len(attitude) == 4:
            self.q = attitude
        elif len(attitude) == 3:
            self.euler = attitude
        else:
            raise TypeError("attitude is not valid")

    @property
    def q(self):
        """
        Get the quaternion
        :returns: array containing the quaternion elements
        """
        if self._q is None:
            if self._euler is not None:
                # get q from euler
                self._q = self._euler_to_q(self.euler)
            elif self._dcm is not None:
                # get q from DCM
                self._q = self._dcm_to_q(self.dcm)
        return self._q

    def __getitem__(self, index):
        """Returns the quaternion entry at index"""
        return self.q[index]

    @q.setter
    def q(self, q):
        """
        Set the quaternion
        :param q: list or array of quaternion values [w, x, y, z]
        """
        self._q = np.array(q)

        # mark other representations as outdated, will get generated on next
        # read
        self._euler = None
        self._dcm = None

    @property
    def euler(self):
        """
        Get the euler angles.
        The convention is Tait-Bryan (ZY'X'')

        :returns: array containing the euler angles [roll, pitch, yaw]
        """
        if self._euler is None:
            if self._q is not None:
                # try to get euler angles from q via DCM
                self._dcm = self._q_to_dcm(self.q)
                self._euler = self._dcm_to_euler(self.dcm)
            elif self._dcm is not None:
                # get euler angles from DCM
                self._euler = self._dcm_to_euler(self.dcm)
        return self._euler

    @euler.setter
    def euler(self, euler):
        """
        Set the euler angles
        :param euler: list or array of the euler angles [roll, pitch, yaw]

        """
        assert(len(euler) == 3)
        self._euler = np.array(euler)

        # mark other representations as outdated, will get generated on next
        # read
        self._q = None
        self._dcm = None

    @property
    def dcm(self):
        """
        Get the DCM

        :returns: 3x3 array
        """
        if self._dcm is None:
            if self._q is not None:
                # try to get dcm from q
                self._dcm = self._q_to_dcm(self.q)
            elif self._euler is not None:
                # try to get get dcm from euler
                self._dcm = self._euler_to_dcm(self._euler)
        return self._dcm

    @dcm.setter
    def dcm(self, dcm):
        """
        Set the DCM
        :param dcm: 3x3 array

        """
        assert(len(dcm) == 3)
        for sub in dcm:
            assert(len(sub) == 3)

        self._dcm = np.array(dcm)

        # mark other representations as outdated, will get generated on next
        # read
        self._q = None
        self._euler = None

    def transform(self, v):
        """
        Calculates the vector transformed by this quaternion
        :param v: array with len 3 to be transformed
        :returns: transformed vector
        """
        assert(len(v) == 3)
        assert(np.allclose(self.norm, 1))
        # perform transformation t = q * [0, v] * q^-1 but avoid multiplication
        # because terms cancel out
        q0 = self.q[0]
        qi = self.q[1:4]
        ui = np.array(v)
        a = q0 * ui + np.cross(qi, ui)
        t = np.dot(qi, ui) * qi + q0 * a - np.cross(a, qi)
        return t

    @property
    def norm(self):
        """
        Returns norm of quaternion

        :returns: norm (scalar)
        """
        return QuaternionBase.norm_array(self.q)

    def normalize(self):
        """Normalizes the quaternion"""
        self.q = QuaternionBase.normalize_array(self.q)

    @property
    def inversed(self):
        """
        Get inversed quaternion

        :returns: inversed quaternion
        """
        q_inv = self._q_inversed(self.q)
        return QuaternionBase(q_inv)

    def __eq__(self, other):
        """
        Equality test (same orientation, not necessarily same rotation)

        :param other: a QuaternionBase
        :returns: true if the quaternions are equal
        """
        if isinstance(other, QuaternionBase):
            return abs(self.q.dot(other.q)) > 1 - np.finfo(float).eps
        return NotImplemented

    def close(self, other):
        """
        Equality test with tolerance
        (same orientation, not necessarily same rotation)


        :param other: a QuaternionBase
        :returns: true if the quaternions are almost equal
        """
        if isinstance(other, QuaternionBase):
            return np.allclose(self.q, other.q) or np.allclose(self.q, -other.q)
        return NotImplemented

    def __mul__(self, other):
        """
        :param other: QuaternionBase
        :returns: multiplaction of this Quaternion with other
        """
        if isinstance(other, QuaternionBase):
            o = other.q
        elif len(other) == 4:
            o = other
        else:
            return NotImplemented

        return QuaternionBase(self._mul_array(self.q, o))

    def __truediv__(self, other):
        """
        :param other: QuaternionBase
        :returns: division of this Quaternion with other
        """
        if isinstance(other, QuaternionBase):
            o = other
        elif len(other) == 4:
            o = QuaternionBase(other)
        else:
            return NotImplemented
        return self * o.inversed

    @staticmethod
    def normalize_array(q):
        """
        Normalizes the list with len 4 so that it can be used as quaternion
        :param q: array of len 4
        :returns: normalized array
        """
        assert(len(q) == 4)
        q = np.array(q)
        n = QuaternionBase.norm_array(q)
        return q / n

    @staticmethod
    def norm_array(q):
        """
        Calculate quaternion norm on array q
        :param quaternion: array of len 4
        :returns: norm (scalar)
        """
        assert(len(q) == 4)
        return np.sqrt(np.dot(q, q))

    def _mul_array(self, p, q):
        """
        Performs multiplication of the 2 quaterniona arrays p and q
        :param p: array of len 4
        :param q: array of len 4
        :returns: array of len, result of p * q (with p, q quaternions)
        """
        assert(len(q) == len(p) == 4)
        p0 = p[0]
        pi = p[1:4]
        q0 = q[0]
        qi = q[1:4]

        res = np.zeros(4)
        res[0] = p0 * q0 - np.dot(pi, qi)
        res[1:4] = p0 * qi + q0 * pi + np.cross(pi, qi)

        return res

    def _euler_to_q(self, euler):
        """
        Create q array from euler angles
        :param euler: array [roll, pitch, yaw] in rad
        :returns: array q which represents a quaternion [w, x, y, z]
        """
        assert(len(euler) == 3)
        phi = euler[0]
        theta = euler[1]
        psi = euler[2]
        c_phi_2 = np.cos(phi / 2)
        s_phi_2 = np.sin(phi / 2)
        c_theta_2 = np.cos(theta / 2)
        s_theta_2 = np.sin(theta / 2)
        c_psi_2 = np.cos(psi / 2)
        s_psi_2 = np.sin(psi / 2)
        q = np.zeros(4)
        q[0] = (c_phi_2 * c_theta_2 * c_psi_2 +
                s_phi_2 * s_theta_2 * s_psi_2)
        q[1] = (s_phi_2 * c_theta_2 * c_psi_2 -
                c_phi_2 * s_theta_2 * s_psi_2)
        q[2] = (c_phi_2 * s_theta_2 * c_psi_2 +
                s_phi_2 * c_theta_2 * s_psi_2)
        q[3] = (c_phi_2 * c_theta_2 * s_psi_2 -
                s_phi_2 * s_theta_2 * c_psi_2)
        return q

    def _q_to_dcm(self, q):
        """
        Create DCM from q
        :param q: array q which represents a quaternion [w, x, y, z]
        :returns: 3x3 dcm array
        """
        assert(len(q) == 4)
        assert(np.allclose(QuaternionBase.norm_array(q), 1))
        dcm = np.zeros([3, 3])
        a = q[0]
        b = q[1]
        c = q[2]
        d = q[3]
        a_sq = a * a
        b_sq = b * b
        c_sq = c * c
        d_sq = d * d
        dcm[0][0] = a_sq + b_sq - c_sq - d_sq
        dcm[0][1] = 2 * (b * c - a * d)
        dcm[0][2] = 2 * (a * c + b * d)
        dcm[1][0] = 2 * (b * c + a * d)
        dcm[1][1] = a_sq - b_sq + c_sq - d_sq
        dcm[1][2] = 2 * (c * d - a * b)
        dcm[2][0] = 2 * (b * d - a * c)
        dcm[2][1] = 2 * (a * b + c * d)
        dcm[2][2] = a_sq - b_sq - c_sq + d_sq
        return dcm

    def _dcm_to_q(self, dcm):
        """
        Create q from dcm
        Reference:
            - Shoemake, Quaternions,
            http://www.cs.ucr.edu/~vbz/resources/quatut.pdf

        :param dcm: 3x3 dcm array
        returns: quaternion array
        """
        assert(dcm.shape == (3, 3))
        q = np.zeros(4)

        tr = np.trace(dcm)
        if tr > 0:
            s = np.sqrt(tr + 1.0)
            q[0] = s * 0.5
            s = 0.5 / s
            q[1] = (dcm[2][1] - dcm[1][2]) * s
            q[2] = (dcm[0][2] - dcm[2][0]) * s
            q[3] = (dcm[1][0] - dcm[0][1]) * s
        else:
            dcm_i = np.argmax(np.diag(dcm))
            dcm_j = (dcm_i + 1) % 3
            dcm_k = (dcm_i + 2) % 3

            s = np.sqrt((dcm[dcm_i][dcm_i] - dcm[dcm_j][dcm_j] -
                         dcm[dcm_k][dcm_k]) + 1.0)
            q[dcm_i + 1] = s * 0.5
            s = 0.5 / s
            q[dcm_j + 1] = (dcm[dcm_i][dcm_j] + dcm[dcm_j][dcm_i]) * s
            q[dcm_k + 1] = (dcm[dcm_k][dcm_i] + dcm[dcm_i][dcm_k]) * s
            q[0] = (dcm[dcm_k][dcm_j] - dcm[dcm_j][dcm_k]) * s

        return q

    def _euler_to_dcm(self, euler):
        """
        Create DCM from euler angles
        :param euler: array [roll, pitch, yaw] in rad
        :returns: 3x3 dcm array
        """
        assert(len(euler) == 3)
        phi = euler[0]
        theta = euler[1]
        psi = euler[2]
        dcm = np.zeros([3, 3])
        c_phi = np.cos(phi)
        s_phi = np.sin(phi)
        c_theta = np.cos(theta)
        s_theta = np.sin(theta)
        c_psi = np.cos(psi)
        s_psi = np.sin(psi)

        dcm[0][0] = c_theta * c_psi
        dcm[0][1] = -c_phi * s_psi + s_phi * s_theta * c_psi
        dcm[0][2] = s_phi * s_psi + c_phi * s_theta * c_psi

        dcm[1][0] = c_theta * s_psi
        dcm[1][1] = c_phi * c_psi + s_phi * s_theta * s_psi
        dcm[1][2] = -s_phi * c_psi + c_phi * s_theta * s_psi

        dcm[2][0] = -s_theta
        dcm[2][1] = s_phi * c_theta
        dcm[2][2] = c_phi * c_theta
        return dcm

    def _dcm_to_euler(self, dcm):
        """
        Create DCM from euler angles
        :param dcm: 3x3 dcm array
        :returns: array [roll, pitch, yaw] in rad
        """
        assert(dcm.shape == (3, 3))
        theta = np.arcsin(min(1, max(-1, -dcm[2][0])))

        if abs(theta - np.pi/2) < 1.0e-3:
            phi = 0.0
            psi = (np.arctan2(dcm[1][2] - dcm[0][1],
                              dcm[0][2] + dcm[1][1]) + phi)
        elif abs(theta + np.pi/2) < 1.0e-3:
            phi = 0.0
            psi = np.arctan2(dcm[1][2] - dcm[0][1],
                             dcm[0][2] + dcm[1][1] - phi)
        else:
            phi = np.arctan2(dcm[2][1], dcm[2][2])
            psi = np.arctan2(dcm[1][0], dcm[0][0])

        return np.array([phi, theta, psi])

    def _q_inversed(self, q):
        """
        Returns inversed quaternion q
        :param q: array q which represents a quaternion [w, x, y, z]
        :returns: inversed array q which is a quaternion [w, x, y ,z]
        """
        assert(len(q) == 4)
        return np.hstack([q[0], -q[1:4]])

    def __str__(self):
        """String of quaternion values"""
        return str(self.q)


class Quaternion(QuaternionBase):

    """
    Quaternion class that supports pymavlink's Vector3 and Matrix3

    Usage:
        >>> from quaternion import Quaternion
        >>> from rotmat import Vector3, Matrix3
        >>> m = Matrix3()
        >>> m.from_euler(45, 0, 0)
        >>> print(m)
        Matrix3((1.00, 0.00, 0.00), (0.00, 0.53, -0.85), (-0.00, 0.85, 0.53))
        >>> q = Quaternion(m)
        >>> print(q)
        [ 0.87330464  0.48717451  0.          0.        ]
        >>> print(q.dcm)
        Matrix3((1.00, 0.00, 0.00), (0.00, 0.53, -0.85), (-0.00, 0.85, 0.53))
        >>> v = Vector3(0, 1, 0)
        >>> v2 = q.transform(v)
        >>> print(v2)
        Vector3(0.00, 0.53, 0.85)
    """

    def __init__(self, attitude):
        """
        Construct a quaternion from an attitude

        :param attitude: another Quaternion, QuaternionBase,
            3 element list [roll, pitch, yaw],
            4 element list [w, x, y ,z], DCM (3x3 array or Matrix3)
        """
        if isinstance(attitude, Quaternion):
            self.q = attitude.q
        if isinstance(attitude, Matrix3):
            self.dcm = attitude
        elif np.array(attitude).shape == (3, 3):
            # convert dcm array to Matrix3
            self.dcm = self._dcm_array_to_matrix3(attitude)
        elif isinstance(attitude, Vector3):
            # provided euler angles
            euler = [attitude.x, attitude.y, attitude.z]
            super(Quaternion, self).__init__(euler)
        else:
            super(Quaternion, self).__init__(attitude)

    @property
    def dcm(self):
        """
        Get the DCM

        :returns: Matrix3
        """
        if self._dcm is None:
            if self._q is not None:
                # try to get dcm from q
                self._dcm = self._q_to_dcm(self.q)
            elif self._euler is not None:
                # try to get get dcm from euler
                self._dcm = self._euler_to_dcm(self._euler)
        return self._dcm

    @dcm.setter
    def dcm(self, dcm):
        """
        Set the DCM
        :param dcm: Matrix3

        """
        assert(isinstance(dcm, Matrix3))
        self._dcm = dcm.copy()

        # mark other representations as outdated, will get generated on next
        # read
        self._q = None
        self._euler = None

    @property
    def inversed(self):
        """
        Get inversed quaternion

        :returns: inversed quaternion
        """
        return Quaternion(super(Quaternion, self).inversed)

    def transform(self, v3):
        """
        Calculates the vector transformed by this quaternion
        :param v3: Vector3 to be transformed
        :returns: transformed vector
        """
        if isinstance(v3, Vector3):
            t = super(Quaternion, self).transform([v3.x, v3.y, v3.z])
            return Vector3(t[0], t[1], t[2])
        elif len(v3) == 3:
            return super(Quaternion, self).transform(v3)
        else:
            raise TypeError("param v3 is not a vector type")

    def _dcm_array_to_matrix3(self, dcm):
        """
        Converts dcm array into Matrix3
        :param dcm: 3x3 dcm array
        :returns: Matrix3
        """
        assert(dcm.shape == (3, 3))
        a = Vector3(dcm[0][0], dcm[0][1], dcm[0][2])
        b = Vector3(dcm[1][0], dcm[1][1], dcm[1][2])
        c = Vector3(dcm[2][0], dcm[2][1], dcm[2][2])
        return Matrix3(a, b, c)

    def _matrix3_to_dcm_array(self, m):
        """
        Converts Matrix3 in an array
        :param m: Matrix3
        :returns: 3x3 array
        """
        assert(isinstance(m, Matrix3))
        return np.array([[m.a.x, m.a.y, m.a.z],
                         [m.b.x, m.b.y, m.b.z],
                         [m.c.x, m.c.y, m.c.z]])

    def _q_to_dcm(self, q):
        """
        Create DCM (Matrix3) from q
        :param q: array q which represents a quaternion [w, x, y, z]
        :returns: Matrix3
        """
        assert(len(q) == 4)
        arr = super(Quaternion, self)._q_to_dcm(q)
        return self._dcm_array_to_matrix3(arr)

    def _dcm_to_q(self, dcm):
        """
        Create q from dcm (Matrix3)
        :param dcm: Matrix3
        :returns: array q which represents a quaternion [w, x, y, z]
        """
        assert(isinstance(dcm, Matrix3))
        arr = self._matrix3_to_dcm_array(dcm)
        return super(Quaternion, self)._dcm_to_q(arr)

    def _euler_to_dcm(self, euler):
        """
        Create DCM (Matrix3) from euler angles
        :param euler: array [roll, pitch, yaw] in rad
        :returns: Matrix3
        """
        assert(len(euler) == 3)
        m = Matrix3()
        m.from_euler(*euler)
        return m

    def _dcm_to_euler(self, dcm):
        """
        Create DCM from euler angles
        :param dcm: Matrix3
        :returns: array [roll, pitch, yaw] in rad
        """
        assert(isinstance(dcm, Matrix3))
        return np.array(dcm.to_euler())
    
    def __mul__(self, other):
        """
        :param other: Quaternion
        :returns: multiplaction of this Quaternion with other
        """
        return Quaternion(super(Quaternion, self).__mul__(other))

    def __truediv__(self, other):
        """
        :param other: Quaternion
        :returns: division of this Quaternion with other
        """
        return Quaternion(super(Quaternion, self).__truediv__(other))

if __name__ == "__main__":
    import doctest
    doctest.testmod()
    import unittest
    unittest.main('quaterniontest')
