// Include necessary headers
#include <chrono>              // For time-related functions
#include <fmt/format.h>        // For string formatting
#include <shaderc/shaderc.hpp> // For shader compilation
#include <unordered_set>       // For unordered_set container

// GLM (OpenGL Mathematics) configuration
#define GLM_FORCE_DEPTH_ZERO_TO_ONE        // Use Vulkan's depth range (0 to 1) instead of OpenGL's (-1 to 1)
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES // Force GLM to use aligned types
#include <glm/glm.hpp>                     // Include GLM for mathematics operations

// TinyObjLoader for loading 3D models
#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>

// Vulkan configuration and inclusion
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1 // Use dynamic dispatch for Vulkan functions
#define VK_ENABLE_BETA_EXTENSIONS            // Enable beta Vulkan extensions
#define VULKAN_HPP_NO_CONSTRUCTORS           // Use aggregate initialization for Vulkan structs
#include <vulkan/vulkan.hpp>                 // Include Vulkan C++ bindings

// Necessary for dynamic dispatch to work
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE

// Include custom utility headers
#include "util/shaders.hpp"      // Compiled shader code
#include "util/types.hpp"        // Custom type definitions
#include "util/unique_image.hpp" // Custom image handling utilities
#include "util/vertex.hpp"       // Custom vertex structure definition

// ImGui headers for GUI
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h>

// GLFW configuration and inclusion
#define VKFW_NO_STRUCT_CONSTRUCTORS // Use aggregate initialization for GLFW structs
#include "vkfw.hpp"                 // Include GLFW C++ bindings

// Constants for window dimensions
constexpr i32 WIDTH  = 800;
constexpr i32 HEIGHT = 600;

// Maximum number of frames that can be processed concurrently
constexpr i32 MAX_FRAMES_IN_FLIGHT = 2;

// Vertex shader
constexpr const char* vertShaderSrc = R"glsl(
  #version 450

  layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
  } ubo;

  layout(location = 0) in vec3 inPosition;
  layout(location = 1) in vec3 inColor;
  layout(location = 2) in vec2 inTexCoord;

  layout(location = 0) out vec3 fragColor;
  layout(location = 1) out vec2 fragTexCoord;

  void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
    fragColor = inColor;
    fragTexCoord = inTexCoord;
  }
)glsl";

// Fragment shader
constexpr const char* fragShaderSrc = R"glsl(
  #version 450

  layout(binding = 1) uniform sampler2D texSampler;

  layout(location = 0) in vec3 fragColor;
  layout(location = 1) in vec2 fragTexCoord;

  layout(location = 0) out vec4 outColor;

  void main() {
    outColor = texture(texSampler, fragTexCoord);
  }
)glsl";

// Validation layers for debug builds
#ifndef NDEBUG
constexpr std::array<const char*, 1> validationLayers = { "VK_LAYER_KHRONOS_validation" };
#endif

// Required device extensions (platform-specific)
#ifdef __APPLE__
constexpr std::array<const char*, 2> deviceExtensions = { vk::KHRSwapchainExtensionName,
                                                          vk::KHRPortabilitySubsetExtensionName };
#else
constexpr std::array<const char*, 1> deviceExtensions = { vk::KHRSwapchainExtensionName };
#endif

/**
 * @brief The Vulkan application class.
 *
 * This class encapsulates the entire Vulkan application, managing the Vulkan
 * instance, window, rendering loop, and resource cleanup. It handles the
 * initialization of Vulkan, manages resources, and orchestrates rendering.
 */
class VulkanApp {
 public:
  /**
   * @brief Runs the Vulkan application.
   *
   * This function initializes the application window, sets up Vulkan, and enters
   * the main rendering loop. It also cleans up resources when the application is closed.
   */
  fn run() -> void {
    initWindow(); // Initialize the application window
    initVulkan(); // Initialize Vulkan
    mainLoop();   // Enter the main rendering loop

    cleanupSwapChain(); // Clean up swap chain resources

    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++)
      if (mUniformBuffersMapped[i]) {
        mDevice->unmapMemory(mUniformBuffersMemory[i].get());
        mUniformBuffersMapped[i] = nullptr;
      }

