-- | Textures.
--

{-# LANGUAGE RecordWildCards, ScopedTypeVariables, NoImplicitPrelude #-}
{-# LANGUAGE MultiWayIf, ViewPatterns, DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}

module Graphics.Caramia.Texture
    (
    -- * Creating textures
      newTexture
    , Texture()
    , TextureSpecification(..)
    , textureSpecification
    , Topology(..)
    -- * Uploading to textures
    , uploadToTexture
    , Uploading(..)
    , uploading1D
    , uploading2D
    , uploading3D
    , UploadFormat(..)
    , CubeSide(..)
    -- * Texture units
    , TextureUnit
    -- * Mipmapping
    , generateMipmaps
    -- * Texture parameters
    , setWrapping
    , getWrapping
    , setMinFilter
    , setMagFilter
    , getMinFilter
    , getMagFilter
    , setAnisotropy
    , getAnisotropy
    , setCompareMode
    , getCompareMode
    , MinFilter(..)
    , MagFilter(..)
    , Wrapping(..)
    , CompareMode(..)
    -- * Views
    , viewSpecification
    , viewWidth
    , viewHeight
    , viewDepth
    , viewMipmapLevels
    , viewSize2D
    , viewSize3D
    -- * Utilities
    , maxMipmapLevels )
    where


import Control.Monad.IO.Class
import Control.Monad.Catch
import Data.Data ( Data )
import GHC.Generics
import qualified Graphics.Caramia.Buffer.Internal as Buf
import Graphics.Caramia.ImageFormats.Internal
import Graphics.Caramia.Internal.Exception
import Graphics.Caramia.Internal.TexStorage
import Graphics.Caramia.Internal.OpenGLCApi
import Graphics.Caramia.Prelude
import Graphics.Caramia.Resource
import Graphics.Caramia.Texture.Internal
import Graphics.GL.Ext.ARB.TextureBufferObject
import Graphics.GL.Ext.EXT.TextureFilterAnisotropic
import Graphics.GL.Ext.ARB.TextureMultisample
import Graphics.GL.Ext.ARB.TextureStorage
import Foreign
import Linear.V2 ( V2(..) )
import Linear.V3 ( V3(..) )

textureSpecification :: TextureSpecification
textureSpecification = TextureSpecification {
    topology = error "textureSpecification: topology is not set."
  , imageFormat = error "textureSpecification: image format is not set."
  , mipmapLevels = 1 }

-- | Returns the width of a texture.
viewWidth :: Texture -> Int
viewWidth (viewSpecification -> spec) = viewWidth' (topology spec)
  where
    viewWidth' (Tex1D {..}) = width1D
    viewWidth' (Tex2D {..}) = width2D
    viewWidth' (Tex3D {..}) = width3D
    viewWidth' (Tex1DArray {..}) = width1DArray
    viewWidth' (Tex2DArray {..}) = width2DArray
    viewWidth' (Tex2DMultisample {..}) = width2DMS
    viewWidth' (Tex2DMultisampleArray {..}) = width2DMSArray
    viewWidth' (TexCube {..}) = widthCube
    viewWidth' (TexBuffer {}) =
        error "viewWidth: buffer texture has no meaningful width."
        -- TODO: you can actually infer that from the buffer size
        -- so implement it

-- | Returns the size of a texture, as a `V2`. Width and height.
--
-- @
-- viewSize2D tex = V2 (viewWidth tex) (viewHeight tex)
-- @
viewSize2D :: Texture -> V2 Int
viewSize2D tex = V2 (viewWidth tex) (viewHeight tex)

-- | Returns the size of a texture, as a `V3`. Width, height and depth.
--
-- @
-- viewSize3D tex = V3 (viewWidth tex) (viewHeight tex) (viewDepth tex)
-- @
viewSize3D :: Texture -> V3 Int
viewSize3D tex = V3 (viewWidth tex) (viewHeight tex) (viewDepth tex)

-- | Returns the height of a texture.
--
-- This is 1 for one-dimensional textures.
viewHeight :: Texture -> Int
viewHeight (viewSpecification -> spec) = viewHeight' (topology spec)
  where
    viewHeight' (Tex1D {..}) = 1
    viewHeight' (Tex2D {..}) = height2D
    viewHeight' (Tex3D {..}) = height3D
    viewHeight' (Tex1DArray {..}) = 1
    viewHeight' (Tex2DArray {..}) = height2DArray
    viewHeight' (Tex2DMultisample {..}) = height2DMS
    viewHeight' (Tex2DMultisampleArray {..}) = height2DMSArray
    viewHeight' (TexCube {..}) = widthCube
    viewHeight' (TexBuffer {}) = 1

-- | Returns the depth of a 3D texture or number of layers in array textures.
--
-- This is 1 for any other type of texture.
viewDepth :: Texture -> Int
viewDepth (viewSpecification -> spec) = viewDepth' (topology spec)
  where
    viewDepth' (Tex1D {..}) = 1
    viewDepth' (Tex2D {..}) = 1
    viewDepth' (Tex3D {..}) = depth3D
    viewDepth' (Tex1DArray {..}) = layers1D
    viewDepth' (Tex2DArray {..}) = layers2D
    viewDepth' (Tex2DMultisample {..}) = 1
    viewDepth' (Tex2DMultisampleArray {..}) = layers2DMS
    viewDepth' (TexCube {..}) = 1
    viewDepth' (TexBuffer {}) = 1

viewMipmapLevels :: Texture -> Int
viewMipmapLevels = mipmapLevels . viewSpecification

isMultisamplingTopology :: Topology -> Bool
isMultisamplingTopology (Tex2DMultisample {..}) = True
isMultisamplingTopology (Tex2DMultisampleArray {..}) = True
isMultisamplingTopology _ = False

-- | Creates a new texture.
--
-- Initially the contents of the texture are undefined.
--
-- Texture dimensions must be positive.
newTexture :: MonadIO m
           => TextureSpecification
           -> m Texture
newTexture spec = liftIO $ mask_ $ do
    topologySanityCheck (topology spec)
    when (not (isMultisamplingTopology (topology spec)) &&
          mipmapLevels spec < 1) $
        error "newTexture: mipmapLevels is not positive."

    res <- newResource creator
                       deleter
                       (return ())

    index <- newUnique
    return Texture { resource = res
                   , ordIndex = index
                   , viewSpecification = spec }
  where
    num_mipmaps = mipmapLevels spec

    -- a lot of code just to check that all the dimensions are positive...
    topologySanityCheck t@(Tex1D {..})
        | width1D <= 0 = badTopology t
        | not (isValidMipmap width1D num_mipmaps) = badMipmaps
        | otherwise = return ()
    topologySanityCheck t@(Tex2D {..})
        | width2D <= 0 || height2D <= 0 = badTopology t
        | not (isValidMipmap (max width2D height2D) num_mipmaps) = badMipmaps
        | otherwise = return ()
    topologySanityCheck t@(Tex3D {..})
        | width3D <= 0 || height3D <= 0 || depth3D <= 0 = badTopology t
        | not (isValidMipmap (max width3D $ max height3D depth3D) num_mipmaps) =
              badMipmaps
        | otherwise = return ()
    topologySanityCheck t@(Tex1DArray {..})
        | width1DArray <= 0 || layers1D <= 0 = badTopology t
        | not (isValidMipmap width1DArray num_mipmaps) =
              badMipmaps
        | otherwise = return ()
    topologySanityCheck t@(Tex2DArray {..})
        | width2DArray <= 0 || height2DArray <= 0 ||
          layers2D <= 0 = badTopology t
        | not (isValidMipmap (max width2DArray height2DArray) num_mipmaps) =
              badMipmaps
        | otherwise = return ()
    topologySanityCheck t@(Tex2DMultisample {..})
        | width2DMS <= 0 || height2DMS <= 0 = badTopology t
        | otherwise = return ()
    topologySanityCheck t@(Tex2DMultisampleArray {..})
        | width2DMSArray <= 0 || height2DMSArray <= 0 ||
          layers2DMS <= 0 = badTopology t
        | otherwise = return ()
    topologySanityCheck t@(TexCube {..})
        | widthCube <= 0 = badTopology t
        | not (isValidMipmap widthCube num_mipmaps) =
              badMipmaps
        | otherwise = return ()
    topologySanityCheck (TexBuffer {}) = return ()

    badTopology _ = error "newTexture: bad topology."

    badMipmaps =
        error $ "newTexture: bad number of mipmap levels: " <> show num_mipmaps

    deleter (Texture_ name) =
        with name $ glDeleteTextures 1

    creator = do
        name <- bracketOnError
            (alloca $ \name_ptr ->
                glGenTextures 1 name_ptr *> peek name_ptr)
            (deleter . Texture_ )
            (\name -> do
                if gl_ARB_texture_storage
                  then createByTopologyTexStorage name (topology spec)
                  else createByTopologyFakeTextureStorage name (topology spec)
                return name)
        return (Texture_ name)

    createByTopologyFakeTextureStorage :: GLuint -> Topology -> IO ()
    createByTopologyFakeTextureStorage name (Tex1D {..}) =
        fakeTextureStorage1D name
                             GL_TEXTURE_1D
                             (safeFromIntegral num_mipmaps)
                             (toConstantIF (imageFormat spec))
                             (safeFromIntegral width1D)
    createByTopologyFakeTextureStorage name (Tex2D {..}) =
        fakeTextureStorage2D name
                             GL_TEXTURE_2D
                             (safeFromIntegral num_mipmaps)
                             (toConstantIF (imageFormat spec))
                             (safeFromIntegral width2D)
                             (safeFromIntegral height2D)
    createByTopologyFakeTextureStorage name (Tex3D {..}) =
        fakeTextureStorage3D name
                             GL_TEXTURE_3D
                             (safeFromIntegral num_mipmaps)
                             (toConstantIF (imageFormat spec))
                             (safeFromIntegral width3D)
                             (safeFromIntegral height3D)
                             (safeFromIntegral depth3D)
    createByTopologyFakeTextureStorage name (Tex1DArray {..}) =
        fakeTextureStorage2D name
                             GL_TEXTURE_1D_ARRAY
                             (safeFromIntegral num_mipmaps)
                             (toConstantIF (imageFormat spec))
                             (safeFromIntegral width1DArray)
                             (safeFromIntegral layers1D)
    createByTopologyFakeTextureStorage name (Tex2DArray {..}) =
        fakeTextureStorage3D name
                             GL_TEXTURE_2D_ARRAY
                             (safeFromIntegral num_mipmaps)
                             (toConstantIF (imageFormat spec))
                             (safeFromIntegral width2DArray)
                             (safeFromIntegral height2DArray)
                             (safeFromIntegral layers2D)
    createByTopologyFakeTextureStorage name tex@(Tex2DMultisample {..}) =
        createByTopologyTexStorage name tex
    createByTopologyFakeTextureStorage name tex@(Tex2DMultisampleArray {..}) =
        createByTopologyTexStorage name tex
    createByTopologyFakeTextureStorage name (TexCube {..}) =
        fakeTextureStorage2D name
                             GL_TEXTURE_CUBE_MAP
                             (safeFromIntegral num_mipmaps)
                             (toConstantIF (imageFormat spec))
                             (safeFromIntegral widthCube)
                             (safeFromIntegral widthCube)
    createByTopologyFakeTextureStorage name tex@(TexBuffer {..}) =
        createByTopologyTexStorage name tex

    -- TODO: use DSA when available, perhaps add mglTextureStorage* functions
    -- to Caramia.Internal.OpenGLCApi?
    createByTopologyTexStorage :: GLuint -> Topology -> IO ()
    createByTopologyTexStorage name (Tex1D {..}) =
        withBinding GL_TEXTURE_1D GL_TEXTURE_BINDING_1D name $
            glTexStorage1D GL_TEXTURE_1D
                           (safeFromIntegral num_mipmaps)
                           (toConstantIF (imageFormat spec))
                           (safeFromIntegral width1D)
    createByTopologyTexStorage name (Tex2D {..}) =
        withBinding GL_TEXTURE_2D GL_TEXTURE_BINDING_2D name $
            glTexStorage2D GL_TEXTURE_2D
                           (safeFromIntegral num_mipmaps)
                           (toConstantIF (imageFormat spec))
                           (safeFromIntegral width2D)
                           (safeFromIntegral height2D)
    createByTopologyTexStorage name (Tex3D {..}) =
        withBinding GL_TEXTURE_3D GL_TEXTURE_BINDING_3D name $
            glTexStorage3D GL_TEXTURE_3D
                           (safeFromIntegral num_mipmaps)
                           (toConstantIF (imageFormat spec))
                           (safeFromIntegral width3D)
                           (safeFromIntegral height3D)
                           (safeFromIntegral depth3D)
    createByTopologyTexStorage name (Tex1DArray {..}) =
        withBinding GL_TEXTURE_1D_ARRAY GL_TEXTURE_BINDING_1D_ARRAY name $
            glTexStorage2D GL_TEXTURE_1D_ARRAY
                           (safeFromIntegral num_mipmaps)
                           (toConstantIF (imageFormat spec))
                           (safeFromIntegral width1DArray)
                           (safeFromIntegral layers1D)
    createByTopologyTexStorage name (Tex2DArray {..}) =
        withBinding GL_TEXTURE_2D_ARRAY GL_TEXTURE_BINDING_2D_ARRAY name $
            glTexStorage3D GL_TEXTURE_2D_ARRAY
                           (safeFromIntegral num_mipmaps)
                           (toConstantIF (imageFormat spec))
                           (safeFromIntegral width2DArray)
                           (safeFromIntegral height2DArray)
                           (safeFromIntegral layers2D)
    createByTopologyTexStorage name (Tex2DMultisample {..}) =
        checkOpenGLOrExtensionM (OpenGLVersion 3 2)
                                "GL_ARB_texture_multisample"
                                gl_ARB_texture_multisample $
        withBinding GL_TEXTURE_2D_MULTISAMPLE
                    GL_TEXTURE_BINDING_2D_MULTISAMPLE
                    name $
            glTexImage2DMultisample
                           GL_TEXTURE_2D_MULTISAMPLE
                           (safeFromIntegral samples2DMS)
                           (fromIntegral $ toConstantIF (imageFormat spec))
                           (safeFromIntegral width2DMS)
                           (safeFromIntegral height2DMS)
                           (if fixedSampleLocations2DMS
                             then 1 else 0)
    createByTopologyTexStorage name (Tex2DMultisampleArray {..}) =
        checkOpenGLOrExtensionM (OpenGLVersion 3 2)
                                "GL_ARB_texture_multisample"
                                gl_ARB_texture_multisample $
        withBinding GL_TEXTURE_2D_MULTISAMPLE_ARRAY
                    GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY
                    name $
            glTexImage3DMultisample
                           GL_TEXTURE_2D_MULTISAMPLE_ARRAY
                           (safeFromIntegral samples2DMSArray)
                           (fromIntegral $ toConstantIF (imageFormat spec))
                           (safeFromIntegral width2DMSArray)
                           (safeFromIntegral height2DMSArray)
                           (safeFromIntegral layers2DMS)
                           (if fixedSampleLocations2DMSArray
                             then 1 else 0)
    createByTopologyTexStorage name (TexCube {..}) =
        withBinding GL_TEXTURE_CUBE_MAP
                    GL_TEXTURE_BINDING_CUBE_MAP
                    name $
            glTexStorage2D GL_TEXTURE_CUBE_MAP
                           (safeFromIntegral num_mipmaps)
                           (fromIntegral $ toConstantIF (imageFormat spec))
                           (safeFromIntegral widthCube)
                           (safeFromIntegral widthCube)
    createByTopologyTexStorage name (TexBuffer {..}) =
        checkOpenGLOrExtensionM (OpenGLVersion 3 1)
                                "GL_ARB_texture_buffer_object"
                                gl_ARB_texture_buffer_object $
        withBinding GL_TEXTURE_BUFFER
                    GL_TEXTURE_BINDING_BUFFER
                    name $
            withResource (Buf.resource texBuffer) $ \(Buf.Buffer_ bufname) ->
                glTexBuffer GL_TEXTURE_BUFFER
                            (fromIntegral $ toConstantIF (imageFormat spec))
                            bufname

-- | Generate all mipmaps for a texture. If mipmap levels were specified, that
-- is.
generateMipmaps :: (MonadIO m, MonadMask m) => Texture -> m ()
generateMipmaps = flip withBindingByTopology glGenerateMipmap

-- | Specifies the format in which buffer data is for the purposes of uploading
-- said data to a texture.
data UploadFormat =
    UR    -- ^ Just red.
  | URG   -- ^ Red and green.
  | URGB  -- ^ You know the drill.
  | URGBA
  | UBGR
  | UBGRA
  | UDEPTH_COMPONENT   -- ^ Depth values.
  | USTENCIL_INDEX     -- ^ Stencil values.
  deriving ( Eq, Ord, Show, Read, Typeable, Data, Generic )

-- TODO: add UDEPTH_STENCIL when `SpecificationType` has special interpretation
-- formats.

toConstantUF :: UploadFormat -> GLenum
toConstantUF UR = GL_RED
toConstantUF URG = GL_RG
toConstantUF URGB = GL_RGB
toConstantUF URGBA = GL_RGBA
toConstantUF UBGR = GL_BGR
toConstantUF UBGRA = GL_BGRA
toConstantUF UDEPTH_COMPONENT = GL_DEPTH_COMPONENT
toConstantUF USTENCIL_INDEX = GL_STENCIL_INDEX

-- | Used to specify how to move the data from a `Buffer` to a `Texture` in
-- `uploadToTexture`.
--
-- This is common for all texture topologies. However, some fields are ignored
-- depending on the topology.
--
-- For example, if you upload into a 1D texture, then all fields that deal with
-- higher dimensions (`yOffset`, `zOffset`, `uHeight` etc.) are ignored.
--
-- It is recommended that you use one of the smart constructors as they
-- implement the common use cases so you don't have to fill all these fields by
-- yourself.
data Uploading = Uploading
    { fromBuffer    :: !Buf.Buffer  -- ^ From which buffer to upload.
    , bufferOffset  :: !Int     -- ^ Offset in the buffer, in bytes,
                                --   from where to start uploading.
    , toMipmapLevel :: !Int     -- ^ To which mipmap level to upload.
                                --   (0 = base level).
    , specificationType :: !SpecificationType
    -- ^ What data type is used for each component value in a pixel.
    , uploadFormat  :: !UploadFormat
    -- ^ What format is the source data in.
    , xOffset       :: !Int     -- ^ X offset where to put the data.
    , yOffset       :: !Int     -- ^ Y offset where to put the data.
    , zOffset       :: !Int     -- ^ Z offset where to put the data.
    , uWidth        :: !Int     -- ^ Width of the data to put.
    , uHeight       :: !Int     -- ^ Height of the data to put.
    , uDepth        :: !Int     -- ^ Number of 2D images to put.
    , cubeSide      :: CubeSide  -- ^ Only used for cube map textures.
                                 -- Specifies which side of the cube to upload.
                                 -- Not evaluated if the texture is not a cube
                                 -- texture.
    , numColumns    :: !Int
    -- ^ Number of columns in the image in the source buffer. This value is
    -- also sometimes known as \'pitch\'. It is the same as `uWidth` except in
    -- cases where the next row in source data does not come immediately after
    -- the current row but after `numColumns` from the first pixel in the row.
    , numRows       :: !Int
    -- ^ Same as `numColumns` but for images in 3D uploading.
    , pixelAlignment     :: !Int
    -- ^ Alignment in which the source texture data is. Every row is aligned to
    -- this value. Allowed values are 1, 2, 4 and 8. The default value in smart
    -- constructors is 1.
    }
    deriving ( Eq, Typeable )

-- | Values of this type refer to sides of a cube.
data CubeSide =
    PositiveY
  | NegativeY
  | PositiveX
  | NegativeX
  | PositiveZ
  | NegativeZ
    deriving ( Eq, Ord, Show, Read, Typeable, Data, Generic )

toConstantCS :: CubeSide -> GLenum
toConstantCS PositiveX = GL_TEXTURE_CUBE_MAP_POSITIVE_X
toConstantCS NegativeX = GL_TEXTURE_CUBE_MAP_NEGATIVE_X
toConstantCS PositiveY = GL_TEXTURE_CUBE_MAP_POSITIVE_Y
toConstantCS NegativeY = GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
toConstantCS PositiveZ = GL_TEXTURE_CUBE_MAP_POSITIVE_Z
toConstantCS NegativeZ = GL_TEXTURE_CUBE_MAP_NEGATIVE_Z

-- | Constructs a common 1D uploading.
uploading1D :: Buf.Buffer
            -> Int     -- ^ How many pixels to upload.
            -> SpecificationType
            -> UploadFormat
            -> Uploading
uploading1D buffer pixels stype uf =
    Uploading {
         fromBuffer = buffer
       , bufferOffset = 0
       , specificationType = stype
       , uploadFormat = uf
       , toMipmapLevel = 0
       , xOffset = 0
       , yOffset = 0
       , zOffset = 0
       , uWidth = pixels
       , uHeight = 1
       , uDepth = 1
       , numColumns = pixels
       , cubeSide = PositiveY
       , numRows = 1
       , pixelAlignment = 1 }

-- | Constructs a common 2D uploading.
--
-- This can also be used for uploading into 1D texture arrays.
uploading2D :: Buf.Buffer
            -> Int     -- ^ Width of the image to upload.
            -> Int     -- ^ Height of the image to upload.
            -> SpecificationType
            -> UploadFormat
            -> Uploading
uploading2D buffer width height stype uf =
    Uploading {
         fromBuffer = buffer
       , bufferOffset = 0
       , specificationType = stype
       , uploadFormat = uf
       , toMipmapLevel = 0
       , xOffset = 0
       , yOffset = 0
       , zOffset = 0
       , uWidth = width
       , uHeight = height
       , uDepth = 1
       , numColumns = width
       , cubeSide = PositiveY
       , numRows = height
       , pixelAlignment = 1 }

-- | Constructs a common 3D uploading.
--
-- This can also be used for uploading into 2D texture arrays.
uploading3D :: Buf.Buffer
            -> Int     -- ^ Width of the image to upload.
            -> Int     -- ^ Height of the image to upload.
            -> Int     -- ^ Number of images to upload.
            -> SpecificationType
            -> UploadFormat
            -> Uploading
uploading3D buffer width height depth stype uf =
    Uploading {
         fromBuffer = buffer
       , bufferOffset = 0
       , specificationType = stype
       , uploadFormat = uf
       , toMipmapLevel = 0
       , xOffset = 0
       , yOffset = 0
       , zOffset = 0
       , uWidth = width
       , uHeight = height
       , uDepth = depth
       , numColumns = width
       , cubeSide = PositiveY
       , numRows = height
       , pixelAlignment = 1 }

-- | Uploads an image to a texture.
uploadToTexture :: MonadIO m
                => Uploading
                -> Texture
                -> m ()
uploadToTexture uploading tex = liftIO $ mask_ $
    withResource (Buf.resource (fromBuffer uploading)) $ \(Buf.Buffer_ buf) ->
    withBoundPixelUnpackBuffer buf $ do
        old_num_cols  <- fromIntegral <$> gi GL_UNPACK_ROW_LENGTH
        old_num_rows  <- fromIntegral <$> gi GL_UNPACK_IMAGE_HEIGHT
        old_alignment <- fromIntegral <$> gi GL_UNPACK_ALIGNMENT
        glPixelStorei GL_UNPACK_ROW_LENGTH
                      (safeFromIntegral $ numColumns uploading)
        flip finally (glPixelStorei GL_UNPACK_ROW_LENGTH old_num_cols) $ do
         glPixelStorei GL_UNPACK_IMAGE_HEIGHT
                       (safeFromIntegral $ numRows uploading)
         flip finally (glPixelStorei GL_UNPACK_IMAGE_HEIGHT old_num_rows) $ do
          glPixelStorei GL_UNPACK_ALIGNMENT
                        (safeFromIntegral $ pixelAlignment uploading)
          flip finally (glPixelStorei GL_UNPACK_ALIGNMENT old_alignment) $
           withResource (resource tex) $ \(Texture_ texname) ->
            case topology $ viewSpecification tex of
                Tex1D {..} ->
                    upload1D GL_TEXTURE_1D GL_TEXTURE_BINDING_1D
                             texname uploading
                Tex2D {..} ->
                    upload2D GL_TEXTURE_2D GL_TEXTURE_BINDING_2D
                             texname uploading
                Tex3D {..} ->
                    upload3D GL_TEXTURE_3D GL_TEXTURE_BINDING_3D
                             texname uploading
                Tex1DArray {..} ->
                    upload2D GL_TEXTURE_1D_ARRAY GL_TEXTURE_BINDING_1D_ARRAY
                             texname uploading
                Tex2DArray {..} ->
                    upload3D GL_TEXTURE_2D_ARRAY GL_TEXTURE_BINDING_2D_ARRAY
                             texname uploading
                Tex2DMultisample {..} ->
                    error $ "uploadToTexture: cannot upload to " <>
                            "multisampling textures."
                Tex2DMultisampleArray {..} ->
                    error $ "uploadToTexture: cannot upload to " <>
                            "multisampling array textures."
                TexCube {..} ->
                    uploadCube GL_TEXTURE_CUBE_MAP
                               GL_TEXTURE_BINDING_CUBE_MAP
                               texname uploading
                TexBuffer {..} ->
                    error $ "uploadToTexture: cannot upload to " <>
                            "buffer textures. (please upload directly to the " <>
                            "associated buffer instead.)"

upload1D :: GLenum -> GLenum -> GLuint -> Uploading -> IO ()
upload1D target binding tex (Uploading {..}) =
    withBinding target binding tex $
        glTexSubImage1D target
                        (safeFromIntegral toMipmapLevel)
                        (safeFromIntegral xOffset)
                        (safeFromIntegral uWidth)
                        (toConstantUF uploadFormat)
                        (toConstantST specificationType)
                        (intPtrToPtr $
                         fromIntegral bufferOffset)

upload2D :: GLenum -> GLenum -> GLuint -> Uploading -> IO ()
upload2D target binding tex (Uploading {..}) =
    withBinding target binding tex $
        glTexSubImage2D target
                        (safeFromIntegral toMipmapLevel)
                        (safeFromIntegral xOffset)
                        (safeFromIntegral yOffset)
                        (safeFromIntegral uWidth)
                        (safeFromIntegral uHeight)
                        (toConstantUF uploadFormat)
                        (toConstantST specificationType)
                        (intPtrToPtr $
                         fromIntegral bufferOffset)

upload3D :: GLenum -> GLenum -> GLuint -> Uploading -> IO ()
upload3D target binding tex (Uploading {..}) =
    withBinding target binding tex $
        glTexSubImage3D target
                        (safeFromIntegral toMipmapLevel)
                        (safeFromIntegral xOffset)
                        (safeFromIntegral yOffset)
                        (safeFromIntegral zOffset)
                        (safeFromIntegral uWidth)
                        (safeFromIntegral uHeight)
                        (safeFromIntegral uDepth)
                        (toConstantUF uploadFormat)
                        (toConstantST specificationType)
                        (intPtrToPtr $
                         fromIntegral bufferOffset)

uploadCube :: GLenum -> GLenum -> GLuint -> Uploading -> IO ()
uploadCube target binding tex (Uploading {..}) =
    withBinding target binding tex $
        glTexSubImage2D (toConstantCS cubeSide)
                        (safeFromIntegral toMipmapLevel)
                        (safeFromIntegral xOffset)
                        (safeFromIntegral yOffset)
                        (safeFromIntegral uWidth)
                        (safeFromIntegral uHeight)
                        (toConstantUF uploadFormat)
                        (toConstantST specificationType)
                        (intPtrToPtr $
                         fromIntegral bufferOffset)

isValidMipmap :: Int -> Int -> Bool
isValidMipmap w level
    | w <= 0 = False
    | level < 0 = False
    | level > floor (logBase (2 :: Double) (fromIntegral w)) + 1 = False
    | otherwise = True

-- | Returns the maximal number of mipmap levels when given a side length.
maxMipmapLevels :: Int -> Int
maxMipmapLevels width
    | width <= 0 = 0
    | otherwise = floor (logBase (2 :: Double) (fromIntegral width)) + 1

class TexParam a where
    tpEnum :: a -> GLenum
    tpToConstant :: a -> GLenum
    tpFromConstant :: GLenum -> a

data MinFilter =
    MiNearest
  | MiLinear
  | MiNearestMipmapNearest
  | MiLinearMipmapNearest
  | MiNearestMipmapLinear
  | MiLinearMipmapLinear
  deriving ( Eq, Ord, Show, Read, Typeable, Data, Generic, Enum )

data MagFilter =
   MaNearest
 | MaLinear
 deriving ( Eq, Ord, Show, Read, Typeable, Data, Generic, Enum )

data Wrapping =
   Clamp
 | Repeat
 deriving ( Eq, Ord, Show, Read, Typeable, Data, Generic, Enum )

-- | Texture comparison modes.
--
-- See @ glTexParameteri @ documentation in OpenGL.
data CompareMode
 = NoCompare
 | CompareRefToTexture
 deriving ( Eq, Ord, Show, Read, Typeable, Data, Generic, Enum )

toConstantC :: CompareMode -> GLenum
toConstantC NoCompare = GL_NONE
toConstantC CompareRefToTexture = GL_COMPARE_REF_TO_TEXTURE

toConstantW :: Wrapping -> GLenum
toConstantW Clamp = GL_CLAMP_TO_EDGE
toConstantW Repeat = GL_REPEAT

instance TexParam MinFilter where
    tpEnum _ = GL_TEXTURE_MIN_FILTER
    tpToConstant MiNearest = GL_NEAREST
    tpToConstant MiLinear  = GL_LINEAR
    tpToConstant MiNearestMipmapNearest = GL_NEAREST_MIPMAP_NEAREST
    tpToConstant MiLinearMipmapNearest = GL_LINEAR_MIPMAP_NEAREST
    tpToConstant MiNearestMipmapLinear = GL_NEAREST_MIPMAP_LINEAR
    tpToConstant MiLinearMipmapLinear = GL_LINEAR_MIPMAP_LINEAR
    tpFromConstant c
        | c == GL_NEAREST = MiNearest
        | c == GL_LINEAR  = MiLinear
        | c == GL_NEAREST_MIPMAP_NEAREST = MiNearestMipmapNearest
        | c == GL_LINEAR_MIPMAP_NEAREST = MiLinearMipmapNearest
        | c == GL_NEAREST_MIPMAP_LINEAR = MiNearestMipmapLinear
        | c == GL_LINEAR_MIPMAP_LINEAR = MiLinearMipmapLinear
        | otherwise = error "MinFilter: unexpected filtering value."

instance TexParam MagFilter where
    tpEnum _ = GL_TEXTURE_MAG_FILTER
    tpToConstant MaNearest = GL_NEAREST
    tpToConstant MaLinear = GL_LINEAR

    tpFromConstant c
        | c == GL_NEAREST = MaNearest
        | c == GL_LINEAR  = MaLinear
        | otherwise = error "MagFilter: unexpected filtering value."

setMinFilter :: (MonadIO m, MonadMask m) => MinFilter -> Texture -> m ()
setMinFilter = setTexParam

setMagFilter :: (MonadIO m, MonadMask m) => MagFilter -> Texture -> m ()
setMagFilter = setTexParam

getMinFilter :: MonadIO m => Texture -> m MinFilter
getMinFilter = getTexParam

getMagFilter :: MonadIO m => Texture -> m MagFilter
getMagFilter = getTexParam

setTexParam :: (MonadIO m, MonadMask m, TexParam a) => a -> Texture -> m ()
setTexParam param tex = withBindingByTopology tex $ \target ->
    glTexParameteri target (tpEnum param) (fromIntegral $ tpToConstant param)

getTexParam :: forall m a. (MonadIO m, TexParam a) => Texture -> m a
getTexParam tex = liftIO $ withBindingByTopology tex $ \target ->
    alloca $ \result_ptr -> do
        glGetTexParameteriv target (tpEnum (undefined :: a)) result_ptr
        tpFromConstant . fromIntegral <$> peek result_ptr

setWrapping :: (MonadIO m, MonadMask m) => Wrapping -> Texture -> m ()
setWrapping wrapping tex = withBindingByTopology tex $ \target -> do
    glTexParameteri target GL_TEXTURE_WRAP_S
                           (fromIntegral $ toConstantW wrapping)
    glTexParameteri target GL_TEXTURE_WRAP_T
                           (fromIntegral $ toConstantW wrapping)
    glTexParameteri target GL_TEXTURE_WRAP_R
                           (fromIntegral $ toConstantW wrapping)

setCompareMode :: (MonadIO m, MonadMask m) => CompareMode -> Texture -> m ()
setCompareMode cmp_mode tex = withBindingByTopology tex $ \target ->
    glTexParameteri target GL_TEXTURE_COMPARE_MODE
                    (fromIntegral $ toConstantC cmp_mode)

getCompareMode :: (MonadIO m, MonadMask m) => Texture -> m CompareMode
getCompareMode tex = liftIO $ withBindingByTopology tex $ \target ->
    alloca $ \result_ptr -> do
        glGetTexParameteriv target GL_TEXTURE_COMPARE_MODE result_ptr
        result <- peek result_ptr
        return $ if
            | result == GL_NONE -> NoCompare
            | result == GL_COMPARE_REF_TO_TEXTURE -> CompareRefToTexture
            | otherwise -> error "getCompareMode: unexpected comparing mode."

getWrapping :: (MonadIO m, MonadMask m) => Texture -> m Wrapping
getWrapping tex = liftIO $ withBindingByTopology tex $ \target ->
    alloca $ \result_ptr -> do
        glGetTexParameteriv target GL_TEXTURE_WRAP_S result_ptr
        result <- peek result_ptr
        return $ if
            | result == GL_CLAMP_TO_EDGE -> Clamp
            | result == GL_REPEAT -> Repeat
            | otherwise -> error "getWrapping: unexpected wrapping mode."

setAnisotropy :: (MonadIO m, MonadMask m) => Float -> Texture -> m ()
setAnisotropy ani tex = withBindingByTopology tex $ \target ->
    glTexParameterf target GL_TEXTURE_MAX_ANISOTROPY_EXT ani

getAnisotropy :: (MonadIO m, MonadMask m) => Texture -> m Float
getAnisotropy tex = liftIO $ withBindingByTopology tex $ \target ->
    alloca $ \ani_ptr -> do
        glGetTexParameterfv target GL_TEXTURE_MAX_ANISOTROPY_EXT ani_ptr
        peek ani_ptr

{-
nextMipmapLevel :: Int -> Int
nextMipmapLevel 0 = 0
nextMipmapLevel 1 = 1
nextMipmapLevel x = max 1 (x `div` 2)

nthMipmapLevel :: Int -> Int -> Int
nthMipmapLevel x 0 = x
nthMipmapLevel x n = nthMipmapLevel (nextMipmapLevel x) (n-1)
-}