#ifndef _VULKAN_UTILS_H
#define _VULKAN_UTILS_H

#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

#include <vulkan/vulkan.h>

// TODO: Ideally, vulkan-utils should not have things speciic to windowing apis (glfw, sdl, sfml, etc.).
// Check what these inclydes are for and if that functionality can be moved
#include <SDL2/SDL.h>
#include <SDL2/SDL_vulkan.h>

using namespace std;

struct QueueFamilyIndices {
   optional<uint32_t> graphicsFamily;
   optional<uint32_t> presentFamily;

   bool isComplete() {
      return graphicsFamily.has_value() && presentFamily.has_value();
   }
};

struct VulkanImage {
   VkImage image;
   VkDeviceMemory imageMemory;
   VkImageView imageView;
};

class VulkanUtils {
   public:
      static string resultString(VkResult result);

      static bool checkValidationLayerSupport(const vector<const char*> &validationLayers);

      static VkResult createDebugUtilsMessengerEXT(VkInstance instance,
                                                   const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo,
                                                   const VkAllocationCallbacks* pAllocator,
                                                   VkDebugUtilsMessengerEXT* pDebugMessenger);

      static void destroyDebugUtilsMessengerEXT(VkInstance instance,
                                                VkDebugUtilsMessengerEXT debugMessenger,
                                                const VkAllocationCallbacks* pAllocator);

