{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE ViewPatterns          #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Diagrams.ThreeD.Transform
-- Copyright   :  (c) 2013 diagrams-lib team (see LICENSE)
-- License     :  BSD-style (see LICENSE)
-- Maintainer  :  diagrams-discuss@googlegroups.com
--
-- Transformations specific to three dimensions, with a few generic
-- transformations (uniform scaling, translation) also re-exported for
-- convenience.
--
-----------------------------------------------------------------------------

module Diagrams.ThreeD.Transform
       ( T3

         -- * Rotation
       , aboutX, aboutY, aboutZ
       , rotationAbout, rotateAbout
       , pointAt, pointAt'

       -- * Scaling
       , scalingX, scalingY, scalingZ
       , scaleX, scaleY, scaleZ
       , scaling, scale

       -- * Translation
       , translationX, translateX
       , translationY, translateY
       , translationZ, translateZ
       , translation, translate

         -- * Reflection
       , reflectionX, reflectX
       , reflectionY, reflectY
       , reflectionZ, reflectZ
       , reflectionAcross, reflectAcross

       ) where

import           Diagrams.Core
import           Diagrams.Core.Transform

import           Diagrams.Angle
import           Diagrams.Direction
import           Diagrams.ThreeD.Types
import           Diagrams.Transform
import           Diagrams.Points

import           Control.Lens            (view, (&), (*~), (.~), (//~))
import           Data.Semigroup
import           Diagrams.TwoD.Transform

import           Linear.Affine
import           Linear.Metric
import           Linear.V3               (cross)
import           Linear.Vector

-- | Create a transformation which rotates by the given angle about
--   a line parallel the Z axis passing through the local origin.
--   A positive angle brings positive x-values towards the positive-y axis.
--
--   The angle can be expressed using any type which is an
--   instance of 'Angle'.  For example, @aboutZ (1\/4 \@\@
--   'turn')@, @aboutZ (tau\/4 \@\@ 'rad')@, and @aboutZ (90 \@\@
--   'deg')@ all represent the same transformation, namely, a
--   counterclockwise rotation by a right angle.  For more general rotations,
--   see 'rotationAbout'.
--
--   Note that writing @aboutZ (1\/4)@, with no type annotation, will
--   yield an error since GHC cannot figure out which sort of angle
--   you want to use.
aboutZ :: Floating n => Angle n -> Transformation V3 n
aboutZ (view rad -> a) = fromOrthogonal r where
  r = rot a <-> rot (-a)
  rot θ (V3 x y z) = V3 (cos θ * x - sin θ * y)
                        (sin θ * x + cos θ * y)
                        z

-- | Like 'aboutZ', but rotates about the X axis, bringing positive y-values
-- towards the positive z-axis.
aboutX :: Floating n => Angle n -> Transformation V3 n
aboutX (view rad -> a) = fromOrthogonal r where
  r = rot a <-> rot (-a)
  rot θ (V3 x y z) = V3 x
                        (cos θ * y - sin θ * z)
                        (sin θ * y + cos θ * z)

-- | Like 'aboutZ', but rotates about the Y axis, bringing postive
-- x-values towards the negative z-axis.
aboutY :: Floating n => Angle n -> Transformation V3 n
aboutY (view rad -> a) = fromOrthogonal r where
  r = rot a <-> rot (-a)
  rot θ (V3 x y z) = V3 (cos θ * x + sin θ * z)
                        y
                        (-sin θ * x + cos θ * z)

-- | @rotationAbout p d a@ is a rotation about a line parallel to @d@
--   passing through @p@.
rotationAbout
  :: Floating n
  => Point V3 n         -- ^ origin of rotation
  -> Direction V3 n     -- ^ direction of rotation axis
  -> Angle n            -- ^ angle of rotation
  -> Transformation V3 n
rotationAbout (P t) d (view rad -> a)
  = mconcat [translation (negated t),
             fromOrthogonal r,
             translation t] where
    r = rot a <-> rot (-a)
    w = fromDirection d

    rot θ v =          v ^* cos θ
           ^+^ cross w v ^* sin θ
           ^+^         w ^* ((w `dot` v) * (1 - cos θ))

-- | @rotationAbout p d a@ is a rotation about a line parallel to @d@
--   passing through @p@.
rotateAbout
  :: (InSpace V3 n t, Floating n, Transformable t)
  => Point V3 n         -- ^ origin of rotation
  -> Direction V3 n     -- ^ direction of rotation axis
  -> Angle n            -- ^ angle of rotation
  -> t -> t
rotateAbout p d theta = transform (rotationAbout p d theta)

-- | @pointAt about initial final@ produces a rotation which brings
-- the direction @initial@ to point in the direction @final@ by first
-- panning around @about@, then tilting about the axis perpendicular
-- to @about@ and @final@.  In particular, if this can be accomplished
-- without tilting, it will be, otherwise if only tilting is
-- necessary, no panning will occur.  The tilt will always be between
-- ± 1/4 turn.
pointAt :: Floating n
        => Direction V3 n -> Direction V3 n -> Direction V3 n
        -> Transformation V3 n
pointAt a i f = pointAt' (fromDirection a) (fromDirection i) (fromDirection f)

-- | pointAt' has the same behavior as 'pointAt', but takes vectors
-- instead of directions.
pointAt' :: Floating n => V3 n -> V3 n -> V3 n -> Transformation V3 n
pointAt' about initial final = pointAtUnit (signorm about) (signorm initial) (signorm final)

-- | pointAtUnit has the same behavior as @pointAt@, but takes unit vectors.
pointAtUnit :: Floating n => V3 n -> V3 n -> V3 n -> Transformation V3 n
pointAtUnit about initial final = tilt <> pan where
  -- rotating u by (signedAngle rel u v) about rel gives a vector in the direction of v
  signedAngle rel u v = signum (cross u v `dot` rel) *^ angleBetween u v
  inPanPlaneF = final ^-^ project about final
  inPanPlaneI = initial ^-^ project about initial
  panAngle    = signedAngle about inPanPlaneI inPanPlaneF
  pan         = rotationAbout origin (direction about) panAngle
  tiltAngle   = signedAngle tiltAxis (transform pan initial) final
  tiltAxis    = cross final about
  tilt        = rotationAbout origin (direction tiltAxis) tiltAngle

-- Scaling -------------------------------------------------

-- | Construct a transformation which scales by the given factor in
--   the z direction.
scalingZ :: (Additive v, R3 v, Fractional n) => n -> Transformation v n
scalingZ c = fromSymmetric $ (_z *~ c) <-> (_z //~ c)

-- | Scale a diagram by the given factor in the z direction.  To scale
-- uniformly, use 'scale'.
scaleZ :: (InSpace v n t, R3 v, Fractional n, Transformable t) => n -> t -> t
scaleZ = transform . scalingZ

-- Translation ----------------------------------------

-- | Construct a transformation which translates by the given distance
--   in the z direction.
translationZ :: (Additive v, R3 v, Num n) => n -> Transformation v n
translationZ z = translation (zero & _z .~ z)

-- | Translate a diagram by the given distance in the y
--   direction.
translateZ :: (InSpace v n t, R3 v, Transformable t) => n -> t -> t
translateZ = transform . translationZ

-- Reflection ----------------------------------------------

-- | Construct a transformation which flips a diagram across z=0,
-- i.e. sends the point (x,y,z) to (x,y,-z).
reflectionZ :: (Additive v, R3 v, Num n) => Transformation v n
reflectionZ = fromSymmetric $ (_z *~ (-1)) <-> (_z *~ (-1))

-- | Flip a diagram across z=0, i.e. send the point (x,y,z) to
-- (x,y,-z).
reflectZ :: (InSpace v n t, R3 v, Transformable t) => t -> t
reflectZ = transform reflectionZ

-- | @reflectionAcross p v@ is a reflection across the plane through
--   the point @p@ and normal to vector @v@. This also works as a 2D
--   transform where @v@ is the normal to the line passing through point
--   @p@.
reflectionAcross :: (Metric v, Fractional n)
  => Point v n -> v n -> Transformation v n
reflectionAcross p v =
  conjugate (translation (origin .-. p)) reflect
    where
      reflect = fromLinear t (linv t)
      t       = f v <-> f (negated v)
      f u w   = w ^-^ 2 *^ project u w

-- | @reflectAcross p v@ reflects a diagram across the plane though
--   the point @p@ and the vector @v@. This also works as a 2D transform
--   where @v@ is the normal to the line passing through point @p@.
reflectAcross :: (InSpace v n t, Metric v, Fractional n, Transformable t)
  => Point v n -> v n -> t -> t
reflectAcross p v = transform (reflectionAcross p v)