{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}

module Backend where

-- base
import Control.Category
  ( (>>>) )
import Control.Monad
  ( guard, unless, void )
import Data.Bits
  ( (.&.), (.|.) )
import Data.Coerce
  ( coerce )
import Data.Foldable
  ( toList )
import Data.Functor
  ( (<&>) )
import Data.List
  ( sortOn )
import Data.Maybe
  ( fromMaybe )
import Data.Ord
  ( Down(..) )
import Data.Semigroup
  ( First(..) )
import Data.String
  ( fromString )
import Data.Traversable
  ( for )
import Data.Word
  ( Word32 )
import Foreign.C.String
  ( peekCString )
import Foreign.C.Types
  ( CInt )
import Foreign.Ptr
  ( castPtr )

-- bytestring
import Data.ByteString
  ( ByteString )

-- containers
import qualified Data.Map.Strict as Map
  ( empty, insertWith, toList )

-- logging-effect
import Control.Monad.Log
  ( MonadLog, Severity(..), WithSeverity(..)
  , logDebug, logInfo
  )

-- resourcet
import Control.Monad.Trans.Resource
  ( MonadResource )
import qualified Control.Monad.Trans.Resource as ResourceT
  ( ReleaseKey, allocate )

-- sdl2
import qualified SDL
import qualified SDL.Raw
import qualified SDL.Video.Vulkan

-- text
import Data.Text
  ( Text )
import qualified Data.Text as Text
  ( intercalate, pack, unpack )
import Data.Text.Encoding
  ( encodeUtf8 )

-- transformers
import Control.Monad.IO.Class
  ( MonadIO(liftIO) )

-- unliftio-core
import Control.Monad.IO.Unlift
  ( MonadUnliftIO )

-- vector
import qualified Data.Vector as Boxed
  ( Vector )
import qualified Data.Vector as Boxed.Vector
  ( (!?), empty, find, fromList, imap, imapMaybe, singleton, toList )

-- vulkan
import qualified Vulkan
import qualified Vulkan as LayerProperties (LayerProperties(..))
import Vulkan.Core10.DeviceInitialization (pattern INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR)
import qualified Vulkan.CStruct.Extends as Vulkan
import qualified Vulkan.Requirement     as Vulkan
import qualified Vulkan.Zero            as Vulkan

-- vulkan-utils
import qualified Vulkan.Utils.Initialization as Vulkan.Utils
  ( createInstanceFromRequirements, createDebugInstanceFromRequirements
  , createDeviceFromRequirements
  )

-- dear-imgui
import Attachments
  ( AttachmentType, SubpassAttachments, SubpassAttachmentReferences
  , attachmentReferencesAndDescriptions
  , createSubpass
  )

--------------------------------------------------------------------------------

type LogMessage = WithSeverity Text
class    ( MonadUnliftIO m, MonadResource m, MonadLog LogMessage m ) => MonadVulkan m
instance ( MonadUnliftIO m, MonadResource m, MonadLog LogMessage m ) => MonadVulkan m

----------------------------------------------------------------------------
-- Logging.

logHandler :: MonadIO m => LogMessage -> m ()
logHandler ( WithSeverity sev mess )
  = liftIO . putStrLn . Text.unpack $ showSeverity sev <> " " <> mess

showSeverity :: Severity -> Text
showSeverity Emergency     = "! PANIC !"
showSeverity Alert         = "! ALERT !"
showSeverity Critical      = "! CRIT !"
showSeverity Error         = "[ERR]  "
showSeverity Warning       = "[WARN] "
showSeverity Notice        = "(note) "
showSeverity Informational = "(info) "
showSeverity Debug         = "(debug)"


data VulkanContext =
  VulkanContext
    { instance'      :: !Vulkan.Instance
    , physicalDevice :: !Vulkan.PhysicalDevice
    , device         :: !Vulkan.Device
    , queueFamily    :: !Word32
    , queue          :: !Vulkan.Queue
    }

data InstanceType
  = NormalInstance
  | DebugInstance
  deriving stock Show

