# -*- coding: utf-8 -*-
# Copyright 2020 by Forschungszentrum Juelich GmbH
# Author: J. Caron
#
"""This module provides the :class:`~.Quaternion` class which can be used for rotations."""


import logging

import numpy as np


__all__ = ['Quaternion']


class Quaternion(object):
    R"""Class representing a rotation expressed by a quaternion.

    A quaternion is a four-dimensional description of a rotation which can also be described by
    a rotation vector (`v1`, `v2`, `v3`) and a rotation angle :math:`\theta`. The four components
    are calculated to:

    .. math::

        w = \cos(\theta/2)
        x = v_1 \cdot \sin(\theta/2)
        y = v_2 \cdot \sin(\theta/2)
        z = v_3 \cdot \sin(\theta/2)

    Use the :func:`~.from_axisangle` and :func:`~.to_axisangle` to convert to axis-angle
    representation and vice versa. Quaternions can be multiplied by other quaternions, which
    results in a new rotation or with a vector, which results in a rotated vector.

    Attributes
    ----------
    values : float
        The four quaternion values `w`, `x`, `y`, `z`.

    """

    NORM_TOLERANCE = 1E-6

    _log = logging.getLogger(__name__ + '.Quaternion')

    @property
    def conj(self):
        """The conjugate of the quaternion, representing a tilt in opposite direction."""
        w, x, y, z = self.values
        return Quaternion((w, -x, -y, -z))

    @property
    def matrix(self):
        """The rotation matrix representation of the quaternion."""
        w, x, y, z = self.values
        return np.array([[1 - 2 * (y ** 2 + z ** 2), 2 * (x * y - w * z), 2 * (x * z + w * y)],
                         [2 * (x * y + w * z), 1 - 2 * (x ** 2 + z ** 2), 2 * (y * z - w * x)],
                         [2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x ** 2 + y ** 2)]])

    def __init__(self, values):
        self._log.debug('Calling __init__')
        self.values = values
        self._normalize()
        self._log.debug('Created ' + str(self))

    def __mul__(self, other):  # self * other
        self._log.debug('Calling __mul__')
        if isinstance(other, Quaternion):  # Quaternion multiplication
            return self.dot_quat(self, other)
        elif len(other) == 3:  # vector multiplication (Caution: normalises!)
            q_vec = Quaternion((0,) + tuple(other))
            q = self.dot_quat(self.dot_quat(self, q_vec), self.conj)
            return q.values[1:]

    def _normalize(self):
        self._log.debug('Calling _normalize')
        mag2 = np.sum(n ** 2 for n in self.values)
        if abs(mag2 - 1.0) > self.NORM_TOLERANCE:
            mag = np.sqrt(mag2)
            self.values = tuple(n / mag for n in self.values)

    def dot_quat(self, q1, q2):
        """Multiply two :class:`~.Quaternion` objects to create a new one (always normalized).

        Parameters
        ----------
        q1, q2 : :class:`~.Quaternion`
            The quaternion which should be multiplied.

        Returns
        -------
        quaternion : :class:`~.Quaternion`
            The resulting quaternion.

        """
        self._log.debug('Calling dot_quat')
        w1, x1, y1, z1 = q1.values
        w2, x2, y2, z2 = q2.values
        w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
        x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
        y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2
        z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2
        return Quaternion((w, x, y, z))

    @classmethod
    def from_axisangle(cls, vector, theta):
        """Create a quaternion from an axis-angle representation

        Parameters
        ----------
        vector : :class:`~numpy.ndarray` (N=3)
            Vector around which the rotation is executed.
        theta : float
            Rotation angle.

        Returns
        -------
        quaternion : :class:`~.Quaternion`
            The resulting quaternion.

        """
        cls._log.debug('Calling from_axisangle')
        x, y, z = vector
        theta /= 2.
        w = np.cos(theta)
        x *= np.sin(theta)
        y *= np.sin(theta)
        z *= np.sin(theta)
        return cls((w, x, y, z))

    def to_axisangle(self):
        """Convert the quaternion to axis-angle-representation.

        Returns
        -------
        vector, theta : :class:`~numpy.ndarray` (N=3), float
            Vector around which the rotation is executed and rotation angle.

        """
        self._log.debug('Calling to_axisangle')
        w, x, y, z = self.values
        theta = 2.0 * np.arccos(w)
        return np.array((x, y, z)), theta
