####################################################################################################
#
# Musica - A Music Theory Package for Python
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
####################################################################################################
import math
import numpy as np
from IntervalArithmetic import Interval2D, IntervalInt2D
####################################################################################################
from .MathFunctions import sign, trignometric_clamp #, is_in_trignometric_range
from .Primitive import Primitive2D
####################################################################################################
[docs]class VectorAbc(Primitive2D):
    __data_type__ = None
    ##############################################
[docs]    def __init__(self, array_size, v_size):
        # Fixme: v_size versus homogeneous
        self._array = np.zeros(array_size, dtype=self.__data_type__)
        self._v = self._array[:v_size] # view 
    ##############################################
[docs]    def clone(self):
        """ Return a copy of self """
        return self.__class__(self) 
    ##############################################
    @property
    def array(self):
        return self._array
    @property
    def v(self):
        return self._v
    # @v.setter
    # def v(self, value):
    #     self._v = value
    @property
    def x(self):
        return self.__data_type__(self._array[0])
    @property
    def y(self):
        return self.__data_type__(self._array[1])
    @x.setter
    def x(self, x):
        self._array[0] = x
    @y.setter
    def y(self, y):
        self._array[1] = y
    ##############################################
[docs]    def __repr__(self):
        return self.__class__.__name__ + str(self._array) 
    ##############################################
[docs]    def __len__(self):
        return NotImplementedError 
    ##############################################
[docs]    def __iter__(self):
        return iter(self._array) 
    ##############################################
[docs]    def __getitem__(self, a_slice):
        return self._array[a_slice] 
    ##############################################
[docs]    def __setitem__(self, index, value):
        self._array[index] = value 
    ##############################################
[docs]    def to_numpy(self):
        return np.array(self._array, dtype=self.__data_type__) 
    ##############################################
[docs]    def to_int_list(self):
        return [int(x) for x in self] 
    ##############################################
[docs]    def __nonzero__(self):
        return bool(self._array.any()) 
    ##############################################
[docs]    def __eq__(v1, v2):
        """ self == other """
        return np.array_equal(v1._array, v2.array) 
    ##############################################
 
####################################################################################################
[docs]class VectorArithmeticMixin:
    ##############################################
[docs]    def __add__(self, other):
        """ Return a new vector equal to the addition of self and other """
        return self.__class__(self._v + other.v) 
    ##############################################
[docs]    def __iadd__(self, other):
        """ Add other to self """
        self._v += other.v
        return self 
    ##############################################
[docs]    def __sub__(self, other):
        """ Return a new vector """
        return self.__class__(self._v - other.v) 
    ##############################################
[docs]    def __isub__(self, other):
        """ Return a new vector equal to the subtraction of self and other """
        self._v -= other.v
        return self 
    ##############################################
[docs]    def __pos__(self):
        """ Return a new vector equal to self """
        return self.__class__(self._v) 
    ##############################################
[docs]    def __neg__(self):
        """ Return a new vector equal to the negation of self """
        return self.__class__(-self._v) 
    ##############################################
[docs]    def __abs__(self):
        """ Return a new vector equal to abs of self """
        return self.__class__(np.abs(self._v)) 
    ##############################################
[docs]    def __mul__(self, scale):
        """ Return a new vector equal to the self scaled by scale """
        return self.__class__(scale * self._v) 
    ##############################################
[docs]    def __imul__(self, scale):
        """ Scale self by scale """
        self._v *= scale
        return self 
    ##############################################
[docs]    def magnitude_square(self):
        """ Return the square of the magnitude of the vector """
        return np.dot(self._v, self._v) 
    ##############################################
[docs]    def normal(self):
        """ Return a new vector equal to self rotated of 90 degree in the counter clockwise
        direction
        """
        xp = -self._v[1]
        yp =  self._v[0]
        return self.__class__((xp, yp)) 
    ##############################################
[docs]    def anti_normal(self):
        """ Return a new vector equal to self rotated of 90 degree in the clockwise direction
        """
        xp =  self._v[1]
        yp = -self._v[0]
        return self.__class__((xp, yp)) 
    ##############################################