data VulkanRequirements =
  VulkanRequirements
    { instanceRequirements    :: [ Vulkan.InstanceRequirement ]
    , instanceRequirementsOpt :: [ Vulkan.InstanceRequirement ]
    , deviceRequirements      :: [ Vulkan.DeviceRequirement   ]
    , queueFlags              :: Vulkan.QueueFlags
    }

data ValidationLayerName
  = LunarG
  | Khronos
  deriving stock ( Eq, Show )

initialiseVulkanContext :: MonadVulkan m => InstanceType -> ByteString -> VulkanRequirements -> m VulkanContext
initialiseVulkanContext instanceType appName ( VulkanRequirements { instanceRequirements, instanceRequirementsOpt, deviceRequirements, queueFlags } ) = do
  logDebug "Creating Vulkan instance"
  instanceInfo    <- vulkanInstanceInfo appName
  instance'       <- case instanceType of
    NormalInstance -> Vulkan.Utils.createInstanceFromRequirements      instanceRequirements instanceRequirementsOpt instanceInfo
    DebugInstance  -> Vulkan.Utils.createDebugInstanceFromRequirements instanceRequirements instanceRequirementsOpt instanceInfo
  physicalDevice  <- logDebug "Creating physical device"      *> createPhysicalDevice instance'
  queueFamily     <- logDebug "Finding suitable queue family" *> findQueueFamilyIndex physicalDevice queueFlags
  let
    queueCreateInfo :: Vulkan.DeviceQueueCreateInfo '[]
    queueCreateInfo = Vulkan.zero
      { Vulkan.queueFamilyIndex = fromIntegral queueFamily
      , Vulkan.queuePriorities  = Boxed.Vector.singleton ( 1.0 :: Float )
      }
    deviceCreateInfo :: Vulkan.DeviceCreateInfo '[]
    deviceCreateInfo = Vulkan.zero { Vulkan.queueCreateInfos = Boxed.Vector.singleton ( Vulkan.SomeStruct queueCreateInfo ) }
    swapchainDeviceRequirements :: [ Vulkan.DeviceRequirement ]
    swapchainDeviceRequirements
      = Vulkan.RequireDeviceExtension Nothing Vulkan.KHR_SWAPCHAIN_EXTENSION_NAME 0
      : deviceRequirements
  device <- logDebug "Creating logical device" *>
    Vulkan.Utils.createDeviceFromRequirements swapchainDeviceRequirements [] physicalDevice deviceCreateInfo
  queue  <- Vulkan.getDeviceQueue device ( fromIntegral queueFamily ) 0

  pure ( VulkanContext { .. } )



