#ifndef _GRAPHICS_PIPELINE_VULKAN_H
#define _GRAPHICS_PIPELINE_VULKAN_H

#include "graphics-pipeline.hpp"

#include <fstream>
#include <iostream>
#include <stdexcept>
#include <vector>

#include <vulkan/vulkan.h>

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE // Since, in Vulkan, the depth range is 0 to 1 instead of -1 to 1
#define GLM_FORCE_RIGHT_HANDED

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include "vulkan-utils.hpp"

using namespace glm;

// TODO: Maybe change the name of this struct so I can call the list something other than descriptorInfoList
struct DescriptorInfo {
   VkDescriptorType type;
   VkShaderStageFlags stageFlags;

   // Only one of the below properties should be set
   vector<VkDescriptorBufferInfo>* bufferDataList;
   VkDescriptorImageInfo* imageData;
};

template<class VertexType>
class GraphicsPipeline_Vulkan : public GraphicsPipeline {
   public:
      string vertShaderFile, fragShaderFile;

      GraphicsPipeline_Vulkan();

      GraphicsPipeline_Vulkan(VkPrimitiveTopology topology, VkPhysicalDevice physicalDevice, VkDevice device,
         VkRenderPass renderPass, Viewport viewport, size_t vertexCapacity, size_t indexCapacity);
      ~GraphicsPipeline_Vulkan();

      size_t getNumVertices();

      void updateRenderPass(VkRenderPass renderPass);

      // Maybe I should rename these to addVertexAttribute (addVaryingAttribute) and addUniformAttribute

      void addAttribute(VkFormat format, size_t offset);

      // TODO: I might be able to use a single VkDescriptorBufferInfo here and reuse it when creating the descriptor sets
      void addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags,
                             vector<VkDescriptorBufferInfo>* bufferData);
      void addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags, VkDescriptorImageInfo* imageData);

      void updateDescriptorInfo(uint32_t index, vector<VkDescriptorBufferInfo>* bufferData, uint32_t size);

      void createPipeline(string vertShaderFile, string fragShaderFile);
      void createDescriptorSetLayout();
      void createDescriptorPool(uint32_t size);
      void createDescriptorSets(uint32_t size);

      void createRenderCommands(VkCommandBuffer& commandBuffer, uint32_t currentImage,
                                const vector<uint32_t>& dynamicOffsets);

      void addObject(const vector<VertexType>& vertices, vector<uint16_t> indices, VkCommandPool commandPool,
                     VkQueue graphicsQueue);

      void updateObjectVertices(size_t objIndex, const vector<VertexType>& vertices, VkCommandPool commandPool,
                                VkQueue graphicsQueue);

      void cleanup();
      void cleanupBuffers();
   
   private:
      VkPrimitiveTopology topology;
      VkPhysicalDevice physicalDevice;
      VkDevice device;
      VkRenderPass renderPass;

      VkPipeline pipeline;
      VkPipelineLayout pipelineLayout;

      VkVertexInputBindingDescription bindingDescription;

      vector<VkVertexInputAttributeDescription> attributeDescriptions;
      vector<DescriptorInfo> descriptorInfoList;

      VkDescriptorSetLayout descriptorSetLayout;
      VkDescriptorPool descriptorPool;
      vector<VkDescriptorSet> descriptorSets;

      size_t numVertices;
      size_t vertexCapacity;
      VkBuffer vertexBuffer;
      VkDeviceMemory vertexBufferMemory;

      size_t numIndices;
      size_t indexCapacity;
      VkBuffer indexBuffer;
      VkDeviceMemory indexBufferMemory;

      VkShaderModule createShaderModule(const vector<char>& code);
      vector<char> readFile(const string& filename);

      void resizeVertexBuffer(VkCommandPool commandPool, VkQueue graphicsQueue);
      void resizeIndexBuffer(VkCommandPool commandPool, VkQueue graphicsQueue);
};

/*** PUBLIC METHODS ***/

