#include <chrono>
#include <fmt/format.h>
#include <fstream>
#include <iostream>
#include <set>

#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/hash.hpp>

#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>

#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
#define VK_ENABLE_BETA_EXTENSIONS
#define VULKAN_HPP_NO_CONSTRUCTORS
#include <vulkan/vulkan.hpp>

VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE

#include "util/types.h"
#include "util/unique_image.h"

#define VKFW_NO_STD_FUNCTION_CALLBACKS
#include "vkfw.hpp"

constexpr i32 WIDTH  = 800;
constexpr i32 HEIGHT = 600;

constexpr const char* MODEL_PATH   = "models/viking_room.obj";
constexpr const char* TEXTURE_PATH = "textures/viking_room.png";

constexpr i32 MAX_FRAMES_IN_FLIGHT = 2;

constexpr std::array<const char*, 1> validationLayers = { "VK_LAYER_KHRONOS_validation" };

#ifdef __APPLE__
constexpr std::array<const char*, 2> deviceExtensions = { vk::KHRSwapchainExtensionName,
                                                          vk::KHRPortabilitySubsetExtensionName };
#else
constexpr std::array<const char*, 1> deviceExtensions = { vk::KHRSwapchainExtensionName };
#endif

#ifdef NDEBUG
constexpr bool enableValidationLayers = false;
#else
constexpr bool enableValidationLayers = true;
#endif

struct Vertex {
  glm::vec3 pos;
  glm::vec3 color;
  glm::vec2 tex_coord;

  static fn getBindingDescription() -> vk::VertexInputBindingDescription {
    return { .binding = 0, .stride = sizeof(Vertex), .inputRate = vk::VertexInputRate::eVertex };
  }

  static fn getAttributeDescriptions() -> std::array<vk::VertexInputAttributeDescription, 3> {
    using namespace vk;

    return {
      VertexInputAttributeDescription { 0, 0, Format::eR32G32B32Sfloat, offsetof(Vertex,       pos) },
      VertexInputAttributeDescription { 1, 0, Format::eR32G32B32Sfloat, offsetof(Vertex,     color) },
      VertexInputAttributeDescription { 2, 0,    Format::eR32G32Sfloat, offsetof(Vertex, tex_coord) }
    };
  }

  fn operator==(const Vertex& other) const->bool {
    return pos == other.pos && color == other.color && tex_coord == other.tex_coord;
  }
};

namespace std {
  template <>
  struct hash<Vertex> {
    fn operator()(Vertex const& vertex) const->size_t {
      return ((hash<glm::vec3>()(vertex.pos) ^ (hash<glm::vec3>()(vertex.color) << 1)) >> 1) ^
             (hash<glm::vec2>()(vertex.tex_coord) << 1);
    }
  };
}

class VulkanApp {
 public:
  fn run() -> void {
    initWindow();
    initVulkan();
    mainLoop();
  }

 private:
  vkfw::UniqueInstance mGLFWInstance;
  vkfw::UniqueWindow   mWindow;

  vk::UniqueInstance mInstance;

  vk::UniqueDebugUtilsMessengerEXT mDebugMessenger;
  vk::UniqueSurfaceKHR             mSurface;

  vk::PhysicalDevice      mPhysicalDevice;
  vk::SampleCountFlagBits mMsaaSamples = vk::SampleCountFlagBits::e1;
  vk::UniqueDevice        mDevice;

  vk::Queue mGraphicsQueue;
  vk::Queue mPresentQueue;

  vk::UniqueSwapchainKHR             mSwapChain;
  std::vector<vk::Image>             mSwapChainImages;
  vk::Format                         mSwapChainImageFormat;
  vk::Extent2D                       mSwapChainExtent;
  std::vector<vk::UniqueImageView>   mSwapChainImageViews;
  std::vector<vk::UniqueFramebuffer> mSwapChainFramebuffers;

  vk::UniqueRenderPass          mRenderPass;
  vk::UniqueDescriptorSetLayout mDescriptorSetLayout;
  vk::UniquePipelineLayout      mPipelineLayout;
  vk::UniquePipeline            mGraphicsPipeline;