    // Shut down ImGui
    ImGui_ImplVulkan_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();
  }

 private:
  vkfw::UniqueInstance mVKFWInstance; ///< GLFW instance
  vkfw::UniqueWindow   mWindow;       ///< Application window
  vk::UniqueInstance   mInstance;     ///< Vulkan instance

  vk::UniqueDebugUtilsMessengerEXT mDebugMessenger; ///< Debug messenger

  vk::UniqueSurfaceKHR mSurface; ///< Vulkan surface for rendering

  vk::PhysicalDevice      mPhysicalDevice; ///< Physical GPU
  vk::SampleCountFlagBits mMsaaSamples;    ///< Multisampling count
  vk::UniqueDevice        mDevice;         ///< Logical Vulkan device

  vk::Queue mGraphicsQueue; ///< Queue for graphics commands
  vk::Queue mPresentQueue;  ///< Queue for presentation commands

  vk::UniqueSwapchainKHR             mSwapChain;             ///< Swap chain for frame buffering
  std::vector<vk::Image>             mSwapChainImages;       ///< Images in the swap chain
  vk::Format                         mSwapChainImageFormat;  ///< Format of swap chain images
  vk::Extent2D                       mSwapChainExtent;       ///< Dimensions of swap chain images
  std::vector<vk::UniqueImageView>   mSwapChainImageViews;   ///< Image views for swap chain images
  std::vector<vk::UniqueFramebuffer> mSwapChainFramebuffers; ///< Framebuffers for rendering

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

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

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

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

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

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

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

  vk::UniqueDescriptorPool       mDescriptorPool;      ///< Descriptor pool for the application
  vk::UniqueDescriptorPool       mImGuiDescriptorPool; ///< Separate descriptor pool for ImGui
  std::vector<vk::DescriptorSet> mDescriptorSets;      ///< Descriptor sets for binding resources

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

  std::vector<vk::UniqueSemaphore>
    mImageAvailableSemaphores; ///< Signals that an image is available for rendering
  std::vector<vk::UniqueSemaphore> mRenderFinishedSemaphores; ///< Signals that rendering has finished
  std::vector<vk::UniqueFence>     mInFlightFences;           ///< Ensures CPU-GPU synchronization

  bool mFramebufferResized = false; ///< Flag indicating if the framebuffer was resized
  u32  mCurrentFrame       = 0;     ///< Index of the current frame being rendered

  glm::mat4 mView; ///< View matrix

  /**
   * @brief Struct to store queue family indices.
   *
   * This struct contains the indices of the graphics and presentation queue families.
   */
  struct QueueFamilyIndices {
    std::optional<u32> graphics_family; ///< Index of graphics queue family
    std::optional<u32> present_family;  ///< Index of presentation queue family

    /**
     * @brief Check if all required queue families are found.
     *
     * @return True if both graphics and presentation families are found, false otherwise.
     */
    fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); }
  };

  /**
   * @brief Struct to hold swap chain support details.
   *
   * This struct contains information about the surface capabilities,
   * supported formats, and presentation modes.
   */
  struct SwapChainSupportDetails {
    vk::SurfaceCapabilitiesKHR        capabilities;  ///< Surface capabilities
    std::vector<vk::SurfaceFormatKHR> formats;       ///< Supported surface formats
    std::vector<vk::PresentModeKHR>   present_modes; ///< Supported presentation modes
  };

  /**
   * @brief Struct representing a uniform buffer object.
   *
   * This struct holds the model, view, and projection matrices for use in shaders.
   */
  struct UniformBufferObject {
    alignas(16) glm::mat4 model; ///< Model transformation matrix
    alignas(16) glm::mat4 view;  ///< View transformation matrix
    alignas(16) glm::mat4 proj;  ///< Projection matrix
  };

  struct Camera {
    glm::dvec3 position;
    glm::dvec3 up;
    glm::dvec3 look_at;
    f64        speed {};

    Camera() : position(2.0, 2.0, 2.0), up(0.0, 0.0, 1.0), look_at(0.0) {}

    [[nodiscard]] fn getViewMatrix() const -> glm::mat4 { return glm::lookAt(position, look_at, up); }

    fn moveForward(f64 deltaTime) -> void { position += (look_at - position) * deltaTime; }

    fn moveBackward(f64 deltaTime) -> void { position -= (look_at - position) * deltaTime; }

    fn moveLeft(f64 deltaTime) -> void {
      position -= glm::normalize(glm::cross((look_at - position), up)) * deltaTime;
    }

    fn moveRight(f64 deltaTime) -> void {
      position += glm::normalize(glm::cross((look_at - position), up)) * deltaTime;
    }
  };

  static fn processInput(vkfw::Window& window, Camera& camera, const f64& deltaTime) -> void {
    if (window.getKey(vkfw::Key::eW) == vkfw::eTrue)
      camera.moveForward(deltaTime);
    if (window.getKey(vkfw::Key::eA) == vkfw::eTrue)
      camera.moveLeft(deltaTime);
    if (window.getKey(vkfw::Key::eS) == vkfw::eTrue)
      camera.moveBackward(deltaTime);
    if (window.getKey(vkfw::Key::eD) == vkfw::eTrue)
      camera.moveRight(deltaTime);
  }

  /**
   * @brief Update the FPS counter in the window title.
   *
   * @param window The window to update.
   * @param baseTitle The base title string to prepend the FPS to.
   *
   * This function updates the window title with the current frames per second.
   */
  static fn updateFPS(const vkfw::Window& window, const string& baseTitle) -> void {
    static u32 FrameCount = 0;
    static f64 LastTime   = glfwGetTime();
    static f64 Fps        = 0.0;

    // Get the current time
    f64 currentTime = glfwGetTime();
    FrameCount++;

    // If one second has passed, calculate the FPS
    if (currentTime - LastTime >= 0.1) {
      Fps        = FrameCount / (currentTime - LastTime);
      LastTime   = currentTime;
      FrameCount = 0;

      // Update window title
      string newTitle = format("{} - {:.0F} FPS", baseTitle, Fps);
      glfwSetWindowTitle(window, newTitle.c_str());
    }
  }

  /**
   * @brief Initializes the application window using GLFW.
   *
   * This function performs the following tasks:
   * 1. Initializes the GLFW library.
   * 2. Creates a window with the specified dimensions and title.
   * 3. Sets up a callback for window resize events.
   *
   * The window is created without a default OpenGL context, as we'll be using Vulkan.
   */
  fn initWindow() -> void {
    // Initialize GLFW
    mVKFWInstance = vkfw::initUnique();

    // Set window creation hints
    vkfw::WindowHints hints { .clientAPI = vkfw::ClientAPI::eNone }; // No OpenGL context

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

    // Set the user pointer to this instance, allowing us to access it in callbacks
    mWindow->setUserPointer(this);

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

  /**
   * @brief Initializes Vulkan by setting up all necessary components.
   *
   * This function calls a series of helper functions to set up the Vulkan environment.
   * It creates and configures all the Vulkan objects needed for rendering, including:
   * - Vulkan instance
   * - Debug messenger (for validation layers)
   * - Surface (for presenting rendered images)
   * - Physical and logical devices
   * - Swap chain
   * - Render pass and graphics pipeline
   * - Buffers and images
   * - Synchronization objects
   *
   * The order of these function calls is important, as many Vulkan objects depend on
   * others that must be created first.
   */
  fn initVulkan() -> void {
    createInstance();            // Create the Vulkan instance
    setupDebugMessenger();       // Set up debug messaging (validation layers)
    createSurface();             // Create the window surface
    pickPhysicalDevice();        // Select a suitable GPU
    createLogicalDevice();       // Create a logical device from the chosen GPU
    createSwapChain();           // Create the swap chain for presenting images
    createImageViews();          // Create image views for the swap chain images
    createRenderPass();          // Set up the render pass
    createDescriptorSetLayout(); // Create the descriptor set layout
    createGraphicsPipeline();    // Create the graphics pipeline
    createCommandPool();         // Create a command pool for allocating command buffers
    createColorResources();      // Create resources for multisampling
    createDepthResources();      // Create resources for depth testing
    createFramebuffers();        // Create framebuffers for rendering
    createTextureImage();        // Load and create the texture image
    createTextureImageView();    // Create an image view for the texture
    createTextureSampler();      // Create a sampler for the texture
    loadModel();                 // Load the 3D model
    createVertexBuffer();        // Create a buffer for vertex data
    createIndexBuffer();         // Create a buffer for index data
    createUniformBuffers();      // Create uniform buffers for shader parameters
    createDescriptorPool();      // Create a descriptor pool
    createDescriptorSets();      // Allocate and update descriptor sets
    createCommandBuffers();      // Create command buffers for rendering commands
    createSyncObjects();         // Create synchronization objects (semaphores and fences)
    initImGui();                 // Initialize Dear ImGui for GUI rendering
  }

  /**
   * @brief Initializes the ImGui library for use with GLFW and Vulkan.
   *
   * This function sets up ImGui for rendering with Vulkan and GLFW. It creates the ImGui context,
   * sets up the style, initializes ImGui for GLFW and Vulkan, and creates a descriptor pool for ImGui.
   */
  fn initImGui() -> void {
    // Create ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();

    // Remember to use a reference here, otherwise
    // the ImGui::GetIO() function will return a
    // copy and the changes won't be saved.
    ImGuiIO& imGuiIO = ImGui::GetIO();

    // Disable writing imgui.ini
    imGuiIO.IniFilename = nullptr;

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

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

    vk::DescriptorPoolSize descriptorPoolSize = {
      .type            = vk::DescriptorType::eCombinedImageSampler,
      .descriptorCount = 1,
    };

    vk::DescriptorPoolCreateInfo poolInfo {
      .flags         = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet,
      .maxSets       = 1,
      .poolSizeCount = 1,
      .pPoolSizes    = &descriptorPoolSize,
    };

    mImGuiDescriptorPool = mDevice->createDescriptorPoolUnique(poolInfo);

    ImGui_ImplVulkan_InitInfo initInfo = {
      .Instance                    = mInstance.get(),
      .PhysicalDevice              = mPhysicalDevice,
      .Device                      = mDevice.get(),
      .QueueFamily                 = findQueueFamilies(mPhysicalDevice).graphics_family.value(),
      .Queue                       = mGraphicsQueue,
      .DescriptorPool              = mImGuiDescriptorPool.get(),
      .RenderPass                  = mRenderPass.get(),
      .MinImageCount               = MAX_FRAMES_IN_FLIGHT,
      .ImageCount                  = static_cast<u32>(mSwapChainImages.size()),
      .MSAASamples                 = static_cast<VkSampleCountFlagBits>(mMsaaSamples),
      .PipelineCache               = VK_NULL_HANDLE,
      .Subpass                     = 0,
      .UseDynamicRendering         = false,
      .PipelineRenderingCreateInfo = {},
      .Allocator                   = nullptr,
      .CheckVkResultFn             = nullptr,
      .MinAllocationSize           = 0,
    };

    ImGui_ImplVulkan_Init(&initInfo);
  }

  /**
   * @brief The main rendering loop of the application.
   *
   * This function contains the main loop that runs while the window is open. It continuously
   * polls for events and draws frames until the window is closed.
   */
  fn mainLoop() -> void {
    Camera camera;

    // Set initial time variables
    f64 lastFrame = 0.0;
    f64 deltaTime = 0.0;

    // While the window is open,
    while (!mWindow->shouldClose()) {
      // Calculate time between frames
      f64 currentFrame = vkfw::getTime();
      deltaTime        = currentFrame - lastFrame;
      lastFrame        = currentFrame;

      // Process input for camera movement
      processInput(mWindow.get(), camera, deltaTime);

      // Create view matrix from camera
      mView = camera.getViewMatrix();

      // Update the FPS counter
      updateFPS(mWindow.get(), "Vulkan");

      // Poll for events
      vkfw::pollEvents();

      // Draw a frame
      drawFrame();
    }

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

  /**
   * @brief Cleans up the swap chain resources.
   *
   * This function destroys the framebuffers and image views associated with the swap chain.
   */
  fn cleanupSwapChain() -> void {
    for (vk::UniqueFramebuffer& mSwapChainFramebuffer : mSwapChainFramebuffers) mSwapChainFramebuffer.reset();
    for (vk::UniqueImageView& mSwapChainImageView : mSwapChainImageViews) mSwapChainImageView.reset();

    mSwapChain.reset();
  }

  /**
   * @brief Recreates the swap chain.
   *
   * This function is called when the swap chain needs to be recreated, such as when the window is resized.
   * It cleans up the old swap chain and creates a new one with updated properties.
   */
  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();
  }

  /**
   * @brief Creates the Vulkan instance.
   *
   * This function sets up the Vulkan instance, including application info, extensions, and validation layers.
   */
  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   = "Vulkan App",
      .applicationVersion = 1,
      .pEngineName        = "No Engine",
      .engineVersion      = 1,
      .apiVersion         = vk::ApiVersion13,
    };

    // 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());
  }

  /**
   * @brief Sets up the debug messenger for Vulkan validation layers.
   *
   * This function creates a debug messenger that handles validation layer messages in debug builds.
   */
  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);
  }

  /**
   * @brief Creates the window surface for rendering.
   *
   * This function creates a Vulkan surface for the GLFW window, which is used for presenting rendered images.
   */
  fn createSurface() -> void { mSurface = vkfw::createWindowSurfaceUnique(mInstance.get(), mWindow.get()); }

  /**
   * @brief Selects a suitable physical device (GPU) for Vulkan.
   *
   * This function enumerates available physical devices and selects the first one that meets
   * the application's requirements.
   */
  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!");
  }

  /**
   * @brief Creates the logical device and retrieves queue handles.
   *
   * This function creates a logical device from the selected physical device, enabling required
   * features and retrieving handles to the graphics and presentation queues.
   */
  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 (const 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);
  }

  /**
   * @brief Creates the swap chain for image presentation.
   *
   * This function sets up the swap chain, which is a queue of images that can be presented to the screen.
   * It determines the format, presentation mode, and extent of the swap chain images.
   */
  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;
  }

  /**
   * @brief Creates image views for the swap chain images.
   *
   * This function creates a view for each image in the swap chain, which is used to access the image
   * contents.
   */
  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);
  }

  /**
   * @brief Creates the render pass.
   *
   * This function sets up the render pass, which describes the structure of rendering operations,
   * including the number and format of attachments used during rendering.
   */
  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);
  }

  /**
   * @brief Creates the descriptor set layout.
   *
   * This function defines the layout of descriptor sets used in the shader, specifying the types
   * of resources that will be accessed by the shader.
   */
  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);
  }

  /**
   * @brief Creates the graphics pipeline.
   *
   * This function sets up the entire graphics pipeline, including shader stages, vertex input,
   * input assembly, viewport, rasterization, multisampling, depth testing, color blending, and dynamic
   * states.
   */
  fn createGraphicsPipeline() -> void {
    std::vector<u32> vertShaderCode =
      ShaderCompiler::getCompiledShader(vertShaderSrc, shaderc_shader_kind::shaderc_vertex_shader, "vert");
    std::vector<u32> fragShaderCode =
      ShaderCompiler::getCompiledShader(fragShaderSrc, shaderc_shader_kind::shaderc_fragment_shader, "frag");

    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);
  }

  /**
   * @brief Creates framebuffers for the swap chain images.
   *
   * This function creates a framebuffer for each image view in the swap chain, attaching
   * the color, depth, and resolve attachments.
   */
  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);
    }
  }

  /**
   * @brief Creates the command pool.
   *
   * This function creates a command pool, which is used to manage the memory used to store
   * the buffers from which command buffer memory is allocated.
   */
  fn createCommandPool() -> void {
    QueueFamilyIndices queueFamilyIndices = findQueueFamilies(mPhysicalDevice);

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

    mCommandPool = mDevice->createCommandPoolUnique(poolInfo);
  }

  /**
   * @brief Creates resources for color attachment.
   *
   * This function creates the image, memory, and view for the color attachment used in multisampling.
   */
  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);
  }

  /**
   * @brief Creates resources for depth attachment.
   *
   * This function creates the image, memory, and view for the depth attachment used in depth testing.
   */
  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);
  }

  /**
   * @brief Finds a supported format from a list of candidates.
   *
   * @param candidates A vector of candidate formats to check.
   * @param tiling The desired tiling arrangement of the format.
   * @param features The required format features.
   * @return The first supported format from the list of candidates.
   *
   * This function iterates through a list of candidate formats and returns the first one
   * that is supported with the specified tiling and features.
   */
  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!");
  }

  /**
   * @brief Finds a supported depth format.
   *
   * @return A supported depth format.
   *
   * This function tries to find a supported depth format from a predefined list of candidates.
   */
  fn findDepthFormat() -> vk::Format {
    return findSupportedFormat(
      { vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint },
      vk::ImageTiling::eOptimal,
      vk::FormatFeatureFlagBits::eDepthStencilAttachment
    );
  }

  /**
   * @brief Checks if a format has a stencil component.
   *
   * @param format The format to check.
   * @return True if the format has a stencil component, false otherwise.
   */
  static fn hasStencilComponent(const vk::Format& format) {
    return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint;
  }

  /**
   * @brief Creates the texture image.
   *
   * This function loads an image from a file, creates a staging buffer, transfers the image data
   * to the staging buffer, creates the final texture image, and copies the data from the staging
   * buffer to the texture image. It also generates mipmaps for the texture.
   */
  fn createTextureImage() -> void {
    std::filesystem::path texturePath = std::filesystem::current_path() / "textures" / "viking_room.png";
    stb::UniqueImage      image(texturePath);

    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);
  }

  /**
   * @brief Generates mipmaps for a texture image.
   *
   * @param image The image for which to generate mipmaps.
   * @param imageFormat The format of the image.
   * @param texWidth The width of the texture.
   * @param texHeight The height of the texture.
   * @param mipLevels The number of mipmap levels to generate.
   *
   * This function generates mipmaps for the given texture image by repeatedly scaling down
   * the image by half until reaching the smallest mip level.
   */
  fn generateMipmaps(
    const vk::Image&  image,
    const vk::Format& imageFormat,
    const i32&        texWidth,
    const i32&        texHeight,
    const 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);
  }

  /**
   * @brief Gets the maximum usable sample count for multisampling.
   *
   * @return The maximum sample count supported by the device for both color and depth.
   *
   * This function determines the highest sample count that is supported by the device
   * for both color and depth attachments.
   */
  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;
  }

  /**
   * @brief Creates the texture image view.
   *
   * This function creates an image view for the texture image, which can be used
   * to access the texture in shaders.
   */
  fn createTextureImageView() -> void {
    mTextureImageView = createImageView(
      mTextureImage.get(), vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mMipLevels
    );
  }

  /**
   * @brief Creates the texture sampler.
   *
   * This function creates a sampler object that defines how the texture should be sampled in shaders.
   */
  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);
  }

  /**
   * @brief Creates a Vulkan image view.
   *
   * This function creates and returns a unique Vulkan image view using the provided parameters.
   *
   * @param image The Vulkan image for which to create the view.
   * @param format The format of the image.
   * @param aspectFlags The aspect flags for the image view.
   * @param mipLevels The number of mip levels for the image view.
   *
   * @return vk::UniqueImageView A unique handle to the created Vulkan image view.
   *
   * @details
   * The function creates an image view with the following properties:
   * - 2D view type
   * - Subresource range starting from base mip level 0
   * - Single array layer starting from base array layer 0
   */
  fn createImageView(
    const vk::Image&            image,
    const vk::Format&           format,
    const vk::ImageAspectFlags& aspectFlags,
    const u32&                  mipLevels
  ) -> vk::UniqueImageView {
    return mDevice->createImageViewUnique({
      .image            = image,
      .viewType         = vk::ImageViewType::e2D,
      .format           = format,
      .subresourceRange = {
                           .aspectMask     = aspectFlags,
                           .baseMipLevel   = 0,
                           .levelCount     = mipLevels,
                           .baseArrayLayer = 0,
                           .layerCount     = 1,
                           },
    });
  }

  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);
  }

  /**
   * @brief Loads the 3D model.
   *
   * This function loads a 3D model from an OBJ file, extracting vertex and index data.
   * It also removes duplicate vertices to optimize the model.
   */
  fn loadModel() -> void {
    tinyobj::attrib_t                attrib;
    std::vector<tinyobj::shape_t>    shapes;
    std::vector<tinyobj::material_t> materials;
    string                           warn, err;

    std::filesystem::path modelPath = std::filesystem::current_path() / "models" / "viking_room.obj";

    if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, modelPath.string().c_str()))
      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]);
      }
    }
  }

  /**
   * @brief Creates the vertex buffer.
   *
   * This function creates a vertex buffer on the GPU and transfers vertex data from CPU memory.
   * It uses a staging buffer for the transfer to allow for better performance.
   */
  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();
  }

  /**
   * @brief Creates the index buffer.
   *
   * This function creates an index buffer on the GPU and transfers index data from CPU memory.
   * It uses a staging buffer for the transfer to allow for better performance.
   */
  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();
  }

  /**
   * @brief Creates uniform buffers.
   *
   * This function creates uniform buffers for each frame in flight. These buffers are used
   * to pass uniform data (like transformation matrices) to shaders.
   */
  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);
    }
  }

  /**
   * @brief Creates the descriptor pool.
   *
   * This function creates a descriptor pool from which descriptor sets can be allocated.
   * The pool is sized to accommodate the number of frames in flight.
   */
  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);
  }

  /**
   * @brief Creates descriptor sets.
   *
   * This function allocates and updates descriptor sets for each frame in flight.
   * These sets bind the uniform buffers and texture sampler to the shader.
   */
  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, {});
    }
  }

  /**
   * @brief Creates a Vulkan buffer.
   *
   * @param deviceSize The size of the buffer to create.
   * @param bufferUsageFlags The usage flags for the buffer.
   * @param memoryPropertyFlags The desired properties of the memory to be allocated.
   * @return A pair containing the created buffer and its associated device memory.
   *
   * This function creates a Vulkan buffer with the specified size, usage, and memory properties.
   */
  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) };
  }

  /**
   * @brief Begins a single-time command buffer.
   *
   * @return A command buffer ready for recording commands.
   *
   * This function allocates and begins a command buffer for one-time use operations.
   */
  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;
  }

  /**
   * @brief Ends and submits a single-time command buffer.
   *
   * @param commandBuffer The command buffer to end and submit.
   *
   * This function ends the recording of a single-time command buffer, submits it to the queue,
   * and waits for it to complete before freeing the 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);
  }

  /**
   * @brief Copies data to a mapped memory.
   *
   * @param stagingBufferMemory The device memory to copy to.
   * @param bufferSize The size of the data to copy.
   * @param src Pointer to the source data.
   *
   * This function maps a memory, copies data to it, and then unmaps the memory.
   */
  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);
  }

  /**
   * @brief Copies data from one buffer to another.
   *
   * @param srcBuffer The source buffer.
   * @param dstBuffer The destination buffer.
   * @param deviceSize The size of data to copy.
   *
   * This function records and submits a command to copy data between two buffers.
   */
  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);
  }

  /**
   * @brief Finds a memory type index that satisfies the given properties.
   *
   * @param typeFilter A bit field of memory types that are suitable for the buffer.
   * @param properties The properties the memory type must have.
   * @return The index of a suitable memory type.
   *
   * This function finds a memory type that satisfies both the type filter and the desired properties.
   */
  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!");
  }

  /**
   * @brief Creates command buffers.
   *
   * This function allocates command buffers from the command pool. One command buffer is
   * allocated for each frame in flight.
   */
  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);
  }

  /**
   * @brief Records a command buffer.
   *
   * @param commandBuffer The command buffer to record into.
   * @param imageIndex The index of the swap chain image to render to.
   *
   * This function records drawing commands into the given 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();
  }

  /**
   * @brief Creates synchronization objects for frame rendering.
   *
   * This function creates semaphores and fences used for synchronizing operations
   * between the CPU and GPU, and between different stages of rendering.
   */
  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);
    }
  }

  /**
   * @brief Updates the uniform buffer for the current frame.
   *
   * @param currentImage The index of the current swap chain image.
   *
   * This function updates the uniform buffer object (UBO) with new transformation
   * matrices for each frame. It calculates a new model matrix based on time,
   * and updates the view and projection matrices.
   */
  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 = mView,
      // 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,
        100.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));
  }

  /**
   * @brief Renders a single frame.
   *
   * This function performs all the steps necessary to render a single frame:
   * 1. Waits for the previous frame to finish
   * 2. Acquires an image from the swap chain
   * 3. Records a command buffer
   * 4. Submits the command buffer
   * 5. Presents the swap chain image
   */
  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;
    }
  }

  /**
   * @brief Creates a shader module from compiled shader code.
   *
   * @param code A vector of chars containing the compiled shader code.
   * @return A unique shader module.
   *
   * This function takes compiled shader code and creates a Vulkan shader module from it.
   */
  fn createShaderModule(const std::vector<u32>& code) -> vk::UniqueShaderModule {
    vk::ShaderModuleCreateInfo createInfo {
      .codeSize = code.size() * sizeof(u32),
      .pCode    = code.data(),
    };

    return mDevice->createShaderModuleUnique(createInfo);
  }

  /**
   * @brief Chooses the best surface format for the swap chain.
   *
   * @param availableFormats A vector of available surface formats.
   * @return The chosen surface format.
   *
   * This function selects the best surface format from the available options,
   * preferring SRGB color space when available.
   */
  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];
  }

  /**
   * @brief Chooses the best presentation mode for the swap chain.
   *
   * @param availablePresentModes A vector of available presentation modes.
   * @return The chosen presentation mode.
   *
   * This function selects the best presentation mode from the available options,
   * preferring mailbox mode (triple buffering) when available.
   */
  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;
  }

  /**
   * @brief Chooses the swap extent (resolution) for the swap chain.
   *
   * @param capabilities The surface capabilities of the device.
   * @return The chosen swap extent.
   *
   * This function determines the resolution of the swap chain images,
   * taking into account the current window size and device limitations.
   */
  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),
    };
  }

  /**
   * @brief Queries the swap chain support details for a physical device.
   *
   * @param device The physical device to query.
   * @return A SwapChainSupportDetails struct containing the support information.
   *
   * This function retrieves information about the swap chain support,
   * including surface capabilities, formats, and presentation modes.
   */
  fn querySwapChainSupport(const vk::PhysicalDevice& device) -> SwapChainSupportDetails {
    return {
      .capabilities  = device.getSurfaceCapabilitiesKHR(mSurface.get()),
      .formats       = device.getSurfaceFormatsKHR(mSurface.get()),
      .present_modes = device.getSurfacePresentModesKHR(mSurface.get()),
    };
  }

  /**
   * @brief Checks if a physical device is suitable for the application.
   *
   * @param device The physical device to check.
   * @return True if the device is suitable, false otherwise.
   *
   * This function checks if a physical device meets all the requirements
   * of the application, including queue families, extensions, and features.
   */
  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;
  }

  /**
   * @brief Checks if a device supports all required extensions.
   *
   * @param device The physical device to check.
   * @return True if all required extensions are supported, false otherwise.
   *
   * This function verifies that a physical device supports all the
   * extensions required by the application.
   */
  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();
  }

  /**
   * @brief Finds queue families that support required operations.
   *
   * @param device The physical device to check.
   * @return A QueueFamilyIndices struct with the found queue family indices.
   *
   * This function finds queue families that support graphics operations
   * and presentation to the window surface.
   */
  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;
  }

  /**
   * @brief Checks if all requested validation layers are available.
   *
   * @return True if all validation layers are available, false otherwise.
   *
   * This function verifies that all requested validation layers are
   * supported by the Vulkan implementation.
   */
  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
  }

  /**
   * @brief Debug callback function for Vulkan validation layers.
   *
   * @param pCallbackData Pointer to a structure containing the message details.
   * @return vk::False to indicate the call should not be aborted.
   *
   * This function is called by Vulkan to report debug messages from
   * validation layers. It prints the message to the console.
   */
  static VKAPI_ATTR fn VKAPI_CALL debugCallback(
    VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/,
    VkDebugUtilsMessageTypeFlagsEXT /*messageType*/,
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
    void* /*pUserData*/
  ) -> 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;
  }
};

/**
 * @brief The main function of the application.
 *
 * @return 0 if the application runs successfully, non-zero otherwise.
 *
 * This function initializes the Vulkan dynamic dispatcher, creates an instance
 * of the VulkanApp class, and runs the application. It catches and reports any
 * exceptions that occur during execution.
 */
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;
}
