{-# LANGUAGE CPP                   #-}
{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude     #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}

-- | Utilities for working with vertex buffer objects (VBOs) filled
-- with vertices represented as `Records`.
module Graphics.RecordGL.Vertex (bufferVertices, bindVertices, reloadVertices
                                , deleteVertices, enableVertices, enableVertices'
                                , enableVertexFields, fieldToVAD
                                , ViableVertex, BufferedVertices(..)) where

import           Graphics.RecordGL.Uniforms
import           Record.Introspection
import           Record.Types

import           BasePrelude
import qualified Data.Map                   as M
import           Data.Proxy
import qualified Data.Vector.Storable       as V
import           Foreign.Ptr                (plusPtr)
import           Foreign.Storable
import           GHC.TypeLits
import           Graphics.GLUtil            hiding (Elem, throwError)
import           Graphics.Rendering.OpenGL  (BufferTarget (..),
                                             VertexArrayDescriptor (..),
                                             bindBuffer, ($=))
import qualified Graphics.Rendering.OpenGL  as GL

-- | Representation of a VBO whose type describes the vertices.
data BufferedVertices a  =
  BufferedVertices {getVertexBuffer :: GL.BufferObject}

-- | Load vertex data into a GPU-accessible buffer.
bufferVertices :: (Storable rs, BufferSource (v rs)) => v rs -> IO (BufferedVertices rs)
bufferVertices = fmap BufferedVertices . fromSource ArrayBuffer

-- | Reload 'BufferedVertices' with a 'V.Vector' of new vertex data.
reloadVertices :: Storable rs
               => BufferedVertices rs
               -> V.Vector rs
               -> IO ()
reloadVertices b v = do
  bindBuffer ArrayBuffer $= Just (getVertexBuffer b)
  replaceVector ArrayBuffer v

-- | Delete the object name associated with 'BufferedVertices'
deleteVertices :: BufferedVertices a -> IO ()
deleteVertices = GL.deleteObjectNames . (: []) . getVertexBuffer

-- | Bind previously-buffered vertex data.
bindVertices :: BufferedVertices a -> IO ()
bindVertices = (bindBuffer ArrayBuffer $=) . Just . getVertexBuffer

-- | Constraint alias capturing the requirements of a vertex type.
type ViableVertex t = (HasFieldNames t, HasFieldSizes t, HasFieldDims t,
                       HasFieldGLTypes t, Storable t)

-- | Line up a shader's attribute inputs with a vertex record. This
-- maps vertex fields to GLSL attributes on the basis of record field names
-- on the Haskell side, and variable names on the GLSL side.
enableVertices :: forall f r. ViableVertex r
               => ShaderProgram -> f r -> IO (Maybe String)
enableVertices s _ = enableAttribs s (Proxy :: Proxy r)

-- | Behaves like 'enableVertices', but raises an exception if the
-- supplied vertex record does not include a field required by the
-- shader.
enableVertices' :: forall f r. ViableVertex r
               => ShaderProgram -> f r -> IO ()
enableVertices' s _ = enableAttribs s (Proxy::Proxy r) >>=
                      maybe (return ()) error

data FieldDescriptor = FieldDescriptor { fieldName   :: String
                                       , fieldOffset :: Int
                                       , fieldDim    :: Int
                                       , fieldType   :: GL.VariableType }
                       deriving Show

fieldDescriptors :: ViableVertex t => t -> [FieldDescriptor]
fieldDescriptors x = getZipList $
                     FieldDescriptor <$> zl (fieldNames x)
                                     <*> zl (scanl (+) 0 $ fieldSizes x)
                                     <*> zl (fieldDims x)
                                     <*> zl (fieldGLTypes x)
  where zl = ZipList

-- | Bind some of a shader's attribute inputs to a vertex record. This
-- is useful when the inputs of a shader are split across multiple
-- arrays.
enableVertexFields :: forall p r. ViableVertex r
                   => ShaderProgram -> p r -> IO ()
enableVertexFields s _ = enableSomeAttribs s p >>= maybe (return ()) error
  where
    p = Proxy::Proxy r

-- | Do not raise an error is some of a shader's inputs are not bound
-- by a vertex record.
enableSomeAttribs :: forall v. ViableVertex v
                  => ShaderProgram -> Proxy v -> IO (Maybe String)
enableSomeAttribs s p = go $ fieldDescriptors (undefined::v)
  where go [] = return Nothing
        go (fd:fds) =
          let n = fieldName fd
              shaderAttribs = attribs s
          in case M.lookup n shaderAttribs of
               Nothing -> return (Just $ "Unexpected attribute " ++ n)
               Just (_,t)
                 | fieldType fd == t -> do enableAttrib s n
                                           setAttrib s n GL.ToFloat $
                                             descriptorVAD p fd
                                           go fds
                 | otherwise -> return . Just $ "Type mismatch in " ++ n

enableAttribs :: forall v. ViableVertex v
              => ShaderProgram -> Proxy v -> IO (Maybe String)
enableAttribs s p = go (map (second snd) $ M.assocs (attribs s))
  where
    go [] = return Nothing
    go ((l, t):as) = case find ((== l) . fieldName) fs of
                       Nothing -> return (Just $ "GLSL expecting " ++ l)
                       Just fd
                         | fieldType fd == t -> do
                           enableAttrib s l
                           setAttrib s l GL.ToFloat $
                             descriptorVAD p fd
                           go as
                         | otherwise -> return . Just $ "Type mismatch in " ++ l
    fs = fieldDescriptors (undefined::v)

descriptorVAD :: forall t a. Storable t
              => Proxy t -> FieldDescriptor -> VertexArrayDescriptor a
descriptorVAD _ fd = VertexArrayDescriptor (fromIntegral $ fieldDim fd)
                                           (variableDataType $ fieldType fd)
                                           (fromIntegral $
                                            sizeOf (undefined::t))
                                           (offset0 `plusPtr` fieldOffset fd)

namesAndOffsets :: (HasFieldNames t, HasFieldSizes t) => t -> [(String, Int)]
namesAndOffsets x = zip (fieldNames x) (scanl (+) 0 (fieldSizes x))

-- | Produce a 'GL.VertexArrayDescriptor' for a particular field of a
-- vertex record.
-- fieldToVAD :: forall r v a sy proxy.
--               (
--                Foldable v)
--            => String -> proxy r -> GL.VertexArrayDescriptor a
fieldToVAD :: forall sy r v a proxy.
              (Field' sy r (v a), HasFieldNames r, HasFieldSizes r, HasGLType a, Storable r, Num (v a),
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ >= 707
               KnownSymbol sy,
#else
               SingI sy,
#endif
               Foldable v)
           => FieldName sy -> r -> GL.VertexArrayDescriptor a
fieldToVAD _ _ = GL.VertexArrayDescriptor dim
                                          (glType (undefined::a))
                                          (fromIntegral sz)
                                          (offset0 `plusPtr` offset)
  where
    sz = sizeOf (undefined::r)
    dim = getSum $ foldMap (const (Sum 1)) (0::v a)
    Just offset = lookup n $ namesAndOffsets (undefined::r)
#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ >= 707
    n = symbolVal (Proxy::Proxy sy)
#else
    n = fromSing (sing::Sing sy)
#endif