// Time measurements
#include <chrono>
// String formatting
#include <fmt/format.h>
// Unordered sets
#include <unordered_set>

// Make depth go from 0 to 1 instead of -1 to 1
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
// Provides various math-related data structures
#include <glm/glm.hpp>

// Used to load models
#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>

// Dynamic function loading
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
// Use the beta extensions
#define VK_ENABLE_BETA_EXTENSIONS
// Use {} instead of ()
#define VULKAN_HPP_NO_CONSTRUCTORS
// Vulkan itself
#include <vulkan/vulkan.hpp>

// Needed for dynamic function loading to work
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE

// Type aliases
#include "util/types.h"

// STB image helper
#include "util/unique_image.h"

// Vertex class
#include "util/vertex.h"

// ImGui
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h>

// Use {} instead of ()
#define VKFW_NO_STRUCT_CONSTRUCTORS
// GLFW C++ wrapper
#include "vkfw.hpp"

// Initial width and height
constexpr i32 WIDTH  = 800;
constexpr i32 HEIGHT = 600;

// Paths for the model and texture to render
constexpr const char* MODEL_PATH   = "models/viking_room.obj";
constexpr const char* TEXTURE_PATH = "textures/viking_room.png";

// Paths for the shaders
constexpr const char* FRAGMENT_SHADER_PATH = "shaders/frag.spv";
constexpr const char* VERTEX_SHADER_PATH   = "shaders/vert.spv";

// Maximum number of frames to be worked on at the same time
constexpr i32 MAX_FRAMES_IN_FLIGHT = 2;

// Enable validation layers only in debug mode
#ifndef NDEBUG
constexpr std::array<const char*, 1> validationLayers = { "VK_LAYER_KHRONOS_validation" };
#endif

// macOS requires the portability extension to be enabled
#ifdef __APPLE__
constexpr std::array<const char*, 2> deviceExtensions = { vk::KHRSwapchainExtensionName,
                                                          vk::KHRPortabilitySubsetExtensionName };
#else
constexpr std::array<const char*, 1> deviceExtensions = { vk::KHRSwapchainExtensionName };
#endif

class VulkanApp {
 public:
  fn run() -> void {
    // Create window
    initWindow();
    // Setup vulkan
    initVulkan();
    // Render loop
    mainLoop();

    cleanupSwapChain();

    ImGui_ImplVulkan_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();
  }

 private:
  // Instances
  vkfw::UniqueInstance mVKFWInstance; // Unique handle to the GLFW instance
  vkfw::UniqueWindow   mWindow;       // Unique handle to the GLFW window
  vk::UniqueInstance   mInstance;     // Unique handle to the Vulkan instance

  // Debug messenger for Vulkan validation layers
  vk::UniqueDebugUtilsMessengerEXT mDebugMessenger; // Unique handle to the debug messenger

  // Surface for rendering
  vk::UniqueSurfaceKHR mSurface; // Unique handle to the Vulkan surface

  // Physical device and logical device
  vk::PhysicalDevice      mPhysicalDevice; // Handle to the selected physical device (GPU)
  vk::SampleCountFlagBits mMsaaSamples;    // Multisample anti-aliasing (MSAA) sample count
  vk::UniqueDevice        mDevice;         // Unique handle to the logical device

  // Vulkan queues for graphics and presentation
  vk::Queue mGraphicsQueue; // Handle to the graphics queue
  vk::Queue mPresentQueue;  // Handle to the presentation queue

  // Swapchain and related resources
  vk::UniqueSwapchainKHR             mSwapChain;             // The swapchain handle
  std::vector<vk::Image>             mSwapChainImages;       // Images in the swapchain
  vk::Format                         mSwapChainImageFormat;  // Format of the swapchain images
  vk::Extent2D                       mSwapChainExtent;       // Dimensions of the swapchain images
  std::vector<vk::UniqueImageView>   mSwapChainImageViews;   // Image views for the swapchain images
  std::vector<vk::UniqueFramebuffer> mSwapChainFramebuffers; // Framebuffers for the swapchain images

  // Render pass and pipeline configurations
  vk::UniqueRenderPass          mRenderPass;          // Render pass configuration
  vk::UniqueDescriptorSetLayout mDescriptorSetLayout; // Descriptor set layout
  vk::UniquePipelineLayout      mPipelineLayout;      // Pipeline layout
  vk::UniquePipeline            mGraphicsPipeline;    // Graphics pipeline

  // Command pool for allocating command buffers
  vk::UniqueCommandPool mCommandPool;

  // Color image resources
  vk::UniqueImage        mColorImage;       // Color image
  vk::UniqueDeviceMemory mColorImageMemory; // Memory for the color image
  vk::UniqueImageView    mColorImageView;   // Image view for the color image

  // Depth image resources
  vk::UniqueImage        mDepthImage;       // Depth image
  vk::UniqueDeviceMemory mDepthImageMemory; // Memory for the depth image
  vk::UniqueImageView    mDepthImageView;   // Image view for the depth image

  // Texture resources
  u32                    mMipLevels;          // Number of mipmap levels
  vk::UniqueImage        mTextureImage;       // Texture image
  vk::UniqueDeviceMemory mTextureImageMemory; // Memory for the texture image
  vk::UniqueImageView    mTextureImageView;   // Image view for the texture image
  vk::UniqueSampler      mTextureSampler;     // Sampler for the texture

  // Vertex and index buffers
  std::vector<Vertex>    mVertices;           // Vertex data
  std::vector<u32>       mIndices;            // Index data
  vk::UniqueBuffer       mVertexBuffer;       // Vertex buffer
  vk::UniqueDeviceMemory mVertexBufferMemory; // Memory for the vertex buffer
  vk::UniqueBuffer       mIndexBuffer;        // Index buffer
  vk::UniqueDeviceMemory mIndexBufferMemory;  // Memory for the index buffer

  // Uniform buffers
  std::vector<vk::UniqueBuffer>       mUniformBuffers;       // Uniform buffers
  std::vector<vk::UniqueDeviceMemory> mUniformBuffersMemory; // Memory for the uniform buffers
  std::vector<void*>                  mUniformBuffersMapped; // Mapped pointers for the uniform buffers

  // Descriptor pool and sets
  vk::UniqueDescriptorPool       mDescriptorPool; // Descriptor pool
  std::vector<vk::DescriptorSet> mDescriptorSets; // Descriptor sets

  // Command buffers
  std::vector<vk::UniqueCommandBuffer> mCommandBuffers; // Command buffers

  // Synchronization primitives
  std::vector<vk::UniqueSemaphore> mImageAvailableSemaphores; // Semaphores for image availability
  std::vector<vk::UniqueSemaphore> mRenderFinishedSemaphores; // Semaphores for render completion
  std::vector<vk::UniqueFence>     mInFlightFences;           // Fences for in-flight frames

  // Framebuffer resize flag and current frame index
  bool mFramebufferResized = false; // Flag indicating if the framebuffer was resized
  u32  mCurrentFrame       = 0;     // Index of the current frame

  // Struct to hold queue family indices
  struct QueueFamilyIndices {
    std::optional<u32> graphics_family; // Index of the graphics queue family
    std::optional<u32> present_family;  // Index of the presentation queue family