[docs]    def parity(self):
        """ Return a new vector equal to self rotated of 180 degree
        """
        # parity
        xp = -self._v[0]
        yp = -self._v[1]
        return self.__class__((xp, yp)) 
    ##############################################
[docs]    def dot(self, other):
        """ Return the dot product of self with other """
        return self.__data_type__(np.dot(self._v, other.v)) 
    ##############################################
[docs]    def cross(self, other):
        """ Return the cross product of self with other """
        return self.__data_type__(np.cross(self._v, other.v))  
####################################################################################################
[docs]class VectorIntMixin:
    __data_type__ = np.int
    ##############################################
[docs]    def bounding_box(self):
        x, y = self.x, self.y
        return IntervalInt2D((x, x) , (y, y))  
####################################################################################################
[docs]class VectorFloatMixin:
    __data_type__ = np.float
    ##############################################
[docs]    def bounding_box(self):
        x, y = self.x, self.y
        return Interval2D((x, x) , (y, y)) 
    ##############################################
[docs]    def almost_equal(v1, v2, rtol=1e-05, atol=1e-08, equal_nan=False):
        """ self ~= other """
        return np.allclose(tuple(v1), tuple(v2), rtol, atol, equal_nan) 
    ##############################################
[docs]    def __truediv__(self, scale):
        """ Return a new vector equal to the self dvivided by scale """
        return self.__class__(self._v / scale) 
    ##############################################
[docs]    def __itruediv__(self, scale):
        """ Scale self by 1/scale """
        self._v /= scale
        return self 
    ##############################################
[docs]    def normalise(self):
        """ Normalise the vector """
        self._v /= self.magnitude() 
    ##############################################
[docs]    def magnitude(self):
        """ Return the magnitude of the vector """
        return math.sqrt(self.magnitude_square()) 
    ##############################################
[docs]    def orientation(self):
        """ Return the orientation in degree """
        #
        # 2 | 1
        # - + -
        # 4 | 3
        #
        #       | 1    | 2         | 3    | 4         |
        # x     | +    | -         | +    | -         |
        # y     | +    | +         | -    | -         |
        # tan   | +    | -         | -    | +         |
        # atan  | +    | -         | -    | +         |
        # theta | atan | atan + pi | atan | atan - pi |
        #
        if not bool(self):
            raise NameError("Null Vector")
        if self.x == 0:
            return math.copysign(90, self.y)
        elif self.y == 0:
            return 0 if self.x >= 0 else 180
        else:
            orientation = math.degrees(math.atan(self.tan()))
            if self.x < 0:
                if self.y > 0:
                    orientation += 180
                else:
                    orientation -= 180
            return orientation 
    ##############################################
[docs]    def rotate(self, angle, counter_clockwise=True):
        """ Return a new vector equal to self rotated of angle degree in the counter clockwise
        direction
        """
        radians = math.radians(angle)
        if not counter_clockwise:
            radians = -radians
        c = math.cos(radians)
        s = math.sin(radians)
        # Fixme: np matrice
        xp = c * self._v[0] -s * self._v[1]
        yp = s * self._v[0] +c * self._v[1]
        return self.__class__((xp, yp)) 
    ##############################################
[docs]    def tan(self):
        """ Return the tangent """
        # RuntimeWarning: divide by zero encountered in double_scalars
        return self.y / self.x 
    ##############################################
[docs]    def inverse_tan(self):
        """ Return the inverse tangent """
        return self.x / self.y 
    ##############################################
[docs]    def is_parallel(self, other):
        """ Self is parallel with other """
        return round(self.cross(other), 7) == 0 
    ##############################################
[docs]    def is_orthogonal(self, other):
        """ Self is orthogonal with other """
        return round(self.dot(other), 7) == 0 
    ##############################################
[docs]    def cos_with(self, direction):
        """ Return the cosinus of self with direction """
        cos = direction.dot(self) / (direction.magnitude() * self.magnitude())
        return trignometric_clamp(cos) 
    ##############################################
[docs]    def projection_on(self, direction):
        """ Return the projection of self on direction """
        return direction.dot(self) / direction.magnitude() 
    ##############################################
