{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

{-|
Module: DearImGui.Vulkan

Vulkan backend for Dear ImGui.
-}

module DearImGui.Vulkan
  ( InitInfo(..)
  , withVulkan
  , vulkanInit
  , vulkanShutdown
  , vulkanNewFrame
  , vulkanRenderDrawData
  , vulkanCreateFontsTexture
  , vulkanDestroyFontsTexture
  , vulkanSetMinImageCount

  , vulkanAddTexture
  )
  where

-- base
import Data.Maybe
  ( fromMaybe )
import Data.Either
  ( isRight )
import Data.Word
  ( Word32 )
import Foreign.Marshal.Alloc
  ( alloca )
import Foreign.Marshal.Utils
  ( fromBool )
import Foreign.Ptr
  ( FunPtr, Ptr, freeHaskellFunPtr, nullPtr )
import Foreign.Storable
  ( poke )

-- inline-c
import qualified Language.C.Inline as C

-- inline-c-cpp
import qualified Language.C.Inline.Cpp as Cpp

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

-- unliftio
import UnliftIO
  ( MonadUnliftIO )
import UnliftIO.Exception
  ( bracket )

-- vulkan
import qualified Vulkan
import Vulkan.Zero
  ( zero )

-- DearImGui
import DearImGui
  ( DrawData(..) )
import DearImGui.Vulkan.Types
  ( vulkanCtx )

C.context ( Cpp.cppCtx <> C.funCtx <> vulkanCtx )
C.include "string.h"
C.include "imgui.h"
C.include "backends/imgui_impl_vulkan.h"
Cpp.using "namespace ImGui"


data InitInfo =
  InitInfo
  { instance'      :: !Vulkan.Instance
  , physicalDevice :: !Vulkan.PhysicalDevice
  , device         :: !Vulkan.Device
  , queueFamily    :: !Word32
  , queue          :: !Vulkan.Queue
  , pipelineCache  :: !Vulkan.PipelineCache
  , descriptorPool :: !Vulkan.DescriptorPool
  , subpass        :: !Word32
  , minImageCount  :: !Word32
  , imageCount     :: !Word32
  , msaaSamples    :: !Vulkan.SampleCountFlagBits
  , rendering      :: Either Vulkan.RenderPass Vulkan.PipelineRenderingCreateInfo
  , mbAllocator    :: Maybe Vulkan.AllocationCallbacks
  , checkResult    :: Vulkan.Result -> IO ()
  }

-- | Wraps @ImGui_ImplVulkan_Init@ and @ImGui_ImplVulkan_Shutdown@.
withVulkan :: MonadUnliftIO m => InitInfo -> ( Bool -> m a ) -> m a
withVulkan initInfo action =
  bracket
    ( vulkanInit initInfo )
    vulkanShutdown
    ( \ ( _, initResult ) -> action initResult )

-- | Wraps @ImGui_ImplVulkan_Init@.
--
-- Use 'vulkanShutdown' to clean up on shutdown.
-- Prefer using 'withVulkan' when possible, as it automatically handles cleanup.
vulkanInit :: MonadIO m => InitInfo -> m (FunPtr (Vulkan.Result -> IO ()), Bool)
vulkanInit ( InitInfo {..} ) = liftIO do
  let
    instancePtr :: Ptr Vulkan.Instance_T
    instancePtr = Vulkan.instanceHandle instance'
    physicalDevicePtr :: Ptr Vulkan.PhysicalDevice_T
    physicalDevicePtr = Vulkan.physicalDeviceHandle physicalDevice
    devicePtr :: Ptr Vulkan.Device_T
    devicePtr = Vulkan.deviceHandle device
    queuePtr :: Ptr Vulkan.Queue_T
    queuePtr = Vulkan.queueHandle queue
    withCallbacks :: ( Ptr Vulkan.AllocationCallbacks -> IO a ) -> IO a
    withCallbacks f = case mbAllocator of
      Nothing        -> f nullPtr
      Just callbacks -> alloca ( \ ptr -> poke ptr callbacks *> f ptr )
    useDynamicRendering' :: Cpp.CBool
    useDynamicRendering' = fromBool (isRight rendering)
    renderPass :: Vulkan.RenderPass
    renderPass = either id (const zero) rendering
  Vulkan.withCStruct (either (const zero) id rendering) \pipelineRenderingCIPtr -> do
    checkResultFunPtr <- $( C.mkFunPtr [t| Vulkan.Result -> IO () |] ) checkResult
    initResult <- withCallbacks \ callbacksPtr -> do
        [C.block| bool {
          ImGui_ImplVulkan_InitInfo initInfo = {0,};

          #ifdef IMGUI_IMPL_VULKAN_HAS_DYNAMIC_RENDERING
          VkPipelineRenderingCreateInfoKHR pipelineRenderingCI;
          memset(&pipelineRenderingCI, 0, sizeof(VkPipelineRenderingCreateInfoKHR));
          #endif

          VkInstance instance = { $( VkInstance_T* instancePtr ) };
          initInfo.Instance = instance;
          VkPhysicalDevice physicalDevice = { $( VkPhysicalDevice_T* physicalDevicePtr ) };
          initInfo.PhysicalDevice = physicalDevice;
          VkDevice device = { $( VkDevice_T* devicePtr ) };
          initInfo.Device = device;
          initInfo.QueueFamily = $(uint32_t queueFamily);
          VkQueue queue = { $( VkQueue_T* queuePtr ) };
          initInfo.Queue = queue;
          initInfo.PipelineCache = $(VkPipelineCache pipelineCache);
          initInfo.DescriptorPool = $(VkDescriptorPool descriptorPool);
          initInfo.Subpass = $(uint32_t subpass);
          initInfo.MinImageCount = $(uint32_t minImageCount);
          initInfo.ImageCount = $(uint32_t imageCount);
          initInfo.MSAASamples = $(VkSampleCountFlagBits msaaSamples);
          initInfo.Allocator = $(VkAllocationCallbacks* callbacksPtr);
          initInfo.CheckVkResultFn = $( void (*checkResultFunPtr)(VkResult) );

          initInfo.UseDynamicRendering = $(bool useDynamicRendering');
          initInfo.RenderPass = $(VkRenderPass renderPass);
          if ($(VkPipelineRenderingCreateInfo* pipelineRenderingCIPtr))
            memcpy(&initInfo.PipelineRenderingCreateInfo, $(VkPipelineRenderingCreateInfo* pipelineRenderingCIPtr), sizeof(VkPipelineRenderingCreateInfo));
          return ImGui_ImplVulkan_Init(&initInfo);
        }|]
    pure ( checkResultFunPtr, initResult /= 0 )

-- | Wraps @ImGui_ImplVulkan_Shutdown@.
--
-- Counterpart to 'vulkanInit', for clean-up.
vulkanShutdown :: MonadIO m => (FunPtr a, b) -> m ()
vulkanShutdown ( checkResultFunPtr, _ ) = liftIO do
  [C.exp| void { ImGui_ImplVulkan_Shutdown(); } |]
  freeHaskellFunPtr checkResultFunPtr

-- | Wraps @ImGui_ImplVulkan_NewFrame@.
vulkanNewFrame :: MonadIO m => m ()
vulkanNewFrame = liftIO do
  [C.exp| void { ImGui_ImplVulkan_NewFrame(); } |]

-- | Wraps @ImGui_ImplVulkan_RenderDrawData@.
vulkanRenderDrawData :: MonadIO m => DrawData -> Vulkan.CommandBuffer -> Maybe Vulkan.Pipeline -> m ()
vulkanRenderDrawData (DrawData dataPtr) commandBuffer mbPipeline = liftIO do
  let
    commandBufferPtr :: Ptr Vulkan.CommandBuffer_T
    commandBufferPtr = Vulkan.commandBufferHandle commandBuffer
    pipeline :: Vulkan.Pipeline
    pipeline = fromMaybe Vulkan.NULL_HANDLE mbPipeline
  [C.block| void {
    VkCommandBuffer commandBuffer = { $( VkCommandBuffer_T* commandBufferPtr ) };
    ImGui_ImplVulkan_RenderDrawData((ImDrawData*) $(void* dataPtr), commandBuffer, $(VkPipeline pipeline));
  }|]

-- | Wraps @ImGui_ImplVulkan_CreateFontsTexture@.
vulkanCreateFontsTexture :: MonadIO m => m Bool
vulkanCreateFontsTexture = liftIO do
  res <-
    [C.block| bool {
      return ImGui_ImplVulkan_CreateFontsTexture();
    }|]
  pure ( res /= 0 )

-- | You probably never need to call this, as it is called by ImGui_ImplVulkan_CreateFontsTexture() and ImGui_ImplVulkan_Shutdown().
-- | Wraps @ImGui_ImplVulkan_DestroyFontsTexture@.
vulkanDestroyFontsTexture :: MonadIO m => m ()
vulkanDestroyFontsTexture = liftIO do
  [C.block| void {
    return ImGui_ImplVulkan_DestroyFontsTexture();
  }|]

-- | Wraps @ImGui_ImplVulkan_SetMinImageCount@.
vulkanSetMinImageCount :: MonadIO m => Word32 -> m ()
vulkanSetMinImageCount minImageCount = liftIO do
  [C.exp| void { ImGui_ImplVulkan_SetMinImageCount($(uint32_t minImageCount)); } |]

-- | Wraps @ImGui_ImplVulkan_AddTexture@.
vulkanAddTexture :: MonadIO m => Vulkan.Sampler -> Vulkan.ImageView -> Vulkan.ImageLayout -> m Vulkan.DescriptorSet
vulkanAddTexture sampler imageView imageLayout = liftIO do
  [C.block|
    VkDescriptorSet {
      return ImGui_ImplVulkan_AddTexture(
        $(VkSampler sampler),
        $(VkImageView imageView),
        $(VkImageLayout imageLayout)
      );
    }
  |]
