```-- | Affine Transformations of Arbitrary Geometric Objects
module RSAGL.Math.Affine
(AffineTransformable(..),
scale',
inverseTransform,
withTransformation,
translateToFrom,
rotateToFrom,
scaleAlong)
where

import Graphics.Rendering.OpenGL.GL as GL hiding (R)
import RSAGL.Math.Vector
import RSAGL.Math.Matrix
import RSAGL.Math.Angle
import RSAGL.Types
import Data.Maybe

-- | 'AffineTransformable' objects are subject to affine transformations using matrix multiplication.
class AffineTransformable a where
-- | Apply an affine transformation, defined by a 4x4 matrix.  (This is the only required method.)
transform :: RSAGL.Math.Matrix.Matrix -> a -> a
-- | Scale an entity along the @x@ @y@ and @z@ axes.  For example, @scale (Vector3D 2 3 4)@ will make an object twice as wide, three times as tall,
-- and four times as deep.  It may be helpful to think of the vector as a control point on the vertex of a unit cube.
scale :: Vector3D -> a -> a
scale vector = transform \$ scaleMatrix vector
-- | Translate an entity along the specified vector.
translate :: Vector3D -> a -> a
translate vector = transform \$ translationMatrix vector
-- | Rotate an entity about the origin, using the specified vector as the axis of rotation.
rotate :: Vector3D -> Angle -> a -> a
rotate vector angle = transform \$ rotationMatrix vector angle
-- | Specific rotation around the x-axis.
rotateX :: Angle -> a -> a
rotateX = RSAGL.Math.Affine.rotate (Vector3D 1 0 0)
-- | Specific rotation around the y-axis.
rotateY :: Angle -> a -> a
rotateY = RSAGL.Math.Affine.rotate (Vector3D 0 1 0)
-- | Specific rotation around the z-axis.
rotateZ :: Angle -> a -> a
rotateZ = RSAGL.Math.Affine.rotate (Vector3D 0 0 1)

-- | Apply the inverse of an affine transformation, defined by a 4x4 matrix.
{-# INLINE inverseTransform #-}
inverseTransform :: (AffineTransformable a) => RSAGL.Math.Matrix.Matrix -> a -> a
inverseTransform m = transform (matrixInverse m)

-- | Specific scale preserving proportions.
{-# INLINE scale' #-}
scale' :: (AffineTransformable a) => RSdouble -> a -> a
scale' x = RSAGL.Math.Affine.scale (Vector3D x x x)

-- | Apply a function under an affine transformation.  @withTransformation m id@ is an identity if @m@ is invertable.
{-# INLINE withTransformation #-}
withTransformation :: (AffineTransformable a) => RSAGL.Math.Matrix.Matrix -> (a -> a) -> a -> a
withTransformation m f = inverseTransform m . f . transform m

-- | Apply a function treating a particular point as the origin.  For example, combining 'transformAbout' with 'RSAGL.Math.Affine.rotate'
-- performs a rotation about an arbitrary point rather than the origin.
transformAbout :: (AffineTransformable a) => Point3D -> (a -> a) -> a -> a
transformAbout center f = withTransformation (translateToFrom origin_point_3d center identity_matrix) f

-- | Specific translation along the vector between two points.
-- This ordinary use is to set the second point as the center of a model (typically origin_point_3d)
-- and the first point as the desired position of the model.
{-# INLINE translateToFrom #-}
translateToFrom :: (AffineTransformable a) => Point3D -> Point3D -> a -> a
translateToFrom a b = RSAGL.Math.Affine.translate (vectorToFrom a b)

-- | Specific rotation along the shortest path that brings the second vector in line with the first.
{-# INLINE rotateToFrom #-}
rotateToFrom :: (AffineTransformable a) => Vector3D -> Vector3D -> a -> a
rotateToFrom u v = RSAGL.Math.Affine.rotate c a
where c = vectorNormalize \$ vectorScale (-1) \$ fromMaybe (fst \$ orthos u) \$ aNonZeroVector \$ crossProduct u v
a = angleBetween u v

-- | Specific scale along an arbitary axis.
{-# INLINE scaleAlong #-}
scaleAlong :: (AffineTransformable a) => Vector3D -> RSdouble -> a -> a
scaleAlong v u = withTransformation (rotateToFrom (Vector3D 0 1 0) v identity_matrix) (RSAGL.Math.Affine.scale (Vector3D 1 u 1))

instance AffineTransformable a => AffineTransformable (Maybe a) where
transform m = fmap (transform m)

instance AffineTransformable a => AffineTransformable [a] where
transform m = map (transform m)

instance (AffineTransformable a,AffineTransformable b) => AffineTransformable (a,b) where
transform m (a,b) = (transform m a,transform m b)

instance (AffineTransformable a,AffineTransformable b,AffineTransformable c) => AffineTransformable (a,b,c) where
transform m (a,b,c) = (transform m a,transform m b,transform m c)

instance AffineTransformable RSAGL.Math.Matrix.Matrix where
transform mat = matrixMultiply mat

instance AffineTransformable Vector3D where
transform m (Vector3D x y z) = transformHomogenous x y z 0 Vector3D m
scale (Vector3D x1 y1 z1) (Vector3D x2 y2 z2) = Vector3D (x1*x2) (y1*y2) (z1*z2)
translate _ = id
rotateX a (Vector3D x y z) = Vector3D x (c*y-s*z) (c*z+s*y)
where s = sine a
c = cosine a
rotateY a (Vector3D x y z) = Vector3D (c*x+s*z) y (c*z-s*x)
where s = sine a
c = cosine a
rotateZ a (Vector3D x y z) = Vector3D (c*x-s*y) (c*y+s*x) z
where s = sine a
c = cosine a

instance AffineTransformable Point3D where
transform m (Point3D x y z) = transformHomogenous x y z 1 Point3D m
scale (Vector3D x1 y1 z1) (Point3D x2 y2 z2) = Point3D (x1*x2) (y1*y2) (z1*z2)
translate (Vector3D x1 y1 z1) (Point3D x2 y2 z2) = Point3D (x1+x2) (y1+y2) (z1+z2)
rotateX a (Point3D x y z) = Point3D x (c*y-s*z) (c*z+s*y)
where s = sine a
c = cosine a
rotateY a (Point3D x y z) = Point3D (c*x+s*z) y (c*z-s*x)
where s = sine a
c = cosine a
rotateZ a (Point3D x y z) = Point3D (c*x-s*y) (c*y+s*x) z
where s = sine a
c = cosine a

instance AffineTransformable SurfaceVertex3D where
transform m (SurfaceVertex3D p v) = SurfaceVertex3D (RSAGL.Math.Affine.transform m p) (RSAGL.Math.Affine.transform (matrixTranspose \$ matrixInverse m) v)
translate vector (SurfaceVertex3D p v) = SurfaceVertex3D (RSAGL.Math.Affine.translate vector p) v

-- | The IO monad itself is AffineTransformable.  This is done by wrapping the IO action in an OpenGL transformation.
instance AffineTransformable (IO a) where
transform mat iofn = preservingMatrix \$ do mat' <- newMatrix RowMajor \$ map f2f \$ concat \$ rowMajorForm mat
multMatrix (mat' :: GLmatrix GLdouble)
iofn
translate (Vector3D x y z) iofn = preservingMatrix \$
do GL.translate \$ Vector3 (f2f x) (f2f y) (f2f z :: GLdouble)
iofn
scale (Vector3D x y z) iofn = preservingMatrix \$
do GL.scale (f2f x) (f2f y) (f2f z :: GLdouble)
iofn
rotate (Vector3D x y z) angle iofn = preservingMatrix \$
do GL.rotate (f2f \$ toDegrees_ angle) (Vector3 (f2f x) (f2f y) (f2f z :: GLdouble))
iofn
```