    // Check if both queue families are available
    fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); }
  };

  // Struct to hold swapchain support details
  struct SwapChainSupportDetails {
    vk::SurfaceCapabilitiesKHR        capabilities;  // Surface capabilities
    std::vector<vk::SurfaceFormatKHR> formats;       // Supported surface formats
    std::vector<vk::PresentModeKHR>   present_modes; // Supported presentation modes
  };

  // Struct to hold uniform buffer object data
  struct UniformBufferObject {
    alignas(16) glm::mat4 model; // Model matrix
    alignas(16) glm::mat4 view;  // View matrix
    alignas(16) glm::mat4 proj;  // Projection matrix
  };

  // Read a file into a vector of chars
  static fn readFile(const std::string& filename) -> std::vector<char> {
    // Open the file in binary mode
    std::ifstream file(filename, std::ios::binary);

    // Make sure the file was opened
    if (!file)
      throw std::runtime_error("Failed to open file: " + filename);

    // Read the contents of the file into a vector
    std::vector<char> buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());

    return buffer;
  }

  // Create a GLFW window
  fn initWindow() -> void {
    // Initialize GLFW
    mVKFWInstance = vkfw::initUnique();

    // Set window hints
    vkfw::WindowHints hints {
      .clientAPI = vkfw::ClientAPI::eNone, // No client API (Vulkan)
    };

    // Create the window
    mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints);

    // Set the window user pointer to this class
    mWindow->setUserPointer(this);

    // Set the framebuffer resize callback
    mWindow->callbacks()->on_window_resize =
      [](const vkfw::Window& window, usize /*width*/, usize /*height*/) -> void {
      // Set the framebuffer resized flag
      std::bit_cast<VulkanApp*>(window.getUserPointer())->mFramebufferResized = true;
    };
  }

  // Initialize Vulkan
  fn initVulkan() -> void {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createDescriptorSetLayout();
    createGraphicsPipeline();
    createCommandPool();
    createColorResources();
    createDepthResources();
    createFramebuffers();
    createTextureImage();
    createTextureImageView();
    createTextureSampler();
    loadModel();
    createVertexBuffer();
    createIndexBuffer();
    createUniformBuffers();
    createDescriptorPool();
    createDescriptorSets();
    createCommandBuffers();
    createSyncObjects();
    initImGui();
  }

  void initImGui() {
    // Create ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();

    // Setup Dear ImGui style
    ImGui::StyleColorsDark();

    // Initialize ImGui for GLFW and Vulkan
    ImGui_ImplGlfw_InitForVulkan(mWindow.get(), true);

    ImGui_ImplVulkan_InitInfo initInfo = {};
    initInfo.Instance                  = mInstance.get();
    initInfo.PhysicalDevice            = mPhysicalDevice;
    initInfo.Device                    = mDevice.get();
    initInfo.QueueFamily               = findQueueFamilies(mPhysicalDevice).graphics_family.value();
    initInfo.Queue                     = mGraphicsQueue;
    initInfo.PipelineCache             = VK_NULL_HANDLE;
    initInfo.DescriptorPool            = mDescriptorPool.get();
    initInfo.RenderPass                = mRenderPass.get();
    initInfo.MSAASamples               = static_cast<VkSampleCountFlagBits>(mMsaaSamples);
    initInfo.Allocator                 = nullptr;
    initInfo.MinImageCount             = MAX_FRAMES_IN_FLIGHT;
    initInfo.ImageCount                = static_cast<uint32_t>(mSwapChainImages.size());
    initInfo.CheckVkResultFn           = nullptr;

    ImGui_ImplVulkan_Init(&initInfo);

    // Upload Fonts
    ImGui_ImplVulkan_CreateFontsTexture();
  }

  // Main loop
  fn mainLoop() -> void {
    // While the window is open,
    while (!mWindow->shouldClose()) {
      // Poll for events
      vkfw::pollEvents();

      // Draw a frame
      drawFrame();
    }

    // Wait for the device to finish
    mDevice->waitIdle();
  }

  // Clean up the swap chain
  fn cleanupSwapChain() -> void {
    for (vk::UniqueFramebuffer& mSwapChainFramebuffer : mSwapChainFramebuffers) mSwapChainFramebuffer.reset();
    for (vk::UniqueImageView& mSwapChainImageView : mSwapChainImageViews) mSwapChainImageView.reset();

    mSwapChain.reset();
  }

  // Recreate the swap chain
  fn recreateSwapChain() -> void {
    // Get the new width and height
    auto [width, height] = mWindow->getFramebufferSize();

    // If the width or height are 0, wait for events
    while (width == 0 || height == 0) {
      std::tie(width, height) = mWindow->getFramebufferSize();
      vkfw::waitEvents();
    }

    // Wait for the device to finish
    mDevice->waitIdle();

    // Clean up the swap chain
    cleanupSwapChain();

    // Create a new swap chain
    createSwapChain();
    createImageViews();
    createColorResources();
    createDepthResources();
    createFramebuffers();
  }

  // Create a Vulkan instance
  fn createInstance() -> void {
#ifndef NDEBUG
    // Make sure validation layers are supported
    if (!checkValidationLayerSupport())
      throw std::runtime_error("Validation layers requested, but not available!");
#endif

    // Application metadata
    vk::ApplicationInfo appInfo {
      .pApplicationName   = "Hello Triangle",
      .applicationVersion = 1,
      .pEngineName        = "No Engine",
      .engineVersion      = 1,
      .apiVersion         = vk::ApiVersion12,
    };

    // Get the required extensions
    std::span<const char*> extensionsSpan = vkfw::getRequiredInstanceExtensions();

    // Convert the span to a vector
    std::vector extensions(extensionsSpan.begin(), extensionsSpan.end());

#ifndef NDEBUG
    // Add the debug utils extension
    extensions.emplace_back(vk::EXTDebugUtilsExtensionName);
#endif

#ifdef __APPLE__
    // Add the portability extension
    extensions.emplace_back(vk::KHRPortabilityEnumerationExtensionName);
    // Technically deprecated but Vulkan complains if I don't include it for macOS,
    // so instead of using the vk::KHRPortabilitySubsetExtensionName, I just use
    // the direct string.
    extensions.emplace_back("VK_KHR_get_physical_device_properties2");
#endif

    // Create the instance
    vk::InstanceCreateInfo createInfo {
#ifdef __APPLE__
      // Enable the portability extension
      .flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
#endif
      .pApplicationInfo = &appInfo,
#ifdef NDEBUG
      .enabledLayerCount   = 0,
      .ppEnabledLayerNames = nullptr,
#else
      .enabledLayerCount   = static_cast<u32>(validationLayers.size()),
      .ppEnabledLayerNames = validationLayers.data(),
#endif
      .enabledExtensionCount   = static_cast<u32>(extensions.size()),
      .ppEnabledExtensionNames = extensions.data()
    };

#ifndef NDEBUG
    fmt::println("Available extensions:");
    for (const char* extension : extensions) fmt::println("\t{}", extension);
#endif

    // Create the instance
    mInstance = vk::createInstanceUnique(createInfo);

    // Load the instance functions
    VULKAN_HPP_DEFAULT_DISPATCHER.init(mInstance.get());
  }

  // Create the debug messenger
  fn setupDebugMessenger() -> void {
#ifdef NDEBUG
    return;
#endif

    vk::DebugUtilsMessengerCreateInfoEXT messengerCreateInfo {
      .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose |
                         vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning |
                         vk::DebugUtilsMessageSeverityFlagBitsEXT::eError,
      .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral |
                     vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation |
                     vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance,
      .pfnUserCallback = debugCallback
    };

    mDebugMessenger = mInstance->createDebugUtilsMessengerEXTUnique(messengerCreateInfo, nullptr);
  }

  // Create the GLFW window surface
  fn createSurface() -> void { mSurface = vkfw::createWindowSurfaceUnique(mInstance.get(), mWindow.get()); }

  // Choose the best-suited physical device
  fn pickPhysicalDevice() -> void {
    // Get all physical devices
    std::vector<vk::PhysicalDevice> devices = mInstance->enumeratePhysicalDevices();

    // Make sure there are supported devices
    if (devices.empty())
      throw std::runtime_error("Failed to find GPUs with Vulkan support!");

#ifndef NDEBUG
    fmt::println("Available devices:");
#endif

    // For each device,
    for (const vk::PhysicalDevice& device : devices) {
#ifndef NDEBUG
      vk::PhysicalDeviceProperties properties = device.getProperties();
      fmt::println("\t{}", properties.deviceName.data());
#endif

      // Set the first suitable device as the physical device
      if (isDeviceSuitable(device)) {
        mPhysicalDevice = device;
        mMsaaSamples    = getMaxUsableSampleCount();
        break;
      }
    }

    // If no suitable device was found, throw an error
    if (!mPhysicalDevice)
      throw std::runtime_error("Failed to find a suitable GPU!");
  }

  // Create the logical device
  fn createLogicalDevice() -> void {
    // Get the queue families
    QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice);

    std::vector<vk::DeviceQueueCreateInfo> queueCreateInfos;

    std::set<u32> uniqueQueueFamilies = {
      qfIndices.graphics_family.value(),
      qfIndices.present_family.value(),
    };

    // Set the queue priority
    f32 queuePriority = 1.0F;

    // For each unique queue family, create a new queue
    for (u32 queueFamily : uniqueQueueFamilies) {
      vk::DeviceQueueCreateInfo queueCreateInfo {
        .queueFamilyIndex = queueFamily,
        .queueCount       = 1,
        .pQueuePriorities = &queuePriority,
      };

      queueCreateInfos.emplace_back(queueCreateInfo);
    }

    // Enable anisotropic filtering
    vk::PhysicalDeviceFeatures deviceFeatures {
      .samplerAnisotropy = vk::True,
    };

    vk::DeviceCreateInfo createInfo {
      .queueCreateInfoCount    = static_cast<u32>(queueCreateInfos.size()),
      .pQueueCreateInfos       = queueCreateInfos.data(),
      .enabledExtensionCount   = static_cast<u32>(deviceExtensions.size()),
      .ppEnabledExtensionNames = deviceExtensions.data(),
      .pEnabledFeatures        = &deviceFeatures,
    };

    // Create the logical device and set the graphics and present queues
    mDevice        = mPhysicalDevice.createDeviceUnique(createInfo);
    mGraphicsQueue = mDevice->getQueue(qfIndices.graphics_family.value(), 0);
    mPresentQueue  = mDevice->getQueue(qfIndices.present_family.value(), 0);
  }

  fn createSwapChain() -> void {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(mPhysicalDevice);

    vk::SurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
    vk::PresentModeKHR   presentMode   = chooseSwapPresentMode(swapChainSupport.present_modes);
    vk::Extent2D         extent        = chooseSwapExtent(swapChainSupport.capabilities);

    u32 imageCount = swapChainSupport.capabilities.minImageCount + 1;

    if (swapChainSupport.capabilities.maxImageCount > 0 &&
        imageCount > swapChainSupport.capabilities.maxImageCount)
      imageCount = swapChainSupport.capabilities.maxImageCount;

    QueueFamilyIndices qfIndices          = findQueueFamilies(mPhysicalDevice);
    std::array<u32, 2> queueFamilyIndices = {
      qfIndices.graphics_family.value(),
      qfIndices.present_family.value(),
    };

    vk::SwapchainCreateInfoKHR createInfo {
      .surface          = mSurface.get(),
      .minImageCount    = imageCount,
      .imageFormat      = surfaceFormat.format,
      .imageColorSpace  = surfaceFormat.colorSpace,
      .imageExtent      = extent,
      .imageArrayLayers = 1,
      .imageUsage       = vk::ImageUsageFlagBits::eColorAttachment,
      .imageSharingMode = qfIndices.graphics_family != qfIndices.present_family ? vk::SharingMode::eConcurrent
                                                                                : vk::SharingMode::eExclusive,
      .queueFamilyIndexCount =
        static_cast<u32>(qfIndices.graphics_family != qfIndices.present_family ? 2 : 0),
      .pQueueFamilyIndices =
        qfIndices.graphics_family != qfIndices.present_family ? queueFamilyIndices.data() : nullptr,
      .preTransform   = swapChainSupport.capabilities.currentTransform,
      .compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque,
      .presentMode    = presentMode,
      .clipped        = vk::True,
      .oldSwapchain   = nullptr,
    };

    mSwapChain = mDevice->createSwapchainKHRUnique(createInfo);

    mSwapChainImages      = mDevice->getSwapchainImagesKHR(mSwapChain.get());
    mSwapChainImageFormat = surfaceFormat.format;
    mSwapChainExtent      = extent;
  }

  fn createImageViews() -> void {
    mSwapChainImageViews.resize(mSwapChainImages.size());

    for (u32 i = 0; i < mSwapChainImages.size(); i++)
      mSwapChainImageViews[i] =
        createImageView(mSwapChainImages[i], mSwapChainImageFormat, vk::ImageAspectFlagBits::eColor, 1);
  }

  fn createRenderPass() -> void {
    vk::AttachmentDescription colorAttachment {
      .format         = mSwapChainImageFormat,
      .samples        = mMsaaSamples,
      .loadOp         = vk::AttachmentLoadOp::eClear,
      .storeOp        = vk::AttachmentStoreOp::eStore,
      .stencilLoadOp  = vk::AttachmentLoadOp::eDontCare,
      .stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
      .initialLayout  = vk::ImageLayout::eUndefined,
      .finalLayout    = vk::ImageLayout::eColorAttachmentOptimal,
    };

    vk::AttachmentDescription depthAttachment {
      .format         = findDepthFormat(),
      .samples        = mMsaaSamples,
      .loadOp         = vk::AttachmentLoadOp::eClear,
      .storeOp        = vk::AttachmentStoreOp::eDontCare,
      .stencilLoadOp  = vk::AttachmentLoadOp::eDontCare,
      .stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
      .initialLayout  = vk::ImageLayout::eUndefined,
      .finalLayout    = vk::ImageLayout::eDepthStencilAttachmentOptimal,
    };

    vk::AttachmentDescription colorAttachmentResolve {
      .format         = mSwapChainImageFormat,
      .samples        = vk::SampleCountFlagBits::e1,
      .loadOp         = vk::AttachmentLoadOp::eDontCare,
      .storeOp        = vk::AttachmentStoreOp::eStore,
      .stencilLoadOp  = vk::AttachmentLoadOp::eDontCare,
      .stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
      .initialLayout  = vk::ImageLayout::eUndefined,
      .finalLayout    = vk::ImageLayout::ePresentSrcKHR,
    };

    vk::AttachmentReference colorAttachmentRef {
      .attachment = 0,
      .layout     = vk::ImageLayout::eColorAttachmentOptimal,
    };

    vk::AttachmentReference depthAttachmentRef {
      .attachment = 1,
      .layout     = vk::ImageLayout::eDepthStencilAttachmentOptimal,
    };

    vk::AttachmentReference colorAttachmentResolveRef {
      .attachment = 2,
      .layout     = vk::ImageLayout::eColorAttachmentOptimal,
    };

    vk::SubpassDescription subpass {
      .pipelineBindPoint       = vk::PipelineBindPoint::eGraphics,
      .colorAttachmentCount    = 1,
      .pColorAttachments       = &colorAttachmentRef,
      .pResolveAttachments     = &colorAttachmentResolveRef,
      .pDepthStencilAttachment = &depthAttachmentRef,
    };

    vk::SubpassDependency dependency {
      .srcSubpass = vk::SubpassExternal,
      .dstSubpass = {},
      .srcStageMask =
        vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests,
      .dstStageMask =
        vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eEarlyFragmentTests,
      .srcAccessMask = {},
      .dstAccessMask =
        vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite,
    };

    std::array<vk::AttachmentDescription, 3> attachments = {
      colorAttachment,
      depthAttachment,
      colorAttachmentResolve,
    };

    vk::RenderPassCreateInfo renderPassInfo {
      .attachmentCount = static_cast<u32>(attachments.size()),
      .pAttachments    = attachments.data(),
      .subpassCount    = 1,
      .pSubpasses      = &subpass,
      .dependencyCount = 1,
      .pDependencies   = &dependency,
    };

    mRenderPass = mDevice->createRenderPassUnique(renderPassInfo);
  }

  fn createDescriptorSetLayout() -> void {
    vk::DescriptorSetLayoutBinding uboLayoutBinding {
      .binding            = 0,
      .descriptorType     = vk::DescriptorType::eUniformBuffer,
      .descriptorCount    = 1,
      .stageFlags         = vk::ShaderStageFlagBits::eVertex,
      .pImmutableSamplers = nullptr,
    };

    vk::DescriptorSetLayoutBinding samplerLayoutBinding {
      .binding            = 1,
      .descriptorType     = vk::DescriptorType::eCombinedImageSampler,
      .descriptorCount    = 1,
      .stageFlags         = vk::ShaderStageFlagBits::eFragment,
      .pImmutableSamplers = nullptr,
    };

    std::array<vk::DescriptorSetLayoutBinding, 2> bindings = { uboLayoutBinding, samplerLayoutBinding };

    vk::DescriptorSetLayoutCreateInfo layoutInfo {
      .bindingCount = static_cast<u32>(bindings.size()),
      .pBindings    = bindings.data(),
    };

    mDescriptorSetLayout = mDevice->createDescriptorSetLayoutUnique(layoutInfo);
  }

  fn createGraphicsPipeline() -> void {
    std::vector<char> vertShaderCode = readFile(VERTEX_SHADER_PATH);
    std::vector<char> fragShaderCode = readFile(FRAGMENT_SHADER_PATH);

    vk::UniqueShaderModule vertShaderModule = createShaderModule(vertShaderCode);
    vk::UniqueShaderModule fragShaderModule = createShaderModule(fragShaderCode);

    vk::PipelineShaderStageCreateInfo vertShaderStageInfo {
      .stage  = vk::ShaderStageFlagBits::eVertex,
      .module = vertShaderModule.get(),
      .pName  = "main",
    };

    vk::PipelineShaderStageCreateInfo fragShaderStageInfo {
      .stage  = vk::ShaderStageFlagBits::eFragment,
      .module = fragShaderModule.get(),
      .pName  = "main",
    };

    std::array<vk::PipelineShaderStageCreateInfo, 2> shaderStages = {
      vertShaderStageInfo,
      fragShaderStageInfo,
    };

    vk::VertexInputBindingDescription                  bindingDescription = Vertex::getBindingDescription();
    std::array<vk::VertexInputAttributeDescription, 3> attributeDescriptions =
      Vertex::getAttributeDescriptions();

    vk::PipelineVertexInputStateCreateInfo vertexInputInfo {
      .vertexBindingDescriptionCount   = 1,
      .pVertexBindingDescriptions      = &bindingDescription,
      .vertexAttributeDescriptionCount = static_cast<u32>(attributeDescriptions.size()),
      .pVertexAttributeDescriptions    = attributeDescriptions.data(),
    };

    vk::PipelineInputAssemblyStateCreateInfo inputAssembly {
      .topology               = vk::PrimitiveTopology::eTriangleList,
      .primitiveRestartEnable = vk::False,
    };

    vk::PipelineViewportStateCreateInfo viewportState {
      .viewportCount = 1,
      .scissorCount  = 1,
    };

    vk::PipelineRasterizationStateCreateInfo rasterizer {
      .depthClampEnable        = vk::False,
      .rasterizerDiscardEnable = vk::False,
      .polygonMode             = vk::PolygonMode::eFill,
      .cullMode                = vk::CullModeFlagBits::eBack,
      .frontFace               = vk::FrontFace::eCounterClockwise,
      .depthBiasEnable         = vk::False,
      .lineWidth               = 1.0F,
    };

    vk::PipelineMultisampleStateCreateInfo multisampling {
      .rasterizationSamples = mMsaaSamples,
      .sampleShadingEnable  = vk::False,
    };

    vk::PipelineDepthStencilStateCreateInfo depthStencil {
      .depthTestEnable       = vk::True,
      .depthWriteEnable      = vk::True,
      .depthCompareOp        = vk::CompareOp::eLess,
      .depthBoundsTestEnable = vk::False,
      .stencilTestEnable     = vk::False,
    };

    vk::PipelineColorBlendAttachmentState colorBlendAttachment {
      .blendEnable    = vk::False,
      .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
                        vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
    };

    vk::PipelineColorBlendStateCreateInfo colorBlending {
      .logicOpEnable   = vk::False,
      .logicOp         = vk::LogicOp::eCopy,
      .attachmentCount = 1,
      .pAttachments    = &colorBlendAttachment,
      .blendConstants  = std::array<float, 4> { 0.0F, 0.0F, 0.0F, 0.0F },
    };

    std::vector<vk::DynamicState> dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor };

    vk::PipelineDynamicStateCreateInfo dynamicState {
      .dynamicStateCount = static_cast<u32>(dynamicStates.size()),
      .pDynamicStates    = dynamicStates.data(),
    };

    vk::PipelineLayoutCreateInfo pipelineLayoutInfo {
      .setLayoutCount = 1,
      .pSetLayouts    = &mDescriptorSetLayout.get(),
    };

    mPipelineLayout = mDevice->createPipelineLayoutUnique(pipelineLayoutInfo);

    vk::GraphicsPipelineCreateInfo pipelineInfo {
      .stageCount          = static_cast<u32>(shaderStages.size()),
      .pStages             = shaderStages.data(),
      .pVertexInputState   = &vertexInputInfo,
      .pInputAssemblyState = &inputAssembly,
      .pViewportState      = &viewportState,
      .pRasterizationState = &rasterizer,
      .pMultisampleState   = &multisampling,
      .pDepthStencilState  = &depthStencil,
      .pColorBlendState    = &colorBlending,
      .pDynamicState       = &dynamicState,
      .layout              = mPipelineLayout.get(),
      .renderPass          = mRenderPass.get(),
      .subpass             = 0,
    };

    vk::Result         graphicsPipelineResult = vk::Result::eSuccess;
    vk::UniquePipeline graphicsPipelineValue;

    std::tie(graphicsPipelineResult, graphicsPipelineValue) =
      mDevice->createGraphicsPipelineUnique(nullptr, pipelineInfo).asTuple();

    if (graphicsPipelineResult != vk::Result::eSuccess)
      throw std::runtime_error("Failed to create graphics pipeline!");

    mGraphicsPipeline = std::move(graphicsPipelineValue);
  }

  fn createFramebuffers() -> void {
    mSwapChainFramebuffers.resize(mSwapChainImageViews.size());

    for (usize i = 0; i < mSwapChainImageViews.size(); i++) {
      std::array<vk::ImageView, 3> attachments = { mColorImageView.get(),
                                                   mDepthImageView.get(),
                                                   mSwapChainImageViews[i].get() };

      vk::FramebufferCreateInfo framebufferInfo { .renderPass      = mRenderPass.get(),
                                                  .attachmentCount = static_cast<u32>(attachments.size()),
                                                  .pAttachments    = attachments.data(),
                                                  .width           = mSwapChainExtent.width,
                                                  .height          = mSwapChainExtent.height,
                                                  .layers          = 1 };

      mSwapChainFramebuffers[i] = mDevice->createFramebufferUnique(framebufferInfo);
    }
  }

  fn createCommandPool() -> void {
    QueueFamilyIndices queueFamilyIndices = findQueueFamilies(mPhysicalDevice);

    vk::CommandPoolCreateInfo poolInfo { .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
                                         .queueFamilyIndex = queueFamilyIndices.graphics_family.value() };

    mCommandPool = mDevice->createCommandPoolUnique(poolInfo);
  }

  fn createColorResources() -> void {
    vk::Format colorFormat = mSwapChainImageFormat;

    std::tie(mColorImage, mColorImageMemory) = createImage(
      mSwapChainExtent.width,
      mSwapChainExtent.height,
      1,
      mMsaaSamples,
      colorFormat,
      vk::ImageTiling::eOptimal,
      vk::ImageUsageFlagBits::eTransientAttachment | vk::ImageUsageFlagBits::eColorAttachment,
      vk::MemoryPropertyFlagBits::eDeviceLocal
    );

    mColorImageView = createImageView(mColorImage.get(), colorFormat, vk::ImageAspectFlagBits::eColor, 1);
  }

  fn createDepthResources() -> void {
    vk::Format depthFormat = findDepthFormat();

    std::tie(mDepthImage, mDepthImageMemory) = createImage(
      mSwapChainExtent.width,
      mSwapChainExtent.height,
      1,
      mMsaaSamples,
      depthFormat,
      vk::ImageTiling::eOptimal,
      vk::ImageUsageFlagBits::eDepthStencilAttachment,
      vk::MemoryPropertyFlagBits::eDeviceLocal
    );

    mDepthImageView = createImageView(mDepthImage.get(), depthFormat, vk::ImageAspectFlagBits::eDepth, 1);
  }

  fn findSupportedFormat(
    const std::vector<vk::Format>& candidates,
    const vk::ImageTiling&         tiling,
    const vk::FormatFeatureFlags&  features
  ) -> vk::Format {
    for (vk::Format format : candidates) {
      vk::FormatProperties props = mPhysicalDevice.getFormatProperties(format);

      if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features)
        return format;

      if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features)
        return format;
    }

    throw std::runtime_error("Failed to find supported format!");
  }

  fn findDepthFormat() -> vk::Format {
    return findSupportedFormat(
      { vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint },
      vk::ImageTiling::eOptimal,
      vk::FormatFeatureFlagBits::eDepthStencilAttachment
    );
  }

  static fn hasStencilComponent(vk::Format format) {
    return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint;
  }

  fn createTextureImage() -> void {
    stb::UniqueImage image(TEXTURE_PATH);

    u8* pixels   = image.getData();
    i32 texWidth = image.getWidth(), texHeight = image.getHeight();

    vk::DeviceSize imageSize =
      static_cast<vk::DeviceSize>(texWidth) * static_cast<vk::DeviceSize>(texHeight) * 4;

    mMipLevels = static_cast<u32>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;

    if (!pixels)
      throw std::runtime_error("Failed to load texture image!");

    vk::UniqueBuffer       stagingBuffer;
    vk::UniqueDeviceMemory stagingBufferMemory;

    std::tie(stagingBuffer, stagingBufferMemory) = createBuffer(
      imageSize,
      vk::BufferUsageFlagBits::eTransferSrc,
      vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
    );

    copyData(stagingBufferMemory.get(), imageSize, pixels);

    std::tie(mTextureImage, mTextureImageMemory) = createImage(
      static_cast<u32>(texWidth),
      static_cast<u32>(texHeight),
      mMipLevels,
      vk::SampleCountFlagBits::e1,
      vk::Format::eR8G8B8A8Srgb,
      vk::ImageTiling::eOptimal,
      vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst |
        vk::ImageUsageFlagBits::eSampled,
      vk::MemoryPropertyFlagBits::eDeviceLocal
    );

    transitionImageLayout(
      mTextureImage.get(), vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, mMipLevels
    );

    copyBufferToImage(
      stagingBuffer.get(), mTextureImage.get(), static_cast<u32>(texWidth), static_cast<u32>(texHeight)
    );

    generateMipmaps(mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, texWidth, texHeight, mMipLevels);
  }

  fn generateMipmaps(vk::Image image, vk::Format imageFormat, i32 texWidth, i32 texHeight, u32 mipLevels)
    -> void {
    vk::FormatProperties formatProperties = mPhysicalDevice.getFormatProperties(imageFormat);

    if (!(formatProperties.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear))
      throw std::runtime_error("Texture image format does not support linear blitting!");

    vk::CommandBuffer commandBuffer = beginSingleTimeCommands();

    vk::ImageMemoryBarrier barrier {
      .srcQueueFamilyIndex = vk::QueueFamilyIgnored,
      .dstQueueFamilyIndex = vk::QueueFamilyIgnored,
      .image               = image,
      .subresourceRange    = { .aspectMask     = vk::ImageAspectFlagBits::eColor,
                              .levelCount     = 1,
                              .baseArrayLayer = 0,
                              .layerCount     = 1 }
    };

    i32 mipWidth  = texWidth;
    i32 mipHeight = texHeight;

    for (u32 i = 1; i < mipLevels; i++) {
      barrier.subresourceRange.baseMipLevel = i - 1;
      barrier.oldLayout                     = vk::ImageLayout::eTransferDstOptimal;
      barrier.newLayout                     = vk::ImageLayout::eTransferSrcOptimal;
      barrier.srcAccessMask                 = vk::AccessFlagBits::eTransferWrite;
      barrier.dstAccessMask                 = vk::AccessFlagBits::eTransferRead;

      commandBuffer.pipelineBarrier(
        vk::PipelineStageFlagBits::eTransfer,
        vk::PipelineStageFlagBits::eTransfer,
        {},
        nullptr,
        nullptr,
        barrier
      );

      vk::ImageBlit blit {
        .srcSubresource = { .aspectMask     = vk::ImageAspectFlagBits::eColor,
                           .mipLevel       = i - 1,
                           .baseArrayLayer = 0,
                           .layerCount     = 1 },
        .srcOffsets     = std::array<vk::Offset3D, 2> { { { .x = 0, .y = 0, .z = 0 },
                                                          { .x = mipWidth, .y = mipHeight, .z = 1 } } },
        .dstSubresource = { .aspectMask     = vk::ImageAspectFlagBits::eColor,
                           .mipLevel       = i,
                           .baseArrayLayer = 0,
                           .layerCount     = 1 },
        .dstOffsets     = std::array<vk::Offset3D, 2> { vk::Offset3D {
                                                          .x = 0,
                                                          .y = 0,
                                                          .z = 0,
                                                    }, vk::Offset3D {
                                                          .x = mipWidth > 1 ? mipWidth / 2 : 1,
                                                          .y = mipHeight > 1 ? mipHeight / 2 : 1,
                                                          .z = 1,
                                                    } }
      };

      commandBuffer.blitImage(
        image,
        vk::ImageLayout::eTransferSrcOptimal,
        image,
        vk::ImageLayout::eTransferDstOptimal,
        blit,
        vk::Filter::eLinear
      );

      barrier.oldLayout     = vk::ImageLayout::eTransferSrcOptimal;
      barrier.newLayout     = vk::ImageLayout::eShaderReadOnlyOptimal;
      barrier.srcAccessMask = vk::AccessFlagBits::eTransferRead;
      barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

      commandBuffer.pipelineBarrier(
        vk::PipelineStageFlagBits::eTransfer,
        vk::PipelineStageFlagBits::eFragmentShader,
        {},
        nullptr,
        nullptr,
        barrier
      );

      if (mipWidth > 1)
        mipWidth /= 2;
      if (mipHeight > 1)
        mipHeight /= 2;
    }

    barrier.subresourceRange.baseMipLevel = mMipLevels - 1;
    barrier.oldLayout                     = vk::ImageLayout::eTransferDstOptimal;
    barrier.newLayout                     = vk::ImageLayout::eShaderReadOnlyOptimal;
    barrier.srcAccessMask                 = vk::AccessFlagBits::eTransferWrite;
    barrier.dstAccessMask                 = vk::AccessFlagBits::eShaderRead;

    commandBuffer.pipelineBarrier(
      vk::PipelineStageFlagBits::eTransfer,
      vk::PipelineStageFlagBits::eFragmentShader,
      {},
      nullptr,
      nullptr,
      barrier
    );

    endSingleTimeCommands(commandBuffer);
  }

  fn getMaxUsableSampleCount() -> vk::SampleCountFlagBits {
    vk::PhysicalDeviceProperties physicalDeviceProperties = mPhysicalDevice.getProperties();

    vk::SampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts &
                                  physicalDeviceProperties.limits.framebufferDepthSampleCounts;

    // Define an array of sample counts in descending order
    const std::array<vk::SampleCountFlagBits, 7> sampleCounts = {
      vk::SampleCountFlagBits::e64, vk::SampleCountFlagBits::e32, vk::SampleCountFlagBits::e16,
      vk::SampleCountFlagBits::e8,  vk::SampleCountFlagBits::e4,  vk::SampleCountFlagBits::e2,
      vk::SampleCountFlagBits::e1,
    };

    // Loop through the array and return the first supported sample count
    for (const vk::SampleCountFlagBits& count : sampleCounts)
      if (counts & count)
        return count;

    // Return e1 if no other sample count is supported
    return vk::SampleCountFlagBits::e1;
  }

  fn createTextureImageView() -> void {
    mTextureImageView = createImageView(
      mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mMipLevels
    );
  }

  fn createTextureSampler() -> void {
    vk::PhysicalDeviceProperties properties = mPhysicalDevice.getProperties();

    vk::SamplerCreateInfo samplerInfo {
      .magFilter               = vk::Filter::eLinear,
      .minFilter               = vk::Filter::eLinear,
      .mipmapMode              = vk::SamplerMipmapMode::eLinear,
      .addressModeU            = vk::SamplerAddressMode::eRepeat,
      .addressModeV            = vk::SamplerAddressMode::eRepeat,
      .addressModeW            = vk::SamplerAddressMode::eRepeat,
      .mipLodBias              = 0.0F,
      .anisotropyEnable        = vk::False,
      .maxAnisotropy           = properties.limits.maxSamplerAnisotropy,
      .compareEnable           = vk::False,
      .compareOp               = vk::CompareOp::eAlways,
      .minLod                  = 0.0F,
      .maxLod                  = static_cast<f32>(mMipLevels),
      .borderColor             = vk::BorderColor::eIntOpaqueBlack,
      .unnormalizedCoordinates = vk::False,
    };

    mTextureSampler = mDevice->createSamplerUnique(samplerInfo);
  }

  fn createImageView(
    const vk::Image&            image,
    const vk::Format&           format,
    const vk::ImageAspectFlags& aspectFlags,
    const u32&                  mipLevels
  ) -> vk::UniqueImageView {
    vk::ImageViewCreateInfo viewInfo {
      .image = image, .viewType = vk::ImageViewType::e2D, .format = format, .subresourceRange = {
        .aspectMask     = aspectFlags,
        .baseMipLevel   = 0,
        .levelCount     = mipLevels,
        .baseArrayLayer = 0,
        .layerCount     = 1,
      }
    };

    return mDevice->createImageViewUnique(viewInfo);
  }

  fn createImage(
    const u32&                     width,
    const u32&                     height,
    const u32&                     mipLevels,
    const vk::SampleCountFlagBits& numSamples,
    const vk::Format&              format,
    const vk::ImageTiling&         tiling,
    const vk::ImageUsageFlags&     usage,
    const vk::MemoryPropertyFlags& properties
  ) -> std::pair<vk::UniqueImage, vk::UniqueDeviceMemory> {
    // Define the image creation info
    vk::ImageCreateInfo imageInfo {
      .imageType     = vk::ImageType::e2D,
      .format        = format,
      .extent        = { .width = width, .height = height, .depth = 1 },
      .mipLevels     = mipLevels,
      .arrayLayers   = 1,
      .samples       = numSamples,
      .tiling        = tiling,
      .usage         = usage,
      .sharingMode   = vk::SharingMode::eExclusive,
      .initialLayout = vk::ImageLayout::eUndefined,
    };

    // Create the image
    vk::UniqueImage image = mDevice->createImageUnique(imageInfo);

    // Get the memory requirements for the image
    vk::MemoryRequirements memRequirements = mDevice->getImageMemoryRequirements(image.get());

    // Memory allocation info
    vk::MemoryAllocateInfo allocInfo {
      .allocationSize  = memRequirements.size,
      .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties),
    };

    // Allocate memory
    vk::UniqueDeviceMemory imageMemory = mDevice->allocateMemoryUnique(allocInfo);

    // Bind the allocated memory to the image
    mDevice->bindImageMemory(image.get(), imageMemory.get(), 0);

    // Return the unique image
    return { std::move(image), std::move(imageMemory) };
  }

  // Transition image between layouts
  fn transitionImageLayout(
    const vk::Image&       image,
    const vk::ImageLayout& oldLayout,
    const vk::ImageLayout& newLayout,
    const u32&             mipLevels
  ) -> void {
    // Create a command buffer
    vk::CommandBuffer commandBuffer = beginSingleTimeCommands();

    // Define the image memory barrier
    vk::ImageMemoryBarrier barrier {
      .oldLayout           = oldLayout,
      .newLayout           = newLayout,
      .srcQueueFamilyIndex = vk::QueueFamilyIgnored,
      .dstQueueFamilyIndex = vk::QueueFamilyIgnored,
      .image               = image,
      .subresourceRange    = {
        .aspectMask     = vk::ImageAspectFlagBits::eColor,
        .baseMipLevel   = 0,
        .levelCount     = mipLevels,
        .baseArrayLayer = 0,
        .layerCount     = 1,
      },
    };

    // Define the source and destination stages
    vk::PipelineStageFlags sourceStage;
    vk::PipelineStageFlags destinationStage;

    // Define the access masks
    if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) {
      barrier.srcAccessMask = {};
      barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;

      sourceStage      = vk::PipelineStageFlagBits::eTopOfPipe;
      destinationStage = vk::PipelineStageFlagBits::eTransfer;
    } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal &&
               newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
      barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
      barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

      sourceStage      = vk::PipelineStageFlagBits::eTransfer;
      destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
    } else {
      // Ensure that the layout transition is supported
      throw std::invalid_argument("Unsupported layout transition!");
    }

    // Record the pipeline barrier
    commandBuffer.pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier);

    // End the command buffer
    endSingleTimeCommands(commandBuffer);
  }

  fn copyBufferToImage(const vk::Buffer& buffer, const vk::Image& image, const u32& width, const u32& height)
    -> void {
    vk::CommandBuffer commandBuffer = beginSingleTimeCommands();

    vk::BufferImageCopy region {
      .bufferOffset      = 0,
      .bufferRowLength   = 0,
      .bufferImageHeight = 0,
      .imageSubresource  = { .aspectMask     = vk::ImageAspectFlagBits::eColor,
                            .mipLevel       = 0,
                            .baseArrayLayer = 0,
                            .layerCount     = 1 },
      .imageOffset       = { .x = 0, .y = 0, .z = 0 },
      .imageExtent       = { .width = width, .height = height, .depth = 1 },
    };

    commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, 1, &region);

    endSingleTimeCommands(commandBuffer);
  }

  fn loadModel() -> void {
    tinyobj::attrib_t                attrib;
    std::vector<tinyobj::shape_t>    shapes;
    std::vector<tinyobj::material_t> materials;
    std::string                      warn, err;

    if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH))
      throw std::runtime_error(warn + err);

    std::unordered_map<Vertex, u32> uniqueVertices {};

    for (const tinyobj::shape_t& shape : shapes) {
      for (const tinyobj::index_t& index : shape.mesh.indices) {
        Vertex vertex {
          .pos = {
            attrib.vertices[static_cast<u32>((3 * index.vertex_index) + 0)],
            attrib.vertices[static_cast<u32>((3 * index.vertex_index) + 1)],
            attrib.vertices[static_cast<u32>((3 * index.vertex_index) + 2)],
          },
          .color = { 1.0F, 1.0F, 1.0F },
          .tex_coord = {
            attrib.texcoords[static_cast<u32>((2 * index.texcoord_index) + 0)],
            1.0F - attrib.texcoords[static_cast<u32>((2 * index.texcoord_index) + 1)],
          },
        };

        if (!uniqueVertices.contains(vertex)) {
          uniqueVertices[vertex] = static_cast<u32>(mVertices.size());
          mVertices.push_back(vertex);
        }

        mIndices.push_back(uniqueVertices[vertex]);
      }
    }
  }

  fn createVertexBuffer() -> void {
    vk::DeviceSize bufferSize = sizeof(mVertices[0]) * mVertices.size();

    vk::UniqueBuffer       stagingBuffer;
    vk::UniqueDeviceMemory stagingBufferMemory;

    std::tie(stagingBuffer, stagingBufferMemory) = createBuffer(
      bufferSize,
      vk::BufferUsageFlagBits::eTransferSrc,
      vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
    );

    copyData(stagingBufferMemory.get(), bufferSize, mVertices.data());

    std::tie(mVertexBuffer, mVertexBufferMemory) = createBuffer(
      bufferSize,
      vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst,
      vk::MemoryPropertyFlagBits::eDeviceLocal
    );

    copyBuffer(stagingBuffer.get(), mVertexBuffer.get(), bufferSize);

    stagingBuffer.reset();
    stagingBufferMemory.reset();
  }

  fn createIndexBuffer() -> void {
    vk::DeviceSize bufferSize = sizeof(mIndices[0]) * mIndices.size();

    vk::UniqueBuffer       stagingBuffer;
    vk::UniqueDeviceMemory stagingBufferMemory;

    std::tie(stagingBuffer, stagingBufferMemory) = createBuffer(
      bufferSize,
      vk::BufferUsageFlagBits::eTransferSrc,
      vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
    );

    copyData(stagingBufferMemory.get(), bufferSize, mIndices.data());

    std::tie(mIndexBuffer, mIndexBufferMemory) = createBuffer(
      bufferSize,
      vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eTransferDst,
      vk::MemoryPropertyFlagBits::eDeviceLocal
    );

    copyBuffer(stagingBuffer.get(), mIndexBuffer.get(), bufferSize);

    stagingBuffer.reset();
    stagingBufferMemory.reset();
  }

  fn createUniformBuffers() -> void {
    vk::DeviceSize bufferSize = sizeof(UniformBufferObject);

    mUniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
    mUniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
    mUniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT);

    for (usize i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
      std::tie(mUniformBuffers[i], mUniformBuffersMemory[i]) = createBuffer(
        bufferSize,
        vk::BufferUsageFlagBits::eUniformBuffer,
        vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent
      );

      mUniformBuffersMapped[i] = mDevice->mapMemory(mUniformBuffersMemory[i].get(), 0, bufferSize);
    }
  }

  fn createDescriptorPool() -> void {
    std::array<vk::DescriptorPoolSize, 2> poolSizes = {
      vk::DescriptorPoolSize {
                              .type            = vk::DescriptorType::eUniformBuffer,
                              .descriptorCount = MAX_FRAMES_IN_FLIGHT,
                              },
      vk::DescriptorPoolSize {
                              .type            = vk::DescriptorType::eCombinedImageSampler,
                              .descriptorCount = MAX_FRAMES_IN_FLIGHT,
                              },
    };

    vk::DescriptorPoolCreateInfo poolInfo {
      .maxSets       = MAX_FRAMES_IN_FLIGHT,
      .poolSizeCount = static_cast<u32>(poolSizes.size()),
      .pPoolSizes    = poolSizes.data(),
    };

    mDescriptorPool = mDevice->createDescriptorPoolUnique(poolInfo);
  }

  fn createDescriptorSets() -> void {
    std::vector<vk::DescriptorSetLayout> layouts(MAX_FRAMES_IN_FLIGHT, mDescriptorSetLayout.get());

    vk::DescriptorSetAllocateInfo allocInfo {
      .descriptorPool     = mDescriptorPool.get(),
      .descriptorSetCount = static_cast<u32>(MAX_FRAMES_IN_FLIGHT),
      .pSetLayouts        = layouts.data(),
    };

    mDescriptorSets = mDevice->allocateDescriptorSets(allocInfo);

    for (usize i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
      vk::DescriptorBufferInfo bufferInfo {
        .buffer = mUniformBuffers[i].get(),
        .offset = 0,
        .range  = sizeof(UniformBufferObject),
      };

      vk::DescriptorImageInfo imageInfo {
        .sampler     = mTextureSampler.get(),
        .imageView   = mTextureImageView.get(),
        .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal,
      };

      std::array<vk::WriteDescriptorSet, 2> descriptorWrites = {
        vk::WriteDescriptorSet {
                                .dstSet          = mDescriptorSets[i],
                                .dstBinding      = 0,
                                .dstArrayElement = 0,
                                .descriptorCount = 1,
                                .descriptorType  = vk::DescriptorType::eUniformBuffer,
                                .pBufferInfo     = &bufferInfo,
                                },
        vk::WriteDescriptorSet {
                                .dstSet          = mDescriptorSets[i],
                                .dstBinding      = 1,
                                .dstArrayElement = 0,
                                .descriptorCount = 1,
                                .descriptorType  = vk::DescriptorType::eCombinedImageSampler,
                                .pImageInfo      = &imageInfo,
                                },
      };

      mDevice->updateDescriptorSets(descriptorWrites, {});
    }
  }

  fn createBuffer(
    const vk::DeviceSize&          deviceSize,
    const vk::BufferUsageFlags&    bufferUsageFlags,
    const vk::MemoryPropertyFlags& memoryPropertyFlags
  ) -> std::pair<vk::UniqueBuffer, vk::UniqueDeviceMemory> {
    vk::BufferCreateInfo bufferInfo {
      .size        = deviceSize,
      .usage       = bufferUsageFlags,
      .sharingMode = vk::SharingMode::eExclusive,
    };

    // Create the buffer
    vk::UniqueBuffer buffer = mDevice->createBufferUnique(bufferInfo);

    // Get the memory requirements for the buffer
    vk::MemoryRequirements memRequirements = mDevice->getBufferMemoryRequirements(buffer.get());

    // Memory allocation info
    vk::MemoryAllocateInfo allocInfo {
      .allocationSize  = memRequirements.size,
      .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, memoryPropertyFlags),
    };

    // Allocate memory
    vk::UniqueDeviceMemory bufferMemory = mDevice->allocateMemoryUnique(allocInfo);

    // Bind the allocated memory to the buffer
    mDevice->bindBufferMemory(buffer.get(), bufferMemory.get(), 0);

    // Return both the unique buffer and its associated memory
    return { std::move(buffer), std::move(bufferMemory) };
  }

  // Create the command buffers
  fn beginSingleTimeCommands() -> vk::CommandBuffer {
    // Define the command buffer allocation info
    vk::CommandBufferAllocateInfo allocInfo {
      .commandPool        = mCommandPool.get(),
      .level              = vk::CommandBufferLevel::ePrimary,
      .commandBufferCount = 1,
    };

    // Allocate the command buffer
    vk::CommandBuffer commandBuffer = mDevice->allocateCommandBuffers(allocInfo)[0];

    // Define the command buffer begin info
    vk::CommandBufferBeginInfo beginInfo { .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit };

    // Begin the command buffer
    commandBuffer.begin(beginInfo);

    return commandBuffer;
  }

  // End the single time command buffer
  fn endSingleTimeCommands(const vk::CommandBuffer& commandBuffer) -> void {
    // End the command buffer
    commandBuffer.end();

    // Define the submit info
    vk::SubmitInfo submitInfo { .commandBufferCount = 1, .pCommandBuffers = &commandBuffer };

    // Submit the command buffer
    mGraphicsQueue.submit(submitInfo, nullptr);

    // Wait for the queue to finish
    mGraphicsQueue.waitIdle();

    // Free the command buffer
    mDevice->freeCommandBuffers(mCommandPool.get(), commandBuffer);
  }

  // Copy data from src to dst
  fn copyData(const vk::DeviceMemory& stagingBufferMemory, const vk::DeviceSize& bufferSize, const void* src)
    -> void {
    // Map the memory
    void* data = mDevice->mapMemory(stagingBufferMemory, 0, bufferSize);

    // Copy the data with memcpy - memcpy(dst, src, size)
    memcpy(data, src, static_cast<usize>(bufferSize));

    // Unmap the memory
    mDevice->unmapMemory(stagingBufferMemory);
  }

  // Copy buffer from src to dst
  fn copyBuffer(const vk::Buffer& srcBuffer, const vk::Buffer& dstBuffer, const vk::DeviceSize& deviceSize)
    -> void {
    // Begin a single time command buffer
    vk::CommandBuffer commandBuffer = beginSingleTimeCommands();

    // Define the copy region
    vk::BufferCopy copyRegion { .size = deviceSize };

    // Copy the buffer
    commandBuffer.copyBuffer(srcBuffer, dstBuffer, 1, &copyRegion);

    // End the single time command buffer
    endSingleTimeCommands(commandBuffer);
  }

  // Find the memory type of the physical device
  fn findMemoryType(const u32& typeFilter, const vk::MemoryPropertyFlags& properties) -> u32 {
    // Get the memory properties of the physical device
    vk::PhysicalDeviceMemoryProperties memProperties = mPhysicalDevice.getMemoryProperties();

    // Loop through the memory types and find the one that matches the filter
    for (u32 idx = 0; idx < memProperties.memoryTypeCount; idx++)
      if ((typeFilter & (1 << idx)) &&
          (memProperties.memoryTypes.at(idx).propertyFlags & properties) == properties)
        return idx;

    // Throw an error if no suitable memory type is found
    throw std::runtime_error("Failed to find a suitable memory type!");
  }

  // Create the command buffers
  fn createCommandBuffers() -> void {
    // Resize the command buffers to hold the maximum number of frames in flight
    mCommandBuffers.resize(MAX_FRAMES_IN_FLIGHT);

    // Define the command buffer allocation info
    vk::CommandBufferAllocateInfo allocInfo {
      .commandPool        = mCommandPool.get(),
      .level              = vk::CommandBufferLevel::ePrimary,
      .commandBufferCount = static_cast<u32>(mCommandBuffers.size()),
    };

    // Allocate the command buffers
    mCommandBuffers = mDevice->allocateCommandBuffersUnique(allocInfo);
  }

  // Record the command buffer
  fn recordCommandBuffer(const vk::CommandBuffer& commandBuffer, const u32& imageIndex) -> void {
    // Define the command buffer begin info
    vk::CommandBufferBeginInfo beginInfo {};

    // Begin the command buffer
    commandBuffer.begin(beginInfo);

    // Define the render pass begin info
    std::array<vk::ClearValue, 2> clearValues {
      // Set the color buffer to black
      vk::ClearValue { .color = { .uint32 = std::array<u32, 4> { 0, 0, 0, 255 } } },
      // Set the depth buffer to 1.0F
      vk::ClearValue { .depthStencil = { .depth = 1.0F, .stencil = 0 } },
    };

    // Define the render pass info
    vk::RenderPassBeginInfo renderPassInfo {
      // Render pass itself
      .renderPass = mRenderPass.get(),
      // Current framebuffer
      .framebuffer = mSwapChainFramebuffers[imageIndex].get(),
      // Render area (entire framebuffer)
      .renderArea = { .offset = { .x = 0, .y = 0 }, .extent = mSwapChainExtent },
      // Clear values for the attachments
      .clearValueCount = static_cast<u32>(clearValues.size()),
      .pClearValues    = clearValues.data(),
    };

    // Begin the render pass
    commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);

    // Bind the graphics pipeline to the command buffer
    commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mGraphicsPipeline.get());

    // Create the viewport and scissor.
    // If the scissor is smaller than the
    // viewport, then it will be clipped.
    vk::Viewport viewport {
      .x        = 0.0F,
      .y        = 0.0F,
      .width    = static_cast<f32>(mSwapChainExtent.width),
      .height   = static_cast<f32>(mSwapChainExtent.height),
      .minDepth = 0.0F,
      .maxDepth = 1.0F,
    };

    // Create the scissor
    vk::Rect2D scissor {
      .offset = { .x = 0, .y = 0 },
      .extent = mSwapChainExtent,
    };

    // Set the viewport and scissor
    commandBuffer.setViewport(0, viewport);
    commandBuffer.setScissor(0, scissor);

    // Bind the vertex buffer
    commandBuffer.bindVertexBuffers(0, mVertexBuffer.get(), { 0 });

    // Bind the index buffer
    commandBuffer.bindIndexBuffer(mIndexBuffer.get(), 0, vk::IndexType::eUint32);

    // Bind the descriptor sets
    commandBuffer.bindDescriptorSets(
      vk::PipelineBindPoint::eGraphics,
      mPipelineLayout.get(),
      0,
      1,
      &mDescriptorSets[mCurrentFrame],
      0,
      nullptr
    );

    // Draw the indexed vertices
    commandBuffer.drawIndexed(static_cast<u32>(mIndices.size()), 1, 0, 0, 0);

    ImGui_ImplVulkan_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    // Your ImGui code here
    ImGui::ShowDemoWindow();

    ImGui::Render();
    ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), commandBuffer);

    // End the render pass
    commandBuffer.endRenderPass();

    // End the command buffer
    commandBuffer.end();
  }

  // Create the semaphores and fences
  fn createSyncObjects() -> void {
    // Resize the vectors to hold the maximum number of frames in flight
    mImageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    mRenderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    mInFlightFences.resize(MAX_FRAMES_IN_FLIGHT);

    // Create the semaphore and fence info
    vk::SemaphoreCreateInfo semaphoreInfo {};
    vk::FenceCreateInfo     fenceInfo { .flags = vk::FenceCreateFlagBits::eSignaled };

    // Loop through the maximum number of frames in flight and create the semaphores and fences
    for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) {
      mImageAvailableSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo);
      mRenderFinishedSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo);
      mInFlightFences[idx]           = mDevice->createFenceUnique(fenceInfo);
    }
  }

  fn updateUniformBuffer(const u32& currentImage) -> void {
    // For convenience
    using namespace std::chrono;
    using time_point = high_resolution_clock::time_point;

    // Time of the program start
    static time_point StartTime = high_resolution_clock::now();

    // Current time
    time_point currentTime = high_resolution_clock::now();

    // Time since the program started
    f32 time = duration<f32, seconds::period>(currentTime - StartTime).count();

    // Uniform buffer object
    UniformBufferObject ubo {
      // Model matrix - glm::rotate(matrix, angle, axis)
      .model = glm::rotate(glm::mat4(1.0F), time * glm::radians(90.0F), glm::vec3(0.0F, 0.0F, 1.0F)),
      // View matrix - glm::lookAt(eye, center, up)
      .view =
        glm::lookAt(glm::vec3(2.0F, 2.0F, 2.0F), glm::vec3(0.0F, 0.0F, 0.0F), glm::vec3(0.0F, 0.0F, 1.0F)),
      // Projection matrix - glm::perspective(fov, aspect, near, far)
      .proj = glm::perspective(
        glm::radians(45.0F),
        static_cast<f32>(mSwapChainExtent.width) / static_cast<f32>(mSwapChainExtent.height),
        0.1F,
        10.0F
      )
    };

    // Flip the Y axis, because glm was designed for OpenGL
    ubo.proj[1][1] *= -1;

    // Copy the uniform buffer object to the mapped memory
    memcpy(mUniformBuffersMapped[currentImage], &ubo, sizeof(ubo));
  }

  // Draw a frame to the window
  fn drawFrame() -> void {
    try {
      // Wait for the fence to signal that the frame is finished
      vk::Result result =
        mDevice->waitForFences(mInFlightFences[mCurrentFrame].get(), vk::Bool32(vk::True), UINT64_MAX);

      // Make sure the result is successful
      if (result != vk::Result::eSuccess)
        throw std::runtime_error("Failed to wait for fences!");

      // Acquire the next image from the swap chain
      auto [imageIndexResult, imageIndexValue] = mDevice->acquireNextImageKHR(
        mSwapChain.get(), UINT64_MAX, mImageAvailableSemaphores[mCurrentFrame].get(), nullptr
      );

      // Check if the swap chain needs to be recreated
      if (imageIndexResult == vk::Result::eErrorOutOfDateKHR) {
        recreateSwapChain();
        return;
      }

      // Check if the image index is valid
      if (imageIndexResult != vk::Result::eSuccess && imageIndexResult != vk::Result::eSuboptimalKHR)
        throw std::runtime_error("Failed to acquire swap chain image!");

      // Update the uniform buffer with the current image
      updateUniformBuffer(mCurrentFrame);

      // Reset the current fence
      mDevice->resetFences(mInFlightFences[mCurrentFrame].get());

      // Reset the current command buffer
      mCommandBuffers[mCurrentFrame]->reset(vk::CommandBufferResetFlagBits::eReleaseResources);

      // Define the command buffer submit info
      recordCommandBuffer(mCommandBuffers[mCurrentFrame].get(), imageIndexValue);

      std::array<vk::PipelineStageFlags, 1> waitStages = {
        vk::PipelineStageFlagBits::eColorAttachmentOutput
      };

      vk::SubmitInfo submitInfo {
        .waitSemaphoreCount   = 1,
        .pWaitSemaphores      = &mImageAvailableSemaphores[mCurrentFrame].get(),
        .pWaitDstStageMask    = waitStages.data(),
        .commandBufferCount   = 1,
        .pCommandBuffers      = &mCommandBuffers[mCurrentFrame].get(),
        .signalSemaphoreCount = 1,
        .pSignalSemaphores    = &mRenderFinishedSemaphores[mCurrentFrame].get(),
      };

      // Submit the graphics queue
      mGraphicsQueue.submit(submitInfo, mInFlightFences[mCurrentFrame].get());

      vk::PresentInfoKHR presentInfo {
        .waitSemaphoreCount = 1,
        .pWaitSemaphores    = &mRenderFinishedSemaphores[mCurrentFrame].get(),
        .swapchainCount     = 1,
        .pSwapchains        = &mSwapChain.get(),
        .pImageIndices      = &imageIndexValue,
      };

      // Present the swap chain image
      vk::Result presentResult = mPresentQueue.presentKHR(presentInfo);

      // Check if the swap chain needs to be recreated
      if (presentResult == vk::Result::eErrorOutOfDateKHR || presentResult == vk::Result::eSuboptimalKHR ||
          mFramebufferResized) {
        mFramebufferResized = false;
        recreateSwapChain();
      } else if (presentResult != vk::Result::eSuccess) {
        // Throw if present failed
        throw std::runtime_error("Failed to present swap chain image!");
      }

      // Increment the current frame (or loop back to 0)
      mCurrentFrame = (mCurrentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
    } catch (vk::OutOfDateKHRError& /*err*/) {
      // Recreate the swap chain if it's out of date
      mFramebufferResized = false;
      recreateSwapChain();
      return;
    }
  }

  // Create the shader module
  fn createShaderModule(const std::vector<char>& code) -> vk::UniqueShaderModule {
    vk::ShaderModuleCreateInfo createInfo {
      .codeSize = code.size(),
      .pCode    = std::bit_cast<const u32*>(code.data()),
    };

    return mDevice->createShaderModuleUnique(createInfo);
  }

  // Choose the color format for the swap chain
  static fn chooseSwapSurfaceFormat(const std::vector<vk::SurfaceFormatKHR>& availableFormats
  ) -> vk::SurfaceFormatKHR {
    // If SRGB is available, use it
    for (const vk::SurfaceFormatKHR& availableFormat : availableFormats)
      if (availableFormat.format == vk::Format::eB8G8R8A8Srgb &&
          availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear)
        return availableFormat;

    // Otherwise, use the first available format
    return availableFormats[0];
  }

  // Choose the swap chain presentation mode
  static fn chooseSwapPresentMode(const std::vector<vk::PresentModeKHR>& availablePresentModes
  ) -> vk::PresentModeKHR {
    // Check if mailbox mode is available (adaptive sync)
    for (const vk::PresentModeKHR& availablePresentMode : availablePresentModes)
      if (availablePresentMode == vk::PresentModeKHR::eMailbox)
        return availablePresentMode;

    // If mailbox mode is not available, use FIFO mode (vsync)
    return vk::PresentModeKHR::eFifo;
  }

  // Choose the swap chain extent (resolution)
  fn chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) -> vk::Extent2D {
    // If the resolution is not UINT32_MAX, return it
    // Otherwise, we need to set the resolution manually
    if (capabilities.currentExtent.width != UINT32_MAX)
      return capabilities.currentExtent;

    // Get the window's resolution
    u32 width = 0, height = 0;
    std::tie(width, height) = mWindow->getFramebufferSize();

    // Return the resolution clamped to the supported range
    return {
      .width  = std::clamp(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
      .height = std::clamp(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height),
    };
  }

  // Check if the swap chain is adequate
  fn querySwapChainSupport(const vk::PhysicalDevice& device) -> SwapChainSupportDetails {
    return {
      .capabilities  = device.getSurfaceCapabilitiesKHR(mSurface.get()),
      .formats       = device.getSurfaceFormatsKHR(mSurface.get()),
      .present_modes = device.getSurfacePresentModesKHR(mSurface.get()),
    };
  }

  // Check if the device is suitable for the application
  fn isDeviceSuitable(const vk::PhysicalDevice& device) -> bool {
    // Get the queue families that support the required operations
    QueueFamilyIndices qfIndices = findQueueFamilies(device);

    // Check if the device supports the required extensions
    bool extensionsSupported = checkDeviceExtensionSupport(device);

    bool swapChainAdequate = false;

    if (extensionsSupported) {
      SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
      // Check if the swap chain is adequate (make sure it has
      // at least one supported format and presentation mode)
      swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.present_modes.empty();
    }

    // Check if the device supports the required features
    vk::PhysicalDeviceFeatures supportedFeatures = device.getFeatures();

    // If the device supports everything required, return true
    return qfIndices.isComplete() && extensionsSupported && swapChainAdequate &&
           supportedFeatures.samplerAnisotropy;
  }

  // Check if the device supports the required extensions
  static fn checkDeviceExtensionSupport(const vk::PhysicalDevice& device) -> bool {
    // Get the available extensions
    std::vector<vk::ExtensionProperties> availableExtensions = device.enumerateDeviceExtensionProperties();

    // Create a set of required extension names
    std::set<string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

    // Remove each required extension from the set of available extensions
    for (const vk::ExtensionProperties& extension : availableExtensions)
      requiredExtensions.erase(extension.extensionName);

    // If the set is empty, all required extensions are supported
    return requiredExtensions.empty();
  }

  // Find the queue families that support the required operations
  fn findQueueFamilies(const vk::PhysicalDevice& device) -> QueueFamilyIndices {
    // Create a struct to store the queue family indices
    QueueFamilyIndices qfIndices;

    // Get the queue family properties
    std::vector<vk::QueueFamilyProperties> queueFamilies = device.getQueueFamilyProperties();

    // For every queue family,
    for (u32 i = 0; i < queueFamilies.size(); i++) {
      // Check if the queue family supports the required operations
      if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics)
        qfIndices.graphics_family = i;

      // Check if the queue family supports presentation
      vk::Bool32 queuePresentSupport = device.getSurfaceSupportKHR(i, mSurface.get());

      // If the queue family supports presentation, set the present family index
      if (queuePresentSupport)
        qfIndices.present_family = i;

      // If the queue family supports both operations, we're done
      if (qfIndices.isComplete())
        break;
    }

    return qfIndices;
  }

  // Check if all requested layers are available
  static fn checkValidationLayerSupport() -> bool {
    std::vector<vk::LayerProperties> availableLayers = vk::enumerateInstanceLayerProperties();

    // Create a set of available layer names
    std::unordered_set<std::string_view> availableLayerNames;
    for (const vk::LayerProperties& layerProperties : availableLayers)
      availableLayerNames.emplace(layerProperties.layerName);

    // Check if each validation layer is available
    for (const std::string_view layerName : validationLayers)
      if (availableLayerNames.find(layerName) == availableLayerNames.end())
        return false; // Layer not found

    return true; // All validation layers are available
  }

  // Callback function used to report validation layer messages
  static VKAPI_ATTR fn VKAPI_CALL debugCallback(
    VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/, // Severity: verbose, info, warning, error
    VkDebugUtilsMessageTypeFlagsEXT /*messageType*/,            // Type:     general, validation, performance
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,  // Message details
    void* /*pUserData*/                                         // Optional user data
  ) -> vk::Bool32 {
    // Print the message to the console
    // Because pCallbackData already gives the message severity
    // and type, we don't need to print them, so they're unused.
    fmt::println("Validation layer: {}", pCallbackData->pMessage);

    return vk::False;
  }
};

fn main() -> i32 {
  // Initialize dynamic function dispatcher
  VULKAN_HPP_DEFAULT_DISPATCHER.init();

  // Create an app instance
  VulkanApp app;

  try {
    app.run();
  } catch (const std::exception& e) {
    fmt::println("{}", e.what());
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
