{-# LANGUAGE OverloadedStrings, NamedFieldPuns, ParallelListComp, DataKinds, FlexibleContexts #-}

module Common.GraphicsUtils where

import Data.Bits
import qualified Data.ByteString.Char8 as SB
import qualified Data.Trie as T
import Data.Vect
import qualified Data.Vector.Storable as V
import FRP.Elerea.Param

import LambdaCube.GL
import LambdaCube.GL.Mesh

complexMesh :: [(Proj4, Mesh)] -> Mesh
complexMesh parts = Mesh
    { mAttributes = T.fromList [("position", A_V3F vertices), ("normal", A_V3F normals)]
    , mPrimitive = P_Triangles
    , mGPUData = Nothing
    }
  where
    vertices = V.concat partVertices
    normals = V.concat partNormals
    
    (partVertices, partNormals) = unzip [(getV3F trans "position" attr 1, getV3F (transpose (inverse trans)) "normal" attr 0) |
                                         (trans, mesh) <- parts, let attr = mAttributes (unrollIndices mesh)]
    getV3F trans name attributes w = V.map transform vector
      where
        transform v = fromVec3 (trim ((extendWith w (toVec3 v) :: Vec4) .* fromProjective trans))
        Just (A_V3F vector) = T.lookup name attributes

quad :: Mesh
quad = Mesh
    { mAttributes = T.singleton "position" $ A_V2F $ V.fromList [-1 ^ 1, -1 ^ -1, 1 ^ -1, 1 ^ -1, 1 ^ 1, -1 ^ 1]
    , mPrimitive = P_Triangles
    , mGPUData = Nothing
    }
  where
    infixr 0 ^
    (^) = V2

cube :: Float -> Mesh
cube size = box (Vec3 size size size) 

box :: Vec3 -> Mesh
box (Vec3 scaleX scaleY scaleZ) = addFlatNormals $ Mesh
    { mAttributes = T.singleton "position" (A_V3F vertices)
    , mPrimitive = P_Triangles
    , mGPUData = Nothing
    }
  where
    quads = [[6, 2, 3, 7], [5, 1, 0, 4], [7, 3, 1, 5], [4, 0, 2, 6], [3, 2, 0, 1], [6, 7, 5, 4]]
    indices = V.fromList $ concat [[a, b, c, c, d, a] | [d, c, b, a] <- quads]
    vertices = V.backpermute (V.generate 8 mkVertex) indices
    
    mkVertex n = V3 x y z
      where
        x = if testBit n 2 then scaleX else -scaleX
        y = if testBit n 1 then scaleY else -scaleY
        z = if testBit n 0 then scaleZ else -scaleZ

capsule :: Float -> Float -> Int -> Mesh
capsule radius height n = complexMesh
                          [ (idmtx, cylinderLateralArea height' radius (n * 2))
                          , (translation (Vec3 0 (-height') 0), halfSphere radius n)
                          , (scaling (Vec3 (-1) (-1) 1) .*. translation (Vec3 0 height' 0), halfSphere radius n)
                          ]
  where
    height' = height / 2

halfSphere :: Float -> Int -> Mesh
halfSphere radius n = Mesh
    { mAttributes = T.fromList [("position", A_V3F vertices), ("normal", A_V3F normals)]
    , mPrimitive = P_TrianglesI indices
    , mGPUData = Nothing
    }
  where
    m = pi / fromIntegral n
    vertices = V.map (\(V3 x y z) -> V3 (radius * x) (radius * y) (radius * z)) normals
    normals = V.fromList [V3 (sin a * cos b) (cos a) (sin a * sin b) | i <- [0..n], j <- [0..2 * n - 1],
                          let a = fromIntegral i * m * 0.5 + 0.5 * pi, let b = fromIntegral j * m]
    indices = V.fromList $ concat [[ix i j, ix i' j, ix i' j', ix i' j', ix i j', ix i j] | i <- [0..n - 1], j <- [0..2 * n - 1],
                                   let i' = i + 1, let j' = (j + 1) `mod` (2 * n)]
    ix i j = fromIntegral (i * 2 * n + j)

sphere :: Float -> Int -> Mesh
sphere radius n = Mesh
    { mAttributes = T.fromList [("position", A_V3F vertices), ("normal", A_V3F normals)]
    , mPrimitive = P_TrianglesI indices
    , mGPUData = Nothing
    }
  where
    m = pi / fromIntegral n
    vertices = V.map (\(V3 x y z) -> V3 (radius * x) (radius * y) (radius * z)) normals
    normals = V.fromList [V3 (sin a * cos b) (cos a) (sin a * sin b) | i <- [0..n], j <- [0..2 * n - 1],
                          let a = fromIntegral i * m, let b = fromIntegral j * m]
    indices = V.fromList $ concat [[ix i j, ix i' j, ix i' j', ix i' j', ix i j', ix i j] | i <- [0..n - 1], j <- [0..2 * n - 1],
                                   let i' = i + 1, let j' = (j + 1) `mod` (2 * n)]
    ix i j = fromIntegral (i * 2 * n + j)

cylinder :: Float -> Float -> Int -> Mesh
cylinder height radius n = complexMesh
                           [ (idmtx, cylinderLateralArea height radius n)
                           , (translation (Vec3 0 height 0), regularPolygon radius n)
                           , (scaling (Vec3 1 (-1) 1) .*. translation (Vec3 0 (-height) 0), regularPolygon radius n)
                           ]

regularPolygon :: Float -> Int -> Mesh
regularPolygon radius n = Mesh
    { mAttributes = T.fromList [("position", A_V3F vertices), ("normal", A_V3F normals)]
    , mPrimitive = P_TrianglesI indices
    , mGPUData = Nothing
    }
  where
    vertices = V.cons (V3 0 0 0) (V.generate n mkVertex)
    normals = V.replicate (n + 1) (V3 0 1 0)
    indices = V.map fromIntegral . V.fromList $ concat [[0, i, i `mod` n + 1] | i <- [1..n]]
    mkVertex i = V3 (radius * cos t) 0 (radius * sin t)
      where
        t = fromIntegral i * 2 * pi / fromIntegral n

cylinderLateralArea :: Float -> Float -> Int -> Mesh
cylinderLateralArea height radius n = Mesh
    { mAttributes = T.fromList [("position", A_V3F vertices), ("normal", A_V3F normals)]
    , mPrimitive = P_TrianglesI indices
    , mGPUData = Nothing
    }
  where
    ts = V.generate n (\t -> fromIntegral t * 2 * pi / fromIntegral n)
    ts' = ts V.++ ts
    xs = V.map cos ts'
    ys = V.replicate n height V.++ V.replicate n (-height)
    zs = V.map sin ts'
    is = [t `mod` n | t <- [0..n]]
    vertices = V.zipWith3 (\x y z -> V3 (radius*x) y (radius*z)) xs ys zs
    normals = V.zipWith3 V3 xs (V.replicate (n*2) 0) zs
    indices = V.fromList (map fromIntegral (concat [[i,i+n,i'+n,i'+n,i',i] | i <- is | i' <- tail is]))

addFlatNormals :: Mesh -> Mesh
addFlatNormals mesh@Mesh { mAttributes, mPrimitive = P_Triangles } =
    mesh { mAttributes = T.insert "normal" (A_V3F normals) mAttributes } 
  where
    Just (A_V3F positions) = T.lookup "position" mAttributes
    normals = V.concatMap mkNormal (V.generate (V.length positions `div` 3) id)
    mkNormal i = V.replicate 3 (fromVec3 (normalize ((p3 &- p2) &^ (p2 &- p1))))
      where
        p1 = toVec3 (positions V.! (i*3))
        p2 = toVec3 (positions V.! (i*3 + 1))
        p3 = toVec3 (positions V.! (i*3 + 2))
addFlatNormals mesh@Mesh { mPrimitive = P_TrianglesI indices } = addFlatNormals (unrollIndices mesh)
addFlatNormals _ = error "addFlatNormals: unsupported primitive type"

unrollIndices :: Mesh -> Mesh
unrollIndices mesh@Mesh { mAttributes, mPrimitive = P_Triangles } = mesh
unrollIndices mesh@Mesh { mAttributes, mPrimitive = P_TrianglesI indices } =
    mesh { mAttributes = fmap (unrollAttribute indices') mAttributes, mPrimitive = P_Triangles }
  where
    indices' = V.map fromIntegral indices
unrollIndices _ = error "unrollIndices: unsupported primitive type"

unrollAttribute :: V.Vector Int -> MeshAttribute -> MeshAttribute
unrollAttribute indices attribute = case attribute of
    A_V3F vs -> A_V3F (V.backpermute vs indices)
    _        -> error "unrollAttribute: unsupported attribute type"

toVec3 :: V3F -> Vec3
toVec3 (V3 x y z) = Vec3 x y z

fromVec3 :: Vec3 -> V3F
fromVec3 (Vec3 x y z) = V3 x y z

fromVec4 :: Vec4 -> V4F
fromVec4 (Vec4 x y z w) = V4 x y z w

fromMat4 :: Mat4 -> M44F
fromMat4 (Mat4 a b c d) = V4 (fromVec4 a) (fromVec4 b) (fromVec4 c) (fromVec4 d)

v3v4 :: Exp s V3F -> Exp s V4F
v3v4 v = pack' (V4 x y z (Const 1))
  where 
    V3 x y z = unpack' v

n3v4 :: Exp s V3F -> Exp s V4F
n3v4 v = pack' (V4 x y z (Const 0))
  where 
    V3 x y z = unpack' v

v4v3 :: Exp s V4F -> Exp s V3F
v4v3 v = pack' (V3 x y z)
  where
    V4 x y z _ = unpack' v

floatV :: Float -> Exp V Float
floatV = Const

floatF :: Float -> Exp F Float
floatF = Const

floatG :: Float -> Exp G Float
floatG = Const

intF :: Int32 -> Exp F Int32
intF = Const

intG :: Int32 -> Exp G Int32
intG = Const

-- | Perspective transformation matrix in row major order.
perspective :: Float  -- ^ Near plane clipping distance (always positive).
            -> Float  -- ^ Far plane clipping distance (always positive).
            -> Float  -- ^ Field of view of the y axis, in radians.
            -> Float  -- ^ Aspect ratio, i.e. screen's width\/height.
            -> Mat4
perspective n f fovy aspect = transpose $
    Mat4 (Vec4 (2*n/(r-l))       0       (-(r+l)/(r-l))        0)
         (Vec4     0        (2*n/(t-b))  ((t+b)/(t-b))         0)
         (Vec4     0             0       (-(f+n)/(f-n))  (-2*f*n/(f-n)))
         (Vec4     0             0            (-1)             0)
  where
    t = n*tan(fovy/2)
    b = -t
    r = aspect*t
    l = -r

-- | Pure orientation matrix defined by Euler angles.
rotationEuler :: Vec3 -> Proj4
rotationEuler (Vec3 a b c) = orthogonal $ toOrthoUnsafe $ rotMatrixY a .*. rotMatrixX b .*. rotMatrixZ c

-- | Camera transformation matrix.
lookat :: Vec3   -- ^ Camera position.
       -> Vec3   -- ^ Target position.
       -> Vec3   -- ^ Upward direction.
       -> Proj4
lookat pos target up = translateBefore4 (neg pos) (orthogonal $ toOrthoUnsafe r)
  where
    w = normalize $ pos &- target
    u = normalize $ up &^ w
    v = w &^ u
    r = transpose $ Mat3 u v w

-- | Continuous camera state (rotated with mouse, moved with arrows)
userCamera :: Vec3 -> Signal Vec2 -> Signal (Bool, Bool, Bool, Bool, Bool) -> SignalGen Float (Signal (Vec3, Vec3, Vec3, Vec2))
userCamera startPosition mouseDelta directionKeys = transfer2 (startPosition, zero, zero, zero) calcCam mouseDelta directionKeys
  where
    d0 = Vec4 0 0 (-1) 1
    u0 = Vec4 0 1 0 1
    calcCam dt dm (ka, kw, ks, kd, turbo) (p0, _, _, m) = (p', d, u, m')
      where
        f0 c v = if c then v else zero
        p' = p0 &+ (f0 kw d &- f0 ks d &+ f0 kd v &- f0 ka v) &* (realToFrac dt * if turbo then 5 else 1)
        m' = dm &+ m
        rm = fromProjective $ rotationEuler $ extendZero (m' &* 0.01)
        d  = trim (rm *. d0) :: Vec3
        u  = trim (rm *. u0) :: Vec3
        v  = normalize (d &^ u)