[docs]    def sin_with(self, direction):
        """ Return the sinus of self with other """
        # turn from direction to self
        sin = direction.cross(self) / (direction.magnitude() * self.magnitude())
        return trignometric_clamp(sin) 
    ##############################################
[docs]    def deviation_with(self, direction):
        """ Return the deviation of self with other """
        return direction.cross(self) / direction.magnitude() 
    ##############################################
[docs]    def orientation_with(self, direction):
        # Fixme: check all cases
        # -> angle_with
        """ Return the angle of self on direction """
        angle = math.acos(self.cos_with(direction))
        angle_sign = sign(self.sin_with(direction))
        return angle_sign * math.degrees(angle)  
####################################################################################################
[docs]class Vector2DBase(VectorAbc):
    ##############################################
[docs]    def __init__(self, *args):
        """
        Example of usage::
          Vector(1, 3)
          Vector((1, 3))
          Vector([1, 3])
          Vector(iterable)
          Vector(vector)
        """
        array = self._check_arguments(args)
        super().__init__(2, 2)
        self._v[...] = array[:2] 
    ##############################################
[docs]    def _check_arguments(self, args):
        size = len(args)
        if size == 1:
            array = args[0] # iterable, vector
        elif size == 2:
            array = args # (x, y)
        else:
            raise ValueError("More than 2 arguments where given")
        # if not (np.iterable(array) and len(array) == 2):
        #     raise ValueError("Argument must be iterable and of length 2")
        return array 
    ##############################################
[docs]    def __len__(self):
        return 2 
    ##############################################
 
####################################################################################################
[docs]class Vector2DInt(VectorIntMixin, VectorArithmeticMixin, Vector2DBase):
    pass 
####################################################################################################
[docs]class Vector2D(VectorFloatMixin, VectorArithmeticMixin, Vector2DBase):
    """ 2D Vector """
    ##############################################
[docs]    @classmethod
    def from_angle(cls, angle):
        """ Create the unitary vector (cos(angle), sin(angle)).  The *angle* is in degree. """
        rad = math.radians(angle)
        return cls((math.cos(rad), math.sin(rad))) 
    ##############################################
[docs]    @classmethod
    def middle(cls, p0, p1):
        """ Return the middle point. """
        return cls(p0 + p1) * .5 
    ##############################################
[docs]    def rint(self):
        return Vector2DInt(np.rint(self._v)) 
    ##############################################
[docs]    def to_normalised(self):
        """ Return a normalised vector """
        return NormalisedVector2D(self._v / self.magnitude())  
####################################################################################################
[docs]class NormalisedVector2D(VectorAbc):
    """ 2D Normalised Vector """
    ##############################################
[docs]    def __init__(self, array):
        VectorAbc.__init__(self, 2, 2)
        self._v[...] = array 
        #! if self.magnitude() != 1.:
        #!     raise ValueError("Magnitude != 1")
        # if not (is_in_trignometric_range(self.x) and
        #         is_in_trignometric_range(self.y)):
        #     raise ValueError("Values must be in trignometric range")
    ##############################################
[docs]    def __mul__(self, scale):
        """ Return a new vector equal to the self scaled by scale """
        return Vector2D(scale * self._v)  
####################################################################################################
[docs]class HomogeneousVector2D(VectorAbc):
    """ 2D Homogeneous Coordinate Vector """
    __data_type__ = float
    ##############################################
[docs]    def __init__(self, *args):
        array = self._check_arguments(args)
        VectorAbc.__init__(self, 3, 2)
        if len(array) == 2:
            self._v[...] = array
            self.w = 1
        else:
            self._array[...] = array[:3] 
    ##############################################
[docs]    def _check_arguments(self, args):
        size = len(args)
        if size == 1:
            array = args[0] # iterable, vector
        elif 2 <= size <= 3:
            array = args
        else:
            raise ValueError("More than 3 arguments where given")
        return array 
    ##############################################
[docs]    def to_vector(self):
        return Vector2D(self._v) 
    ##############################################
    @property
    def w(self):
        return self._array[2]
    @w.setter
    def w(self, value):
        self._array[2] = value
    ##############################################
[docs]    def __len__(self):
        return 3