#
##
##  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/.
##
"""opengl/matrix.py

Python OpenGL framework for pyFormex

This OpenGL framework is intended to replace (in due time)
the current OpenGL framework in pyFormex.

(C) 2013 Benedict Verhegghe and the pyFormex project.

"""
from __future__ import absolute_import, division, print_function

import numpy as np
import pyformex.arraytools as at


class Vector4(np.matrix):
    """One or more homogeneous coordinates

    """
    def __new__(clas,data=None):
        """Create a new Vector4 instance"""
        data = np.asarray(data)
        if data.ndim < 1 or data.ndim > 2:
            raise ValueError("Expected 1 or 2-dimensinal data")
        if data.shape[-1] == 4:
            pass
        elif data.shape[-1] == 3:
            data = at.growAxis(data, 1, fill=1)
        else:
            raise ValueError("Expected length 3 or 4 fro last axis")
        ar = data.view(clas)
        return ar


class Matrix4(np.matrix):
    """A 4x4 transformation matrix for homogeneous coordinates.

    The matrix is to be used with post-multiplication on
    row vectors (i.e. OpenGL convention).

    - `data`: if specified, should be a (4,4) float array or compatible. Else
      a 4x4 identity matrix is created.

    Example:

    >>> I = Matrix4()
    >>> print(I)
    [[ 1.  0.  0.  0.]
     [ 0.  1.  0.  0.]
     [ 0.  0.  1.  0.]
     [ 0.  0.  0.  1.]]

    We can first scale and then rotate, or first rotate and then scale (with
    another scaling factor):

    >>> a = I.scale([4.,4.,4.]).rotate(45.,[0.,0.,1.])
    >>> b = I.rotate(45.,[0.,0.,1.]).scale([2.,2.,2.])
    >>> print(a)
    [[ 0.  4.  0.  0.]
     [-4.  0.  0.  0.]
     [ 0.  0.  8.  0.]
     [ 0.  0.  0.  1.]]
    >>> (a==b).all()
    True

    """

    def __new__(clas,data=None):
        """Create a new Matrix instance"""
        if data is None:
            data = np.eye(4, 4)
        else:
            data = at.checkArray(data, (4, 4), 'f')
        ar = data.view(clas)
        ar._gl = None
        return ar


    def __array_finalize__(self, obj):
        """Finalize the new Matrix object.

        When a class is derived from numpy.ndarray and the constructor (the
        :meth:`__new__` method) defines new attributes, these atttributes
        need to be reset in this method.
        """
        self._gl = getattr(obj, '_gl', None)


    def gl(self):
        """Get the transformation matrix as a 'ready-to-use'-gl version.

        Returns the (4,4) Matrix as a rowwise flattened array of type float32.

        Example:

        >>> Matrix4().gl()
        Matrix4([ 1.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,
                 0.,  0.,  1.], dtype=float32)
        """
        if self._gl is None:
            self._gl = self.flatten().astype(np.float32)
        return self._gl


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


    @rot.setter
    def rot(self, value):
        """Set the rotation matrix to (3,3) value"""
        self[:3, :3] = value
        self._gl = None


    @property
    def trl(self):
        """Return the (3,) translation vector"""
        return self[3, :3]


    @trl.setter
    def trl(self, value):
        """Set the translation vector to (3,) value"""
        self[3, :3] = value
        self._gl = None


    def identity(self):
        """Reset the matrix to a 4x4 identity matrix."""
        self = np.matrix(np.eye(4, 4))
        self._gl = None


    def translate(self, vector):
        """Translate a 4x4 matrix by a (3,) vector.

        - `vector`: (3,) float array: the translation vector

        Changes the Matrix in place and also returns the result

        Example:

        >>> Matrix4().translate([1.,2.,3.])
        Matrix4([[ 1.,  0.,  0.,  0.],
                 [ 0.,  1.,  0.,  0.],
                 [ 0.,  0.,  1.,  0.],
                 [ 1.,  2.,  3.,  1.]])
        """
        vector = at.checkArray(vector, (3,), 'f')
        self.trl += vector*self.rot
        return self


    def rotate(self,angle,axis=None):
        """Rotate a Matrix4.

        The rotation can be specified by

        - an angle and axis,
        - a 3x3 rotation matrix,
        - a 4x4 trtransformation matrix (Matrix4).

        Parameters:

        - `angle`: float: the rotation angle. A 3x3 or 4x4 matrix may be
           give instead, to directly specify the roation matrix.
        - `axis`: int or (3,) float: the axis to rotate around

        Changes the Matrix in place and also returns the result.

        Example:

        >>> Matrix4().rotate(90.,[0.,1.,0.])
        Matrix4([[ 0.,  0., -1.,  0.],
                 [ 0.,  1.,  0.,  0.],
                 [ 1.,  0.,  0.,  0.],
                 [ 0.,  0.,  0.,  1.]])

        """
        ## !! TRANSPOSE!!
        ## x^2(1-c)+c     xy(1-c)-zs     xz(1-c)+ys     0
        ##  yx(1-c)+zs     y^2(1-c)+c     yz(1-c)-xs     0
        ##  xz(1-c)-ys     yz(1-c)+xs     z^2(1-c)+c     0
        ##       0              0               0        1

        try:
            rot = at.checkArray(angle, (4, 4), 'f')[:3, :3]
        except:
            try:
                rot = at.checkArray(angle, (3, 3), 'f')
            except:
                angle = at.checkFloat(angle)
                rot = np.matrix(at.rotationMatrix(angle, axis))
        self.rot = rot*self.rot
        return self


    def scale(self, vector):
        """Scale a 4x4 matrix by a (3,) vector.

        - `vector`: (3,) float array: the scaling vector

        Changes the Matrix in place and also returns the result

        Example:

        >>> Matrix4().scale([1.,2.,3.])
        Matrix4([[ 1.,  0.,  0.,  0.],
                 [ 0.,  2.,  0.,  0.],
                 [ 0.,  0.,  3.,  0.],
                 [ 0.,  0.,  0.,  1.]])
        """
        vector = at.checkArray(vector, (3,), 'f')
        for i in range(3):
            self[i, i] *= vector[i]
        self._gl = None
        return self


    # Do we need these?
    def swapRows(self, row1, row2):
        """Swap two rows.

        - `row1`, `row2`: index of the rows to swap
        """
        temp = np.copy(self[row1])
        self[row1] = self[row2]
        self[row2] = temp
        self._gl = None


    def swapCols(self, col1, col2):
        """Swap two columns.

        - `col1`, `col2`: index of the columns to swap
        """
        temp = np.copy(self[:, col1])
        self[:, col1] = self[:, col2]
        self[:, col2] = temp
        self._gl = None


    def inverse(self):
        """Return the inverse matrix"""
        return np.linalg.inv(self)


    def transform(self, x):
        """Transform a vertex using this matrix.

        - `x`: a (3,) or (4,) vector.

        If the vector has length 4, it holds homogeneous coordinates, and
        the result is the dot product of the vector with the Matrix:  x * M.
        If the vector has length 3, the 4th homogeneous coordinate is
        assumed to be 1, and the product is computed in an optimized way.
        """
        try:
            x = at.checkArray(x, (3,), 'f')
            return np.dot(x, self[:3, :3]) + self[3, :3]
        except:
            x = at.checkArray(x, (4,), 'f')
            return np.dot(x, self)


    def invtransform(self, x):
        """Transform a vertex with the inverse of this matrix.

        - `x`: a (3,) or (4,) vector.

        """
        return Vector4(x) * self.inverse()


    def transinv(self):
        """Return the transpose of the inverse."""
        return self.inverse().transpose()


# End
