//========== Includes ==========
// General includes
#include <cstring>
#include <functional>
#include <iostream>
#include <array>
#include <algorithm> // std::clamp
#include <cassert>
#include <cstddef>
#include <cstdint> // uint32_t
#include <ios>
#include <limits> // std::numeric_limits
#include <fstream> // to load shaders
#include <string>
#include <sys/types.h>
#include <utility>
#include <vector>
#include <stdexcept>
#include <cstdlib>
#include <unordered_map>
// Vulkan Includes
#include "vulkan/vulkan.hpp"
#include <vulkan/vulkan_core.h>
#include <vulkan/vulkan_to_string.hpp>
#if defined(__INTELLISENSE__) || !defined(USE_CPP20_MODULES)
#include <vulkan/vulkan_raii.hpp>
#else
import vulkan_hpp;
#endif
// GLFW Includes
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
// GLM - Linear algebra
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/hash.hpp>
#include <chrono>
// Images
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
// OBJ loader
#define TINYOBJLOADER_IMPLEMENTATION
#include <tiny_obj_loader.h>

// Consts
constexpr uint32_t WIDTH = 800;
constexpr uint32_t HEIGHT = 600;
constexpr int MAX_FRAMES_IN_FLIGHT = 2;
const std::string MODEL_PATH = "models/viking_room.obj";
const std::string TEXTURE_PATH = "textures/viking_room.png";

const std::vector<char const*> validationLayers = {
  "VK_LAYER_KHRONOS_validation"
};

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

// Vertices
struct Vertex {
  glm::vec3 pos;
  glm::vec3 color;
  glm::vec2 texCoord;

  static vk::VertexInputBindingDescription getBindingDescription() {
    return { 0, sizeof(Vertex), vk::VertexInputRate::eVertex };
  }

  static std::array<vk::VertexInputAttributeDescription, 3> getAttributeDescriptions() {
    return {
      vk::VertexInputAttributeDescription( 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) ),
      vk::VertexInputAttributeDescription( 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) ),
      vk::VertexInputAttributeDescription( 2, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord) )
    };
  }

  bool operator==(const Vertex& other) const {
    return pos == other.pos && color == other.color && texCoord == other.texCoord;
  }
};

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

/*
const std::vector<Vertex> vertices = {
  {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
  {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
  {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
  {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}},

  {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
  {{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
  {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
  {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}
};

const std::vector<uint16_t> indices = {
  0, 1, 2, 2, 3, 0,
  4, 5, 6, 6, 7, 4
};
*/

//Uniform buffer
struct UniformBufferObject {
  glm::mat4 model;
  glm::mat4 view;
  glm::mat4 proj;
};

class HelloTriangleApplication {
  public:
  void run() {
    #ifndef NDEBUG
    std::cout << "Global initialisation...\n";
    #endif
    initWindow();
    initVulkan();
    mainLoop();
    cleanup();
    #ifndef NDEBUG
    std::cout << "Bye Bye !\n";
    #endif

  }
  private:
  //========== CLASS MEMBERS ==========
  GLFWwindow                       *window                  = nullptr;
  vk::raii::Context                context;
  vk::raii::Instance               instance                 = nullptr;
  vk::raii::DebugUtilsMessengerEXT debugMessenger           = nullptr;
  vk::raii::SurfaceKHR             surface                  = nullptr;
  vk::raii::PhysicalDevice         physicalDevice           = nullptr;
  vk::raii::Device                 device                   = nullptr;
  vk::raii::Queue                  queue                    = nullptr;
  // Swap chain
  vk::raii::SwapchainKHR           swapChain                = nullptr;
  std::vector<vk::Image>           swapChainImages;
  vk::Format                       swapChainImageFormat     = vk::Format::eUndefined;
  vk::Extent2D                     swapChainExtent;
  std::vector<vk::raii::ImageView> swapChainImageViews;
  // Decriptor Set (used for Uniform buffers)
  vk::raii::DescriptorSetLayout    descriptorSetLayout      = nullptr;
  // Pools
  vk::raii::CommandPool            commandPool              = nullptr;
  vk::raii::DescriptorPool         descriptorPool           = nullptr;
  std::vector<vk::raii::DescriptorSet> descriptorSets;
  // Pipeline
  vk::raii::PipelineLayout         pipelineLayout           = nullptr;
  vk::raii::Pipeline               graphicsPipeline         = nullptr;
  // Buffers
  // Vertices - Buffers
  std::vector<vk::raii::CommandBuffer> commandBuffers;
  vk::raii::Buffer                     vertexBuffer         = nullptr;
  vk::raii::DeviceMemory               vertexBufferMemory   = nullptr;
  // Index - Buffers
  vk::raii::Buffer                     indexBuffer          = nullptr;
  vk::raii::DeviceMemory               indexBufferMemory    = nullptr;
  // Uniform Buffers - Buffers
  std::vector<vk::raii::Buffer>        uniformBuffers;
  std::vector<vk::raii::DeviceMemory>  uniformBuffersMemory;
  std::vector<void*>                   uniformBuffersMapped;
  // Sync
  std::vector<vk::raii::Semaphore> presentCompleteSemaphores;
  std::vector<vk::raii::Semaphore> renderFinishedSemaphores;
  std::vector<vk::raii::Fence>     inFlightFences;
  // Image texture
  vk::raii::Image                  textureImage             = nullptr;
  vk::raii::DeviceMemory           textureImageMemory       = nullptr;
  vk::raii::ImageView              textureImageView         = nullptr;
  vk::raii::Sampler                textureSampler           = nullptr;
  // Depth images
  vk::raii::Image                  depthImage               = nullptr;
  vk::raii::DeviceMemory           depthImageMemory         = nullptr;
  vk::raii::ImageView              depthImageView           = nullptr;
  // Indexes
  uint32_t                         queueIndex               = ~0;
  uint32_t                         frameIndex               = 0;
  // Vertices
  std::vector<Vertex>              vertices;
  std::vector<uint32_t>            indices;
  // Bools
  bool                             framebufferResized       = false;

  // Device Extensions
  std::vector<const char*> deviceExtensions = {
    vk::KHRSwapchainExtensionName
  };

  //========== Inits ==========

