#
##
##  This file is part of pyFormex 1.0.5  (Sat Feb 16 10:40:32 CET 2019)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: http://pyformex.org
##  Project page:  http://savannah.nongnu.org/projects/pyformex/
##  Copyright 2004-2018 (C) Benedict Verhegghe (benedict.verhegghe@ugent.be)
##  Distributed under the GNU General Public License version 3 or later.
##
##  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/.
##
"""Coordinate Systems.

"""
from __future__ import absolute_import, division, print_function

import numpy as np

from pyformex import utils
from pyformex import arraytools as at
from pyformex.coords import Coords


###########################################################################
##
##   class CoordSys
##
#########################
#

#
# TODO: This could be improved:
#   - generalized: cartesian, cylindrical, spherical
#

class CoordSys(object):
    """A Cartesian coordinate system in 3D space.

    The coordinate system is stored as a rotation matrix and a translation
    vector, which transform the global coordinate axes into the axes of the
    CoordSys.
    Clearly, the translation vector is the origin of the CoordSys, and the
    rows of the rotation matrix are the unit vectors along the three axes.

    The CoordSys can be initialized in different ways and has only optional
    parameters to achieve this. If none are specified at all, the global
    coordinate axis results.

    Parameters
    ----------
    oab: float array-like (3,3), optional
        Three non-collinear points O, A and B, that define the CoordSys
        in the following way: O is the origin of the coordinate system,
        A is a point along the positive first axis and B is a point B in
        the plane of the first two axes.
    points: float array-like (4,3), optional
        The CoordSys is specified by four points: the first three are
        points on the three coordinate axes, at distance +1 from the
        origin; the fourth point is the origin. The use of this parameter
        is deprecated. It can be replaced with `oab=points[3,0,1]`.
    rot: float array-like (3,3), optional
        Rotation matrix that transforms the global global axes to be
        parallel to the constructed CoordSys. The user is responsible to
        make sure that `rot` is a proper orthonormal rotation matrix.
    trl: float array-like (3,)
        Translation vector that moves the global origin to the origin of the
        CoordSys, in other words, this is the origin of the new CoordSys
        in global coordinates.

    Notes
    -----
    If `oab` is provided, it takes precedence and the other parameters
    are ignored. If not, and `points` is provided, this takes precedence
    and the remaining are ignored. If neither `oab` nor `points` are
    provided, `rot` and `trl` are used and have default values equal
    to the rotation matrix and translation vector of the global coordinatee
    axes.


    A Coords object has a number of attributes that provide quick acces
    to its internal data. Of these, `trl` and `rot` can be used to set
    the data of the CoordSys and thus change the CoordSys in-place.

    Attributes
    ----------
    trl: float array (3,)
        The origin of the CoordSys
    rot: float array (3,3)
        The rotation matrix of the CoordSys
    u: float array (3,)
        The unit vector along the first axis (0).
    v: float array (3,)
        The unit vector along the second axis (1).
    w: float array (3,)
        The unit vector along the third axis (2).
    o: float array (3,)
        The origin of the CoordSys.

    Examples
    --------
    Three points O,A,B in the xy-plane, first two parallel to x-axis,
    third at higher y-value. The resulting CoordSys is global axes
    translated to point O.

    >>> OAB = Coords([[ 2.,1.,0.],[ 5.,1.,0.],[ 0.,3.,0.]])
    >>> print(CoordSys(oab=OAB))
    CoordSys: trl=[ 2. 1. 0.]; rot=[[ 1. 0. 0.]
                                    [ 0. 1. 0.]
                                    [ 0. 0. 1.]]

    Now translate the points so that O is on the z-axis, and rotate the
    points 30 degrees around z-axis.

    >>> OAB = OAB.trl([-2.,-1.,5.]).rot(30)
    >>> print(OAB)
    [[ 0.    0.    5.  ]
     [ 2.6   1.5   5.  ]
     [-2.73  0.73  5.  ]]
    >>> C = CoordSys(oab=OAB)
    >>> print(C)
    CoordSys: trl=[ 0. 0. 5.]; rot=[[ 0.87  0.5   0.  ]
                                    [-0.5   0.87  0.  ]
                                    [ 0.   -0.    1.  ]]

    Reverse axes x and y. The resulting CoordSys is still righthanded. This is
    equivalent to a rotation over 180 degrees around z-axis.

    >>> print(C.reverse(0,1))
    CoordSys: trl=[ 0. 0. 5.]; rot=[[-0.87 -0.5  -0.  ]
                                    [ 0.5  -0.87 -0.  ]
                                    [ 0.   -0.    1.  ]]

    Now rotate C over 150 degrees around z-axis to become parallel with
    the global axes.

    >>> print(C.rotate(150,2))
    CoordSys: trl=[ 0.  0.  5.]; rot=[[ 1.  0.  0.]
                                      [-0.  1.  0.]
                                      [ 0.  0.  1.]]

    """
    def __init__(self,oab=None,points=None,rot=None,trl=None):
        """Initialize the CoordSys"""
        if oab is not None:
            oab = at.checkArray(oab,(3,3),'f')
            rot = at.rotmat(oab)
            trl = oab[0]
        elif points is not None:
            points = at.checkArray(points,(4,3),'f')
            rot = at.normalize(points[:3] - points[3])
            trl = points[3]
        else:
            if rot is None:
                rot = np.eye(3, 3)
            if trl is None:
                trl = np.zeros((3,))

        self.rot = rot
        self.trl = trl


    @property
    def trl(self):
        """Return the origin as a (3,) vector"""
        return self._trl

    @trl.setter
    def trl(self, value):
        """Set the origin to (3,) value"""
        self._trl =  at.checkArray(value,shape=(3,),kind='f')

    @property
    def rot(self):
        """Return the (3,3) rotation matrix"""
        return self._rot

    @rot.setter
    def rot(self, value):
        """Set the rotation matrix to (3,3) value"""
        self._rot = at.checkArray(value,shape=(3,3),kind='f')

    @property
    def u(self):
        """Return unit vector along axis 0 (x)"""
        return self.axis(0)

    @property
    def v(self):
        """Return unit vector along axis 1 (y)"""
        return self.axis(1)

    @property
    def w(self):
        """Return unit vector along axis 2 (z)"""
        return self.axis(2)

    @property
    def o(self):
        """Return the origin"""
        return self.trl

    # Some aliases
    origin = o
    axes = rot


    def axis(self,i):
        """Return the unit vector along an axis.

        Parameters
        ----------
        i: int (0,1,2)
            The axis number.

        Returns
        -------
        float array (3,)
            A unit vector along axis `i`.

        Notes
        -----
        If the axis is known in advance, it is more appropriate
        to use one of the u, v or w attributes

        Examples
        --------
        >>> CoordSys().rotate(30).axis(1)
        array([-0.5 ,  0.87,  0.  ], dtype=float32)
        """
        return self.rot[i]


    def points(self):
        """Return origin and endpoints of unit vectors along axes.

        Returns
        -------
        Coords (4,3)
            A Coords object with four points: the endpoints of the
            unit vectors along the three axes of the CoordSys, and
            the origin of the CoordSys.

        Examples
        --------
        >>> CoordSys().rotate(30).points()
        Coords([[ 0.87,  0.5 ,  0.  ],
                [-0.5 ,  0.87,  0.  ],
                [ 0.  ,  0.  ,  1.  ],
                [ 0.  ,  0.  ,  0.  ]])

        """
        return Coords.concatenate([self.axes+self.trl, self.trl])

    # Simple transformation methods
    # These methods modify the object inplace
    # They still return self, so that they can be concatenated

    def _translate(self,trl):
        """Translate the CoordSys.

        Translates the origin of the CoordSys, keeping its axes directions.

        This changes the CoordSys in place!
        """
        trl = checkArray(rot,shape=(3,),kind='f',allow='i')
        self.trl += trl
        return self


    def _rotate(self,rot):
        """Rotate the CoordSys.

        Parameters: (3,3) rotation matrix

        This changes the CoordSys in place!
        """
        rot = checkArray(rot,shape=(3,3),kind='f',allow='i')
        self.rot = np.dot(self.rot,rot)
        return self


    def translate(self,*args,**kargs):
        """Translate the CoordSys like a Coords object.

        Parameters are like :meth:`Coords.translate`.

        Returns
        -------
            A new CoordSys obtained by giving `self` a translation.

        Examples
        --------
        >>> print(CoordSys().translate([-2.,-1.,5.]))
        CoordSys: trl=[-2. -1.  5.]; rot=[[ 1.  0.  0.]
                                          [ 0.  1.  0.]
                                          [ 0.  0.  1.]]

        """
        return CoordSys(points=self.points().translate(*args,**kargs))


    def rotate(self,*args,**kargs):
        """Rotate the CoordSys like a Coords object.

        Parameters are like :meth:`Coords.rotate`.

        Returns
        -------
            A new CoordSys obtained by giving `self` a rotation.

        Examples
        --------
        >>> print(CoordSys().rotate(30))
        CoordSys: trl=[ 0.  0.  0.]; rot=[[ 0.87  0.5   0.  ]
                                          [-0.5   0.87  0.  ]
                                          [ 0.    0.    1.  ]]

        """
        return CoordSys(points=self.points().rotate(*args,**kargs))


    def reverse(self,*axes):
        """Reverse some axes of the CoordSys.

        Parameters
        ----------
        axes: int (0,1,2) or tuple of ints
            The axes to be reversed.

        Note
        ----
        The reversing a single axis (or all three axes) will change a
        right-hand-sided CoordSys into a left-hand-sided one. Therefore,
        this method is normally used only with two axes.

        Examples
        --------
        >>> print(CoordSys().reverse(0,1))
        CoordSys: trl=[ 0.  0.  0.]; rot=[[-1. -0. -0.]
                                          [-0. -1. -0.]
                                          [ 0.  0.  1.]]

        """
        for axis in axes:
            self.rot[axis] *= -1.0
        return self


    def __str__(self):
        """User friendly string representation"""
        return at.stringar("CoordSys: trl=%s; rot=" % self.trl,self.rot)



class CoordinateSystem(CoordSys):
    @utils.deprecated_by('CoordinateSystem','CoordSys')
    def __init__(coords):
        """Initialize the CoordinateSystem"""
        CoordSys.init(self,points=coords)


### End