  vk::UniqueCommandPool mCommandPool;

  vk::UniqueImage        mColorImage;
  vk::UniqueDeviceMemory mColorImageMemory;
  vk::UniqueImageView    mColorImageView;

  vk::UniqueImage        mDepthImage;
  vk::UniqueDeviceMemory mDepthImageMemory;
  vk::UniqueImageView    mDepthImageView;

  u32                    mMipLevels;
  vk::UniqueImage        mTextureImage;
  vk::UniqueDeviceMemory mTextureImageMemory;
  vk::UniqueImageView    mTextureImageView;
  vk::UniqueSampler      mTextureSampler;

  std::vector<Vertex>    mVertices;
  std::vector<u32>       mIndices;
  vk::UniqueBuffer       mVertexBuffer;
  vk::UniqueDeviceMemory mVertexBufferMemory;
  vk::UniqueBuffer       mIndexBuffer;
  vk::UniqueDeviceMemory mIndexBufferMemory;

  std::vector<vk::UniqueBuffer>       mUniformBuffers;
  std::vector<vk::UniqueDeviceMemory> mUniformBuffersMemory;
  std::vector<void*>                  mUniformBuffersMapped;

  vk::UniqueDescriptorPool       mDescriptorPool;
  std::vector<vk::DescriptorSet> mDescriptorSets;

  std::vector<vk::UniqueCommandBuffer> mCommandBuffers;

  std::vector<vk::UniqueSemaphore> mImageAvailableSemaphores;
  std::vector<vk::UniqueSemaphore> mRenderFinishedSemaphores;
  std::vector<vk::UniqueFence>     mInFlightFences;

  bool mFramebufferResized = false;
  u32  mCurrentFrame       = 0;

  struct QueueFamilyIndices {
    std::optional<u32> graphics_family;
    std::optional<u32> present_family;

    fn isComplete() -> bool { return graphics_family.has_value() && present_family.has_value(); }
  };

  struct SwapChainSupportDetails {
    vk::SurfaceCapabilitiesKHR        capabilities;
    std::vector<vk::SurfaceFormatKHR> formats;
    std::vector<vk::PresentModeKHR>   present_modes;
  };

  struct UniformBufferObject {
    alignas(16) glm::mat4 model;
    alignas(16) glm::mat4 view;
    alignas(16) glm::mat4 proj;
  };

  static fn readFile(const std::string& filename) -> std::vector<char> {
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open())
      throw std::runtime_error("Failed to open file! " + filename);

    usize             fileSize = static_cast<usize>(file.tellg());
    std::vector<char> buffer(fileSize);

    file.seekg(0);
    file.read(buffer.data(), static_cast<std::streamsize>(fileSize));

    file.close();