      static QueueFamilyIndices findQueueFamilies(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
      static bool checkDeviceExtensionSupport(VkPhysicalDevice physicalDevice,
                                              const vector<const char*>& deviceExtensions);
      static VkSurfaceCapabilitiesKHR querySwapChainCapabilities(VkPhysicalDevice physicalDevice,
                                                                 VkSurfaceKHR surface);
      static vector<VkSurfaceFormatKHR> querySwapChainFormats(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
      static vector<VkPresentModeKHR> querySwapChainPresentModes(VkPhysicalDevice physicalDevice,
                                                                 VkSurfaceKHR surface);
      static VkSurfaceFormatKHR chooseSwapSurfaceFormat(const vector<VkSurfaceFormatKHR>& availableFormats,
                                                        const vector<VkFormat>& requestedFormats, VkColorSpaceKHR requestedColorSpace);
      static VkPresentModeKHR chooseSwapPresentMode(const vector<VkPresentModeKHR>& availablePresentModes,
                                                    const vector<VkPresentModeKHR>& requestedPresentModes);
      static VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities, int width, int height);
      static VkImageView createImageView(VkDevice device, VkImage image, VkFormat format,
                                         VkImageAspectFlags aspectFlags);
      static VkFormat findSupportedFormat(VkPhysicalDevice physicalDevice, const vector<VkFormat>& candidates,
                                          VkImageTiling tiling, VkFormatFeatureFlags features);
      static void createBuffer(VkDevice device, VkPhysicalDevice physicalDevice, VkDeviceSize size,
                               VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer,
                               VkDeviceMemory& bufferMemory);
      static uint32_t findMemoryType(VkPhysicalDevice physicalDevice, uint32_t typeFilter,
                                     VkMemoryPropertyFlags properties);

      static void createVulkanImageFromFile(VkDevice device, VkPhysicalDevice physicalDevice,
                                            VkCommandPool commandPool, string filename, VulkanImage& image,
                                            VkQueue graphicsQueue);
      static void createVulkanImageFromSDLTexture(VkDevice device, VkPhysicalDevice physicalDevice,
                                                  SDL_Texture* texture, VulkanImage& image);
      static void populateVulkanImageFromSDLTexture(VkDevice device, VkPhysicalDevice physicalDevice,
                                                    VkCommandPool commandPool, SDL_Texture* texture,
                                                    SDL_Renderer* renderer, VulkanImage& image, VkQueue graphicsQueue);
      static void createDepthImage(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool,
                                   VkFormat depthFormat, VkExtent2D extent, VulkanImage& image, VkQueue graphicsQueue);
      static void createImage(VkDevice device, VkPhysicalDevice physicalDevice, uint32_t width, uint32_t height,
                              VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage,
                              VkMemoryPropertyFlags properties, VulkanImage& image);

      static void transitionImageLayout(VkDevice device, VkCommandPool commandPool, VkImage image,
                                        VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout,
                                        VkQueue graphicsQueue);
      static VkCommandBuffer beginSingleTimeCommands(VkDevice device, VkCommandPool commandPool);
      static void endSingleTimeCommands(VkDevice device, VkCommandPool commandPool,
                                        VkCommandBuffer commandBuffer, VkQueue graphicsQueue);
      static void copyBufferToImage(VkDevice device, VkCommandPool commandPool, VkBuffer buffer, VkImage image,
                                    uint32_t width, uint32_t height, VkQueue graphicsQueue);

      template<class DataType>
      static void copyDataToBuffer(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool,
                                   const vector<DataType>& srcData, VkBuffer dstBuffer, size_t dstVertexOffset,
                                   VkQueue graphicsQueue);

      static void copyBuffer(VkDevice device, VkCommandPool commandPool, VkBuffer srcBuffer, VkBuffer dstBuffer,
                             VkDeviceSize srcOffset, VkDeviceSize dstOffset, VkDeviceSize size,
                             VkQueue graphicsQueue);

      template<class DataType>
      static void copyDataToMemory(VkDevice device, DataType* srcData, VkDeviceMemory bufferMemory,
                                   VkDeviceSize offset, VkDeviceSize size, bool flush);

      template<class DataType>
      static void copyDataToMappedMemory(VkDevice device, DataType* srcData, void* mappedData,
                                         VkDeviceMemory bufferMemory, VkDeviceSize size, bool flush);

      static bool hasStencilComponent(VkFormat format);

      static void destroyVulkanImage(VkDevice& device, VulkanImage& image);
};

template<class DataType>
void VulkanUtils::copyDataToBuffer(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool,
                                   const vector<DataType>& srcData, VkBuffer dstBuffer, size_t dstVertexOffset,
                                   VkQueue graphicsQueue) {
   VkDeviceSize srcDataSize = srcData.size() * sizeof(DataType);

   VkBuffer stagingBuffer;
   VkDeviceMemory stagingBufferMemory;
   createBuffer(device, physicalDevice, srcDataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
      stagingBuffer, stagingBufferMemory);

   void* data;
   vkMapMemory(device, stagingBufferMemory, 0, srcDataSize, 0, &data);
   memcpy(data, srcData.data(), (size_t)srcDataSize);
   vkUnmapMemory(device, stagingBufferMemory);

   copyBuffer(device, commandPool, stagingBuffer, dstBuffer, 0, dstVertexOffset * sizeof(DataType), srcDataSize,
      graphicsQueue);

   vkDestroyBuffer(device, stagingBuffer, nullptr);
   vkFreeMemory(device, stagingBufferMemory, nullptr);
}

template<class DataType>
void VulkanUtils::copyDataToMemory(VkDevice device, DataType* srcData, VkDeviceMemory bufferMemory,
                                   VkDeviceSize offset, VkDeviceSize size, bool flush) {
   void* data;

   vkMapMemory(device, bufferMemory, offset, size, 0, &data);

   memcpy(data, srcData, size);

   if (flush) {
      VkMappedMemoryRange memoryRange{};
      memoryRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
      memoryRange.memory = bufferMemory;
      memoryRange.size = size;

      vkFlushMappedMemoryRanges(device, 1, &memoryRange);
   }

   vkUnmapMemory(device, bufferMemory);
}

template<class DataType>
void VulkanUtils::copyDataToMappedMemory(VkDevice device, DataType* srcData, void* mappedData,
                                         VkDeviceMemory bufferMemory, VkDeviceSize size, bool flush) {
   memcpy(mappedData, srcData, size);

   if (flush) {
      VkMappedMemoryRange memoryRange{};
      memoryRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
      memoryRange.memory = bufferMemory;
      memoryRange.size = VK_WHOLE_SIZE; // memoryRange.size = size;

      // TODO: Think about the best approach here, and when creating a Vulka buffer.
      // The issue arises when calling vkMapMemory, because the size passed to it needs to be
      // VK_WHOLE_SIZE or a multiple of some Vulkan constant. When I create a buffer and matching memory,
      // the memory size (I believe) needs to be a multiple of the same or a similar constant, while
      // the buffer can bind to only a part of the memory and I don't think has a size restriction.
      // As long as I save the actual zize of the allocated memory, I can use that for things
      // like vkMapMemory, but still continue to use the buffer's size for most operations

      vkFlushMappedMemoryRanges(device, 1, &memoryRange);
   }
}

// TODO: Use this in vulkan-utils itself as well
#define VKUTIL_CHECK_RESULT(f, msg) {                                                                                      \
   VkResult res = (f);                                                                                                     \
                                                                                                                           \
   if (res != VK_SUCCESS) {                                                                                                \
      ostringstream oss;                                                                                                   \
      oss << msg << " VkResult is \"" << VulkanUtils::resultString(res) << "\" in " << __FILE__ << " at line " << __LINE__;\
                                                                                                                           \
      if (res < 0) {                                                                                                       \
         throw runtime_error("Fatal: " + oss.str());                                                                       \
      } else {                                                                                                             \
         cerr << oss.str();                                                                                                \
      }                                                                                                                    \
   }                                                                                                                       \
}

#endif // _VULKAN_UTILS_H