#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  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
"""
import numpy as np
import pyformex.arraytools as at
DTYPE = at.Float
[docs]class Matrix4(np.ndarray):
    """A 4x4 transformation matrix for homogeneous coordinates.
    The matrix is to be used with post-multiplication on
    row vectors (i.e. OpenGL convention).
    Parameters
    ----------
    data: array_like (4,4), optional
        If specified, should be a (4,4) float array or compatible. Else
        a 4x4 identity matrix is created.
    Examples
    --------
    >>> 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:
    >>> a = Matrix4().scale([4.,4.,4.]).rotate(45.,[0.,0.,1.])
    >>> a
    Matrix4([[ 2.8284,  2.8284,  0.    ,  0.    ],
             [-2.8284,  2.8284,  0.    ,  0.    ],
             [ 0.    ,  0.    ,  4.    ,  0.    ],
             [ 0.    ,  0.    ,  0.    ,  1.    ]])
    >>> b = Matrix4().rotate(45.,[0.,0.,1.]).scale([4.,4.,4.])
    >>> np.allclose(a,b)
    True
    """
    def __new__(clas, data=None):
        """Create a new Matrix instance"""
        if data is None:
            data = np.eye(4, 4, dtype=DTYPE)
        else:
            data = at.checkArray(data, (4, 4), 'f').astype(DTYPE)
        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.
        """
        if obj is None:
            return
        self._gl = getattr(obj, '_gl', None)
    def __array_wrap__(self, out_arr, context=None):
        res = super().__array_wrap__(out_arr, context)
        if type(res) == Matrix4 and (
                res.dtype != DTYPE or res.shape != (4,4) ):
            res = res.view(np.ndarray)
        return res
[docs]    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
[docs]    def identity(self):
        """Reset the matrix to a 4x4 identity matrix."""
        self = np.eye(4, 4)
        self._gl = None 
[docs]    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 += np.dot(vector, self.rot)
        return self 
[docs]    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 Exception:
            try:
                rot = at.checkArray(angle, (3, 3), 'f')
            except Exception:
                angle = at.checkFloat(angle)
                rot = at.rotationMatrix(angle, axis)
        self.rot = rot @ self.rot
        return self 
[docs]    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')
        scale3 = np.diagflat(vector)
        self[:3, :3] = self[:3, :3] @ scale3
        self._gl = None
        return self 
    # Do we need these?
[docs]    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 
[docs]    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 
[docs]    def inverse(self):
        """Return the inverse matrix"""
        return np.linalg.inv(self) 
[docs]    def transinv(self):
        """Return the transpose of the inverse."""
        return self.inverse().transpose()  
# End