{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

-----------------------------------------------------------------------------
-- |
-- Copyright   : (C) 2015 Dimitri Sabadie
-- License     : BSD3
--
-- Maintainer  : Dimitri Sabadie <dimitri.sabadie@gmail.com>
-- Stability   : experimental
-- Portability : portable
-----------------------------------------------------------------------------

module Graphics.Luminance.Core.Texture where

import Control.Monad ( when )
import Control.Monad.IO.Class ( MonadIO(..) )
import Control.Monad.Trans.Resource ( MonadResource, register )
import Data.Proxy ( Proxy(..) )
import Data.Vector.Storable ( Vector )
import Foreign.Marshal.Alloc ( alloca )
import Foreign.Marshal.Utils ( with )
import Foreign.Storable ( Storable(peek) )
import Graphics.GL
import Graphics.GL.Ext.ARB.BindlessTexture
import Numeric.Natural ( Natural )

----------------------------------------------------------------------------------------------------
-- Texture parameters ------------------------------------------------------------------------------

-- |Wrap texture parameter. Such an object is used to tell how to sampling is performed when going
-- out of the texture coordinates.
--
-- 'ClampToEdge' will clamp the texture coordinates between in '[0,1]'. If you pass '1.1' or
-- '31.456', in both cases you’ll end up with '1'. Same thing for negative values clamped to '0'.
--
-- 'Repeat' will clamp the texture in '[0,1]' after applying a 'fract' on the value, yielding a
-- a repeated '[0,1]' pattern.
data Wrap
  = ClampToEdge
  -- | ClampToBorder
  | Repeat
  | MirroredRepeat
    deriving (Eq,Show)

fromWrap :: (Eq a,Num a) => Wrap -> a
fromWrap w = case w of
  ClampToEdge    -> GL_CLAMP_TO_EDGE
  -- ClampToBorder  -> GL_CLAMP_TO_BORDER
  Repeat         -> GL_REPEAT
  MirroredRepeat -> GL_MIRRORED_REPEAT

-- |Sampling filter. 'Nearest' will sample the nearest texel at the sampling coordinates whilst
-- 'Linear' will perform linear interpolation with the texels nearby.
data Filter
  = Nearest
  | Linear
    deriving (Eq,Show)

fromFilter :: (Eq a,Num a) => Filter -> a
fromFilter f = case f of
  Nearest -> GL_NEAREST
  Linear  -> GL_LINEAR

-- |For textures that might require depth comparison, that type defines all the possible cases for
-- comparison.
data CompareFunc
  = Never
  | Less
  | Equal
  | LessOrEqual
  | Greater
  | GreaterOrEqual
  | NotEqual
  | Always
    deriving (Eq,Show)

fromCompareFunc :: (Eq a,Num a) => CompareFunc -> a
fromCompareFunc f = case f of
  Never          -> GL_NEVER
  Less           -> GL_LESS
  Equal          -> GL_EQUAL
  LessOrEqual    -> GL_LEQUAL
  Greater        -> GL_GREATER
  GreaterOrEqual -> GL_GEQUAL
  NotEqual       -> GL_NOTEQUAL
  Always         -> GL_ALWAYS

----------------------------------------------------------------------------------------------------
-- Textures ----------------------------------------------------------------------------------------

-- |Class of all textures.
class Texture t where
  -- |Size of a texture. This is an associated type – /type family/ – because the dimensionality of
  -- a texture relies on its type.
  type TextureSize t :: *
  -- |In order to index regions of texels in texture, we need another associated type – for the same
  -- dimensionality reason as for 'TextureSize'.
  type TextureOffset t :: *
  fromBaseTexture :: BaseTexture -> TextureSize t -> t
  toBaseTexture :: t -> BaseTexture
  textureTypeEnum :: proxy t -> GLenum
  textureSize :: t -> TextureSize t
  textureStorage :: proxy t
                 -> GLuint -- texture ID
                 -> GLint -- levels
                 -> TextureSize t -- size of the texture
                 -> IO ()
  transferTexelsSub :: (Storable a)
                    => proxy t
                    -> GLuint -- texture ID
                    -> TextureOffset t -- offset
                    -> TextureSize t -- size
                    -> Vector a
                    -> IO ()
  fillTextureSub :: (Storable a)
                 => proxy t
                 -> GLuint
                 -> TextureOffset t -- offset
                 -> TextureSize t -- size
                 -> Vector a
                 -> IO ()

-- OpenGL texture.
data BaseTexture = BaseTexture {
    baseTextureID  :: GLuint
  , baseTextureHnd :: GLuint64
  } deriving (Eq,Show)


-- |'createTexture w h levels sampling' a new 'w'*'h' texture with 'levels' levels. The format is
-- set through the type.
createTexture :: forall m t. (MonadIO m,MonadResource m,Texture t)
              => TextureSize t
              -> Natural
              -> Sampling
              -> m t
createTexture size levels sampling = do
    (tid,texH) <- liftIO . alloca $ \p -> do
      glCreateTextures (textureTypeEnum (Proxy :: Proxy t)) 1 p
      tid <- peek p
      textureStorage (Proxy :: Proxy t) tid (fromIntegral levels) size
      glTextureParameteri tid GL_TEXTURE_BASE_LEVEL 0
      glTextureParameteri tid GL_TEXTURE_MAX_LEVEL (fromIntegral levels - 1)
      setTextureSampling tid sampling
      texH <- glGetTextureHandleARB tid 
      glMakeTextureHandleResidentARB texH
      pure (tid,texH)
    _ <- register $ do
      glMakeTextureHandleNonResidentARB texH
      with tid $ glDeleteTextures 1
    pure $ fromBaseTexture (BaseTexture tid texH) size

----------------------------------------------------------------------------------------------------
-- Sampling objects --------------------------------------------------------------------------------

-- |A sampling configuration type.
data Sampling = Sampling {
    samplingWrapS           :: Wrap
  , samplingWrapT           :: Wrap
  , samplingWrapR           :: Wrap
  , samplingMinFilter       :: Filter
  , samplingMagFilter       :: Filter
  , samplingCompareFunction :: Maybe CompareFunc
  } deriving (Eq,Show)

-- |Default 'Sampling' for convenience.
--
-- @
--   defaultSampling = Sampling {
--       samplingWrapS           = ClampToEdge
--     , samplingWrapT           = ClampToEdge
--     , samplingWrapR           = ClampToEdge
--     , samplingMinFilter       = Linear
--     , samplingMagFilter       = Linear
--     , samplingCompareFunction = Nothing
--     }
-- @
defaultSampling :: Sampling
defaultSampling = Sampling {
    samplingWrapS           = ClampToEdge
  , samplingWrapT           = ClampToEdge
  , samplingWrapR           = ClampToEdge
  , samplingMinFilter       = Linear
  , samplingMagFilter       = Linear
  , samplingCompareFunction = Nothing
  }

-- Apply a 'Sampling' object for a given type of object (texture, sampler, etc.).
setSampling :: (Eq a,Eq b,MonadIO m,Num a,Num b) => (GLenum -> a -> b -> IO ()) -> GLenum -> Sampling -> m ()
setSampling f objID s = liftIO $ do
  -- wraps
  f objID GL_TEXTURE_WRAP_S . fromWrap $ samplingWrapS s
  f objID GL_TEXTURE_WRAP_T . fromWrap $ samplingWrapT s
  f objID GL_TEXTURE_WRAP_R . fromWrap $ samplingWrapR s
  -- filters
  f objID GL_TEXTURE_MIN_FILTER . fromFilter $ samplingMinFilter s
  f objID GL_TEXTURE_MAG_FILTER . fromFilter $ samplingMagFilter s
  -- comparison function
  case samplingCompareFunction s of
    Just cmpf -> do
      f objID GL_TEXTURE_COMPARE_FUNC $ fromCompareFunc cmpf
      f objID GL_TEXTURE_COMPARE_MODE GL_COMPARE_REF_TO_TEXTURE
    Nothing ->
      f objID GL_TEXTURE_COMPARE_MODE GL_NONE

setTextureSampling :: (MonadIO m) => GLenum -> Sampling -> m ()
setTextureSampling = setSampling glTextureParameteri

setSamplerSampling :: (MonadIO m) => GLenum -> Sampling -> m ()
setSamplerSampling = setSampling glSamplerParameteri

----------------------------------------------------------------------------------------------------
-- Samplers ----------------------------------------------------------------------------------------

newtype Sampler = Sampler { samplerID :: GLuint } deriving (Eq,Show)

createSampler :: (MonadIO m,MonadResource m)
              => Sampling
              -> m Sampler
createSampler s = do
  sid <- liftIO . alloca $ \p -> do
    glCreateSamplers 1 p
    sid <- peek p
    setSamplerSampling sid s
    pure sid
  _ <- register . with sid $ glDeleteSamplers 1
  pure $ Sampler sid

----------------------------------------------------------------------------------------------------
-- Texture operations ------------------------------------------------------------------------------

-- |@'uploadSub' tex offset size autolvl texels@ uploads data to a subpart of the texture’s storage.
-- The offset is given with origin at upper-left corner, and @size@ is the size of the area
-- to upload to. @autolvl@ is a 'Bool' that can be used to automatically generate mipmaps.
uploadSub :: forall a m t. (MonadIO m,Storable a,Texture t)
          => t
          -> TextureOffset t
          -> TextureSize t
          -> Bool
          -> Vector a
          -> m ()
uploadSub tex offset size autolvl texels = liftIO $ do
    transferTexelsSub (Proxy :: Proxy t) tid offset size texels
    when autolvl $ glGenerateTextureMipmap tid
  where
    tid = baseTextureID (toBaseTexture tex)

-- |Fill a subpart of the texture’s storage with a given value.
fillSub :: forall a m t. (MonadIO m,Storable a,Texture t)
        => t
        -> TextureOffset t
        -> TextureSize t
        -> Bool
        -> Vector a
        -> m ()
fillSub tex offset size autolvl filling = liftIO $ do
    fillTextureSub (Proxy :: Proxy t) tid offset size filling
    when autolvl $ glGenerateTextureMipmap tid
  where
    tid = baseTextureID (toBaseTexture tex)