    return buffer;
  }

  fn initWindow() -> void {
    mGLFWInstance = vkfw::initUnique();

    vkfw::WindowHints hints;

    hints.clientAPI = vkfw::ClientAPI::eNone;

    mWindow = vkfw::createWindowUnique(WIDTH, HEIGHT, "Vulkan", hints);
    mWindow->setUserPointer(this);
    mWindow->setFramebufferSizeCallback(framebufferResizeCallback);
  }

  static fn framebufferResizeCallback(GLFWwindow* window, int /*width*/, int /*height*/) -> void {
    auto* app = std::bit_cast<VulkanApp*>(glfwGetWindowUserPointer(window));

    app->mFramebufferResized = true;
  }

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

  fn mainLoop() -> void {
    while (!mWindow->shouldClose()) {
      vkfw::pollEvents();
      drawFrame();
    }

    mDevice->waitIdle();
  }

  fn cleanupSwapChain() -> void {
    for (vk::UniqueFramebuffer& mSwapChainFramebuffer : mSwapChainFramebuffers) mSwapChainFramebuffer.reset();
    for (vk::UniqueImageView& mSwapChainImageView : mSwapChainImageViews) mSwapChainImageView.reset();

    mSwapChain.reset();
  }

  fn recreateSwapChain() -> void {
    u32 width = 0, height = 0;
    std::tie(width, height) = mWindow->getFramebufferSize();

    while (width == 0 || height == 0) {
      std::tie(width, height) = mWindow->getFramebufferSize();
      vkfw::waitEvents();
    }

    mDevice->waitIdle();

    cleanupSwapChain();

    createSwapChain();
    createImageViews();
    createColorResources();
    createDepthResources();
    createFramebuffers();
  }

  fn createInstance() -> void {
    if (enableValidationLayers && !checkValidationLayerSupport())
      throw std::runtime_error("Validation layers requested, but not available!");

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

    // Retrieve extensions using custom function
    std::vector<const char*> extensions = getRequiredExtensions();

#ifdef __APPLE__
    // Enable the portability extension and set flags
    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

    vk::InstanceCreateInfo createInfo {
#ifdef __APPLE__
      .flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
#endif
      .pApplicationInfo        = &appInfo,
      .enabledLayerCount       = enableValidationLayers ? static_cast<u32>(validationLayers.size()) : 0,
      .ppEnabledLayerNames     = enableValidationLayers ? validationLayers.data() : nullptr,
      .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

    mInstance = vk::createInstanceUnique(createInfo);

    VULKAN_HPP_DEFAULT_DISPATCHER.init(mInstance.get());
  }

  fn setupDebugMessenger() -> void {
    if (!enableValidationLayers)
      return;

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

  fn createSurface() -> void { mSurface = vkfw::createWindowSurfaceUnique(mInstance.get(), mWindow.get()); }

  fn pickPhysicalDevice() -> void {
    std::vector<vk::PhysicalDevice> devices = mInstance->enumeratePhysicalDevices();

    if (devices.empty())
      throw std::runtime_error("Failed to find GPUs with Vulkan support!");

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

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

      if (isDeviceSuitable(device)) {
        mPhysicalDevice = device;
        mMsaaSamples    = getMaxUsableSampleCount();
        break;
      }
    }

    if (!mPhysicalDevice)
      throw std::runtime_error("Failed to find a suitable GPU!");
  }

  fn createLogicalDevice() -> void {
    QueueFamilyIndices qfIndices = findQueueFamilies(mPhysicalDevice);

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

    f32 queuePriority = 1.0F;

    for (u32 queueFamily : uniqueQueueFamilies) {
      vk::DeviceQueueCreateInfo queueCreateInfo { .queueFamilyIndex = queueFamily,
                                                  .queueCount       = 1,
                                                  .pQueuePriorities = &queuePriority };

      queueCreateInfos.emplace_back(queueCreateInfo);
    }

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

    mDevice        = mPhysicalDevice.createDeviceUnique(createInfo);
    mGraphicsQueue = mDevice->getQueue(qfIndices.graphics_family.value(), 0);
    mPresentQueue  = mDevice->getQueue(qfIndices.present_family.value(), 0);
  }

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

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

    u32 imageCount = swapChainSupport.capabilities.minImageCount + 1;

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

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

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

    mSwapChain = mDevice->createSwapchainKHRUnique(createInfo);

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

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

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

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

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

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

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

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

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

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

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

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

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

    mRenderPass = mDevice->createRenderPassUnique(renderPassInfo);
  }

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

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

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

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

    mDescriptorSetLayout = mDevice->createDescriptorSetLayoutUnique(layoutInfo);
  }

  fn createGraphicsPipeline() -> void {
    std::vector<char> vertShaderCode = readFile("shaders/vert.spv");
    std::vector<char> fragShaderCode = readFile("shaders/frag.spv");

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    mPipelineLayout = mDevice->createPipelineLayoutUnique(pipelineLayoutInfo);

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

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

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

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

    mGraphicsPipeline = std::move(graphicsPipelineValue);
  }

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

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

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

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

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

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

    mCommandPool = mDevice->createCommandPoolUnique(poolInfo);
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    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,
      mTextureImage,
      mTextureImageMemory
    );

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

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

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

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

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

    vk::UniqueCommandBuffer 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> {
                           { { .x = 0, .y = 0, .z = 0 },
              { .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(std::move(commandBuffer));
  }

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

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

    if (counts & vk::SampleCountFlagBits::e64)
      return vk::SampleCountFlagBits::e64;
    if (counts & vk::SampleCountFlagBits::e32)
      return vk::SampleCountFlagBits::e32;
    if (counts & vk::SampleCountFlagBits::e16)
      return vk::SampleCountFlagBits::e16;
    if (counts & vk::SampleCountFlagBits::e8)
      return vk::SampleCountFlagBits::e8;
    if (counts & vk::SampleCountFlagBits::e4)
      return vk::SampleCountFlagBits::e4;
    if (counts & vk::SampleCountFlagBits::e2)
      return vk::SampleCountFlagBits::e2;

    return vk::SampleCountFlagBits::e1;
  }

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

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

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

    mTextureSampler = mDevice->createSamplerUnique(samplerInfo);
  }

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

    return mDevice->createImageViewUnique(viewInfo);
  }

  fn createImage(
    u32                     width,
    u32                     height,
    u32                     mipLevels,
    vk::SampleCountFlagBits numSamples,
    vk::Format              format,
    vk::ImageTiling         tiling,
    vk::ImageUsageFlags     usage,
    vk::MemoryPropertyFlags properties,
    vk::UniqueImage&        image,
    vk::UniqueDeviceMemory& imageMemory
  ) -> void {
    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,
    };

    image = mDevice->createImageUnique(imageInfo);

    vk::MemoryRequirements memRequirements = mDevice->getImageMemoryRequirements(image.get());

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

    imageMemory = mDevice->allocateMemoryUnique(allocInfo);

    mDevice->bindImageMemory(image.get(), imageMemory.get(), 0);
  }

  fn transitionImageLayout(
    vk::Image       image,
    vk::ImageLayout oldLayout,
    vk::ImageLayout newLayout,
    u32             mipLevels
  ) -> void {
    vk::UniqueCommandBuffer commandBuffer = beginSingleTimeCommands();

    vk::ImageMemoryBarrier barrier {
      .oldLayout           = oldLayout,
      .newLayout           = newLayout,
      .srcQueueFamilyIndex = vk::QueueFamilyIgnored,
      .dstQueueFamilyIndex = vk::QueueFamilyIgnored,
      .image               = image,
      // clang-format off
      .subresourceRange    = { .aspectMask     = vk::ImageAspectFlagBits::eColor,
                               .baseMipLevel   = 0,
                               .levelCount     = mipLevels,
                               .baseArrayLayer = 0,
                               .layerCount     = 1 }  // clang-format on
    };

    vk::PipelineStageFlags sourceStage;
    vk::PipelineStageFlags destinationStage;

    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 {
      throw std::invalid_argument("Unsupported layout transition!");
    }

    commandBuffer->pipelineBarrier(sourceStage, destinationStage, {}, {}, {}, barrier);

    endSingleTimeCommands(std::move(commandBuffer));
  }

  fn copyBufferToImage(vk::Buffer buffer, vk::Image image, u32 width, u32 height) -> void {
    vk::UniqueCommandBuffer 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(std::move(commandBuffer));
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    mDescriptorPool = mDevice->createDescriptorPoolUnique(poolInfo);
  }

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

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

    mDescriptorSets = mDevice->allocateDescriptorSets(allocInfo);

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

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

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

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

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

    buffer = mDevice->createBufferUnique(bufferInfo);

    vk::MemoryRequirements memRequirements = mDevice->getBufferMemoryRequirements(buffer.get());

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

    bufferMemory = mDevice->allocateMemoryUnique(allocInfo);

    mDevice->bindBufferMemory(buffer.get(), bufferMemory.get(), 0);
  }

  fn beginSingleTimeCommands() -> vk::UniqueCommandBuffer {
    vk::CommandBufferAllocateInfo allocInfo {
      .commandPool        = mCommandPool.get(),
      .level              = vk::CommandBufferLevel::ePrimary,
      .commandBufferCount = 1,
    };

    vk::UniqueCommandBuffer commandBuffer = std::move(mDevice->allocateCommandBuffersUnique(allocInfo)[0]);

    vk::CommandBufferBeginInfo beginInfo { .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit };

    commandBuffer->begin(beginInfo);

    return commandBuffer;
  }

  fn endSingleTimeCommands(vk::UniqueCommandBuffer commandBuffer) -> void {
    commandBuffer->end();

    vk::SubmitInfo submitInfo { .commandBufferCount = 1, .pCommandBuffers = &commandBuffer.get() };

    mGraphicsQueue.submit(submitInfo, nullptr);
    mGraphicsQueue.waitIdle();
  }

  fn copyData(vk::DeviceMemory stagingBufferMemory, vk::DeviceSize bufferSize, const void* src) -> void {
    void* data = mDevice->mapMemory(stagingBufferMemory, 0, bufferSize);
    memcpy(data, src, static_cast<usize>(bufferSize));
    mDevice->unmapMemory(stagingBufferMemory);
  }

  fn copyBuffer(vk::Buffer srcBuffer, vk::Buffer dstBuffer, vk::DeviceSize deviceSize) -> void {
    vk::UniqueCommandBuffer commandBuffer = beginSingleTimeCommands();

    vk::BufferCopy copyRegion { .size = deviceSize };

    commandBuffer->copyBuffer(srcBuffer, dstBuffer, 1, &copyRegion);

    endSingleTimeCommands(std::move(commandBuffer));
  }

  fn findMemoryType(u32 typeFilter, vk::MemoryPropertyFlags properties) -> u32 {
    vk::PhysicalDeviceMemoryProperties memProperties = mPhysicalDevice.getMemoryProperties();

    for (u32 i = 0; i < memProperties.memoryTypeCount; i++)
      if ((typeFilter & (1 << i)) &&
          (memProperties.memoryTypes.at(i).propertyFlags & properties) == properties)
        return i;

    throw std::runtime_error("Failed to find a suitable memory type!");
  }

  fn createCommandBuffers() -> void {
    mCommandBuffers.resize(MAX_FRAMES_IN_FLIGHT);

    vk::CommandBufferAllocateInfo allocInfo { .commandPool = mCommandPool.get(),
                                              .level       = vk::CommandBufferLevel::ePrimary,
                                              .commandBufferCount =
                                                static_cast<u32>(mCommandBuffers.size()) };

    mCommandBuffers = mDevice->allocateCommandBuffersUnique(allocInfo);
  }

  fn recordCommandBuffer(vk::CommandBuffer commandBuffer, u32 imageIndex) -> void {
    vk::CommandBufferBeginInfo beginInfo {};

    commandBuffer.begin(beginInfo);

    std::array<vk::ClearValue, 2> clearValues {
      { { .color = { .float32 = std::array<float, 4> { 0.0F, 0.0F, 0.0F, 1.0F } } },
       { .depthStencil = { .depth = 1.0F, .stencil = 0 } } }
    };

    vk::RenderPassBeginInfo renderPassInfo {
      .renderPass      = mRenderPass.get(),
      .framebuffer     = mSwapChainFramebuffers[imageIndex].get(),
      .renderArea      = { .offset = { .x = 0, .y = 0 }, .extent = mSwapChainExtent },
      .clearValueCount = static_cast<u32>(clearValues.size()),
      .pClearValues    = clearValues.data()
    };

    commandBuffer.beginRenderPass(renderPassInfo, vk::SubpassContents::eInline);
    commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, mGraphicsPipeline.get());

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

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

    commandBuffer.setViewport(0, viewport);
    commandBuffer.setScissor(0, scissor);

    commandBuffer.bindVertexBuffers(0, mVertexBuffer.get(), { 0 });

    commandBuffer.bindIndexBuffer(mIndexBuffer.get(), 0, vk::IndexType::eUint32);

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

    commandBuffer.drawIndexed(static_cast<u32>(mIndices.size()), 1, 0, 0, 0);

    commandBuffer.endRenderPass();

    commandBuffer.end();
  }

  fn createSyncObjects() -> void {
    mImageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    mRenderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    mInFlightFences.resize(MAX_FRAMES_IN_FLIGHT);

    vk::SemaphoreCreateInfo semaphoreInfo {};
    vk::FenceCreateInfo     fenceInfo { .flags = vk::FenceCreateFlagBits::eSignaled };

    for (usize idx = 0; idx < MAX_FRAMES_IN_FLIGHT; idx++) {
      mImageAvailableSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo);
      mRenderFinishedSemaphores[idx] = mDevice->createSemaphoreUnique(semaphoreInfo);
      mInFlightFences[idx]           = mDevice->createFenceUnique(fenceInfo);
    }
  }

  fn updateUniformBuffer(u32 currentImage) -> void {
    static auto StartTime = std::chrono::high_resolution_clock::now();

    auto currentTime = std::chrono::high_resolution_clock::now();
    f32  time = std::chrono::duration<f32, std::chrono::seconds::period>(currentTime - StartTime).count();

    UniformBufferObject ubo {
      .model = glm::rotate(glm::mat4(1.0F), time * glm::radians(90.0F), glm::vec3(0.0F, 0.0F, 1.0F)),
      .view =
        glm::lookAt(glm::vec3(2.0F, 2.0F, 2.0F), glm::vec3(0.0F, 0.0F, 0.0F), glm::vec3(0.0F, 0.0F, 1.0F)),
      .proj = glm::perspective(
        glm::radians(45.0F),
        static_cast<f32>(mSwapChainExtent.width) / static_cast<f32>(mSwapChainExtent.height),
        0.1F,
        10.0F
      )
    };

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

    memcpy(mUniformBuffersMapped[currentImage], &ubo, sizeof(ubo));
  }

  fn drawFrame() -> void {
    try {
      vk::Result result =
        mDevice->waitForFences(mInFlightFences[mCurrentFrame].get(), vk::Bool32(vk::True), UINT64_MAX);

      if (result != vk::Result::eSuccess)
        throw std::runtime_error("Failed to wait for fences!");

      vk::Result imageIndexResult = vk::Result::eSuccess;
      u32        imageIndexValue  = 0;

      std::tie(imageIndexResult, imageIndexValue) = mDevice->acquireNextImageKHR(
        mSwapChain.get(), UINT64_MAX, mImageAvailableSemaphores[mCurrentFrame].get(), nullptr
      );

      if (imageIndexResult == vk::Result::eErrorOutOfDateKHR) {
        recreateSwapChain();
        return;
      }

      if (imageIndexResult != vk::Result::eSuccess && imageIndexResult != vk::Result::eSuboptimalKHR)
        throw std::runtime_error("Failed to acquire swap chain image!");

      updateUniformBuffer(mCurrentFrame);

      mDevice->resetFences(mInFlightFences[mCurrentFrame].get());

      mCommandBuffers[mCurrentFrame]->reset(vk::CommandBufferResetFlagBits::eReleaseResources);
      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(),
      };

      mGraphicsQueue.submit(submitInfo, mInFlightFences[mCurrentFrame].get());

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

      vk::Result presentResult = mPresentQueue.presentKHR(presentInfo);

      if (presentResult == vk::Result::eErrorOutOfDateKHR || presentResult == vk::Result::eSuboptimalKHR ||
          mFramebufferResized) {
        mFramebufferResized = false;
        recreateSwapChain();
      } else if (presentResult != vk::Result::eSuccess)
        throw std::runtime_error("Failed to present swap chain image!");

      mCurrentFrame = (mCurrentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
    } catch (vk::OutOfDateKHRError& /*err*/) {
      mFramebufferResized = false;
      recreateSwapChain();
      return;
    }
  }

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

    return mDevice->createShaderModuleUnique(createInfo);
  }

  static fn chooseSwapSurfaceFormat(const std::vector<vk::SurfaceFormatKHR>& availableFormats
  ) -> vk::SurfaceFormatKHR {
    for (const auto& availableFormat : availableFormats)
      if (availableFormat.format == vk::Format::eB8G8R8A8Srgb &&
          availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear)
        return availableFormat;

    return availableFormats[0];
  }

  static fn chooseSwapPresentMode(const std::vector<vk::PresentModeKHR>& availablePresentModes
  ) -> vk::PresentModeKHR {
    for (const auto& availablePresentMode : availablePresentModes)
      if (availablePresentMode == vk::PresentModeKHR::eMailbox)
        return availablePresentMode;

    return vk::PresentModeKHR::eFifo;
  }

  fn chooseSwapExtent(const vk::SurfaceCapabilitiesKHR capabilities) -> vk::Extent2D {
    if (capabilities.currentExtent.width != UINT32_MAX)
      return capabilities.currentExtent;

    u32 width = 0, height = 0;
    std::tie(width, height) = mWindow->getFramebufferSize();

    vk::Extent2D actualExtent = { .width = width, .height = height };

    actualExtent.width =
      std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
    actualExtent.height =
      std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);

    return actualExtent;
  }

  fn querySwapChainSupport(vk::PhysicalDevice device) -> SwapChainSupportDetails {
    SwapChainSupportDetails details;

    details.capabilities  = device.getSurfaceCapabilitiesKHR(mSurface.get());
    details.formats       = device.getSurfaceFormatsKHR(mSurface.get());
    details.present_modes = device.getSurfacePresentModesKHR(mSurface.get());

    return details;
  }

  fn isDeviceSuitable(vk::PhysicalDevice device) -> bool {
    QueueFamilyIndices qfIndices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);

    bool swapChainAdequate = false;

    if (extensionsSupported) {
      SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
      swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.present_modes.empty();
    }

    vk::PhysicalDeviceFeatures supportedFeatures = device.getFeatures();

    return qfIndices.isComplete() && extensionsSupported && swapChainAdequate &&
           supportedFeatures.samplerAnisotropy;
  }

  static fn checkDeviceExtensionSupport(vk::PhysicalDevice device) -> bool {
    std::vector<vk::ExtensionProperties> availableExtensions = device.enumerateDeviceExtensionProperties();

    std::set<string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

    for (const vk::ExtensionProperties& extension : availableExtensions)
      requiredExtensions.erase(extension.extensionName);

    return requiredExtensions.empty();
  }

  fn findQueueFamilies(vk::PhysicalDevice device) -> QueueFamilyIndices {
    QueueFamilyIndices qfIndices;

    std::vector<vk::QueueFamilyProperties> queueFamilies = device.getQueueFamilyProperties();

    for (u32 i = 0; i < queueFamilies.size(); i++) {
      if (queueFamilies[i].queueFlags & vk::QueueFlagBits::eGraphics)
        qfIndices.graphics_family = i;

      vk::Bool32 queuePresentSupport = device.getSurfaceSupportKHR(i, mSurface.get());

      if (queuePresentSupport)
        qfIndices.present_family = i;

      if (qfIndices.isComplete())
        break;
    }

    return qfIndices;
  }

  static fn getRequiredExtensions() -> std::vector<const char*> {
    std::span<const char*> extensionsSpan = vkfw::getRequiredInstanceExtensions();

    std::vector extensions(extensionsSpan.begin(), extensionsSpan.end());

    if (enableValidationLayers)
      extensions.emplace_back(vk::EXTDebugUtilsExtensionName);

    return extensions;
  }

  static fn checkValidationLayerSupport() -> bool {
    std::vector<vk::LayerProperties> availableLayers = vk::enumerateInstanceLayerProperties();

    for (const char* layerName : validationLayers) {
      bool layerFound = false;

      for (const vk::LayerProperties& layerProperties : availableLayers)
        if (strcmp(layerName, layerProperties.layerName) == 0) {
          layerFound = true;
          break;
        }

      if (!layerFound)
        return false;
    }

    return true;
  }

  static VKAPI_ATTR fn VKAPI_CALL debugCallback(
    VkDebugUtilsMessageSeverityFlagBitsEXT /*messageSeverity*/,
    VkDebugUtilsMessageTypeFlagsEXT /*messageType*/,
    const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
    void* /*pUserData*/
  ) -> vk::Bool32 {
    fmt::println("Validation layer: {}", pCallbackData->pMessage);

    return vk::False;
  }
};

fn main() -> i32 {
  vk::DynamicLoader dynamicLoader;
  auto              vkGetInstanceProcAddr =
    dynamicLoader.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr");
  VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);

  VulkanApp app;

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

  return EXIT_SUCCESS;
}
