{-|
Module      :  Graphics.Rendering.Handa.Projection
Copyright   :  (c) 2015 Brian W Bush
License     :  MIT
Maintainer  :  Brian W Bush <consult@brianwbush.info>
Stability   :  Stable
Portability :  Portable

Functions for off-axis projection.
-}


{-# LANGUAGE DeriveAnyClass      #-}
{-# LANGUAGE DeriveDataTypeable  #-}
{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE ExplicitForAll      #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}


module Graphics.Rendering.Handa.Projection (
-- * Screens.
  Screen(..)
, upperRight
, aspectRatio
, throwRatio
-- * Projections.
, OffAxisProjection(..)
, projection
, fetchProjection
) where


import Data.AdditiveGroup (AdditiveGroup)
import Data.Aeson (FromJSON)
import Data.AffineSpace ((.+^), (.-.))
import Data.Binary (Binary)
import Data.Cross (cross3)
import Data.Data (Data)
import Data.List.Split (chunksOf)
import Data.VectorSpace ((*^), (<.>), magnitude, normalized)
import GHC.Generics (Generic)
import Graphics.Rendering.OpenGL (GLmatrix, MatrixComponent, MatrixOrder(RowMajor), Vector3(..), Vertex3(..), frustum, get, getMatrixComponents, matrix, multMatrix, newMatrix, translate)
import Graphics.Rendering.OpenGL.GL.Tensor.Instances (origin)


-- | Description of a physical screen geometry.
data Screen a =
  Screen
  {
    lowerLeft  :: Vertex3 a -- ^ The lower left corner.
  , lowerRight :: Vertex3 a -- ^ The lower right corner.
  , upperLeft  :: Vertex3 a -- ^ The upper left corner.
  }
    deriving (Binary, Data, Eq, FromJSON, Generic, Read, Show)

instance Functor Screen where
  fmap f Screen{..} =
    Screen
    {
      lowerLeft  = fmap f lowerLeft
    , lowerRight = fmap f lowerRight
    , upperLeft  = fmap f upperLeft
    }


-- | The upper right corner.
upperRight :: (Num a)
           => Screen a
           -> Vertex3 a
upperRight Screen{..} = lowerRight .+^ (upperLeft .-. lowerLeft)


-- | The aspect ratio.
aspectRatio :: (AdditiveGroup a, RealFloat a)
            => Screen a -- ^ The screen geometry.
            -> a        -- ^ The aspect ratio, namely the screen width divided by its height.
aspectRatio Screen{..} =
  let
    width  = magnitude $ lowerRight .-. lowerLeft
    height = magnitude $ upperLeft  .-. lowerLeft
  in
    width / height


-- | The throw ratio.
throwRatio :: (AdditiveGroup a, RealFloat a)
           => Screen a  -- ^ The screen geometry.
           -> Vertex3 a -- ^ The eye position.
           -> a         -- ^ The throw ratio, name the distance to the screen divided by its width.
throwRatio Screen{..} eye =
  let
    width  = magnitude $ lowerRight .-. lowerLeft
    vn = normalized (lowerRight .-. lowerLeft) `cross3` normalized (upperLeft  .-. lowerLeft)
    throw = - (lowerLeft  .-. eye) <.> vn
  in
    throw / width


-- | The equations to use for off-axis projection.
data OffAxisProjection =
    KooimaOffAxis -- ^ Based on Kooima 2009, \<<http://csc.lsu.edu/~kooima/pdfs/gen-perspective.pdf>\>, which assumes a rectangular screen.
  | VTKOffAxis    -- ^ Based on VTK 6.3.0, \<<https://gitlab.kitware.com/vtk/vtk/blob/v6.3.0/Rendering/Core/vtkCamera.cxx#L414>\>, which does not assume a rectangular screen.
    deriving (Eq, Read, Show)


-- | Make an off-axis projection for a screen.
projection :: forall a . (AdditiveGroup a, MatrixComponent a, RealFloat a)
           => OffAxisProjection -- ^ The off-axis equations to use.
           -> Screen a          -- ^ The screen geometry.
           -> Vertex3 a         -- ^ The eye position.
           -> a                 -- ^ The distance to the near culling plane.
           -> a                 -- ^ The distance to the far culling plane.
           -> IO ()             -- ^ An action for performing the off-axis projection.

-- Based on Kooima 2009, \<<http://csc.lsu.edu/~kooima/pdfs/gen-perspective.pdf>\>, which assumes a rectangular screen .
projection KooimaOffAxis Screen{..} eye near far =
  do
    let
      -- Orthonomal basis for screen.
      vr = normalized $ lowerRight .-. lowerLeft
      vu = normalized $ upperLeft  .-. lowerLeft
      vn = normalized $ vr `cross3` vu
      -- Screen corners relative to eye.
      va = lowerLeft  .-. eye
      vb = lowerRight .-. eye
      vc = upperLeft  .-. eye
      -- Distance from eye to screen.
      throw = - va <.> vn
      -- Extent on near clipping plane.
      scaling = near / throw
      left   = realToFrac $ (vr <.> va) * scaling
      right  = realToFrac $ (vr <.> vb) * scaling
      bottom = realToFrac $ (vu <.> va) * scaling
      top    = realToFrac $ (vu <.> vc) * scaling
      -- Matrix transforming world to screen.
      m = [[x, y, z, 0] | Vector3 x y z <- [vr, vu, vn]] ++ [[0, 0, 0, 1]]
    -- Perpendicator projection.
    frustum left right bottom top (realToFrac near) (realToFrac far)
    -- Rotate to non-perpendicular.
    multMatrix =<< (newMatrix RowMajor $ concat m :: IO (GLmatrix a))
    -- Move apex of frustum.
    translate $ origin .-. eye

-- Rewrite of VTK 6.3.0, \<<https://gitlab.kitware.com/vtk/vtk/blob/v6.3.0/Rendering/Core/vtkCamera.cxx#L414>\>, which does not assume a rectangular screen, in cleaner notation and using vector algebra.
projection VTKOffAxis s@Screen{..} eye near far =
  do
    let
      -- Orthonomal basis for screen.
      vr = normalized $ lowerRight   .-. lowerLeft
      vu = normalized $ upperRight s .-. lowerRight
      vn = normalized $ vr `cross3` vu
      -- Basis for inverse.
      idet = 1 / (vr <.> vu `cross3` vn)
      ur = idet *^ vu `cross3` vn
      uu = idet *^ vn `cross3` vr
      un = idet *^ vr `cross3` vu
      -- Screen corners relative to eye.
      va = lowerLeft    .-. eye
      vd = upperRight s .-. eye
      -- Distance from eye to screen.
      throw = - va <.> un
      -- Extent on near clipping plane.
      scaling = near / throw
      left   = realToFrac $ (ur <.> va) * scaling
      right  = realToFrac $ (ur <.> vd) * scaling
      bottom = realToFrac $ (uu <.> va) * scaling
      top    = realToFrac $ (uu <.> vd) * scaling
      -- Matrix transforming world to screen.
      m = [[x, y, z, 0] | Vector3 x y z <- [ur, uu, un]] ++ [[0, 0, 0, 1]]
    -- Perpendicator projection.
    frustum left right bottom top (realToFrac near) (realToFrac far)
    -- Rotate to non-perpendicular.
    multMatrix =<< (newMatrix RowMajor $ concat m :: IO (GLmatrix a))
    -- Move apex of frustum.
    translate $ origin .-. eye


-- | Retrieve the current projection matrix.
fetchProjection :: forall a . (MatrixComponent a, RealFloat a)
                => IO [[a]] -- ^ An action to retrieve the projection matrix, in row-major order.
fetchProjection =
  do
    m <- get $ matrix Nothing :: IO (GLmatrix a)
    chunksOf 4 <$> getMatrixComponents RowMajor m