template<class VertexType>
GraphicsPipeline_Vulkan<VertexType>::GraphicsPipeline_Vulkan() {
}

// TODO: Verify that vertex capacity and index capacity are both > 0
// TODO: See if it would be feasible to move code in the createPipeline method
// into the constructor. That way, I can also put relevant cleanup code into the destructor
template<class VertexType>
GraphicsPipeline_Vulkan<VertexType>::GraphicsPipeline_Vulkan(VkPrimitiveTopology topology,
                                                             VkPhysicalDevice physicalDevice, VkDevice device,
                                                             VkRenderPass renderPass, Viewport viewport,
                                                             size_t vertexCapacity, size_t indexCapacity)
                                                            : topology(topology)
                                                            , physicalDevice(physicalDevice)
                                                            , device(device)
                                                            , renderPass(renderPass) {
   // This is a member of the base GraphicsPipeline class
   // It currently is not used for the OpenGL pipeline. Either figure out why (since OpenGL certainly has the concept of
   // viewports) and use it there too and add viewport the the base class constructor, or create a second base class
   // constructor which takes the viewport
   this->viewport = viewport;

   // Since there is only one array of vertex data, we use binding = 0
   // I'll probably do that for the foreseeable future
   // I can calculate the stride myself given info about all the varying attributes
   this->bindingDescription.binding = 0;
   this->bindingDescription.stride = sizeof(VertexType);
   this->bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

   this->numVertices = 0;
   this->vertexCapacity = vertexCapacity;

   VulkanUtils::createBuffer(device, physicalDevice, vertexCapacity * sizeof(VertexType),
      VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
      VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory);

   this->numIndices = 0;
   this->indexCapacity = indexCapacity;

   VulkanUtils::createBuffer(device, physicalDevice, indexCapacity * sizeof(uint16_t),
      VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
      VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory);
}

// TODO: Move as much cleanup as I can into the destructor
template<class VertexType>
GraphicsPipeline_Vulkan<VertexType>::~GraphicsPipeline_Vulkan() {
}

