#!/bin/sh -e

# Variables
BASE_INCLUDE=$( realpath $( dirname $0 ) )"/include"
WIN_INCLUDE=$( realpath $( dirname $0 ) )"/include/winapi"
OUT_BIN=vulkan
OUTFILE=$OUT_BIN.nim
OUTFILE_FUTHARK=$OUT_BIN".gen.nim"

# Convert C-headers to Nim using Futhark, if not existing yet
NIM_GEN_CODE='import futhark, os
importc:
  outputPath "'"$OUTFILE_FUTHARK"'"
  define VK_USE_PLATFORM_WIN32_KHR
  define VK_USE_PLATFORM_XLIB_KHR
  sysPath "'"$BASE_INCLUDE"'"
  sysPath "'"$WIN_INCLUDE"'"
  path "/usr/include/vulkan"
  "vulkan.h"'

if [ ! -f $OUTFILE_FUTHARK ] ; then
        nim c --maxLoopIterationsVM:1000000000 --eval:"$NIM_GEN_CODE"
fi

cp $OUTFILE_FUTHARK $OUTFILE

# convert futhark's function definition to function pointers to allow loading functions at runtime
sed -i 's/^  proc \([a-zA-Z]*\*\)/  var \1: proc/' $OUTFILE
sed -i 's/[, ] importc: "vk[a-zA-Z]*"//' $OUTFILE

# ensure struct-name-definitions are on single line
# needed for correct parsing and setting of sType struct values in the next step
sed -i '/struct_Vk.*inheritable,$/{N;s/\n/ /}' $OUTFILE
sed -i '/struct_Vk.*pure,$/{N;s/\n/ /}' $OUTFILE
sed -i '/struct_Vk.* {\.$/{N;s/\n/ /}' $OUTFILE

# set default struct values for member "sType"
# not very clean, as we "abuse" the fact that Nim does not differentiate between camel-case and snake-case for identifiers
awk -i inplace '{
        if ( $0 ~ /^  struct_Vk.* = object/ && ! $0 ~ /VkBaseInStructure/ ) {
                split($0, arr, "_");
                print $0;
                getline;
                if ( $0 ~ / sType\*:/ ) {
                        split($0, arr2, "##");
                        print arr2[1] "= VK_STRUCTURE_TYPE_" substr(arr[2], 3) " ##" arr2[2];
                } else {
                        print;
                }
        } else {
                print;
        }
}' $OUTFILE

# allow to printing Vulkan handles with Nim, as those are using pointer-types
cat $OUTFILE_FUTHARK | awk '/ = ptr struct_/ {print "proc `$`*(val: " $1 "): string = $(cast[int](val))"}' >> $OUTFILE

# add some helper functions, the vulkan loader and instance creation
cat <<EOF >> $OUTFILE

import std/dynlib
import std/strutils
import std/logging

var vkInstance*: VkInstance = VkInstance(nil)
var vkPhysicalDevices: seq[VkPhysicalDevice]

template checkVkResult*(call: untyped) =
  when defined(release):
    discard call
  else:
    # yes, a bit cheap, but this is only for nice debug output
    var callstr = astToStr(call).replace("\n", "")
    while callstr.find("  ") >= 0:
      callstr = callstr.replace("  ", " ")
    debug "Calling vulkan: ", callstr
    let value = call
    if value != VK_SUCCESS:
      error "Vulkan error: ", astToStr(call), " returned ", \$value
      raise newException(
        Exception, "Vulkan error: " & astToStr(call) & " returned " & \$value
      )

proc loadFunc[T](instance: VkInstance, f: var T, name: string) =
  f = cast[T](vkGetInstanceProcAddr(instance, name))

proc initVulkan*() =
  if vkGetInstanceProcAddr != nil:
    return

  when defined(linux):
    let vulkanLib = loadLib("libvulkan.so.1")
  when defined(windows):
    let vulkanLib = loadLib("vulkan-1.dll")
  if vulkanLib == nil:
    raise newException(Exception, "Unable to load vulkan library")

  # load function-pointer resolver function
  vkGetInstanceProcAddr = cast[typeof(vkGetInstanceProcAddr)](checkedSymAddr(vulkanLib, "vkGetInstanceProcAddr"))

  # need to create an instance before loading other function points
  loadFunc(vkInstance, vkCreateInstance, "vkCreateInstance")
  let createInfo = VkInstanceCreateInfo(
    pNext: nil,
    flags: 0,
    pApplicationInfo: nil,
    enabledLayerCount: 0,
    ppEnabledLayerNames: nil,
    enabledExtensionCount: 0,
    ppEnabledExtensionNames: nil,
  )
  checkVkResult vkCreateInstance(addr createInfo, nil, addr vkInstance)

  # load all functions (some might be null, not checking here)
EOF

cat vulkan.nim | grep -o '^  var vk[a-zA-Z]*'  | grep -v vkInstance | grep -v vkGetInstanceProcAddr | grep -v vkCreateInstance | awk '{print "  loadFunc(vkInstance, " $2 ", \"" $2 "\")"}' >> $OUTFILE

cat <<EOF >> $OUTFILE
  
  # load and print all found devices
  var nPhysicalDevices: uint32
  checkVkResult vkEnumeratePhysicalDevices(vkInstance, addr nPhysicalDevices, nil)

  if nPhysicalDevices > 0:
    vkPhysicalDevices.setLen(nPhysicalDevices)
    checkVkResult vkEnumeratePhysicalDevices(vkInstance, addr nPhysicalDevices, addr vkPhysicalDevices[0])
    vkPhysicalDevices.setLen(nPhysicalDevices)
  echo "physical devices: ", vkPhysicalDevices

proc destroyVulkan*() =
  vkDestroyInstance(vkInstance, nil)

initVulkan()
EOF

nim c --run ./$OUTFILE && rm ./$OUT_BIN