vulkanInstanceInfo
  :: MonadVulkan m
  => ByteString
  -> m ( Vulkan.InstanceCreateInfo '[] )
vulkanInstanceInfo appName = do

  ( availableLayers :: Boxed.Vector Vulkan.LayerProperties ) <- snd <$> Vulkan.enumerateInstanceLayerProperties

  let
    validationLayer :: Maybe ValidationLayerName
    validationLayer
      = coerce
      . foldMap
        (  LayerProperties.layerName
        >>> \case
              "VK_LAYER_LUNARG_standard_validation" -> Just ( First LunarG  )
              "VK_LAYER_KHRONOS_validation"         -> Just ( First Khronos )
              _                                     -> Nothing
        )
      $ availableLayers

    enabledLayers :: [ ByteString ]
    enabledLayers = case validationLayer of
      Nothing      -> []
      Just LunarG  -> [ "VK_LAYER_LUNARG_standard_validation" ]
      Just Khronos -> [ "VK_LAYER_KHRONOS_validation" ]

    appInfo :: Vulkan.ApplicationInfo
    appInfo =
      Vulkan.ApplicationInfo
        { Vulkan.applicationName    = Just appName
        , Vulkan.applicationVersion = 0
        , Vulkan.engineName         = Nothing
        , Vulkan.engineVersion      = 0
        , Vulkan.apiVersion         = Vulkan.API_VERSION_1_2
        }

    createInfo :: Vulkan.InstanceCreateInfo '[]
    createInfo =
      Vulkan.InstanceCreateInfo
        { Vulkan.next                  = ()
        , Vulkan.flags                 = INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR
        , Vulkan.applicationInfo       = Just appInfo
        , Vulkan.enabledLayerNames     = Boxed.Vector.fromList enabledLayers
        , Vulkan.enabledExtensionNames = mempty
        }

  case validationLayer of
    Nothing -> logInfo "Validation layer unavailable. Is the Vulkan SDK installed?"
    Just _  -> logInfo ( "Enabled validation layers " <> Text.pack ( show enabledLayers ) )

  pure createInfo

createPhysicalDevice :: MonadVulkan m => Vulkan.Instance -> m Vulkan.PhysicalDevice
createPhysicalDevice vk = do
  physicalDevices <- snd <$> Vulkan.enumeratePhysicalDevices vk

  typedDevices <-
    for physicalDevices \ physicalDevice -> do
      properties <- Vulkan.getPhysicalDeviceProperties physicalDevice
      pure ( physicalDevice, Vulkan.deviceType properties )

  case Boxed.Vector.find ( isSuitableDeviceType . snd ) typedDevices of
    Nothing       -> error "Could not find a suitable physical device"
    Just ( d, _ ) -> pure d

  where
    isSuitableDeviceType :: Vulkan.PhysicalDeviceType -> Bool
    isSuitableDeviceType
      = flip elem
          [ Vulkan.PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
          , Vulkan.PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
          ]

findQueueFamilyIndex
  :: MonadIO m
  => Vulkan.PhysicalDevice
  -> Vulkan.QueueFlags
  -> m Word32
findQueueFamilyIndex physicalDevice requiredFlags = do
  queueFamilies <- Vulkan.getPhysicalDeviceQueueFamilyProperties physicalDevice
  let
    capableFamilyIndices :: Boxed.Vector Int
    capableFamilyIndices = ( `Boxed.Vector.imapMaybe` queueFamilies ) \ i queueFamily -> do
      guard ( Vulkan.queueFlags queueFamily .&. requiredFlags > Vulkan.zero )
      pure i
  case capableFamilyIndices Boxed.Vector.!? 0 of
    Nothing -> error "No queue family has sufficient capabilities"
    Just i  -> pure ( fromIntegral i )

instanceExtensions :: [ ByteString ] -> [ Vulkan.InstanceRequirement ]
instanceExtensions = map mkExtensionRequirement
  where
    mkExtensionRequirement :: ByteString -> Vulkan.InstanceRequirement
    mkExtensionRequirement extName =
      Vulkan.RequireInstanceExtension
        { Vulkan.instanceExtensionLayerName  = Nothing
        , Vulkan.instanceExtensionName       = extName
        , Vulkan.instanceExtensionMinVersion = 0
        }


initialiseWindow :: MonadVulkan m => WindowInfo -> m ( SDL.Window, [ ByteString ] )
initialiseWindow ( WindowInfo { height, width, windowName, mouseMode } ) = do
  logDebug "Initializing SDL"
  SDL.Raw.logSetAllPriority SDL.Raw.SDL_LOG_PRIORITY_VERBOSE
  SDL.initialize [ SDL.InitVideo ]
  void ( SDL.setMouseLocationMode mouseMode )
  window           <- logDebug "Creating SDL window"           *> createWindow width height windowName
  neededExtensions <- logDebug "Loading needed extensions"     *> SDL.Video.Vulkan.vkGetInstanceExtensions window
  extensionNames   <- traverse ( liftIO . fmap fromString . peekCString ) neededExtensions
  logInfo $ "Needed instance extensions are: " <> Text.intercalate ", " extensionNames
  pure ( window, map encodeUtf8 extensionNames )

data WindowInfo
  = WindowInfo
  { width      :: CInt
  , height     :: CInt
  , windowName :: Text
  , mouseMode  :: SDL.LocationMode
  }

createWindow :: MonadVulkan m => CInt -> CInt -> Text -> m SDL.Window
createWindow x y title =
  snd <$> ResourceT.allocate
    ( SDL.createWindow
              ( fromString ( Text.unpack title ) )
              SDL.defaultWindow
                { SDL.windowGraphicsContext = SDL.VulkanContext
                , SDL.windowInitialSize     = SDL.V2 x y
                , SDL.windowResizable       = True
                }
    )
    SDL.destroyWindow

createSurface
  :: MonadVulkan m
  => SDL.Window
  -> Vulkan.Instance
  -> m SDL.Video.Vulkan.VkSurfaceKHR
createSurface window vulkanInstance =
  snd <$> ResourceT.allocate
    ( SDL.Video.Vulkan.vkCreateSurface window ( castPtr $ Vulkan.instanceHandle vulkanInstance ) )
    ( \ surf -> Vulkan.destroySurfaceKHR vulkanInstance ( Vulkan.SurfaceKHR surf ) Nothing )

assertSurfacePresentable
  :: MonadIO m
  => Vulkan.PhysicalDevice
  -> Word32
  -> SDL.Video.Vulkan.VkSurfaceKHR
  -> m ()
assertSurfacePresentable physicalDevice queueFamilyIndex surface = do
  isPresentable <-
    Vulkan.getPhysicalDeviceSurfaceSupportKHR
      physicalDevice
      queueFamilyIndex
      ( Vulkan.SurfaceKHR surface )

  unless isPresentable ( error "Surface is not presentable" )


chooseSwapchainFormat
  :: MonadIO m
  => Vulkan.SurfaceFormatKHR
  -> Vulkan.PhysicalDevice
  -> SDL.Video.Vulkan.VkSurfaceKHR
  -> m Vulkan.SurfaceFormatKHR
chooseSwapchainFormat
  preferredFormat@( Vulkan.SurfaceFormatKHR fmt_p spc_p )
  physicalDevice
  surface
  = do
      surfaceFormats <- snd <$> Vulkan.getPhysicalDeviceSurfaceFormatsKHR physicalDevice ( Vulkan.SurfaceKHR surface )

      case sortOn ( Down . score ) ( Boxed.Vector.toList surfaceFormats ) of
        [] -> error "No formats found."
        Vulkan.SurfaceFormatKHR{format=Vulkan.FORMAT_UNDEFINED} : _rest ->
          pure preferredFormat
        best : _rest
          -> pure best

    where
      match :: Eq a => a -> a -> Int
      match a b
        | a == b    = 1
        | otherwise = 0

      score :: Vulkan.SurfaceFormatKHR -> Int
      score ( Vulkan.SurfaceFormatKHR fmt spc )
        = match fmt fmt_p
        + match spc spc_p

createSwapchain
  :: ( MonadIO m, MonadVulkan m )
  => Vulkan.PhysicalDevice
  -> Vulkan.Device
  -> SDL.Video.Vulkan.VkSurfaceKHR
  -> Vulkan.SurfaceFormatKHR
  -> Vulkan.ImageUsageFlags
  -> Word32
  -> Maybe Vulkan.SwapchainKHR
  -> m ( ResourceT.ReleaseKey, Vulkan.SwapchainKHR, Vulkan.Extent2D )
createSwapchain physicalDevice device surface surfaceFormat imageUsage imageCount oldSwapchain = do

  surfaceCapabilities <- Vulkan.getPhysicalDeviceSurfaceCapabilitiesKHR physicalDevice ( Vulkan.SurfaceKHR surface )

  let
    presentMode :: Vulkan.PresentModeKHR
    presentMode =
      Vulkan.PRESENT_MODE_FIFO_KHR -- run at presentation rate
      -- Vulkan.PRESENT_MODE_MAILBOX_KHR -- max-FPS alternative for benchmarks, input lag debugging, etc.

    Vulkan.SurfaceCapabilitiesKHR{currentExtent, currentTransform} = surfaceCapabilities
    Vulkan.SurfaceFormatKHR{format=fmt, colorSpace=csp} = surfaceFormat

    swapchainCreateInfo :: Vulkan.SwapchainCreateInfoKHR '[]
    swapchainCreateInfo =
      Vulkan.SwapchainCreateInfoKHR
        { Vulkan.next                  = ()
        , Vulkan.flags                 = Vulkan.zero
        , Vulkan.surface               = Vulkan.SurfaceKHR surface
        , Vulkan.minImageCount         = imageCount
        , Vulkan.imageFormat           = fmt
        , Vulkan.imageColorSpace       = csp
        , Vulkan.imageExtent           = currentExtent
        , Vulkan.imageArrayLayers      = 1
        , Vulkan.imageUsage            = imageUsage
        , Vulkan.imageSharingMode      = Vulkan.SHARING_MODE_EXCLUSIVE
        , Vulkan.queueFamilyIndices    = Boxed.Vector.empty
        , Vulkan.preTransform          = currentTransform
        , Vulkan.compositeAlpha        = Vulkan.COMPOSITE_ALPHA_OPAQUE_BIT_KHR
        , Vulkan.presentMode           = presentMode
        , Vulkan.clipped               = True
        , Vulkan.oldSwapchain          = fromMaybe Vulkan.NULL_HANDLE oldSwapchain
        }

  ( key, swapchain ) <- Vulkan.withSwapchainKHR device swapchainCreateInfo Nothing ResourceT.allocate
  pure ( key, swapchain, currentExtent )


simpleRenderPass
  :: MonadVulkan m
  => Vulkan.Device
  -> SubpassAttachments ( Vulkan.AttachmentDescription, AttachmentType )
  -> m ( ResourceT.ReleaseKey, Vulkan.RenderPass )
simpleRenderPass dev attachments = Vulkan.withRenderPass dev createInfo Nothing ResourceT.allocate
  where

    attachmentReferences   :: SubpassAttachmentReferences
    attachmentDescriptions :: [ Vulkan.AttachmentDescription ]
    ( attachmentReferences, attachmentDescriptions )
      = attachmentReferencesAndDescriptions attachments

    subpass :: Vulkan.SubpassDescription
    subpass = createSubpass attachmentReferences

    dependency1 :: Vulkan.SubpassDependency
    dependency1 =
      Vulkan.SubpassDependency
        { Vulkan.srcSubpass      = Vulkan.SUBPASS_EXTERNAL
        , Vulkan.dstSubpass      = Vulkan.zero
        , Vulkan.srcStageMask    = Vulkan.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
        , Vulkan.srcAccessMask   = Vulkan.zero
        , Vulkan.dstStageMask    = Vulkan.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
        , Vulkan.dstAccessMask   = Vulkan.ACCESS_COLOR_ATTACHMENT_READ_BIT
                               .|. Vulkan.ACCESS_COLOR_ATTACHMENT_WRITE_BIT
        , Vulkan.dependencyFlags = Vulkan.zero
        }

    dependency2 :: Vulkan.SubpassDependency
    dependency2 =
      Vulkan.SubpassDependency
        { Vulkan.srcSubpass      = Vulkan.zero
        , Vulkan.dstSubpass      = Vulkan.SUBPASS_EXTERNAL
        , Vulkan.srcStageMask    = Vulkan.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
        , Vulkan.srcAccessMask   = Vulkan.ACCESS_COLOR_ATTACHMENT_READ_BIT
                               .|. Vulkan.ACCESS_COLOR_ATTACHMENT_WRITE_BIT
        , Vulkan.dstStageMask    = Vulkan.PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT
        , Vulkan.dstAccessMask   = Vulkan.zero
        , Vulkan.dependencyFlags = Vulkan.zero
        }

    createInfo :: Vulkan.RenderPassCreateInfo '[]
    createInfo =
      Vulkan.RenderPassCreateInfo
        { Vulkan.next         = ()
        , Vulkan.flags        = Vulkan.zero
        , Vulkan.attachments  = Boxed.Vector.fromList attachmentDescriptions
        , Vulkan.subpasses    = Boxed.Vector.singleton subpass
        , Vulkan.dependencies = Boxed.Vector.fromList [ dependency1, dependency2 ]
        }

cmdBeginRenderPass
  :: MonadIO m
  => Vulkan.CommandBuffer
  -> Vulkan.RenderPass
  -> Vulkan.Framebuffer
  -> [ Vulkan.ClearValue ]
  -> Vulkan.Extent2D
  -> m ()
cmdBeginRenderPass commandBuffer renderPass framebuffer clearValues extent =
  let
    zeroZero :: Vulkan.Offset2D
    zeroZero =
      Vulkan.Offset2D
        { Vulkan.x = 0
        , Vulkan.y = 0
        }

    renderArea :: Vulkan.Rect2D
    renderArea =
      Vulkan.Rect2D
        { Vulkan.offset = zeroZero
        , Vulkan.extent = extent
        }

    beginInfo :: Vulkan.RenderPassBeginInfo '[]
    beginInfo =
      Vulkan.RenderPassBeginInfo
        { Vulkan.next        = ()
        , Vulkan.renderPass  = renderPass
        , Vulkan.framebuffer = framebuffer
        , Vulkan.renderArea  = renderArea
        , Vulkan.clearValues = Boxed.Vector.fromList clearValues
        }
  in
    Vulkan.cmdBeginRenderPass
      commandBuffer
      beginInfo
      Vulkan.SUBPASS_CONTENTS_INLINE

cmdNextSubpass :: MonadIO m => Vulkan.CommandBuffer -> m ()
cmdNextSubpass commandBuffer = Vulkan.cmdNextSubpass commandBuffer Vulkan.SUBPASS_CONTENTS_INLINE

cmdEndRenderPass :: MonadIO m => Vulkan.CommandBuffer -> m ()
cmdEndRenderPass = Vulkan.cmdEndRenderPass

createImageView
  :: MonadVulkan m
  => Vulkan.Device
  -> Vulkan.Image
  -> Vulkan.ImageViewType
  -> Vulkan.Format
  -> Vulkan.ImageAspectFlags
  -> m ( ResourceT.ReleaseKey, Vulkan.ImageView )
createImageView dev image viewType fmt aspect = Vulkan.withImageView dev createInfo Nothing ResourceT.allocate
  where
    components :: Vulkan.ComponentMapping
    components =
      Vulkan.ComponentMapping
        { Vulkan.r = Vulkan.COMPONENT_SWIZZLE_IDENTITY
        , Vulkan.g = Vulkan.COMPONENT_SWIZZLE_IDENTITY
        , Vulkan.b = Vulkan.COMPONENT_SWIZZLE_IDENTITY
        , Vulkan.a = Vulkan.COMPONENT_SWIZZLE_IDENTITY
        }

    subResourceRange :: Vulkan.ImageSubresourceRange
    subResourceRange =
      Vulkan.ImageSubresourceRange
        { Vulkan.aspectMask     = aspect
        , Vulkan.baseMipLevel   = 0
        , Vulkan.levelCount     = 1
        , Vulkan.baseArrayLayer = 0
        , Vulkan.layerCount     = 1
        }

    createInfo :: Vulkan.ImageViewCreateInfo '[]
    createInfo =
      Vulkan.ImageViewCreateInfo
        { Vulkan.next             = ()
        , Vulkan.flags            = Vulkan.zero
        , Vulkan.image            = image
        , Vulkan.viewType         = viewType
        , Vulkan.format           = fmt
        , Vulkan.components       = components
        , Vulkan.subresourceRange = subResourceRange
        }

createFramebuffer
  :: ( MonadVulkan m, Foldable f )
  => Vulkan.Device
  -> Vulkan.RenderPass
  -> Vulkan.Extent2D
  -> f Vulkan.ImageView
  -> m ( ResourceT.ReleaseKey, Vulkan.Framebuffer )
createFramebuffer dev renderPass Vulkan.Extent2D{width, height} attachments = Vulkan.withFramebuffer dev createInfo Nothing ResourceT.allocate
  where
    createInfo :: Vulkan.FramebufferCreateInfo '[]
    createInfo =
      Vulkan.FramebufferCreateInfo
        { Vulkan.next        = ()
        , Vulkan.flags       = Vulkan.zero
        , Vulkan.renderPass  = renderPass
        , Vulkan.attachments = Boxed.Vector.fromList . toList $ attachments
        , Vulkan.width       = width
        , Vulkan.height      = height
        , Vulkan.layers      = 1
        }

createDescriptorPool
  :: MonadVulkan m
  => Vulkan.Device
  -> Int
  -> [ ( Vulkan.DescriptorType, Int ) ]
  -> m ( ResourceT.ReleaseKey, Vulkan.DescriptorPool )
createDescriptorPool device maxSets descTypes = Vulkan.withDescriptorPool device createInfo Nothing ResourceT.allocate

    where
      poolSizes :: [ Vulkan.DescriptorPoolSize ]
      poolSizes =
        counts descTypes <&> \ ( descType, descCount ) ->
          Vulkan.DescriptorPoolSize
          { Vulkan.type'           = descType
          , Vulkan.descriptorCount = fromIntegral $ maxSets * descCount
          }
      createInfo :: Vulkan.DescriptorPoolCreateInfo '[]
      createInfo =
        Vulkan.DescriptorPoolCreateInfo
          { Vulkan.next      = ()
          , Vulkan.flags     = Vulkan.DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
          , Vulkan.poolSizes = Boxed.Vector.fromList poolSizes
          , Vulkan.maxSets   = fromIntegral maxSets
          }

counts :: ( Ord a, Num i ) => [ ( a, i ) ] -> [ ( a, i ) ]
counts = Map.toList . foldr ( uncurry $ Map.insertWith (+) ) Map.empty

createDescriptorSetLayout
  :: MonadVulkan m
  => Vulkan.Device
  -> [ ( Vulkan.DescriptorType, Vulkan.ShaderStageFlags ) ]
  -> m ( ResourceT.ReleaseKey, Vulkan.DescriptorSetLayout )
createDescriptorSetLayout device descriptorTypes = Vulkan.withDescriptorSetLayout device createInfo Nothing ResourceT.allocate

      where
        bindings :: Boxed.Vector Vulkan.DescriptorSetLayoutBinding
        bindings = ( `Boxed.Vector.imap` Boxed.Vector.fromList descriptorTypes ) \ i ( descType, descStageFlags ) ->
          Vulkan.DescriptorSetLayoutBinding
            { Vulkan.binding           = fromIntegral i
            , Vulkan.descriptorType    = descType
            , Vulkan.descriptorCount   = 1
            , Vulkan.stageFlags        = descStageFlags
            , Vulkan.immutableSamplers = Boxed.Vector.empty
            }
        createInfo :: Vulkan.DescriptorSetLayoutCreateInfo '[]
        createInfo =
          Vulkan.DescriptorSetLayoutCreateInfo
            { Vulkan.next     = ()
            , Vulkan.flags    = Vulkan.zero
            , Vulkan.bindings = bindings
            }

createCommandPool
  :: MonadVulkan m
  => Vulkan.Device
  -> Vulkan.CommandPoolCreateFlagBits
  -> Word32
  -> m Vulkan.CommandPool
createCommandPool dev flags queueFamilyIndex = snd <$> Vulkan.withCommandPool dev createInfo Nothing ResourceT.allocate
  where
    createInfo :: Vulkan.CommandPoolCreateInfo
    createInfo =
      Vulkan.CommandPoolCreateInfo
        { Vulkan.flags            = flags
        , Vulkan.queueFamilyIndex = queueFamilyIndex
        }

allocatePrimaryCommandBuffers
  :: MonadVulkan m
  => Vulkan.Device
  -> Vulkan.CommandPool
  -> Word32
  -> m ( ResourceT.ReleaseKey, Boxed.Vector Vulkan.CommandBuffer )
allocatePrimaryCommandBuffers dev commandPool count = Vulkan.withCommandBuffers dev allocInfo ResourceT.allocate
    where
      allocInfo :: Vulkan.CommandBufferAllocateInfo
      allocInfo =
        Vulkan.CommandBufferAllocateInfo
          { Vulkan.commandPool        = commandPool
          , Vulkan.level              = Vulkan.COMMAND_BUFFER_LEVEL_PRIMARY
          , Vulkan.commandBufferCount = count
          }

submitCommandBuffer
  :: MonadIO m
  => Vulkan.Queue
  -> Vulkan.CommandBuffer
  -> [ ( Vulkan.Semaphore, Vulkan.PipelineStageFlags ) ]
  -> [ Vulkan.Semaphore ]
  -> Maybe Vulkan.Fence
  -> m ()
submitCommandBuffer queue commandBuffer wait signal mbFence =
  Vulkan.queueSubmit queue ( Boxed.Vector.singleton $ Vulkan.SomeStruct submitInfo ) ( fromMaybe Vulkan.NULL_HANDLE mbFence )
    where
      submitInfo :: Vulkan.SubmitInfo '[]
      submitInfo =
        Vulkan.SubmitInfo
          { Vulkan.next             = ()
          , Vulkan.waitSemaphores   = Boxed.Vector.fromList $ map fst wait
          , Vulkan.waitDstStageMask = Boxed.Vector.fromList $ map snd wait
          , Vulkan.commandBuffers   = Boxed.Vector.singleton ( Vulkan.commandBufferHandle commandBuffer )
          , Vulkan.signalSemaphores = Boxed.Vector.fromList signal
          }

beginCommandBuffer :: MonadIO m => Vulkan.CommandBuffer -> m ()
beginCommandBuffer commandBuffer = Vulkan.beginCommandBuffer commandBuffer commandBufferBeginInfo
  where
    commandBufferBeginInfo :: Vulkan.CommandBufferBeginInfo '[]
    commandBufferBeginInfo =
      Vulkan.CommandBufferBeginInfo
        { Vulkan.next            = ()
        , Vulkan.flags           = Vulkan.zero
        , Vulkan.inheritanceInfo = Nothing
        }

endCommandBuffer :: MonadIO m => Vulkan.CommandBuffer -> m ()
endCommandBuffer = Vulkan.endCommandBuffer

createFence :: MonadVulkan m => Vulkan.Device -> m ( ResourceT.ReleaseKey, Vulkan.Fence )
createFence device = Vulkan.withFence device fenceCreateInfo Nothing ResourceT.allocate
  where
    fenceCreateInfo :: Vulkan.FenceCreateInfo '[]
    fenceCreateInfo =
      Vulkan.FenceCreateInfo
        { Vulkan.next  = ()
        , Vulkan.flags = Vulkan.zero
        }

data Wait a = WaitAll [a] | WaitAny [a]

waitForFences :: MonadIO m => Vulkan.Device -> Wait Vulkan.Fence -> m ()
waitForFences device fences = void $ Vulkan.waitForFences device ( Boxed.Vector.fromList fenceList ) waitAll maxBound
  where
    waitAll   :: Bool
    fenceList :: [Vulkan.Fence]
    (waitAll, fenceList) =
      case fences of
        WaitAll l -> ( True , l )
        WaitAny l -> ( False, l )

createPipelineLayout
  :: MonadVulkan m
  => Vulkan.Device
  -> [ Vulkan.DescriptorSetLayout ]
  -> [ Vulkan.PushConstantRange ]
  -> m ( ResourceT.ReleaseKey, Vulkan.PipelineLayout )
createPipelineLayout device layouts ranges =
  Vulkan.withPipelineLayout device pipelineLayoutCreateInfo Nothing ResourceT.allocate
    where
      pipelineLayoutCreateInfo :: Vulkan.PipelineLayoutCreateInfo
      pipelineLayoutCreateInfo =
        Vulkan.PipelineLayoutCreateInfo
          { Vulkan.flags              = Vulkan.zero
          , Vulkan.setLayouts         = Boxed.Vector.fromList layouts
          , Vulkan.pushConstantRanges = Boxed.Vector.fromList ranges
          }

present
  :: MonadIO m
  => Vulkan.Queue
  -> Vulkan.SwapchainKHR
  -> Word32
  -> [Vulkan.Semaphore]
  -> m Vulkan.Result
present queue swapchain imageIndex wait = Vulkan.queuePresentKHR queue presentInfo
  where
    presentInfo :: Vulkan.PresentInfoKHR '[]
    presentInfo =
      Vulkan.PresentInfoKHR
        { Vulkan.next           = ()
        , Vulkan.waitSemaphores = Boxed.Vector.fromList wait
        , Vulkan.swapchains     = Boxed.Vector.singleton swapchain
        , Vulkan.imageIndices   = Boxed.Vector.singleton imageIndex
        , Vulkan.results        = Vulkan.zero
        }