template<class VertexType>
size_t GraphicsPipeline_Vulkan<VertexType>::getNumVertices() {
   return numVertices;
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::updateRenderPass(VkRenderPass renderPass) {
   this->renderPass = renderPass;
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::addAttribute(VkFormat format, size_t offset) {
   VkVertexInputAttributeDescription attributeDesc = {};

   attributeDesc.binding = 0;
   attributeDesc.location = this->attributeDescriptions.size();
   attributeDesc.format = format;
   attributeDesc.offset = offset;

   this->attributeDescriptions.push_back(attributeDesc);
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags,
                                                            vector<VkDescriptorBufferInfo>* bufferData) {
   this->descriptorInfoList.push_back({ type, stageFlags, bufferData, nullptr });
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::addDescriptorInfo(VkDescriptorType type, VkShaderStageFlags stageFlags,
                                                            VkDescriptorImageInfo* imageData) {
   this->descriptorInfoList.push_back({ type, stageFlags, nullptr, imageData });
}

// TODO: Maybe make an analogous one for updating image info
// Also, I may want to rewrite this function to call vkUpdateDescriptorSets once and call it once
// per swapchain image from VulkanGame
template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::updateDescriptorInfo(uint32_t index,
                                                               vector<VkDescriptorBufferInfo>* bufferData,
                                                               uint32_t size) {
   this->descriptorInfoList[index].bufferDataList = bufferData;

   // TODO: This code was mostly copied from createDescriptorSets. I should make some common function they both use
   // for updating descriptor sets
   for (size_t i = 0; i < size; i++) {
      VkWriteDescriptorSet descriptorWrite = {};

      descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
      descriptorWrite.dstSet = this->descriptorSets[i];
      descriptorWrite.dstBinding = index;
      descriptorWrite.dstArrayElement = 0;
      descriptorWrite.descriptorType = this->descriptorInfoList[index].type;
      descriptorWrite.descriptorCount = 1;
      descriptorWrite.pBufferInfo = nullptr;
      descriptorWrite.pImageInfo = nullptr;
      descriptorWrite.pTexelBufferView = nullptr;

      // This method is only intended for updated descriptor sets of type VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
      // but I'm leaving that in here for completeness
      switch (descriptorWrite.descriptorType) {
         case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER:
         case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC:
         case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER:
            descriptorWrite.pBufferInfo = &(*this->descriptorInfoList[index].bufferDataList)[i];
            break;
         case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
            descriptorWrite.pImageInfo = this->descriptorInfoList[index].imageData;
            break;
         default:
            throw runtime_error("Unknown descriptor type: " + to_string(descriptorWrite.descriptorType));
      }

      // TODO: Instead, assert that (bufferData->size() == swapChainImages.size() (now just changed to size)
      if (bufferData->size() != size) {
         cout << "ALERT ALERT ALERT: SIZE MISMATCH!!!!!!!" << endl;
      }

      vkUpdateDescriptorSets(this->device, 1, &descriptorWrite, 0, nullptr);
   }
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::createPipeline(string vertShaderFile, string fragShaderFile) {
   this->vertShaderFile = vertShaderFile;
   this->fragShaderFile = fragShaderFile;

   vector<char> vertShaderCode = readFile(vertShaderFile);
   vector<char> fragShaderCode = readFile(fragShaderFile);

   VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
   VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

   VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
   vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
   vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
   vertShaderStageInfo.module = vertShaderModule;
   vertShaderStageInfo.pName = "main";

   VkPipelineShaderStageCreateInfo fragShaderStageInfo = {};
   fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
   fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
   fragShaderStageInfo.module = fragShaderModule;
   fragShaderStageInfo.pName = "main";

   VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo, fragShaderStageInfo };

   VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
   vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;

   vertexInputInfo.vertexBindingDescriptionCount = 1;
   vertexInputInfo.vertexAttributeDescriptionCount = static_cast<uint32_t>(this->attributeDescriptions.size());
   vertexInputInfo.pVertexBindingDescriptions = &this->bindingDescription;
   vertexInputInfo.pVertexAttributeDescriptions = this->attributeDescriptions.data();

   VkPipelineInputAssemblyStateCreateInfo inputAssembly = {};
   inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
   inputAssembly.topology = this->topology;
   inputAssembly.primitiveRestartEnable = VK_FALSE;

   VkViewport viewport = {};
   viewport.x = (float)this->viewport.x;
   viewport.y = (float)this->viewport.y;
   viewport.width = (float)this->viewport.width;
   viewport.height = (float)this->viewport.height;
   viewport.minDepth = 0.0f;
   viewport.maxDepth = 1.0f;

   VkRect2D scissor = {};
   scissor.offset = { 0, 0 };
   scissor.extent = { (uint32_t)this->viewport.width, (uint32_t)this->viewport.height };

   VkPipelineViewportStateCreateInfo viewportState = {};
   viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
   viewportState.viewportCount = 1;
   viewportState.pViewports = &viewport;
   viewportState.scissorCount = 1;
   viewportState.pScissors = &scissor;

   VkPipelineRasterizationStateCreateInfo rasterizer = {};
   rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
   rasterizer.depthClampEnable = VK_FALSE;
   rasterizer.rasterizerDiscardEnable = VK_FALSE;
   rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
   rasterizer.lineWidth = 1.0f;
   rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
   rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
   rasterizer.depthBiasEnable = VK_FALSE;

   VkPipelineMultisampleStateCreateInfo multisampling = {};
   multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
   multisampling.sampleShadingEnable = VK_FALSE;
   multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

   VkPipelineColorBlendAttachmentState colorBlendAttachment = {};
   colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
   colorBlendAttachment.blendEnable = VK_TRUE;
   colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
   colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
   colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
   colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
   colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
   colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;

   VkPipelineColorBlendStateCreateInfo colorBlending = {};
   colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
   colorBlending.logicOpEnable = VK_FALSE;
   colorBlending.logicOp = VK_LOGIC_OP_COPY;
   colorBlending.attachmentCount = 1;
   colorBlending.pAttachments = &colorBlendAttachment;
   colorBlending.blendConstants[0] = 0.0f;
   colorBlending.blendConstants[1] = 0.0f;
   colorBlending.blendConstants[2] = 0.0f;
   colorBlending.blendConstants[3] = 0.0f;

   VkPipelineDepthStencilStateCreateInfo depthStencil = {};
   depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
   depthStencil.depthTestEnable = VK_TRUE;
   depthStencil.depthWriteEnable = VK_TRUE;
   depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
   depthStencil.depthBoundsTestEnable = VK_FALSE;
   depthStencil.minDepthBounds = 0.0f;
   depthStencil.maxDepthBounds = 1.0f;
   depthStencil.stencilTestEnable = VK_FALSE;
   depthStencil.front = {};
   depthStencil.back = {};

   VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
   pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
   pipelineLayoutInfo.setLayoutCount = 1;
   pipelineLayoutInfo.pSetLayouts = &this->descriptorSetLayout;
   pipelineLayoutInfo.pushConstantRangeCount = 0;

   if (vkCreatePipelineLayout(this->device, &pipelineLayoutInfo, nullptr, &this->pipelineLayout) != VK_SUCCESS) {
      throw runtime_error("failed to create pipeline layout!");
   }

   VkGraphicsPipelineCreateInfo pipelineInfo = {};
   pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
   pipelineInfo.stageCount = 2;
   pipelineInfo.pStages = shaderStages;
   pipelineInfo.pVertexInputState = &vertexInputInfo;
   pipelineInfo.pInputAssemblyState = &inputAssembly;
   pipelineInfo.pViewportState = &viewportState;
   pipelineInfo.pRasterizationState = &rasterizer;
   pipelineInfo.pMultisampleState = &multisampling;
   pipelineInfo.pDepthStencilState = &depthStencil;
   pipelineInfo.pColorBlendState = &colorBlending;
   pipelineInfo.pDynamicState = nullptr;
   pipelineInfo.layout = this->pipelineLayout;
   pipelineInfo.renderPass = this->renderPass;
   pipelineInfo.subpass = 0;
   pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
   pipelineInfo.basePipelineIndex = -1;

   if (vkCreateGraphicsPipelines(this->device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &this->pipeline) != VK_SUCCESS) {
      throw runtime_error("failed to create graphics pipeline!");
   }

   vkDestroyShaderModule(this->device, vertShaderModule, nullptr);
   vkDestroyShaderModule(this->device, fragShaderModule, nullptr);
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::createDescriptorSetLayout() {
   vector<VkDescriptorSetLayoutBinding> bindings(this->descriptorInfoList.size());

   for (size_t i = 0; i < bindings.size(); i++) {
      bindings[i].binding = i;
      bindings[i].descriptorCount = 1;
      bindings[i].descriptorType = this->descriptorInfoList[i].type;
      bindings[i].stageFlags = this->descriptorInfoList[i].stageFlags;
      bindings[i].pImmutableSamplers = nullptr;
   }

   VkDescriptorSetLayoutCreateInfo layoutInfo = {};
   layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
   layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
   layoutInfo.pBindings = bindings.data();

   if (vkCreateDescriptorSetLayout(this->device, &layoutInfo, nullptr, &this->descriptorSetLayout) != VK_SUCCESS) {
      throw runtime_error("failed to create descriptor set layout!");
   }
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::createDescriptorPool(uint32_t size) {
   vector<VkDescriptorPoolSize> poolSizes(this->descriptorInfoList.size());

   for (size_t i = 0; i < poolSizes.size(); i++) {
      poolSizes[i].type = this->descriptorInfoList[i].type;
      poolSizes[i].descriptorCount = size;
   }

   VkDescriptorPoolCreateInfo poolInfo = {};
   poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
   poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
   poolInfo.pPoolSizes = poolSizes.data();
   poolInfo.maxSets = size;

   if (vkCreateDescriptorPool(this->device, &poolInfo, nullptr, &this->descriptorPool) != VK_SUCCESS) {
      throw runtime_error("failed to create descriptor pool!");
   }
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::createDescriptorSets(uint32_t size) {
   vector<VkDescriptorSetLayout> layouts(size, this->descriptorSetLayout);

   VkDescriptorSetAllocateInfo allocInfo = {};
   allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
   allocInfo.descriptorPool = this->descriptorPool;
   allocInfo.descriptorSetCount = size;
   allocInfo.pSetLayouts = layouts.data();

   this->descriptorSets.resize(size);
   if (vkAllocateDescriptorSets(device, &allocInfo, this->descriptorSets.data()) != VK_SUCCESS) {
      throw runtime_error("failed to allocate descriptor sets!");
   }

   for (size_t i = 0; i < size; i++) {
      vector<VkWriteDescriptorSet> descriptorWrites(this->descriptorInfoList.size());

      for (size_t j = 0; j < descriptorWrites.size(); j++) {
         descriptorWrites[j].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
         descriptorWrites[j].dstSet = this->descriptorSets[i];
         descriptorWrites[j].dstBinding = j;
         descriptorWrites[j].dstArrayElement = 0;
         descriptorWrites[j].descriptorType = this->descriptorInfoList[j].type;
         descriptorWrites[j].descriptorCount = 1;
         descriptorWrites[j].pBufferInfo = nullptr;
         descriptorWrites[j].pImageInfo = nullptr;
         descriptorWrites[j].pTexelBufferView = nullptr;

         switch (descriptorWrites[j].descriptorType) {
            case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER:
            case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC:
            case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER:
               descriptorWrites[j].pBufferInfo = &(*this->descriptorInfoList[j].bufferDataList)[i];
               break;
            case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
               descriptorWrites[j].pImageInfo = this->descriptorInfoList[j].imageData;
               break;
            default:
               throw runtime_error("Unknown descriptor type: " + to_string(descriptorWrites[j].descriptorType));
         }
      }

      vkUpdateDescriptorSets(this->device, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
   }
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::createRenderCommands(VkCommandBuffer& commandBuffer, uint32_t currentImage,
                                                               const vector<uint32_t>& dynamicOffsets) {
   vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);

   // If more dynamic UBOs are added, this needs to become an array of offsets
   vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1,
      &descriptorSets[currentImage], dynamicOffsets.size(), dynamicOffsets.data());

   VkBuffer vertexBuffers[] = { vertexBuffer };
   VkDeviceSize offsets[] = { 0 };
   vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);

   vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16);

   vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(numIndices), 1, 0, 0, 0);
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::addObject(const vector<VertexType>& vertices, vector<uint16_t> indices,
                                                    VkCommandPool commandPool, VkQueue graphicsQueue) {
   // TODO: When resizing the vertex or index buffer, take deleted objects into account.
   // Remove their data from the buffer and determine the new size of the bufer based on # of remining objects

   // If # non-deleted objects > currentCapacity / 2
   //    - resize and double capacity
   // else If # non-deleted objects < currentCapacity / 4
   //    - resize amd halve capacity
   // else
   //    - don't resize, but rewrite data in the buffer to only have non-deleted objects

   if (this->numVertices + vertices.size() > this->vertexCapacity) {
      resizeVertexBuffer(commandPool, graphicsQueue);
   }
   VulkanUtils::copyDataToBuffer(this->device, this->physicalDevice, commandPool, vertices,
      this->vertexBuffer, this->numVertices, graphicsQueue);
   this->numVertices += vertices.size();

   if (this->numIndices + indices.size() > this->indexCapacity) {
      resizeIndexBuffer(commandPool, graphicsQueue);
   }
   VulkanUtils::copyDataToBuffer(this->device, this->physicalDevice, commandPool, indices,
      this->indexBuffer, this->numIndices, graphicsQueue);
   this->numIndices += indices.size();
}

// Should only be used if the number of vertices has not changed
template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::updateObjectVertices(size_t objIndex,
      const vector<VertexType>& vertices, VkCommandPool commandPool, VkQueue graphicsQueue) {
   VulkanUtils::copyDataToBuffer(this->device, this->physicalDevice, commandPool, vertices,
      this->vertexBuffer, objIndex * vertices.size(), graphicsQueue);
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::cleanup() {
   vkDestroyPipeline(device, pipeline, nullptr);
   vkDestroyDescriptorPool(device, descriptorPool, nullptr);

   // TODO: I read that the pipeline layout does not have to be recreated every time
   // Try only creating it once
   vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::cleanupBuffers() {
   vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);

   vkDestroyBuffer(device, vertexBuffer, nullptr);
   vkFreeMemory(device, vertexBufferMemory, nullptr);
   vkDestroyBuffer(device, indexBuffer, nullptr);
   vkFreeMemory(device, indexBufferMemory, nullptr);
}

/*** PRIVATE METHODS ***/

template<class VertexType>
VkShaderModule GraphicsPipeline_Vulkan<VertexType>::createShaderModule(const vector<char>& code) {
   VkShaderModuleCreateInfo createInfo = {};
   createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
   createInfo.codeSize = code.size();
   createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());

   VkShaderModule shaderModule;
   if (vkCreateShaderModule(this->device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
      throw runtime_error("failed to create shader module!");
   }

   return shaderModule;
}

template<class VertexType>
vector<char> GraphicsPipeline_Vulkan<VertexType>::readFile(const string& filename) {
   ifstream file(filename, ios::ate | ios::binary);

   if (!file.is_open()) {
      throw runtime_error("failed to open file!");
   }

   size_t fileSize = (size_t)file.tellg();
   vector<char> buffer(fileSize);

   file.seekg(0);
   file.read(buffer.data(), fileSize);

   file.close();

   return buffer;
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::resizeVertexBuffer(VkCommandPool commandPool, VkQueue graphicsQueue) {
   VkBuffer newVertexBuffer;
   VkDeviceMemory newVertexBufferMemory;
   this->vertexCapacity *= 2;

   VulkanUtils::createBuffer(this->device, this->physicalDevice, this->vertexCapacity * sizeof(VertexType),
      VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
      VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, newVertexBuffer, newVertexBufferMemory);

   VulkanUtils::copyBuffer(this->device, commandPool, vertexBuffer, newVertexBuffer, 0, 0, numVertices * sizeof(VertexType), graphicsQueue);

   vkDestroyBuffer(this->device, vertexBuffer, nullptr);
   vkFreeMemory(this->device, vertexBufferMemory, nullptr);

   vertexBuffer = newVertexBuffer;
   vertexBufferMemory = newVertexBufferMemory;
}

template<class VertexType>
void GraphicsPipeline_Vulkan<VertexType>::resizeIndexBuffer(VkCommandPool commandPool, VkQueue graphicsQueue) {
   VkBuffer newIndexBuffer;
   VkDeviceMemory newIndexBufferMemory;
   this->indexCapacity *= 2;

   VulkanUtils::createBuffer(this->device, this->physicalDevice, this->indexCapacity * sizeof(uint16_t),
      VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
      VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, newIndexBuffer, newIndexBufferMemory);

   VulkanUtils::copyBuffer(this->device, commandPool, indexBuffer, newIndexBuffer, 0, 0, numIndices * sizeof(uint16_t), graphicsQueue);

   vkDestroyBuffer(this->device, indexBuffer, nullptr);
   vkFreeMemory(this->device, indexBufferMemory, nullptr);

   indexBuffer = newIndexBuffer;
   indexBufferMemory = newIndexBufferMemory;
}

#endif // _GRAPHICS_PIPELINE_VULKAN_H