  void initWindow() {
    #ifndef NDEBUG
    std::cout << "Creating GLFW window...\n";
    #endif
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);


    window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
    glfwSetWindowUserPointer(window, this);
    glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
    #ifndef NDEBUG
    std::cout << "Window created !\n";
    #endif
  }

  void initVulkan() {
    #ifndef NDEBUG
    std::cout << "\tInitialing Vulkan...\n";
    #endif
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createDescriptorSetLayout();
    createGraphicsPipeline();
    createCommandPool();
    createDepthResources();
    createTextureImage();
    createTextureImageView();
    createTextureSampler();
    loadModel();
    createVertexBuffer();
    createIndexBuffer();
    createUniformBuffers();
    createDescriptorPool();
    createDescriptorSets();
    createCommandBuffers();
    createSyncObjects();
    #ifndef NDEBUG
    std::cout << "\tVulkan is initialised !\n";
    #endif
  }

  void mainLoop() {
    #ifndef NDEBUG
    std::cout << "Main Loop...\n";
    #endif
    while (!glfwWindowShouldClose(window)) {
      glfwPollEvents();
      drawFrame();
    }
    device.waitIdle();
  }

  void cleanupSwapChain() {
    swapChainImageViews.clear();
    swapChain = nullptr;
  }

  void cleanup() {
    #ifndef NDEBUG
    std::cout << "Cleaning up...\n";
    #endif
    cleanupSwapChain();
    surface = nullptr;
    glfwDestroyWindow(window);
    glfwTerminate();
    #ifndef NDEBUG
    std::cout << "Done !\n";
    #endif
  }


  //========== UTILS ==========
  void createInstance() {
    #ifndef NDEBUG
    std::cout << "Creating a Vulkan Instance...\n";
    #endif
    constexpr vk::ApplicationInfo appInfo{
      .pApplicationName   = "Hello Vulkan",
      .applicationVersion = VK_MAKE_VERSION( 1, 0, 0),
      .pEngineName        = "No Engine",
      .engineVersion      = VK_MAKE_VERSION(1, 0, 0),
      .apiVersion         = vk::ApiVersion14 };

    // Get the required leyers
    std::vector<char const*> requiredLayers;
    if (enableValidationLayers) {
      requiredLayers.assign(validationLayers.begin(), validationLayers.end());
    }

    // Check if the required layers are supported by the Vulkan implementation
    auto layerProperties = context.enumerateInstanceLayerProperties();
    if (std::ranges::any_of(requiredLayers, [&layerProperties](auto const& requiredLayer) {
        return std::ranges::none_of(layerProperties,
                                   [requiredLayer](auto const& layerProperty)
                                   { return strcmp(layerProperty.layerName, requiredLayer) == 0; });
    }))
    {
        throw std::runtime_error("One or more required layers are not supported!\nAborting...");
    }

    // Get the required extensions
    auto requiredExtensions = getRequiredExtensions();

    // Check if the required extensions are supported by the Vulkan implementation
    auto extensionsProperties = context.enumerateInstanceExtensionProperties();
    for (auto const &requiredExtension : requiredExtensions)
    {
      if (std::ranges::none_of(extensionsProperties,
                               [requiredExtension](auto const &extensionsProperty)
                               { return strcmp(extensionsProperty.extensionName, requiredExtension) == 0; }))
      {
        throw std::runtime_error("Required extension not supported: " + std::string(requiredExtension) + "\nAborting...");
      }
    }

    vk::InstanceCreateInfo createInfo{
      .pApplicationInfo         = &appInfo,
      .enabledLayerCount        = static_cast<uint32_t>(requiredLayers.size()),
      .ppEnabledLayerNames      = requiredLayers.data(),
      .enabledExtensionCount    = static_cast<uint32_t>(requiredExtensions.size()),
      .ppEnabledExtensionNames  = requiredExtensions.data()};

    instance = vk::raii::Instance(context, createInfo);
    #ifndef NDEBUG
    std::cout << requiredExtensions.size() << " extensions loaded !\n";
    std::cout << requiredLayers.size() << " layers loaded !\n";

    std::cout << "Finished creating the Vulkan Instance !\n";
    #endif
  }

  void setupDebugMessenger () {
    #ifndef NDEBUG
    std::cout << "Setting up the messenger !\n";
    #endif
    if (!enableValidationLayers) {
      return;
    }

    vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError);
		vk::DebugUtilsMessageTypeFlagsEXT     messageTypeFlags(vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation);
    vk::DebugUtilsMessengerCreateInfoEXT  debugUtilsMessengerCreateInfoEXT{
      .messageSeverity = severityFlags,
      .messageType     = messageTypeFlags,
      .pfnUserCallback = &debugCallback };

    debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsMessengerCreateInfoEXT);
    #ifndef NDEBUG
    std::cout << "Messenger set up !\n";
    #endif
  }

  void pickPhysicalDevice() {
    #ifndef NDEBUG
    std::cout << "Selecting the GPU...\n";
    #endif
    std::vector<vk::raii::PhysicalDevice> devices = instance.enumeratePhysicalDevices();
		const auto                            devIter = std::ranges::find_if(
            devices,
            [&](auto const &device) {
                // Check if the device supports the Vulkan 1.3 API version
                bool supportsVulkan1_3 = device.getProperties().apiVersion >= VK_API_VERSION_1_3;

                // Check if any of the queue families support graphics operations
                auto queueFamilies = device.getQueueFamilyProperties();
                bool supportsGraphics = std::ranges::any_of(queueFamilies, [](auto const &qfp) { return !!(qfp.queueFlags & vk::QueueFlagBits::eGraphics); });
                return supportsVulkan1_3 && supportsGraphics;
            });
		if (devIter != devices.end()) {
			physicalDevice = *devIter;
			#ifndef NDEBUG
      std::cout << "GPU selected !\n";
      #endif
		}
		else {
			throw std::runtime_error("Failed to find a suitable GPU!\nAborting...");
		}
	}


	void createLogicalDevice() {
	  #ifndef NDEBUG
    std::cout << "Creating a logical device !\n";
    #endif

    //std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice.getQueueFamilyProperties();
    uint32_t queueIndex = findQueueFamilies(physicalDevice); // This is an index


    vk::StructureChain<
      vk::PhysicalDeviceFeatures2,
      vk::PhysicalDeviceVulkan13Features,
      vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT
    > featureChain = {
      {.features = {.samplerAnisotropy = true}},
      {
        .synchronization2 = true,
        .dynamicRendering = true,
      }, // All features from Vulkan 1.3
      { .extendedDynamicState = true }
      };


    // Create the device:
    float queuePriority = 0.5f;
    vk::DeviceQueueCreateInfo deviceQueueCreateInfo {
      .queueFamilyIndex         = queueIndex,
      .queueCount               = 1,
      .pQueuePriorities         = &queuePriority
    };
    vk::DeviceCreateInfo deviceCreateInfo {
      .pNext                    = &featureChain.get<vk::PhysicalDeviceFeatures2>(),
      .queueCreateInfoCount     = 1,
      .pQueueCreateInfos        = &deviceQueueCreateInfo,
      .enabledExtensionCount    = static_cast<uint32_t>(deviceExtensions.size()),
      .ppEnabledExtensionNames  = deviceExtensions.data()
    };

    device = vk::raii::Device( physicalDevice, deviceCreateInfo );
    queue = vk::raii::Queue( device, queueIndex, 0 );
    #ifndef NDEBUG
    std::cout << "Logical device created\n";
    #endif
	}

  void createSurface() {
    #ifndef NDEBUG
    std::cout << "Creating a surface !\n";
    #endif
    VkSurfaceKHR    _surface;
    if (glfwCreateWindowSurface(*instance, window, nullptr, &_surface) != 0) {
      throw std::runtime_error("Failed to create Window surface !\nAborting...");
    }
    surface = vk::raii::SurfaceKHR(instance, _surface);
    #ifndef NDEBUG
    std::cout << "Surface created !\n";
    #endif
  }

  void createSwapChain() {
    #ifndef NDEBUG
    std::cout << "Creating Swap Chain...\n";
    #endif
    auto surfaceCapabilities      = physicalDevice.getSurfaceCapabilitiesKHR(*surface);
    auto swapChainSurfaceFormat   = chooseSwapSurfaceFormat(physicalDevice.getSurfaceFormatsKHR(*surface));
    swapChainExtent          = chooseSwapExtent(surfaceCapabilities);
    auto minImageCount            = std::max(3u, surfaceCapabilities.minImageCount);
    minImageCount                 = ( surfaceCapabilities.maxImageCount > 0 && minImageCount > surfaceCapabilities.maxImageCount ) ? surfaceCapabilities.maxImageCount : minImageCount;

    vk::SwapchainCreateInfoKHR swapChainCreateInfo {
      .flags            = vk::SwapchainCreateFlagsKHR(),
      .surface          = *surface,
      .minImageCount    = minImageCount,
      .imageFormat      = swapChainSurfaceFormat.format,
      .imageColorSpace  = swapChainSurfaceFormat.colorSpace,
      .imageExtent      = swapChainExtent,
      .imageArrayLayers = 1,
      .imageUsage       = vk::ImageUsageFlagBits::eColorAttachment,
      .imageSharingMode = vk::SharingMode::eExclusive,
      .preTransform     = surfaceCapabilities.currentTransform,
      .compositeAlpha   = vk::CompositeAlphaFlagBitsKHR::eOpaque,
      .presentMode      = chooseSwapPresentMode(physicalDevice.getSurfacePresentModesKHR(*surface)),
      .clipped          = true,
      .oldSwapchain     = nullptr
    };
    swapChain           = vk::raii::SwapchainKHR(device, swapChainCreateInfo);
    swapChainImages     = swapChain.getImages();
    swapChainImageFormat = swapChainSurfaceFormat.format;
    #ifndef NDEBUG
    std::cout << "Swap Chain Created !\n";
    #endif
  }

  void createImageViews() {
    #ifndef NDEBUG
    std::cout << "Creating Image View...\n";
    #endif
    swapChainImageViews.clear();

    for (uint32_t i = 0; i < swapChainImages.size(); i++) {
      swapChainImageViews.emplace_back(createImageView(
        swapChainImages[i],
        swapChainImageFormat,
        vk::ImageAspectFlagBits::eColor
      ));
    }
    #ifndef NDEBUG
    std::cout << "Image View created !\n";
    #endif
  }

  void createDescriptorSetLayout() {
    #ifndef NDEBUG
    std::cout << "Creating the descriptor set...\n";
    #endif
    std::array bindings = {
      vk::DescriptorSetLayoutBinding(
        0,
        vk::DescriptorType::eUniformBuffer,
        1,
        vk::ShaderStageFlagBits::eVertex,
        nullptr
      ),
      vk::DescriptorSetLayoutBinding(
        1,
        vk::DescriptorType::eCombinedImageSampler,
        1,
        vk::ShaderStageFlagBits::eFragment,
        nullptr
      )
    };
    vk::DescriptorSetLayoutCreateInfo layoutInfo {
      .bindingCount     = static_cast<uint32_t>(bindings.size()),
      .pBindings        = bindings.data()
    };

    descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo);

    #ifndef NDEBUG
    std::cout << "Descriptor set created !\n";
    #endif
  }

  void createGraphicsPipeline() {
    #ifndef NDEBUG
    std::cout << "Creating the Graphics Pipeline...\n";
    #endif
    vk::raii::ShaderModule shaderModule = createShaderModule(readFile("shaders/slang.spv"));

    vk::PipelineShaderStageCreateInfo vertShaderStageInfo {
      .stage    = vk::ShaderStageFlagBits::eVertex,
      .module   = shaderModule,
      .pName    = "vertMain"
    };
    vk::PipelineShaderStageCreateInfo fragShaderStageInfo {
      .stage    = vk::ShaderStageFlagBits::eFragment,
      .module   = shaderModule,
      .pName    = "fragMain"
    };
    vk::PipelineShaderStageCreateInfo shaderStages[] {
      vertShaderStageInfo,
      fragShaderStageInfo
    };

    // Passing vertices into the pipeline
    auto bindingDescription   = Vertex::getBindingDescription();
    auto attributeDescription = Vertex::getAttributeDescriptions();

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

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

    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,
      .depthBiasSlopeFactor     = 1.0f,
      .lineWidth                = 1.0f
    };

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

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

    // Color blending
    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
    };

    // Dynamic States
    std::vector dynamicStates = {
      vk::DynamicState::eViewport,
      vk::DynamicState::eScissor
    };

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

    vk::PipelineLayoutCreateInfo pipelineLayoutInfo {
      .setLayoutCount         = 1,
      .pSetLayouts            = &*descriptorSetLayout,
      .pushConstantRangeCount = 0
    };

    pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo);
    vk::Format depthFormat = findDepthFormat();

    vk::PipelineRenderingCreateInfo pipelineRenderingCreateInfo {
      .colorAttachmentCount     = 1,
      .pColorAttachmentFormats  = &swapChainImageFormat,
      .depthAttachmentFormat    = depthFormat
    };

    // Pipeline creation
    vk::GraphicsPipelineCreateInfo pipelineInfo {
      .pNext                = &pipelineRenderingCreateInfo,
      .stageCount           = 2,
      .pStages              = shaderStages,
      .pVertexInputState    = &vertexInputInfo,
      .pInputAssemblyState  = &inputAssembly,
      .pViewportState       = &viewportState,
      .pRasterizationState  = &rasterizer,
      .pMultisampleState    = &multisampling,
      .pDepthStencilState   = &depthStencil,
      .pColorBlendState     = &colorBlending,
      .pDynamicState        = &dynamicState,
      .layout               = pipelineLayout,
      .renderPass           = nullptr,
      .basePipelineHandle   = VK_NULL_HANDLE,
      .basePipelineIndex    = -1
    };

    graphicsPipeline = vk::raii::Pipeline(device, nullptr, pipelineInfo);

    #ifndef NDEBUG
    std::cout << "Pipeline created !\n";
    #endif
  }

  void createCommandPool() {
    #ifndef NDEBUG
    std::cout << "Creating a command pool\n";
    #endif
    vk::CommandPoolCreateInfo poolInfo {
      .flags            = vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
      .queueFamilyIndex = queueIndex
    };

    commandPool = vk::raii::CommandPool(device, poolInfo);
    #ifndef NDEBUG
    std::cout << "Command pool created\n";
    #endif
  }

  void createDepthResources() {
    #ifndef NDEBUG
    std::cout << "Creating a depth ressouce...\n";
    #endif
    vk::Format depthFormat = findDepthFormat();

    createImage(
      swapChainExtent.width,
      swapChainExtent.height,
      depthFormat,
      vk::ImageTiling::eOptimal,
      vk::ImageUsageFlagBits::eDepthStencilAttachment,
      vk::MemoryPropertyFlagBits::eDeviceLocal,
      depthImage,
      depthImageMemory
    );
    depthImageView = createImageView(
      depthImage,
      depthFormat,
      vk::ImageAspectFlagBits::eDepth
    );
    #ifndef NDEBUG
    std::cout << "Depth ressouce created !\n";
    #endif
  }

  void createTextureImage(){
    #ifndef NDEBUG
    std::cout << "Creating a texture image...\n";
    #endif
    int texWidth, texHeight, texChannels;
    stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
    vk::DeviceSize imageSize = texWidth * texHeight * 4;

    if (!pixels) {
      throw std::runtime_error("Failed to load texture image!\nAborting...");
    }
    // Temporary buffers
    vk::raii::Buffer       stagingBuffer( {} );
    vk::raii::DeviceMemory stagingBufferMemory( {} );
    createBuffer(
      imageSize,
      vk::BufferUsageFlagBits::eTransferSrc,
      vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent,
      stagingBuffer,
      stagingBufferMemory
    );

    void* data = stagingBufferMemory.mapMemory(0, imageSize);
    memcpy(data, pixels, imageSize);
    stagingBufferMemory.unmapMemory();
    stbi_image_free(pixels);

    createImage(
      texWidth,
      texHeight,
      vk::Format::eR8G8B8A8Srgb,
      vk::ImageTiling::eOptimal,
      vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
      vk::MemoryPropertyFlagBits::eDeviceLocal,
      textureImage,
      textureImageMemory
    );

    transitionImageLayout(textureImage, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
    copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
    transitionImageLayout(textureImage, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal);

    #ifndef NDEBUG
    std::cout << "Texture image created !\n";
    #endif
  }

  void createTextureImageView() {
    #ifndef NDEBUG
    std::cout << "Creating the texture image...\n";
    #endif
    textureImageView = createImageView(
      textureImage,
      vk::Format::eR8G8B8A8Srgb,
      vk::ImageAspectFlagBits::eColor
    );
    #ifndef NDEBUG
    std::cout << "Texture Image created !\n";
    #endif
  }

  void createImage(
    uint32_t width,
    uint32_t height,
    vk::Format format,
    vk::ImageTiling tiling,
    vk::ImageUsageFlags usage,
    vk::MemoryPropertyFlags properties,
    vk::raii::Image &image,
    vk::raii::DeviceMemory &imageMemory
    ) {
      #ifndef NDEBUG
      std::cout << "Creating an image with usage " << vk::to_string(usage) << '\n';
      #endif
      vk::ImageCreateInfo imageInfo {
        .imageType      = vk::ImageType::e2D,
        .format         = format,
        .extent         = { width, height, 1 },
        .mipLevels      = 1,
        .arrayLayers    = 1,
        .samples        = vk::SampleCountFlagBits::e1,
        .tiling         = tiling,
        .usage          = usage,
        .sharingMode    = vk::SharingMode::eExclusive
      };

      image = vk::raii::Image(device, imageInfo);

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

      imageMemory = vk::raii::DeviceMemory(device, allocInfo);
      image.bindMemory(imageMemory, 0);
      #ifndef NDEBUG
      std::cout << "Created image with usage " << vk::to_string(usage) << '\n';
      #endif
  }

  void createTextureSampler() {
    #ifndef NDEBUG
    std::cout << "Creating a texture sampler...\n";
    #endif
    vk::PhysicalDeviceProperties properties = physicalDevice.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::True,
      .maxAnisotropy    = properties.limits.maxSamplerAnisotropy,
      .compareEnable    = vk::True,
      .compareOp        = vk::CompareOp::eAlways,
      .minLod           = 0.0f,
      .maxLod           = 0.0f,
      .borderColor      = vk::BorderColor::eIntOpaqueBlack,
      .unnormalizedCoordinates  = vk::False
    };

    textureSampler = vk::raii::Sampler(device, samplerInfo);

    #ifndef NDEBUG
    std::cout << "Texture sampler created !\n";
    #endif
  }

  void loadModel() {
    #ifndef NDEBUG
    std::cout << "Loading the 3D model...\n";
    #endif

    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.c_str())) {
      throw std::runtime_error(warn + err);
    }

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

    for (const auto& shape : shapes) {
      for (const auto& index : shape.mesh.indices) {
        Vertex  vertex{};

        vertex.pos = {
          attrib.vertices[3 * index.vertex_index + 0],
          attrib.vertices[3 * index.vertex_index + 1],
          attrib.vertices[3 * index.vertex_index + 2]
        };

        vertex.texCoord = {
          attrib.texcoords[2 * index.texcoord_index + 0],
          1.0f - attrib.texcoords[2 * index.texcoord_index + 1]
        };

        vertex.color = {1.0f, 1.0f, 1.0f};

        if (uniqueVertices.count(vertex) == 0) {
          uniqueVertices[vertex] = static_cast<uint32_t>(vertices.size());
          vertices.push_back(vertex);
        }

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

    #ifndef NDEBUG
    std::cout << "3D model loaded !\n";
    std::cout << "3D model has " << vertices.size() << " vertices and " << indices.size() << " index \n";
    #endif
  }

  void createVertexBuffer() {
    #ifndef NDEBUG
    std::cout << "Creating a vertex buffer...\n";
    #endif
    vk::raii::Buffer       stagingBuffer       = nullptr;
    vk::raii::DeviceMemory stagingBufferMemory = nullptr;
    vk::DeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

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

    void* dataStaging = stagingBufferMemory.mapMemory(0, bufferSize);
    memcpy(dataStaging, vertices.data(), (size_t)bufferSize);
    stagingBufferMemory.unmapMemory();

    createBuffer(
      bufferSize,
      vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst,
      vk::MemoryPropertyFlagBits::eDeviceLocal,
      vertexBuffer,
      vertexBufferMemory
    );
    copyBuffer(stagingBuffer, vertexBuffer, bufferSize);
    #ifndef NDEBUG
    std::cout << "Vertex buffer created !\n";
    #endif
  }

  void copyBuffer(
    vk::raii::Buffer & srcBuffer,
    vk::raii::Buffer & dstBuffer,
    vk::DeviceSize size
  ){
    #ifndef NDEBUG
    std::cout << "Copying buffer " << vk::to_string(srcBuffer.objectType) << " to " << vk::to_string(dstBuffer.objectType) << "...\n";
    #endif
    vk::raii::CommandBuffer commandCopyBuffer = beginSingleTimeCommands();

    commandCopyBuffer.copyBuffer(srcBuffer, dstBuffer, vk::BufferCopy(0, 0, size));

    endSingleTimeCommands(commandCopyBuffer);

    #ifndef NDEBUG
    std::cout << "Copied buffer " << vk::to_string(srcBuffer.objectType) << " to " << vk::to_string(dstBuffer.objectType) << "!\n";
    #endif
  }

  void createIndexBuffer() {
    #ifndef NDEBUG
    std::cout << "Creating an index buffer...\n";
    #endif
    vk::DeviceSize bufferSize = sizeof( indices[0] ) * indices.size();

    vk::raii::Buffer stagingBuffer( {} );
    vk::raii::DeviceMemory stagingBufferMemory( {} );
    createBuffer(
      bufferSize,
      vk::BufferUsageFlagBits::eTransferSrc,
      vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent,
      stagingBuffer,
      stagingBufferMemory
    );

    void* data = stagingBufferMemory.mapMemory(0, bufferSize);
    memcpy(data, indices.data(), (size_t) bufferSize);
    stagingBufferMemory.unmapMemory();

    createBuffer(
      bufferSize,
      vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer,
      vk::MemoryPropertyFlagBits::eDeviceLocal,
      indexBuffer,
      indexBufferMemory
    );

    copyBuffer(stagingBuffer, indexBuffer, bufferSize);
    #ifndef NDEBUG
    std::cout << "Index buffer created !\n";
    #endif
  }

  void createUniformBuffers() {
    #ifndef NDEBUG
    std::cout << "Creating uniform buffers...\n";
    #endif
    uniformBuffers.clear();
    uniformBuffersMemory.clear();
    uniformBuffersMapped.clear();

    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
      vk::DeviceSize bufferSize = sizeof(UniformBufferObject);
      vk::raii::Buffer buffer( {} );
      vk::raii::DeviceMemory bufferMem( {} );
      createBuffer(
        bufferSize,
        vk::BufferUsageFlagBits::eUniformBuffer,
        vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent,
        buffer,
        bufferMem
      );

      uniformBuffers.emplace_back(std::move(buffer));
      uniformBuffersMemory.emplace_back(std::move(bufferMem));
      uniformBuffersMapped.emplace_back( uniformBuffersMemory[i].mapMemory(0, bufferSize));
    }
    #ifndef NDEBUG
    std::cout << "Uniform buffers created !\n";
    #endif
  }

  void createDescriptorPool() {
    #ifndef NDEBUG
    std::cout << "Creating a descriptor pool...\n";
    #endif
    std::array poolSize {
      vk::DescriptorPoolSize(
        vk::DescriptorType::eUniformBuffer,
        MAX_FRAMES_IN_FLIGHT
      ),
      vk::DescriptorPoolSize(
        vk::DescriptorType::eCombinedImageSampler,
        MAX_FRAMES_IN_FLIGHT
      )
    };
    vk::DescriptorPoolCreateInfo poolInfo {
      .flags    = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet,
      .maxSets  = MAX_FRAMES_IN_FLIGHT,
      .poolSizeCount = static_cast<uint32_t>(poolSize.size()),
      .pPoolSizes    = poolSize.data()
    };

    descriptorPool = vk::raii::DescriptorPool(device, poolInfo);

    #ifndef NDEBUG
    std::cout << "Descriptor pool created !\n";
    #endif
  }

  void createDescriptorSets() {
    #ifndef NDEBUG
    std::cout << "Creating a descriptor set...\n";
    #endif
    std::vector<vk::DescriptorSetLayout> layouts( MAX_FRAMES_IN_FLIGHT, *descriptorSetLayout );
    vk::DescriptorSetAllocateInfo allocInfo {
      .descriptorPool       = descriptorPool,
      .descriptorSetCount   = static_cast<uint32_t>(layouts.size()),
      .pSetLayouts          = layouts.data()
    };

    descriptorSets.clear();
    descriptorSets = device.allocateDescriptorSets(allocInfo);

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

      vk::DescriptorImageInfo imageInfo {
        .sampler      = textureSampler,
        .imageView    = textureImageView,
        .imageLayout  = vk::ImageLayout::eShaderReadOnlyOptimal
      };
      std::array descriptorWrites {
        vk::WriteDescriptorSet  {
          .dstSet     = descriptorSets[i],
          .dstBinding = 0,
          .dstArrayElement = 0,
          .descriptorCount = 1,
          .descriptorType  = vk::DescriptorType::eUniformBuffer,
          .pBufferInfo     = &bufferInfo
        },
        vk::WriteDescriptorSet {
          .dstSet          = descriptorSets[i],
          .dstBinding      = 1,
          .dstArrayElement = 0,
          .descriptorCount = 1,
          .descriptorType  = vk::DescriptorType::eCombinedImageSampler,
          .pImageInfo      = &imageInfo
        }
      };

      device.updateDescriptorSets(descriptorWrites, {});
    }
    #ifndef NDEBUG
    std::cout << "Descriptor sets created !\n";
    #endif
  }

  void createCommandBuffers() {
    #ifndef NDEBUG
    std::cout << "Creating a command buffers\n";
    #endif

    vk::CommandBufferAllocateInfo allocInfo{
      .commandPool        = commandPool,
      .level              = vk::CommandBufferLevel::ePrimary,
      .commandBufferCount = MAX_FRAMES_IN_FLIGHT
    };
    commandBuffers = vk::raii::CommandBuffers(device, allocInfo);

    #ifndef NDEBUG
    std::cout << "Command buffer created\n";
    #endif
  }

  void recordCommandBuffer(uint32_t imageIndex) {
    auto &commandBuffer = commandBuffers[frameIndex];
    commandBuffer.begin( {} );

    transition_image_layout(
      swapChainImages[imageIndex],
      vk::ImageLayout::eUndefined,
      vk::ImageLayout::eColorAttachmentOptimal,
      {},
      vk::AccessFlagBits2::eColorAttachmentWrite,
      vk::PipelineStageFlagBits2::eColorAttachmentOutput,
      vk::PipelineStageFlagBits2::eColorAttachmentOutput,
      vk::ImageAspectFlagBits::eColor
    );

    transition_image_layout(
      *depthImage,
      vk::ImageLayout::eUndefined,
      vk::ImageLayout::eDepthAttachmentOptimal,
      vk::AccessFlagBits2::eDepthStencilAttachmentWrite,
      vk::AccessFlagBits2::eDepthStencilAttachmentWrite,
      vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests,
      vk::PipelineStageFlagBits2::eEarlyFragmentTests | vk::PipelineStageFlagBits2::eLateFragmentTests,
      vk::ImageAspectFlagBits::eDepth
    );

    vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f);
    vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0);
    vk::RenderingAttachmentInfo attachementInfo = {
      .imageView    = swapChainImageViews[imageIndex],
      .imageLayout  = vk::ImageLayout::eColorAttachmentOptimal,
      .loadOp       = vk::AttachmentLoadOp::eClear,
      .storeOp      = vk::AttachmentStoreOp::eStore,
      .clearValue   = clearColor
    };

    vk::RenderingAttachmentInfo depthAttachmentInfo = {
      .imageView    = depthImageView,
      .imageLayout  = vk::ImageLayout::eDepthAttachmentOptimal,
      .loadOp       = vk::AttachmentLoadOp::eClear,
      .storeOp      = vk::AttachmentStoreOp::eDontCare,
      .clearValue   = clearDepth
    };

    vk::RenderingInfo renderingInfo = {
      .renderArea = {
        .offset   = { 0, 0 },
        .extent   = swapChainExtent
      },
      .layerCount           = 1,
      .colorAttachmentCount = 1,
      .pColorAttachments    = &attachementInfo,
      .pDepthAttachment     = &depthAttachmentInfo
    };

    commandBuffer.beginRendering(renderingInfo);
    commandBuffer.bindPipeline( vk::PipelineBindPoint::eGraphics, graphicsPipeline );
    commandBuffer.bindVertexBuffers(0, *vertexBuffer, {0});
    commandBuffer.bindIndexBuffer( *indexBuffer, 0, vk::IndexType::eUint32 );
    // Dynamic ViewPort
    commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast<float>(swapChainExtent.width), static_cast<float>(swapChainExtent.height), 0.0f, 1.0f));
    commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0,0), swapChainExtent));
    // The actual rendering
    commandBuffer.bindDescriptorSets(
      vk::PipelineBindPoint::eGraphics,
      pipelineLayout,
      0,
      *descriptorSets[frameIndex],
      nullptr
    );
    commandBuffer.drawIndexed( indices.size(), 1, 0, 0, 0);
    commandBuffer.endRendering();
    transition_image_layout(
      swapChainImages[imageIndex],
      vk::ImageLayout::eColorAttachmentOptimal,
      vk::ImageLayout::ePresentSrcKHR,
      vk::AccessFlagBits2::eColorAttachmentWrite,
      {},
      vk::PipelineStageFlagBits2::eColorAttachmentOutput,
      vk::PipelineStageFlagBits2::eBottomOfPipe,
      vk::ImageAspectFlagBits::eColor
    );

    commandBuffer.end();
  }

  void transition_image_layout (
    vk::Image               image,
    vk::ImageLayout         oldLayout,
    vk::ImageLayout         newLayout,
    vk::AccessFlags2        srcAccessMask,
    vk::AccessFlags2        dstAccessMask,
    vk::PipelineStageFlags2 srcStageMask,
    vk::PipelineStageFlags2 dstStageMask,
    vk::ImageAspectFlags    image_aspect_flags
  ){
    vk::ImageMemoryBarrier2 barrier = {
      .srcStageMask        = srcStageMask,
      .srcAccessMask       = srcAccessMask,
      .dstStageMask        = dstStageMask,
      .dstAccessMask       = dstAccessMask,
      .oldLayout           = oldLayout,
      .newLayout           = newLayout,
      .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
      .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
      .image               = image,
      .subresourceRange    = {
        .aspectMask        = image_aspect_flags,
        .baseMipLevel      = 0,
        .levelCount        = 1,
        .baseArrayLayer    = 0,
        .layerCount        = 1
      }
    };

    vk::DependencyInfo dependencyInfo = {
      .dependencyFlags         = {},
      .imageMemoryBarrierCount = 1,
      .pImageMemoryBarriers    = &barrier
    };

    commandBuffers[frameIndex].pipelineBarrier2(dependencyInfo);
  }

  // Warning, the above function and the following have the same name, but not the same purpose,
  // one is for the Pipeline (image displayed) and the other is for the texture image.

  void transitionImageLayout(
    const vk::raii::Image& image,
    vk::ImageLayout oldLayout,
    vk::ImageLayout newLayout
  ) {
    auto commandBuffer = beginSingleTimeCommands();

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

    vk::ImageMemoryBarrier barrier {
      .oldLayout         = oldLayout,
      .newLayout         = newLayout,
      .image             = image,
      .subresourceRange  = {
        vk::ImageAspectFlagBits::eColor,
        0,
        1,
        0,
        1
      }
    };

    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, {}, {}, nullptr, barrier);

    endSingleTimeCommands(commandBuffer);
  }

  void copyBufferToImage(
    const vk::raii::Buffer &buffer,
    vk::raii::Image &image,
    uint32_t width,
    uint32_t height
  ) {
    vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands();

    vk::BufferImageCopy region{
      .bufferOffset       = 0,
      .bufferRowLength    = 0,
      .bufferImageHeight  = 0,
      .imageSubresource   = {
        vk::ImageAspectFlagBits::eColor,
        0,
        0,
        1
      },
      .imageOffset        = {0, 0, 0},
      .imageExtent        = {width, height, 1}
    };
    commandBuffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, {region});
    endSingleTimeCommands(commandBuffer);
  }

  void updateUniformBuffer(uint32_t currentImage) {
    static auto startTime   = std::chrono::high_resolution_clock::now();

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

    UniformBufferObject ubo {};
    ubo.model = rotate(glm::mat4(1.0f), time/10 * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
    ubo.view = 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));
    ubo.proj = glm::perspective(glm::radians(45.0f), static_cast<float>(swapChainExtent.width) / static_cast<float>(swapChainExtent.height), 0.1f, 10.0f);
    ubo.proj[1][1] *= -1;

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

  void drawFrame(){
    auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX);
    if (fenceResult != vk::Result::eSuccess) {
      throw std::runtime_error("Failed to wait for fence !\nAborting...");
    }
    device.resetFences(*inFlightFences[frameIndex]);

    auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr);

    if (result == vk::Result::eErrorOutOfDateKHR) {
      #ifndef NDEBUG
      std::cout << "SwapChain out of date ! \n";
      #endif
      recreateSwapChain();
      #ifndef NDEBUG
      std::cout << "Fixed the swapchain !\n";
      #endif
      return;
    } else if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) {
      assert(result == vk::Result::eTimeout || result == vk::Result::eNotReady);
      throw std::runtime_error("Failed to acquire SwapChain image !\nAborting...");
    }

    device.resetFences(*inFlightFences[frameIndex]);

    commandBuffers[frameIndex].reset();
    recordCommandBuffer(imageIndex);

    vk::PipelineStageFlags waitDestinationStageMask( vk::PipelineStageFlagBits::eColorAttachmentOutput );
    const vk::SubmitInfo   submitInfo {
      .waitSemaphoreCount   = 1,
      .pWaitSemaphores      = &*presentCompleteSemaphores[frameIndex],
      .pWaitDstStageMask    = &waitDestinationStageMask,
      .commandBufferCount   = 1,
      .pCommandBuffers      = &*commandBuffers[frameIndex],
      .signalSemaphoreCount = 1,
      .pSignalSemaphores    = &*renderFinishedSemaphores[imageIndex]
    };
    updateUniformBuffer(frameIndex);
    queue.submit(submitInfo, *inFlightFences[frameIndex]);

    const vk::PresentInfoKHR presentInfoKHR {
      .waitSemaphoreCount = 1,
      .pWaitSemaphores    = &*renderFinishedSemaphores[imageIndex],
      .swapchainCount     = 1,
      .pSwapchains        = &*swapChain,
      .pImageIndices      = &imageIndex
    };

    result = queue.presentKHR(presentInfoKHR);

    if ((result == vk::Result::eSuboptimalKHR) || (result == vk::Result::eErrorOutOfDateKHR) || framebufferResized) {
      framebufferResized = false;
      recreateSwapChain();
    } else {
      assert(result == vk::Result::eSuccess);
    }

    frameIndex = (frameIndex + 1) % MAX_FRAMES_IN_FLIGHT;
  }

  void createSyncObjects() {
    assert(presentCompleteSemaphores.empty() && renderFinishedSemaphores.empty() && inFlightFences.empty());

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

    for (size_t i = 0; i < swapChainImages.size(); i++) {
      renderFinishedSemaphores.emplace_back(device, semaphoreInfo);
    }
    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
      presentCompleteSemaphores.emplace_back(device, semaphoreInfo);
      inFlightFences.emplace_back(device, fenceInfo);
    }
  }

  void recreateSwapChain() {
    #ifndef NDEBUG
    std::cout << "Re-creating the SwapChain...\n";
    #endif

    int width { 0 };
    int height { 0 };
    glfwGetFramebufferSize(window, &width, &height);
    while (width == 0 || height == 0) {
      glfwGetFramebufferSize(window, &width, &height);
      glfwWaitEvents();
    }

    device.waitIdle();

    cleanupSwapChain();

    createSwapChain();
    createImageViews();
    createDepthResources();

    #ifndef NDEBUG
    std::cout << "SwapChain re-created sucessfully !\n";
    #endif
  }

  void createBuffer(
    vk::DeviceSize size,
    vk::BufferUsageFlags usage,
    vk::MemoryPropertyFlags properties,
    vk::raii::Buffer& buffer,
    vk::raii::DeviceMemory& bufferMemory
  ) {
    #ifndef NDEBUG
    std::cout << "Creating a buffer of type: " << vk::to_string(usage) << '\n';
    #endif

    vk::BufferCreateInfo bufferInfo {
      .size        = size,
      .usage       = usage,
      .sharingMode = vk::SharingMode::eExclusive
    };
    buffer = vk::raii::Buffer(device, bufferInfo);
    vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements();
    vk::MemoryAllocateInfo allocInfo {
      .allocationSize   = memRequirements.size,
      .memoryTypeIndex  = findMemoryType(memRequirements.memoryTypeBits, properties)
    };

    bufferMemory = vk::raii::DeviceMemory(device, allocInfo);
    buffer.bindMemory(*bufferMemory, 0);

    #ifndef NDEBUG
    std::cout << "Buffer of type: " << vk::to_string(usage) << " sucessfully created !\n";
    #endif
  }

	static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(
	  vk::DebugUtilsMessageSeverityFlagBitsEXT severity,
		vk::DebugUtilsMessageTypeFlagsEXT type,
		const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData,
		void *
	){
    if (severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError || severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
      #ifndef NDEBUG
      std::cerr << "Validation layer: type " << to_string(type) << " msg: " << pCallbackData->pMessage << std::endl;
      #endif
    }

    return vk::False;
  }

	static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
    auto app = reinterpret_cast<HelloTriangleApplication*>(glfwGetWindowUserPointer(window));
    app->framebufferResized = true;
  }

  uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) {
    vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties();
    for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
      if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
        return i;
      }
    }
    throw std::runtime_error("Failed to find a suitable memory type !\nAborting...");
  }

  std::vector<const char*> getRequiredExtensions() {
    uint32_t glfwExtensionCount = 0;
    auto glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);

    std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount);
    if (enableValidationLayers) {
      extensions.push_back(vk::EXTDebugUtilsExtensionName);
    }

    return extensions;
  }

  uint32_t findQueueFamilies(vk::raii::PhysicalDevice physicalDevice) {
    #ifndef NDEBUG
    std::cout << "Selecting a queue...\n";
    #endif
    // Get the index of the first queue that support graphics
    std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice.getQueueFamilyProperties();

    for (uint32_t qfpIndex = 0; qfpIndex < queueFamilyProperties.size(); qfpIndex++) {
      if ((queueFamilyProperties[qfpIndex].queueFlags & vk::QueueFlagBits::eGraphics) && physicalDevice.getSurfaceSupportKHR(qfpIndex, *surface)) {
        // The queue has both graphics and present
        #ifndef NDEBUG
        std::cout << "Found a graphics and present queue !\n";
        #endif
        queueIndex = qfpIndex;
        return static_cast<uint32_t>(qfpIndex);
      }
    }

    //return static_cast<uint32_t>( std::distance( queueFamilyProperties.begin(), queueFamilyProperty ) );
    throw std::runtime_error("Failed to find a queue with graphics and present !\n Aborting...\n");
  }

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

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

  vk::Extent2D chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) {
    if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
      return capabilities.currentExtent;
    }
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    return {
      std::clamp<uint32_t>(width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width),
      std::clamp<uint32_t>(height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height)
    };
  }

  static std::vector<char> readFile(const std::string& filename) {
    #ifndef NDEBUG
    std::cout << "Loading " << filename << " from disk...\n";
    #endif
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open()) {
      throw std::runtime_error("Failed to open file !\n");
    }

    std::vector<char> buffer(file.tellg());
    file.seekg(0, std::ios::beg);
    file.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
    file.close();
    #ifndef NDEBUG
    std::cout << "File loaded with size of " << buffer.size() << '\n';
    #endif
    return buffer;
  }

  [[nodiscard]] vk::raii::ShaderModule createShaderModule(const std::vector<char>& code) const {
    #ifndef NDEBUG
    std::cout << "Creating shader module\n";
    #endif
    vk::ShaderModuleCreateInfo createInfo{
      .codeSize     = code.size() * sizeof(char),
      .pCode        = reinterpret_cast<const uint32_t*>(code.data())
    };
    vk::raii::ShaderModule shaderModule { device, createInfo };
    #ifndef NDEBUG
    std::cout << "Shader module created\n";
    #endif
    return shaderModule;
  }

  vk::raii::CommandBuffer beginSingleTimeCommands() {
    vk::CommandBufferAllocateInfo allocInfo {
      .commandPool = commandPool,
      .level = vk::CommandBufferLevel::ePrimary,
      .commandBufferCount = 1
    };

    vk::raii::CommandBuffer commandBuffer = std::move(device.allocateCommandBuffers(allocInfo).front());

    vk::CommandBufferBeginInfo beginInfo{ .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit };
    commandBuffer.begin(beginInfo);

    return commandBuffer;
  }

  void endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer){
    commandBuffer.end();

    vk::SubmitInfo submitInfo {
      .commandBufferCount   = 1,
      .pCommandBuffers      = &*commandBuffer
    };
    queue.submit(submitInfo, nullptr);
    queue.waitIdle();
  }

  vk::raii::ImageView createImageView(
    vk::Image image,
    vk::Format format,
    vk::ImageAspectFlags aspectFlags
  ) {
    vk::ImageViewCreateInfo viewInfo{
      .image            = image,
      .viewType         = vk::ImageViewType::e2D,
      .format           = format,
      .subresourceRange = {
        aspectFlags,
        0,
        1,
        0,
        1
      }
    };

    return vk::raii::ImageView( device, viewInfo );
  }

  vk::Format findSupportedFormat(
    const std::vector<vk::Format>& candidates,
    vk::ImageTiling tiling,
    vk::FormatFeatureFlags features
  ){
    for (const auto format : candidates) {
      vk::FormatProperties props = physicalDevice.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 !");
  }

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

  bool hasStensilComponent(vk::Format format) {
    return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint;
  }

};

int main() {
  #ifndef NDEBUG
  std::cout << "Starting the app...\n";
  #endif
  HelloTriangleApplication app;

  try {
    app.run();
  } catch (const std::exception& e) {
    std::cerr << e.what();
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
