#include "vulkan-game.hpp"

#include <array>
#include <iostream>
#include <numeric>
#include <set>
#include <stdexcept>

#include "IMGUI/imgui_impl_sdl.h"
#include "IMGUI/imgui_internal.h" // For CalcItemSize

#include "logger.hpp"

#include "gui/imgui/button-imgui.hpp"

using namespace std;

// TODO: Update all instances of the "... != VK_SUCCESS" check to something similar to
// the agp error checking function, which prints an appropriate error message based on the error code

// TODO: Update all occurances of instance variables to use this-> (Actually, not sure if I really want to do this)

/* TODO: Try doing the following tasks based on the Vulkan implementation of IMGUI (Also maybe looks at Sascha Willems' code to see how he does these things)
 *
 * - When recreating the swapchain, pass the old one in and destroy the old one after the new one is created
 * - Recreate semaphores when recreating the swapchain
 *     - imgui uses one image acquired and one render complete sem  and once fence per frame\
 * - IMGUI creates one command pool per framebuffer
 */

/* NOTES WHEN ADDING IMGUI
 *
 * Possibly cleanup the imgui pipeline in cleanupSwapchain or call some imgui function that does this for me
 * call ImGui_ImplVulkan_RenderDrawData, without passing in a pipeline, to do the rendering
 */

static void check_imgui_vk_result(VkResult res) {
   if (res == VK_SUCCESS) {
      return;
   }

   ostringstream oss;
   oss << "[imgui] Vulkan error! VkResult is \"" << VulkanUtils::resultString(res) << "\"" << __LINE__;
   if (res < 0) {
      throw runtime_error("Fatal: " + oss.str());
   } else {
      cerr << oss.str();
   }
}

VulkanGame::VulkanGame()
                     : swapChainImageCount(0)
                     , swapChainMinImageCount(0)
                     , swapChainSurfaceFormat({})
                     , swapChainPresentMode(VK_PRESENT_MODE_MAX_ENUM_KHR)
                     , swapChainExtent{ 0, 0 }
                     , swapChain(VK_NULL_HANDLE)
                     , vulkanSurface(VK_NULL_HANDLE)
                     , sdlVersion({ 0, 0, 0 })
                     , instance(VK_NULL_HANDLE)
                     , physicalDevice(VK_NULL_HANDLE)
                     , device(VK_NULL_HANDLE)
                     , debugMessenger(VK_NULL_HANDLE)
                     , resourceCommandPool(VK_NULL_HANDLE)
                     , renderPass(VK_NULL_HANDLE)
                     , graphicsQueue(VK_NULL_HANDLE)
                     , presentQueue(VK_NULL_HANDLE)
                     , depthImage({})
                     , shouldRecreateSwapChain(false)
                     , frameCount(0)
                     , currentFrame(0)
                     , imageIndex(0)
                     , fpsStartTime(0.0f)
                     , curTime(0.0f)
                     , done(false)
                     , currentRenderScreenFn(nullptr)
                     , gui(nullptr)
                     , window(nullptr)
                     , uniforms_modelPipeline()
                     , objects_modelPipeline()
                     , uniforms_shipPipeline()
                     , objects_shipPipeline()
                     , uniforms_asteroidPipeline()
                     , objects_asteroidPipeline()
                     , uniforms_laserPipeline()
                     , objects_laserPipeline()
                     , uniforms_explosionPipeline()
                     , objects_explosionPipeline()
                     , score(0)
                     , fps(0.0f) {
}

VulkanGame::~VulkanGame() {
}

void VulkanGame::run(int width, int height, unsigned char guiFlags) {
   seedRandomNums();

   cout << "Vulkan Game" << endl;

   cout << "DEBUGGING IS " << (ENABLE_VALIDATION_LAYERS ? "ON" : "OFF") << endl;

   if (initUI(width, height, guiFlags) == RTWO_ERROR) {
      return;
   }

   initVulkan();

   VkPhysicalDeviceProperties deviceProperties;
   vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties);

   uniforms_modelPipeline = VulkanBuffer<UBO_VP_mats>(1, deviceProperties.limits.maxUniformBufferRange,
                                                      deviceProperties.limits.minUniformBufferOffsetAlignment);

   objects_modelPipeline = VulkanBuffer<SSBO_ModelObject>(10, deviceProperties.limits.maxUniformBufferRange,
                                                          deviceProperties.limits.minUniformBufferOffsetAlignment);

   uniforms_shipPipeline = VulkanBuffer<UBO_VP_mats>(1, deviceProperties.limits.maxUniformBufferRange,
                                                     deviceProperties.limits.minUniformBufferOffsetAlignment);

   objects_shipPipeline = VulkanBuffer<SSBO_ModelObject>(10, deviceProperties.limits.maxUniformBufferRange,
                                                         deviceProperties.limits.minUniformBufferOffsetAlignment);

   uniforms_asteroidPipeline = VulkanBuffer<UBO_VP_mats>(1, deviceProperties.limits.maxUniformBufferRange,
                                                         deviceProperties.limits.minUniformBufferOffsetAlignment);

   objects_asteroidPipeline = VulkanBuffer<SSBO_Asteroid>(10, deviceProperties.limits.maxUniformBufferRange,
                                                          deviceProperties.limits.minUniformBufferOffsetAlignment);

   uniforms_laserPipeline = VulkanBuffer<UBO_VP_mats>(1, deviceProperties.limits.maxUniformBufferRange,
                                                      deviceProperties.limits.minUniformBufferOffsetAlignment);

   objects_laserPipeline = VulkanBuffer<SSBO_Laser>(2, deviceProperties.limits.maxUniformBufferRange,
                                                    deviceProperties.limits.minUniformBufferOffsetAlignment);

   uniforms_explosionPipeline = VulkanBuffer<UBO_Explosion>(1, deviceProperties.limits.maxUniformBufferRange,
                                                            deviceProperties.limits.minUniformBufferOffsetAlignment);

   objects_explosionPipeline = VulkanBuffer<SSBO_Explosion>(2, deviceProperties.limits.maxUniformBufferRange,
                                                            deviceProperties.limits.minUniformBufferOffsetAlignment);

   initImGuiOverlay();

   // TODO: Figure out how much of ubo creation and associated variables should be in the pipeline class
   // Maybe combine the ubo-related objects into a new class

   initGraphicsPipelines();

   initMatrices();

   cout << "INITIALIZING OBJECTS" << endl;

   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::pos));
   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::color));
   modelPipeline.addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&ModelVertex::texCoord));
   modelPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::normal));
   modelPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&ModelVertex::objIndex));

   createBufferSet(uniforms_modelPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_modelPipeline);

   uniforms_modelPipeline.map(uniformBuffers_modelPipeline.memory, device);

   createBufferSet(objects_modelPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_modelPipeline);

   objects_modelPipeline.map(objectBuffers_modelPipeline.memory, device);

   modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
      VK_SHADER_STAGE_VERTEX_BIT, &uniformBuffers_modelPipeline.infoSet);
   modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
      VK_SHADER_STAGE_VERTEX_BIT, &objectBuffers_modelPipeline.infoSet);
   modelPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
      VK_SHADER_STAGE_FRAGMENT_BIT, &floorTextureImageDescriptor);

   SceneObject<ModelVertex>* texturedSquare = nullptr;

   texturedSquare = &addObject(modelObjects, modelPipeline,
      addObjectIndex<ModelVertex>(modelObjects.size(),
         addVertexNormals<ModelVertex>({
            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}},
            {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}},
            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
            {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}},
            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}
         })),
      {
         0, 1, 2, 3, 4, 5
      }, objects_modelPipeline, {
         mat4(1.0f)
      });

   texturedSquare->model_base =
      translate(mat4(1.0f), vec3(0.0f, 0.0f, -2.0f));

   texturedSquare = &addObject(modelObjects, modelPipeline,
      addObjectIndex<ModelVertex>(modelObjects.size(),
         addVertexNormals<ModelVertex>({
            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}},
            {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}},
            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
            {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
            {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}},
            {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}
         })),
      {
         0, 1, 2, 3, 4, 5
      }, objects_modelPipeline, {
         mat4(1.0f)
      });

   texturedSquare->model_base =
      translate(mat4(1.0f), vec3(0.0f, 0.0f, -1.5f));

   modelPipeline.createDescriptorSetLayout();
   modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv");
   modelPipeline.createDescriptorPool(swapChainImages.size());
   modelPipeline.createDescriptorSets(swapChainImages.size());

   // START UNREVIEWED SECTION
   shipPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::pos));
   shipPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::color));
   shipPipeline.addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&ModelVertex::texCoord));
   shipPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::normal));
   shipPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&ModelVertex::objIndex));

   createBufferSet(uniforms_shipPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_shipPipeline);

   uniforms_shipPipeline.map(uniformBuffers_shipPipeline.memory, device);

   createBufferSet(objects_shipPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_shipPipeline);

   objects_shipPipeline.map(objectBuffers_shipPipeline.memory, device);

   shipPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
      VK_SHADER_STAGE_VERTEX_BIT, &uniformBuffers_shipPipeline.infoSet);
   shipPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
      VK_SHADER_STAGE_VERTEX_BIT, &objectBuffers_shipPipeline.infoSet);

   // TODO: With the normals, indexing basically becomes pointless since no vertices will have exactly
   // the same data. Add an option to make some pipelines not use indexing
   SceneObject<ModelVertex>& ship = addObject(shipObjects, shipPipeline,
      addObjectIndex<ModelVertex>(shipObjects.size(),
         addVertexNormals<ModelVertex>({

            //back
            {{ -0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // left back
            {{ -0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // right back
            {{  0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 0.3f}},

            // left mid
            {{-0.25f,   0.3f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{-0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{-0.25f,   0.3f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 0.3f}},

            // right mid
            {{  0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{ 0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 0.3f}},
            {{ 0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{ 0.25f,   0.3f,  -3.0f}, {0.0f, 0.0f, 0.3f}},

            // left front
            {{  0.0f,   0.0f,  -3.5f}, {0.0f, 0.0f, 1.0f}},
            {{-0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 1.0f}},
            {{-0.25f,   0.3f,  -3.0f}, {0.0f, 0.0f, 1.0f}},

            // right front
            {{ 0.25f,   0.3f,  -3.0f}, {0.0f, 0.0f, 1.0f}},
            {{ 0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.0f,   0.0f,  -3.5f}, {0.0f, 0.0f, 1.0f}},

            // top back
            {{ -0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{ -0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 1.0f}},
            {{ -0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.3f,  -2.0f}, {0.0f, 0.0f, 1.0f}},

            // bottom back
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 1.0f}},
            {{ -0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 1.0f}},
            {{ -0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 1.0f}},

            // top mid
            {{-0.25f,   0.3f, -3.0f}, {0.0f, 0.0f, 1.0f}},
            {{ -0.5f,   0.3f, -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.3f, -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{ -0.25f,  0.3f, -3.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.3f, -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{ 0.25f,   0.3f, -3.0f}, {0.0f, 0.0f, 1.0f}},

            // bottom mid
            {{ -0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{-0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{  0.5f,   0.0f,  -2.0f}, {0.0f, 0.0f, 1.0f}},
            {{-0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 1.0f}},
            {{ 0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 1.0f}},

            // top front
            {{-0.25f,   0.3f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{ 0.25f,   0.3f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.0f,   0.0f,  -3.5f}, {0.0f, 0.0f, 0.3f}},

            // bottom front
            {{ 0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{-0.25f,   0.0f,  -3.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.0f,   0.0f,  -3.5f}, {0.0f, 0.0f, 0.3f}},

            // left wing start back
            {{ -1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // left wing start top
            {{ -0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // left wing start front
            {{ -0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},

            // left wing start bottom
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -0.5f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},

            // left wing end outside
            {{ -1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -2.2f,   0.15f, -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // left wing end top
            {{ -1.3f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -2.2f,   0.15f, -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // left wing end front
            {{ -1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ -2.2f,  0.15f,  -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},

            // left wing end bottom
            {{ -1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ -2.2f,  0.15f,  -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{ -1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},

            // right wing start back
            {{  1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // right wing start top
            {{  1.3f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // right wing start front
            {{  0.5f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  1.3f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},

            // right wing start bottom
            {{  1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  0.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ 1.3f,    0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ 1.3f,    0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{ 0.5f,    0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{ 0.5f,    0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},

            // right wing end outside
            {{  2.2f,   0.15f, -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // right wing end top
            {{  2.2f,  0.15f,  -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{  1.3f,   0.3f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.3f,   0.0f}, {0.0f, 0.0f, 0.3f}},

            // right wing end front
            {{  2.2f,   0.15f,  -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{  1.3f,   0.0f,   -0.3f}, {0.0f, 0.0f, 0.3f}},
            {{  1.3f,   0.3f,   -0.3f}, {0.0f, 0.0f, 0.3f}},

            // right wing end bottom
            {{  2.2f,  0.15f,  -0.8f}, {0.0f, 0.0f, 0.3f}},
            {{  1.5f,   0.0f,   0.0f}, {0.0f, 0.0f, 0.3f}},
            {{  1.3f,   0.0f,  -0.3f}, {0.0f, 0.0f, 0.3f}},
         })),
      {
           0,   1,   2,   3,   4,   5,
           6,   7,   8,   9,  10,  11,
          12,  13,  14,  15,  16,  17,
          18,  19,  20,  21,  22,  23,
          24,  25,  26,  27,  28,  29,
          30,  31,  32,
          33,  34,  35,
          36,  37,  38,  39,  40,  41,
          42,  43,  44,  45,  46,  47,
          48,  49,  50,  51,  52,  53,
          54,  55,  56,  57,  58,  59,
          60,  61,  62,
          63,  64,  65,
          66,  67,  68,  69,  70,  71,
          72,  73,  74,  75,  76,  77,
          78,  79,  80,  81,  82,  83,
          84,  85,  86,  87,  88,  89,
          90,  91,  92,
          93,  94,  95,
          96,  97,  98,
          99, 100, 101,
         102, 103, 104, 105, 106, 107,
         108, 109, 110, 111, 112, 113,
         114, 115, 116, 117, 118, 119,
         120, 121, 122, 123, 124, 125,
         126, 127, 128,
         129, 130, 131,
         132, 133, 134,
         135, 136, 137,
      }, objects_shipPipeline, {
         mat4(1.0f)
      });

   ship.model_base =
      translate(mat4(1.0f), vec3(0.0f, -1.2f, 1.65f)) *
      scale(mat4(1.0f), vec3(0.1f, 0.1f, 0.1f));

   shipPipeline.createDescriptorSetLayout();
   shipPipeline.createPipeline("shaders/ship-vert.spv", "shaders/ship-frag.spv");
   shipPipeline.createDescriptorPool(swapChainImages.size());
   shipPipeline.createDescriptorSets(swapChainImages.size());

   asteroidPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::pos));
   asteroidPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::color));
   asteroidPipeline.addAttribute(VK_FORMAT_R32G32_SFLOAT, offset_of(&ModelVertex::texCoord));
   asteroidPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ModelVertex::normal));
   asteroidPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&ModelVertex::objIndex));

   createBufferSet(uniforms_asteroidPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_asteroidPipeline);

   uniforms_asteroidPipeline.map(uniformBuffers_asteroidPipeline.memory, device);

   createBufferSet(objects_asteroidPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_asteroidPipeline);

   objects_asteroidPipeline.map(objectBuffers_asteroidPipeline.memory, device);

   asteroidPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
      VK_SHADER_STAGE_VERTEX_BIT, &uniformBuffers_asteroidPipeline.infoSet);
   asteroidPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
      VK_SHADER_STAGE_VERTEX_BIT, &objectBuffers_asteroidPipeline.infoSet);

   asteroidPipeline.createDescriptorSetLayout();
   asteroidPipeline.createPipeline("shaders/asteroid-vert.spv", "shaders/asteroid-frag.spv");
   asteroidPipeline.createDescriptorPool(swapChainImages.size());
   asteroidPipeline.createDescriptorSets(swapChainImages.size());

   laserPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&LaserVertex::pos));
   laserPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&LaserVertex::texCoord));
   laserPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&LaserVertex::objIndex));

   createBufferSet(uniforms_laserPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_laserPipeline);

   uniforms_laserPipeline.map(uniformBuffers_laserPipeline.memory, device);

   createBufferSet(objects_laserPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_laserPipeline);

   objects_laserPipeline.map(objectBuffers_laserPipeline.memory, device);

   laserPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
      VK_SHADER_STAGE_VERTEX_BIT, &uniformBuffers_laserPipeline.infoSet);
   laserPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
      VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, &objectBuffers_laserPipeline.infoSet);
   laserPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
      VK_SHADER_STAGE_FRAGMENT_BIT, &laserTextureImageDescriptor);

   laserPipeline.createDescriptorSetLayout();
   laserPipeline.createPipeline("shaders/laser-vert.spv", "shaders/laser-frag.spv");
   laserPipeline.createDescriptorPool(swapChainImages.size());
   laserPipeline.createDescriptorSets(swapChainImages.size());

   explosionPipeline.addAttribute(VK_FORMAT_R32G32B32_SFLOAT, offset_of(&ExplosionVertex::particleStartVelocity));
   explosionPipeline.addAttribute(VK_FORMAT_R32_SFLOAT, offset_of(&ExplosionVertex::particleStartTime));
   explosionPipeline.addAttribute(VK_FORMAT_R32_UINT, offset_of(&ExplosionVertex::objIndex));

   createBufferSet(uniforms_explosionPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_explosionPipeline);

   uniforms_explosionPipeline.map(uniformBuffers_explosionPipeline.memory, device);

   createBufferSet(objects_explosionPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_explosionPipeline);

   objects_explosionPipeline.map(objectBuffers_explosionPipeline.memory, device);

   explosionPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
      VK_SHADER_STAGE_VERTEX_BIT, &uniformBuffers_explosionPipeline.infoSet);
   explosionPipeline.addDescriptorInfo(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC,
      VK_SHADER_STAGE_VERTEX_BIT, &objectBuffers_explosionPipeline.infoSet);

   explosionPipeline.createDescriptorSetLayout();
   explosionPipeline.createPipeline("shaders/explosion-vert.spv", "shaders/explosion-frag.spv");
   explosionPipeline.createDescriptorPool(swapChainImages.size());
   explosionPipeline.createDescriptorSets(swapChainImages.size());

   // END UNREVIEWED SECTION

   currentRenderScreenFn = &VulkanGame::renderMainScreen;

   ImGuiIO& io = ImGui::GetIO();

   initGuiValueLists(valueLists);

   valueLists["stats value list"].push_back(UIValue(UIVALUE_INT, "Score", &score));
   valueLists["stats value list"].push_back(UIValue(UIVALUE_DOUBLE, "FPS", &fps));
   valueLists["stats value list"].push_back(UIValue(UIVALUE_DOUBLE, "IMGUI FPS", &io.Framerate));

   renderLoop();
   cleanup();

   close_log();
}

bool VulkanGame::initUI(int width, int height, unsigned char guiFlags) {
   // TODO: Create a game-gui function to get the gui version and retrieve it that way

   SDL_VERSION(&sdlVersion); // This gets the compile-time version
   SDL_GetVersion(&sdlVersion); // This gets the runtime version

   cout << "SDL "<<
      to_string(sdlVersion.major) << "." <<
      to_string(sdlVersion.minor) << "." <<
      to_string(sdlVersion.patch) << endl;

   // TODO: Refactor the logger api to be more flexible,
   // esp. since gl_log() and gl_log_err() have issues printing anything besides strings
   restart_gl_log();
   gl_log("starting SDL\n%s.%s.%s",
      to_string(sdlVersion.major).c_str(),
      to_string(sdlVersion.minor).c_str(),
      to_string(sdlVersion.patch).c_str());

   // TODO: Use open_Log() and related functions instead of gl_log ones
   // TODO: In addition, delete the gl_log functions
   open_log();
   get_log() << "starting SDL" << endl;
   get_log() <<
      (int)sdlVersion.major << "." <<
      (int)sdlVersion.minor << "." <<
      (int)sdlVersion.patch << endl;

   // TODO: Put all fonts, textures, and images in the assets folder
   gui = new GameGui_SDL();

   if (gui->init() == RTWO_ERROR) {
      // TODO: Also print these sorts of errors to the log
      cout << "UI library could not be initialized!" << endl;
      cout << gui->getError() << endl;
      // TODO: Rename RTWO_ERROR to something else
      return RTWO_ERROR;
   }

   window = (SDL_Window*)gui->createWindow("Vulkan Game", width, height, guiFlags & GUI_FLAGS_WINDOW_FULLSCREEN);
   if (window == nullptr) {
      cout << "Window could not be created!" << endl;
      cout << gui->getError() << endl;
      return RTWO_ERROR;
   }

   cout << "Target window size: (" << width << ", " << height << ")" << endl;
   cout << "Actual window size: (" << gui->getWindowWidth() << ", " << gui->getWindowHeight() << ")" << endl;

   return RTWO_SUCCESS;
}

void VulkanGame::initVulkan() {
   const vector<const char*> validationLayers = {
      "VK_LAYER_KHRONOS_validation"
   };
   const vector<const char*> deviceExtensions = {
      VK_KHR_SWAPCHAIN_EXTENSION_NAME
   };

   createVulkanInstance(validationLayers);
   setupDebugMessenger();
   createVulkanSurface();
   pickPhysicalDevice(deviceExtensions);
   createLogicalDevice(validationLayers, deviceExtensions);
   chooseSwapChainProperties();
   createSwapChain();
   createImageViews();

   createResourceCommandPool();
   createImageResources();

   createRenderPass();
   createCommandPools();
   createFramebuffers();
   createCommandBuffers();
   createSyncObjects();
}

void VulkanGame::initGraphicsPipelines() {
   modelPipeline = GraphicsPipeline_Vulkan<ModelVertex>(
      VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, physicalDevice, device, renderPass,
      { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height }, 24, 24);

   shipPipeline = GraphicsPipeline_Vulkan<ModelVertex>(
      VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, physicalDevice, device, renderPass,
      { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height }, 138, 138);

   asteroidPipeline = GraphicsPipeline_Vulkan<ModelVertex>(
      VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, physicalDevice, device, renderPass,
      { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height }, 24, 36);

   laserPipeline = GraphicsPipeline_Vulkan<LaserVertex>(
      VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, physicalDevice, device, renderPass,
      { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height }, 8, 18);

   explosionPipeline = GraphicsPipeline_Vulkan<ExplosionVertex>(
      VK_PRIMITIVE_TOPOLOGY_POINT_LIST, physicalDevice, device, renderPass,
      { 0, 0, (int)swapChainExtent.width, (int)swapChainExtent.height },
      EXPLOSION_PARTICLE_COUNT, EXPLOSION_PARTICLE_COUNT);
}

// TODO: Maybe changes the name to initScene() or something similar
void VulkanGame::initMatrices() {
   cam_pos = vec3(0.0f, 0.0f, 2.0f);

   float cam_yaw = 0.0f;
   float cam_pitch = -50.0f;

   mat4 yaw_mat = rotate(mat4(1.0f), radians(-cam_yaw), vec3(0.0f, 1.0f, 0.0f));
   mat4 pitch_mat = rotate(mat4(1.0f), radians(-cam_pitch), vec3(1.0f, 0.0f, 0.0f));

   mat4 R_view = pitch_mat * yaw_mat;
   mat4 T_view = translate(mat4(1.0f), vec3(-cam_pos.x, -cam_pos.y, -cam_pos.z));
   viewMat = R_view * T_view;

   projMat = perspective(radians(FOV_ANGLE), (float)swapChainExtent.width / (float)swapChainExtent.height, NEAR_CLIP, FAR_CLIP);
   projMat[1][1] *= -1; // flip the y-axis so that +y is up
}

void VulkanGame::renderLoop() {
   startTime = steady_clock::now();
   curTime = duration<float, seconds::period>(steady_clock::now() - startTime).count();

   fpsStartTime = curTime;
   frameCount = 0;

   lastSpawn_asteroid = curTime;

   ImGuiIO& io = ImGui::GetIO();

   done = false;
   while (!done) {

      prevTime = curTime;
      curTime = duration<float, seconds::period>(steady_clock::now() - startTime).count();
      elapsedTime = curTime - prevTime;

      if (curTime - fpsStartTime >= 1.0f) {
         fps = (float)frameCount / (curTime - fpsStartTime);

         frameCount = 0;
         fpsStartTime = curTime;
      }

      frameCount++;

      gui->processEvents();

      UIEvent uiEvent;
      while (gui->pollEvent(&uiEvent)) {
         GameEvent& e = uiEvent.event;
         SDL_Event sdlEvent = uiEvent.rawEvent.sdl;

         ImGui_ImplSDL2_ProcessEvent(&sdlEvent);
         if ((e.type == UI_EVENT_MOUSEBUTTONDOWN || e.type == UI_EVENT_MOUSEBUTTONUP || e.type == UI_EVENT_UNKNOWN) &&
             io.WantCaptureMouse) {
            if (sdlEvent.type == SDL_MOUSEWHEEL || sdlEvent.type == SDL_MOUSEBUTTONDOWN ||
                sdlEvent.type == SDL_MOUSEBUTTONUP) {
               continue;
            }
         }
         if ((e.type == UI_EVENT_KEYDOWN || e.type == UI_EVENT_KEYUP) && io.WantCaptureKeyboard) {
            if (sdlEvent.type == SDL_KEYDOWN || sdlEvent.type == SDL_KEYUP) {
               continue;
            }
         }
         if (io.WantTextInput) {
            // show onscreen keyboard if on mobile
         }

         switch (e.type) {
            case UI_EVENT_QUIT:
               cout << "Quit event detected" << endl;
               done = true;
               break;
            case UI_EVENT_WINDOWRESIZE:
               cout << "Window resize event detected" << endl;
               shouldRecreateSwapChain = true;
               break;
            case UI_EVENT_KEYDOWN:
               if (e.key.repeat) {
                  break;
               }

               if (e.key.keycode == SDL_SCANCODE_ESCAPE) {
                  done = true;
               } else if (e.key.keycode == SDL_SCANCODE_SPACE) {
                  cout << "Adding a plane" << endl;
                  float zOffset = -2.0f + (0.5f * modelObjects.size());

                  SceneObject<ModelVertex>& texturedSquare = addObject(modelObjects, modelPipeline,
                     addObjectIndex<ModelVertex>(modelObjects.size(),
                        addVertexNormals<ModelVertex>({
                           {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}},
                           {{ 0.5f, -0.5f,  0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 1.0f}},
                           {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
                           {{ 0.5f,  0.5f,  0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
                           {{-0.5f,  0.5f,  0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 0.0f}},
                           {{-0.5f, -0.5f,  0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f}}
                        })),
                     {
                        0, 1, 2, 3, 4, 5
                     }, objects_modelPipeline, {
                        mat4(1.0f)
                     });

                  texturedSquare.model_base =
                     translate(mat4(1.0f), vec3(0.0f, 0.0f, zOffset));
               // START UNREVIEWED SECTION
               } else if (e.key.keycode == SDL_SCANCODE_Z && leftLaserIdx == -1) {
                  // TODO: When I start actually removing objects from the object vectors,
                  // I will need to update the indices since they might become incorrect
                  // or invalid as objects get moved around

                  vec3 offset(shipObjects[0].model_transform * vec4(0.0f, 0.0f, 0.0f, 1.0f));

                  addLaser(
                     vec3(-0.21f, -1.19f, 1.76f) + offset,
                     vec3(-0.21f, -1.19f, -3.0f) + offset,
                     LASER_COLOR, 0.03f);

                  leftLaserIdx = laserObjects.size() - 1;
               } else if (e.key.keycode == SDL_SCANCODE_X && rightLaserIdx == -1) {
                  vec3 offset(shipObjects[0].model_transform * vec4(0.0f, 0.0f, 0.0f, 1.0f));

                  addLaser(
                     vec3(0.21f, -1.19f, 1.76f) + offset,
                     vec3(0.21f, -1.19f, -3.0f) + offset,
                     LASER_COLOR, 0.03f);

                  rightLaserIdx = laserObjects.size() - 1;
               // END UNREVIEWED SECTION
               } else {
                  cout << "Key event detected" << endl;
               }
               break;
            case UI_EVENT_KEYUP:
               // START UNREVIEWED SECTION
               if (e.key.keycode == SDL_SCANCODE_Z && leftLaserIdx != -1) {
                  objects_laserPipeline.get(leftLaserIdx).deleted = true;
                  leftLaserIdx = -1;

                  if (leftLaserEffect != nullptr) {
                     leftLaserEffect->deleted = true;
                     leftLaserEffect = nullptr;
                  }
               } else if (e.key.keycode == SDL_SCANCODE_X && rightLaserIdx != -1) {
                  objects_laserPipeline.get(rightLaserIdx).deleted = true;
                  rightLaserIdx = -1;

                  if (rightLaserEffect != nullptr) {
                     rightLaserEffect->deleted = true;
                     rightLaserEffect = nullptr;
                  }
               }
               // END UNREVIEWED SECTION
               break;
            case UI_EVENT_WINDOW:
            case UI_EVENT_MOUSEBUTTONDOWN:
            case UI_EVENT_MOUSEBUTTONUP:
            case UI_EVENT_MOUSEMOTION:
               break;
            case UI_EVENT_UNHANDLED:
               cout << "Unhandled event type: 0x" << hex << sdlEvent.type << dec << endl;
               break;
            case UI_EVENT_UNKNOWN:
            default:
               cout << "Unknown event type: 0x" << hex << sdlEvent.type << dec << endl;
               break;
         }

         // This was left ovedr from the previous SDL UI implementation.
         // Might need something like this again when I start processing screen-specific UI events not related
         // to the IMGUI ui, such as arrow keys for movement and other buttons for shotting or something
         // currentScreen->handleEvent(e);
      }

      // Check which keys are held down

      SceneObject<ModelVertex>& ship = shipObjects[0];

      if (gui->keyPressed(SDL_SCANCODE_LEFT)) {
         float distance = -this->shipSpeed * this->elapsedTime;

         ship.model_transform = translate(mat4(1.0f), vec3(distance, 0.0f, 0.0f))
            * shipObjects[0].model_transform;

         if (leftLaserIdx != -1) {
            translateLaser(leftLaserIdx, vec3(distance, 0.0f, 0.0f));
         }
         if (rightLaserIdx != -1) {
            translateLaser(rightLaserIdx, vec3(distance, 0.0f, 0.0f));
         }
      } else if (gui->keyPressed(SDL_SCANCODE_RIGHT)) {
         float distance = this->shipSpeed * this->elapsedTime;

         ship.model_transform = translate(mat4(1.0f), vec3(distance, 0.0f, 0.0f))
            * shipObjects[0].model_transform;

         if (leftLaserIdx != -1) {
            translateLaser(leftLaserIdx, vec3(distance, 0.0f, 0.0f));
         }
         if (rightLaserIdx != -1) {
            translateLaser(rightLaserIdx, vec3(distance, 0.0f, 0.0f));
         }
      }

      if (shouldRecreateSwapChain) {
         gui->refreshWindowSize();
         const bool isMinimized = gui->getWindowWidth() == 0 || gui->getWindowHeight() == 0;

         if (!isMinimized) {
            // TODO: This should be used if the min image count changes, presumably because a new surface was created
            // with a different image count or something like that. Maybe I want to add code to query for a new min image count
            // during swapchain recreation to take advantage of this
            ImGui_ImplVulkan_SetMinImageCount(swapChainMinImageCount);

            recreateSwapChain();

            shouldRecreateSwapChain = false;
         }
      }// REVIEWED TO THIS POINT

      updateScene();

      // TODO: Move this into a renderImGuiOverlay() function
      ImGui_ImplVulkan_NewFrame();
      ImGui_ImplSDL2_NewFrame(window);
      ImGui::NewFrame();

      int w, h;
      SDL_GetWindowSize(((GameGui_SDL*)gui)->window, &w, &h);

      // Probably a retina display
      // TODO: Find a better fix for this. Maybe I should use SDL_Vulkan_GetWindowSize here instead
      // of SDL_Vulkan_GetDrawableSize
      if (w < gui->getWindowWidth() && h < gui->getWindowHeight()) {
         (this->*currentRenderScreenFn)(w, h);
      } else {
         (this->*currentRenderScreenFn)(gui->getWindowWidth(), gui->getWindowHeight());
      }

      ImGui::Render();

      gui->refreshWindowSize();
      const bool isMinimized = gui->getWindowWidth() == 0 || gui->getWindowHeight() == 0;

      if (!isMinimized) {
         renderFrame(ImGui::GetDrawData());
         presentFrame();
      }
   }
}

// TODO: The only updates that need to happen once per Vulkan image are the SSBO ones,
// which are already handled by updateObject(). Move this code to a different place,
// where it will run just once per frame
void VulkanGame::updateScene() {

   // TODO: I can probably use one buffer to store the view and projection mats
   // and share it across all the shader pipelines

   uniforms_modelPipeline.data()->view = viewMat;
   uniforms_modelPipeline.data()->proj = projMat;

   uniforms_shipPipeline.data()->view = viewMat;
   uniforms_shipPipeline.data()->proj = projMat;

   uniforms_asteroidPipeline.data()->view = viewMat;
   uniforms_asteroidPipeline.data()->proj = projMat;

   uniforms_laserPipeline.data()->view = viewMat;
   uniforms_laserPipeline.data()->proj = projMat;

   uniforms_explosionPipeline.data()->view = viewMat;
   uniforms_explosionPipeline.data()->proj = projMat;
   uniforms_explosionPipeline.data()->cur_time = curTime;

   for (vector<BaseEffectOverTime*>::iterator it = effects.begin(); it != effects.end(); ) {
      if ((*it)->deleted) {
         delete *it;
         it = effects.erase(it);
      } else {
         BaseEffectOverTime* eot = *it;

         eot->applyEffect(curTime);

         it++;
      }
   }

   if (curTime - lastSpawn_asteroid > spawnRate_asteroid) {
      lastSpawn_asteroid = curTime;

      SceneObject<ModelVertex>& asteroid = addObject(asteroidObjects, asteroidPipeline,
         addObjectIndex<ModelVertex>(asteroidObjects.size(),
            addVertexNormals<ModelVertex>({

               // front
               {{ 1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},

               // top
               {{ 1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},

               // bottom
               {{ 1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f, -1.0f, -1.0}, {0.4f, 0.4f, 0.4f}},

               // back
               {{ 1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},

               // right
               {{ 1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{ 1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},

               // left
               {{-1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f,  1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f,  1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f, -1.0f}, {0.4f, 0.4f, 0.4f}},
               {{-1.0f, -1.0f,  1.0f}, {0.4f, 0.4f, 0.4f}},
            })),
         {
             0,  1,  2,  3,  4,  5,
             6,  7,  8,  9, 10, 11,
            12, 13, 14, 15, 16, 17,
            18, 19, 20, 21, 22, 23,
            24, 25, 26, 27, 28, 29,
            30, 31, 32, 33, 34, 35,
         }, objects_asteroidPipeline, {
            mat4(1.0f),
            10.0f,
            false
         });

      // This accounts for the scaling in model_base.
      // Dividing by 8 instead of 10 since the bounding radius algorithm
      // under-calculates the true value.
      // TODO: Figure out the best way to take scaling into account when calculating the radius
      // Keep in mind that the main complicating factor is the currently poor radius calculation
      asteroid.radius /= 8.0f;

      asteroid.model_base =
         translate(mat4(1.0f), vec3(getRandomNum(-1.3f, 1.3f), -1.2f, getRandomNum(-5.5f, -4.5f))) *
         rotate(mat4(1.0f), radians(60.0f), vec3(1.0f, 1.0f, -1.0f)) *
         scale(mat4(1.0f), vec3(0.1f, 0.1f, 0.1f));
   }

   // TODO: Probably move the resizing to the VulkanBuffer class
   // TODO: Figure out a way to make updateDescriptorInfo easier to use, maybe store the binding index in the buffer set

   if (objects_modelPipeline.resized) {
      objects_modelPipeline.unmap(objectBuffers_modelPipeline.memory, device);

      resizeBufferSet(objectBuffers_modelPipeline, objects_modelPipeline.memorySize(), resourceCommandPool,
                      graphicsQueue, true);

      objects_modelPipeline.map(objectBuffers_modelPipeline.memory, device);

      objects_modelPipeline.resize();

      modelPipeline.updateDescriptorInfo(1, &objectBuffers_modelPipeline.infoSet, swapChainImages.size());
   }

   for (size_t i = 0; i < modelObjects.size(); i++) {
      SceneObject<ModelVertex>& obj = modelObjects[i];
      SSBO_ModelObject& objData = objects_modelPipeline.get(i);

      // Rotate the textured squares
      obj.model_transform =
         translate(mat4(1.0f), vec3(0.0f, -2.0f, -0.0f)) *
         rotate(mat4(1.0f), curTime * radians(90.0f), vec3(0.0f, 0.0f, 1.0f));

      objData.model = obj.model_transform * obj.model_base;
      obj.center = vec3(objData.model * vec4(0.0f, 0.0f, 0.0f, 1.0f));
   }

   VulkanUtils::copyDataToMappedMemory(device, uniforms_modelPipeline.data(),
                                       uniforms_modelPipeline.mapped(imageIndex),
                                       uniformBuffers_modelPipeline.memory[imageIndex],
                                       uniforms_modelPipeline.memorySize(), true);

   VulkanUtils::copyDataToMappedMemory(device, objects_modelPipeline.data(),
                                       objects_modelPipeline.mapped(imageIndex),
                                       objectBuffers_modelPipeline.memory[imageIndex],
                                       objects_modelPipeline.memorySize(), true);

   if (objects_shipPipeline.resized) {
      objects_shipPipeline.unmap(objectBuffers_shipPipeline.memory, device);

      resizeBufferSet(objectBuffers_shipPipeline, objects_shipPipeline.memorySize(), resourceCommandPool,
                      graphicsQueue, true);

      objects_shipPipeline.map(objectBuffers_shipPipeline.memory, device);

      objects_shipPipeline.resize();

      shipPipeline.updateDescriptorInfo(1, &objectBuffers_shipPipeline.infoSet, swapChainImages.size());
   }

   // TODO: Move ship position updates from the ui event handling code into this function
   for (size_t i = 0; i < shipObjects.size(); i++) {
      SceneObject<ModelVertex>& obj = shipObjects[i];
      SSBO_ModelObject& objData = objects_shipPipeline.get(i);

      objData.model = obj.model_transform * obj.model_base;
      obj.center = vec3(objData.model * vec4(0.0f, 0.0f, 0.0f, 1.0f));
   }

   VulkanUtils::copyDataToMappedMemory(device, uniforms_shipPipeline.data(),
                                       uniforms_shipPipeline.mapped(imageIndex),
                                       uniformBuffers_shipPipeline.memory[imageIndex],
                                       uniforms_shipPipeline.memorySize(), true);

   VulkanUtils::copyDataToMappedMemory(device, objects_shipPipeline.data(),
                                       objects_shipPipeline.mapped(imageIndex),
                                       objectBuffers_shipPipeline.memory[imageIndex],
                                       objects_shipPipeline.memorySize(), true);

   if (objects_asteroidPipeline.resized) {
      objects_asteroidPipeline.unmap(objectBuffers_asteroidPipeline.memory, device);

      resizeBufferSet(objectBuffers_asteroidPipeline, objects_asteroidPipeline.memorySize(), resourceCommandPool,
                      graphicsQueue, true);

      objects_asteroidPipeline.map(objectBuffers_asteroidPipeline.memory, device);

      objects_asteroidPipeline.resize();

      asteroidPipeline.updateDescriptorInfo(1, &objectBuffers_asteroidPipeline.infoSet, swapChainImages.size());
   }

   for (size_t i = 0; i < asteroidObjects.size(); i++) {
      SceneObject<ModelVertex>& obj = asteroidObjects[i];
      SSBO_Asteroid& objData = objects_asteroidPipeline.get(i);

      if (!objData.deleted) {
         vec3 objCenter = vec3(viewMat * vec4(obj.center, 1.0f));

         if (objData.hp <= 0.0f) {
            objData.deleted = true;

            // TODO: Optimize this so I don't recalculate the camera rotation every time
            // TODO: Also, avoid re-declaring cam_pitch
            float cam_pitch = -50.0f;
            mat4 pitch_mat = rotate(mat4(1.0f), radians(cam_pitch), vec3(1.0f, 0.0f, 0.0f));
            mat4 model_mat = translate(mat4(1.0f), obj.center) * pitch_mat;

            addExplosion(model_mat, 0.5f, curTime);

            this->score++;
         } else if ((objCenter.z - obj.radius) > -NEAR_CLIP) {
            objData.deleted = true;
         } else {
            obj.model_transform =
               translate(mat4(1.0f), vec3(0.0f, 0.0f, this->asteroidSpeed * this->elapsedTime)) *
               obj.model_transform;
         }

         objData.model = obj.model_transform * obj.model_base;
         obj.center = vec3(objData.model * vec4(0.0f, 0.0f, 0.0f, 1.0f));
      }
   }

   VulkanUtils::copyDataToMappedMemory(device, uniforms_asteroidPipeline.data(),
                                       uniforms_asteroidPipeline.mapped(imageIndex),
                                       uniformBuffers_asteroidPipeline.memory[imageIndex],
                                       uniforms_asteroidPipeline.memorySize(), true);

   VulkanUtils::copyDataToMappedMemory(device, objects_asteroidPipeline.data(),
                                       objects_asteroidPipeline.mapped(imageIndex),
                                       objectBuffers_asteroidPipeline.memory[imageIndex],
                                       objects_asteroidPipeline.memorySize(), true);

   if (objects_laserPipeline.resized) {
      objects_laserPipeline.unmap(objectBuffers_laserPipeline.memory, device);

      resizeBufferSet(objectBuffers_laserPipeline, objects_laserPipeline.memorySize(), resourceCommandPool,
                      graphicsQueue, true);

      objects_laserPipeline.map(objectBuffers_laserPipeline.memory, device);

      objects_laserPipeline.resize();

      laserPipeline.updateDescriptorInfo(1, &objectBuffers_laserPipeline.infoSet, swapChainImages.size());
   }

   if (leftLaserIdx != -1) {
      updateLaserTarget(leftLaserIdx);
   }
   if (rightLaserIdx != -1) {
      updateLaserTarget(rightLaserIdx);
   }

   for (size_t i = 0; i < laserObjects.size(); i++) {
      SceneObject<LaserVertex>& obj = laserObjects[i];
      SSBO_Laser& objData = objects_laserPipeline.get(i);

      objData.model = obj.model_transform * obj.model_base;
      obj.center = vec3(objData.model * vec4(0.0f, 0.0f, 0.0f, 1.0f));
   }

   VulkanUtils::copyDataToMappedMemory(device, uniforms_laserPipeline.data(),
                                       uniforms_laserPipeline.mapped(imageIndex),
                                       uniformBuffers_laserPipeline.memory[imageIndex],
                                       uniforms_laserPipeline.memorySize(), true);

   VulkanUtils::copyDataToMappedMemory(device, objects_laserPipeline.data(),
                                       objects_laserPipeline.mapped(imageIndex),
                                       objectBuffers_laserPipeline.memory[imageIndex],
                                       objects_laserPipeline.memorySize(), true);

   if (objects_explosionPipeline.resized) {
      objects_explosionPipeline.unmap(objectBuffers_explosionPipeline.memory, device);

      resizeBufferSet(objectBuffers_explosionPipeline, objects_explosionPipeline.memorySize(), resourceCommandPool,
                      graphicsQueue, true);

      objects_explosionPipeline.map(objectBuffers_explosionPipeline.memory, device);

      objects_explosionPipeline.resize();

      explosionPipeline.updateDescriptorInfo(1, &objectBuffers_explosionPipeline.infoSet, swapChainImages.size());
   }

   for (size_t i = 0; i < explosionObjects.size(); i++) {
      SceneObject<ExplosionVertex>& obj = explosionObjects[i];
      SSBO_Explosion& objData = objects_explosionPipeline.get(i);

      if (!objData.deleted) {
         if (curTime > (objData.explosionStartTime + objData.explosionDuration)) {
            objData.deleted = true;
         }
      }

      objData.model = obj.model_transform * obj.model_base;
      obj.center = vec3(objData.model * vec4(0.0f, 0.0f, 0.0f, 1.0f));
   }

   VulkanUtils::copyDataToMappedMemory(device, uniforms_explosionPipeline.data(),
                                       uniforms_explosionPipeline.mapped(imageIndex),
                                       uniformBuffers_explosionPipeline.memory[imageIndex],
                                       uniforms_explosionPipeline.memorySize(), true);

   VulkanUtils::copyDataToMappedMemory(device, objects_explosionPipeline.data(),
                                       objects_explosionPipeline.mapped(imageIndex),
                                       objectBuffers_explosionPipeline.memory[imageIndex],
                                       objects_explosionPipeline.memorySize(), true);
}

void VulkanGame::cleanup() {
   // FIXME: We could wait on the Queue if we had the queue in wd-> (otherwise VulkanH functions can't use globals)
   //vkQueueWaitIdle(g_Queue);
   VKUTIL_CHECK_RESULT(vkDeviceWaitIdle(device), "failed to wait for device!");

   cleanupImGuiOverlay();

   cleanupSwapChain();

   VulkanUtils::destroyVulkanImage(device, floorTextureImage);
   // START UNREVIEWED SECTION
   VulkanUtils::destroyVulkanImage(device, laserTextureImage);

   vkDestroySampler(device, textureSampler, nullptr);

   modelPipeline.cleanupBuffers();
   shipPipeline.cleanupBuffers();
   asteroidPipeline.cleanupBuffers();
   laserPipeline.cleanupBuffers();
   explosionPipeline.cleanupBuffers();

   // END UNREVIEWED SECTION

   vkDestroyCommandPool(device, resourceCommandPool, nullptr);

   vkDestroyDevice(device, nullptr);
   vkDestroySurfaceKHR(instance, vulkanSurface, nullptr);

   if (ENABLE_VALIDATION_LAYERS) {
      VulkanUtils::destroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
   }

   vkDestroyInstance(instance, nullptr);

   gui->destroyWindow();
   gui->shutdown();
   delete gui;
}

void VulkanGame::createVulkanInstance(const vector<const char*>& validationLayers) {
   if (ENABLE_VALIDATION_LAYERS && !VulkanUtils::checkValidationLayerSupport(validationLayers)) {
      throw runtime_error("validation layers requested, but not available!");
   }

   VkApplicationInfo appInfo = {};
   appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
   appInfo.pApplicationName = "Vulkan Game";
   appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
   appInfo.pEngineName = "No Engine";
   appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
   appInfo.apiVersion = VK_API_VERSION_1_0;

   VkInstanceCreateInfo createInfo = {};
   createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
   createInfo.pApplicationInfo = &appInfo;

   vector<const char*> extensions = gui->getRequiredExtensions();
   if (ENABLE_VALIDATION_LAYERS) {
      extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
   }

   createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
   createInfo.ppEnabledExtensionNames = extensions.data();

   cout << endl << "Extensions:" << endl;
   for (const char* extensionName : extensions) {
      cout << extensionName << endl;
   }
   cout << endl;

   VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo;
   if (ENABLE_VALIDATION_LAYERS) {
      createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
      createInfo.ppEnabledLayerNames = validationLayers.data();

      populateDebugMessengerCreateInfo(debugCreateInfo);
      createInfo.pNext = &debugCreateInfo;
   } else {
      createInfo.enabledLayerCount = 0;

      createInfo.pNext = nullptr;
   }

   if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
      throw runtime_error("failed to create instance!");
   }
}

void VulkanGame::setupDebugMessenger() {
   if (!ENABLE_VALIDATION_LAYERS) {
      return;
   }

   VkDebugUtilsMessengerCreateInfoEXT createInfo;
   populateDebugMessengerCreateInfo(createInfo);

   if (VulkanUtils::createDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) {
      throw runtime_error("failed to set up debug messenger!");
   }
}

void VulkanGame::populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) {
   createInfo = {};
   createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
   createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
   createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
   createInfo.pfnUserCallback = debugCallback;
}

VKAPI_ATTR VkBool32 VKAPI_CALL VulkanGame::debugCallback(
      VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
      VkDebugUtilsMessageTypeFlagsEXT messageType,
      const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
      void* pUserData) {
   cerr << "validation layer: " << pCallbackData->pMessage << endl;

   // TODO: Figure out what the return value means and if it should always be VK_FALSE
   return VK_FALSE;
}

void VulkanGame::createVulkanSurface() {
   if (gui->createVulkanSurface(instance, &vulkanSurface) == RTWO_ERROR) {
      throw runtime_error("failed to create window surface!");
   }
}

void VulkanGame::pickPhysicalDevice(const vector<const char*>& deviceExtensions) {
   uint32_t deviceCount = 0;
   // TODO: Check VkResult
   vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);

   if (deviceCount == 0) {
      throw runtime_error("failed to find GPUs with Vulkan support!");
   }

   vector<VkPhysicalDevice> devices(deviceCount);
   // TODO: Check VkResult
   vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

   cout << endl << "Graphics cards:" << endl;
   for (const VkPhysicalDevice& device : devices) {
      if (isDeviceSuitable(device, deviceExtensions)) {
         physicalDevice = device;
         break;
      }
   }
   cout << endl;

   if (physicalDevice == VK_NULL_HANDLE) {
      throw runtime_error("failed to find a suitable GPU!");
   }
}

bool VulkanGame::isDeviceSuitable(VkPhysicalDevice physicalDevice, const vector<const char*>& deviceExtensions) {
   VkPhysicalDeviceProperties deviceProperties;
   vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties);

   cout << "Device: " << deviceProperties.deviceName << endl;

   // TODO: Eventually, maybe let the user pick out of a set of GPUs in case the user does want to use
   // an integrated GPU. On my laptop, this function returns TRUE for the integrated GPU, but crashes
   // when trying to use it to render. Maybe I just need to figure out which other extensions and features
   // to check.
   if (deviceProperties.deviceType != VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
      return false;
   }

   QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface);
   bool extensionsSupported = VulkanUtils::checkDeviceExtensionSupport(physicalDevice, deviceExtensions);
   bool swapChainAdequate = false;

   if (extensionsSupported) {
      vector<VkSurfaceFormatKHR> formats = VulkanUtils::querySwapChainFormats(physicalDevice, vulkanSurface);
      vector<VkPresentModeKHR> presentModes = VulkanUtils::querySwapChainPresentModes(physicalDevice, vulkanSurface);

      swapChainAdequate = !formats.empty() && !presentModes.empty();
   }

   VkPhysicalDeviceFeatures supportedFeatures;
   vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);

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

void VulkanGame::createLogicalDevice(const vector<const char*>& validationLayers,
      const vector<const char*>& deviceExtensions) {
   QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface);

   if (!indices.isComplete()) {
      throw runtime_error("failed to find required queue families!");
   }

   // TODO: Using separate graphics and present queues currently works, but I should verify that I'm
   // using them correctly to get the most benefit out of separate queues

   vector<VkDeviceQueueCreateInfo> queueCreateInfoList;
   set<uint32_t> uniqueQueueFamilies = { indices.graphicsFamily.value(), indices.presentFamily.value() };

   float queuePriority = 1.0f;
   for (uint32_t queueFamily : uniqueQueueFamilies) {
      VkDeviceQueueCreateInfo queueCreateInfo = {};
      queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
      queueCreateInfo.queueCount = 1;
      queueCreateInfo.queueFamilyIndex = queueFamily;
      queueCreateInfo.pQueuePriorities = &queuePriority;

      queueCreateInfoList.push_back(queueCreateInfo);
   }

   VkPhysicalDeviceFeatures deviceFeatures = {};
   deviceFeatures.samplerAnisotropy = VK_TRUE;

   VkDeviceCreateInfo createInfo = {};
   createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;

   createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfoList.size());
   createInfo.pQueueCreateInfos = queueCreateInfoList.data();

   createInfo.pEnabledFeatures = &deviceFeatures;

   createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
   createInfo.ppEnabledExtensionNames = deviceExtensions.data();

   // These fields are ignored  by up-to-date Vulkan implementations,
   // but it's a good idea to set them for backwards compatibility
   if (ENABLE_VALIDATION_LAYERS) {
      createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
      createInfo.ppEnabledLayerNames = validationLayers.data();
   } else {
      createInfo.enabledLayerCount = 0;
   }

   if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) {
      throw runtime_error("failed to create logical device!");
   }

   vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
   vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
}

void VulkanGame::chooseSwapChainProperties() {
   vector<VkSurfaceFormatKHR> availableFormats = VulkanUtils::querySwapChainFormats(physicalDevice, vulkanSurface);
   vector<VkPresentModeKHR> availablePresentModes = VulkanUtils::querySwapChainPresentModes(physicalDevice, vulkanSurface);

   swapChainSurfaceFormat = VulkanUtils::chooseSwapSurfaceFormat(availableFormats,
      { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM },
      VK_COLOR_SPACE_SRGB_NONLINEAR_KHR);

   vector<VkPresentModeKHR> presentModes{
      VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_FIFO_KHR
   };
   //vector<VkPresentModeKHR> presentModes{ VK_PRESENT_MODE_FIFO_KHR };

   swapChainPresentMode = VulkanUtils::chooseSwapPresentMode(availablePresentModes, presentModes);

   cout << "[vulkan] Selected PresentMode = " << swapChainPresentMode << endl;

   VkSurfaceCapabilitiesKHR capabilities = VulkanUtils::querySwapChainCapabilities(physicalDevice, vulkanSurface);

   if (swapChainPresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
      swapChainMinImageCount = 3;
   } else if (swapChainPresentMode == VK_PRESENT_MODE_FIFO_KHR || swapChainPresentMode == VK_PRESENT_MODE_FIFO_RELAXED_KHR) {
      swapChainMinImageCount = 2;
   } else if (swapChainPresentMode == VK_PRESENT_MODE_IMMEDIATE_KHR) {
      swapChainMinImageCount = 1;
   } else {
      throw runtime_error("unexpected present mode!");
   }

  if (swapChainMinImageCount < capabilities.minImageCount) {
      swapChainMinImageCount = capabilities.minImageCount;
   } else if (capabilities.maxImageCount != 0 && swapChainMinImageCount > capabilities.maxImageCount) {
      swapChainMinImageCount = capabilities.maxImageCount;
   }
}

void VulkanGame::createSwapChain() {
   VkSurfaceCapabilitiesKHR capabilities = VulkanUtils::querySwapChainCapabilities(physicalDevice, vulkanSurface);

   swapChainExtent = VulkanUtils::chooseSwapExtent(capabilities, gui->getWindowWidth(), gui->getWindowHeight());

   VkSwapchainCreateInfoKHR createInfo = {};
   createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
   createInfo.surface = vulkanSurface;
   createInfo.minImageCount = swapChainMinImageCount;
   createInfo.imageFormat = swapChainSurfaceFormat.format;
   createInfo.imageColorSpace = swapChainSurfaceFormat.colorSpace;
   createInfo.imageExtent = swapChainExtent;
   createInfo.imageArrayLayers = 1;
   createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

   // TODO: Maybe save this result so I don't have to recalculate it every time
   QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface);
   uint32_t queueFamilyIndices[] = { indices.graphicsFamily.value(), indices.presentFamily.value() };

   if (indices.graphicsFamily != indices.presentFamily) {
      createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
      createInfo.queueFamilyIndexCount = 2;
      createInfo.pQueueFamilyIndices = queueFamilyIndices;
   } else {
      createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
      createInfo.queueFamilyIndexCount = 0;
      createInfo.pQueueFamilyIndices = nullptr;
   }

   createInfo.preTransform = capabilities.currentTransform;
   createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
   createInfo.presentMode = swapChainPresentMode;
   createInfo.clipped = VK_TRUE;
   createInfo.oldSwapchain = VK_NULL_HANDLE;

   if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
      throw runtime_error("failed to create swap chain!");
   }

   if (vkGetSwapchainImagesKHR(device, swapChain, &swapChainImageCount, nullptr) != VK_SUCCESS) {
      throw runtime_error("failed to get swap chain image count!");
   }

   swapChainImages.resize(swapChainImageCount);
   if (vkGetSwapchainImagesKHR(device, swapChain, &swapChainImageCount, swapChainImages.data()) != VK_SUCCESS) {
      throw runtime_error("failed to get swap chain images!");
   }
}

void VulkanGame::createImageViews() {
   swapChainImageViews.resize(swapChainImageCount);

   for (size_t i = 0; i < swapChainImageCount; i++) {
      swapChainImageViews[i] = VulkanUtils::createImageView(device, swapChainImages[i], swapChainSurfaceFormat.format,
         VK_IMAGE_ASPECT_COLOR_BIT);
   }
}

void VulkanGame::createResourceCommandPool() {
   QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface);

   VkCommandPoolCreateInfo poolInfo = {};
   poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
   poolInfo.queueFamilyIndex = indices.graphicsFamily.value();
   poolInfo.flags = 0;

   if (vkCreateCommandPool(device, &poolInfo, nullptr, &resourceCommandPool) != VK_SUCCESS) {
      throw runtime_error("failed to create resource command pool!");
   }
}

void VulkanGame::createImageResources() {
   VulkanUtils::createDepthImage(device, physicalDevice, resourceCommandPool, findDepthFormat(), swapChainExtent,
                                 depthImage, graphicsQueue);

   createTextureSampler();

   // TODO: Move all images/textures somewhere into the assets folder

   VulkanUtils::createVulkanImageFromFile(device, physicalDevice, resourceCommandPool, "textures/texture.jpg",
      floorTextureImage, graphicsQueue);

   floorTextureImageDescriptor = {};
   floorTextureImageDescriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
   floorTextureImageDescriptor.imageView = floorTextureImage.imageView;
   floorTextureImageDescriptor.sampler = textureSampler;

   VulkanUtils::createVulkanImageFromFile(device, physicalDevice, resourceCommandPool, "textures/laser.png",
      laserTextureImage, graphicsQueue);

   laserTextureImageDescriptor = {};
   laserTextureImageDescriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
   laserTextureImageDescriptor.imageView = laserTextureImage.imageView;
   laserTextureImageDescriptor.sampler = textureSampler;
}

VkFormat VulkanGame::findDepthFormat() {
   return VulkanUtils::findSupportedFormat(
      physicalDevice,
      { VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D32_SFLOAT, VK_FORMAT_D24_UNORM_S8_UINT },
      VK_IMAGE_TILING_OPTIMAL,
      VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT
   );
}

void VulkanGame::createRenderPass() {
   VkAttachmentDescription colorAttachment = {};
   colorAttachment.format = swapChainSurfaceFormat.format;
   colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
   colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
   colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
   colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
   colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
   colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
   colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

   VkAttachmentReference colorAttachmentRef = {};
   colorAttachmentRef.attachment = 0;
   colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

   VkAttachmentDescription depthAttachment = {};
   depthAttachment.format = findDepthFormat();
   depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
   depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
   depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
   depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
   depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
   depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
   depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

   VkAttachmentReference depthAttachmentRef = {};
   depthAttachmentRef.attachment = 1;
   depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

   VkSubpassDescription subpass = {};
   subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
   subpass.colorAttachmentCount = 1;
   subpass.pColorAttachments = &colorAttachmentRef;
   subpass.pDepthStencilAttachment = &depthAttachmentRef;

   VkSubpassDependency dependency = {};
   dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
   dependency.dstSubpass = 0;
   dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
   dependency.srcAccessMask = 0;
   dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
   dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

   array<VkAttachmentDescription, 2> attachments = { colorAttachment, depthAttachment };
   VkRenderPassCreateInfo renderPassInfo = {};
   renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
   renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
   renderPassInfo.pAttachments = attachments.data();
   renderPassInfo.subpassCount = 1;
   renderPassInfo.pSubpasses = &subpass;
   renderPassInfo.dependencyCount = 1;
   renderPassInfo.pDependencies = &dependency;

   if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
      throw runtime_error("failed to create render pass!");
   }
}

void VulkanGame::createCommandPools() {
   commandPools.resize(swapChainImageCount);

   QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface);

   for (size_t i = 0; i < swapChainImageCount; i++) {
      VkCommandPoolCreateInfo poolInfo = {};
      poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
      poolInfo.queueFamilyIndex = indices.graphicsFamily.value();
      poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;

      VKUTIL_CHECK_RESULT(vkCreateCommandPool(device, &poolInfo, nullptr, &commandPools[i]),
         "failed to create graphics command pool!");
   }
}

void VulkanGame::createTextureSampler() {
   VkSamplerCreateInfo samplerInfo = {};
   samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
   samplerInfo.magFilter = VK_FILTER_LINEAR;
   samplerInfo.minFilter = VK_FILTER_LINEAR;

   samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
   samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
   samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;

   samplerInfo.anisotropyEnable = VK_TRUE;
   samplerInfo.maxAnisotropy = 16;
   samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
   samplerInfo.unnormalizedCoordinates = VK_FALSE;
   samplerInfo.compareEnable = VK_FALSE;
   samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
   samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
   samplerInfo.mipLodBias = 0.0f;
   samplerInfo.minLod = 0.0f;
   samplerInfo.maxLod = 0.0f;

   VKUTIL_CHECK_RESULT(vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler),
      "failed to create texture sampler!");
}

void VulkanGame::createFramebuffers() {
   swapChainFramebuffers.resize(swapChainImageCount);

   VkFramebufferCreateInfo framebufferInfo = {};
   framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
   framebufferInfo.renderPass = renderPass;
   framebufferInfo.width = swapChainExtent.width;
   framebufferInfo.height = swapChainExtent.height;
   framebufferInfo.layers = 1;

   for (uint32_t i = 0; i < swapChainImageCount; i++) {
      array<VkImageView, 2> attachments = {
         swapChainImageViews[i],
         depthImage.imageView
      };

      framebufferInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
      framebufferInfo.pAttachments = attachments.data();

      if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) {
         throw runtime_error("failed to create framebuffer!");
      }
   }
}

void VulkanGame::createCommandBuffers() {
   commandBuffers.resize(swapChainImageCount);

   for (size_t i = 0; i < swapChainImageCount; i++) {
      VkCommandBufferAllocateInfo allocInfo = {};
      allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
      allocInfo.commandPool = commandPools[i];
      allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
      allocInfo.commandBufferCount = 1;

      if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffers[i]) != VK_SUCCESS) {
         throw runtime_error("failed to allocate command buffer!");
      }
   }
}

void VulkanGame::createSyncObjects() {
   imageAcquiredSemaphores.resize(swapChainImageCount);
   renderCompleteSemaphores.resize(swapChainImageCount);
   inFlightFences.resize(swapChainImageCount);

   VkSemaphoreCreateInfo semaphoreInfo = {};
   semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

   VkFenceCreateInfo fenceInfo = {};
   fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
   fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;

   for (size_t i = 0; i < swapChainImageCount; i++) {
      VKUTIL_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAcquiredSemaphores[i]),
         "failed to create image acquired sempahore for a frame!");

      VKUTIL_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderCompleteSemaphores[i]),
         "failed to create render complete sempahore for a frame!");

      VKUTIL_CHECK_RESULT(vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]),
         "failed to create fence for a frame!");
   }
}

void VulkanGame::renderFrame(ImDrawData* draw_data) {
   VkResult result = vkAcquireNextImageKHR(device, swapChain, numeric_limits<uint64_t>::max(),
      imageAcquiredSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

   if (result == VK_SUBOPTIMAL_KHR) {
      shouldRecreateSwapChain = true;
   } else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
      shouldRecreateSwapChain = true;
      return;
   } else {
      VKUTIL_CHECK_RESULT(result, "failed to acquire swap chain image!");
   }

   VKUTIL_CHECK_RESULT(
      vkWaitForFences(device, 1, &inFlightFences[imageIndex], VK_TRUE, numeric_limits<uint64_t>::max()),
      "failed waiting for fence!");

   VKUTIL_CHECK_RESULT(vkResetFences(device, 1, &inFlightFences[imageIndex]),
      "failed to reset fence!");

   VKUTIL_CHECK_RESULT(vkResetCommandPool(device, commandPools[imageIndex], 0),
      "failed to reset command pool!");

   VkCommandBufferBeginInfo beginInfo = {};
   beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
   beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

   VKUTIL_CHECK_RESULT(vkBeginCommandBuffer(commandBuffers[imageIndex], &beginInfo),
      "failed to begin recording command buffer!");

   VkRenderPassBeginInfo renderPassInfo = {};
   renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
   renderPassInfo.renderPass = renderPass;
   renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
   renderPassInfo.renderArea.offset = { 0, 0 };
   renderPassInfo.renderArea.extent = swapChainExtent;

   array<VkClearValue, 2> clearValues = {};
   clearValues[0].color = { { 0.0f, 0.0f, 0.0f, 1.0f } };
   clearValues[1].depthStencil = { 1.0f, 0 };

   renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
   renderPassInfo.pClearValues = clearValues.data();

   vkCmdBeginRenderPass(commandBuffers[imageIndex], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

   // TODO: Find a more elegant, per-screen solution for this
   if (currentRenderScreenFn == &VulkanGame::renderGameScreen) {
      modelPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex, { 0 });
      shipPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex, { 0 });
      asteroidPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex, { 0 });
      laserPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex, { 0 });
      explosionPipeline.createRenderCommands(commandBuffers[imageIndex], imageIndex, { 0 });
   }

   ImGui_ImplVulkan_RenderDrawData(draw_data, commandBuffers[imageIndex]);

   vkCmdEndRenderPass(commandBuffers[imageIndex]);

   VKUTIL_CHECK_RESULT(vkEndCommandBuffer(commandBuffers[imageIndex]),
      "failed to record command buffer!");

   VkSemaphore waitSemaphores[] = { imageAcquiredSemaphores[currentFrame] };
   VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
   VkSemaphore signalSemaphores[] = { renderCompleteSemaphores[currentFrame] };

   VkSubmitInfo submitInfo = {};
   submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
   submitInfo.waitSemaphoreCount = 1;
   submitInfo.pWaitSemaphores = waitSemaphores;
   submitInfo.pWaitDstStageMask = waitStages;
   submitInfo.commandBufferCount = 1;
   submitInfo.pCommandBuffers = &commandBuffers[imageIndex];
   submitInfo.signalSemaphoreCount = 1;
   submitInfo.pSignalSemaphores = signalSemaphores;

   VKUTIL_CHECK_RESULT(vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[imageIndex]),
      "failed to submit draw command buffer!");
}

void VulkanGame::presentFrame() {
   VkSemaphore signalSemaphores[] = { renderCompleteSemaphores[currentFrame] };

   VkPresentInfoKHR presentInfo = {};
   presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
   presentInfo.waitSemaphoreCount = 1;
   presentInfo.pWaitSemaphores = signalSemaphores;
   presentInfo.swapchainCount = 1;
   presentInfo.pSwapchains = &swapChain;
   presentInfo.pImageIndices = &imageIndex;
   presentInfo.pResults = nullptr;

   VkResult result = vkQueuePresentKHR(presentQueue, &presentInfo);

   if (result == VK_SUBOPTIMAL_KHR) {
      shouldRecreateSwapChain = true;
   } else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
      shouldRecreateSwapChain = true;
      return;
   } else {
      VKUTIL_CHECK_RESULT(result, "failed to present swap chain image!");
   }

   currentFrame = (currentFrame + 1) % swapChainImageCount;
}

void VulkanGame::initImGuiOverlay() {
   vector<VkDescriptorPoolSize> pool_sizes{
       { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 },
       { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 },
       { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 },
       { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 },
       { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 },
       { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 },
       { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 },
       { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 },
       { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 },
       { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 },
       { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 }
   };

   VkDescriptorPoolCreateInfo pool_info = {};
   pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
   pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
   pool_info.maxSets = 1000 * pool_sizes.size();
   pool_info.poolSizeCount = static_cast<uint32_t>(pool_sizes.size());
   pool_info.pPoolSizes = pool_sizes.data();

   VKUTIL_CHECK_RESULT(vkCreateDescriptorPool(device, &pool_info, nullptr, &imguiDescriptorPool),
      "failed to create IMGUI descriptor pool!");

   // TODO: Do this in one place and save it instead of redoing it every time I need a queue family index
   QueueFamilyIndices indices = VulkanUtils::findQueueFamilies(physicalDevice, vulkanSurface);

   // Setup Dear ImGui context
   IMGUI_CHECKVERSION();
   ImGui::CreateContext();
   ImGuiIO& io = ImGui::GetIO();
   //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
   //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls

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

   // Setup Platform/Renderer bindings
   ImGui_ImplSDL2_InitForVulkan(window);
   ImGui_ImplVulkan_InitInfo init_info = {};
   init_info.Instance = instance;
   init_info.PhysicalDevice = physicalDevice;
   init_info.Device = device;
   init_info.QueueFamily = indices.graphicsFamily.value();
   init_info.Queue = graphicsQueue;
   init_info.DescriptorPool = imguiDescriptorPool;
   init_info.Allocator = nullptr;
   init_info.MinImageCount = swapChainMinImageCount;
   init_info.ImageCount = swapChainImageCount;
   init_info.CheckVkResultFn = check_imgui_vk_result;
   ImGui_ImplVulkan_Init(&init_info, renderPass);

   // Load Fonts
   // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them.
   // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple.
   // - If the file cannot be loaded, the function will return NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit).
   // - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling ImFontAtlas::Build()/GetTexDataAsXXXX(), which ImGui_ImplXXXX_NewFrame below will call.
   // - Read 'docs/FONTS.md' for more instructions and details.
   // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ !
   //io.Fonts->AddFontDefault();
   //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf", 16.0f);
   //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf", 15.0f);
   //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf", 16.0f);
   //io.Fonts->AddFontFromFileTTF("../../misc/fonts/ProggyTiny.ttf", 10.0f);
   //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf", 18.0f, NULL, io.Fonts->GetGlyphRangesJapanese());
   //assert(font != NULL);

   // Upload Fonts
   {
      VkCommandBuffer commandBuffer = VulkanUtils::beginSingleTimeCommands(device, resourceCommandPool);

      ImGui_ImplVulkan_CreateFontsTexture(commandBuffer);

      VulkanUtils::endSingleTimeCommands(device, resourceCommandPool, commandBuffer, graphicsQueue);

      ImGui_ImplVulkan_DestroyFontUploadObjects();
   }
}

void VulkanGame::cleanupImGuiOverlay() {
   ImGui_ImplVulkan_Shutdown();
   ImGui_ImplSDL2_Shutdown();
   ImGui::DestroyContext();

   vkDestroyDescriptorPool(device, imguiDescriptorPool, nullptr);
}

void VulkanGame::createBufferSet(VkDeviceSize bufferSize, VkBufferUsageFlags usages, VkMemoryPropertyFlags properties,
                                 BufferSet& set) {
   set.usages = usages;
   set.properties = properties;

   set.buffers.resize(swapChainImageCount);
   set.memory.resize(swapChainImageCount);
   set.infoSet.resize(swapChainImageCount);

   for (size_t i = 0; i < swapChainImageCount; i++) {
      VulkanUtils::createBuffer(device, physicalDevice, bufferSize, usages, properties, set.buffers[i], set.memory[i]);

      set.infoSet[i].buffer = set.buffers[i];
      set.infoSet[i].offset = 0; // This is the offset from the start of the buffer, so always 0 for now
      set.infoSet[i].range = bufferSize; // Size of the update starting from offset, or VK_WHOLE_SIZE
   }
}

void VulkanGame::resizeBufferSet(BufferSet& set, VkDeviceSize newSize, VkCommandPool commandPool,
                                 VkQueue graphicsQueue, bool copyData) {
   for (size_t i = 0; i < set.buffers.size(); i++) {
      VkBuffer newBuffer;
      VkDeviceMemory newMemory;

      VulkanUtils::createBuffer(device, physicalDevice, newSize, set.usages, set.properties, newBuffer, newMemory);

      if (copyData) {
         VulkanUtils::copyBuffer(device, commandPool, set.buffers[i], newBuffer, 0, 0, set.infoSet[i].range,
                                 graphicsQueue);
      }

      vkDestroyBuffer(device, set.buffers[i], nullptr);
      vkFreeMemory(device, set.memory[i], nullptr);

      set.buffers[i] = newBuffer;
      set.memory[i] = newMemory;

      set.infoSet[i].buffer = set.buffers[i];
      set.infoSet[i].offset = 0; // This is the offset from the start of the buffer, so always 0 for now
      set.infoSet[i].range = newSize; // Size of the update starting from offset, or VK_WHOLE_SIZE
   }
}

void VulkanGame::addLaser(vec3 start, vec3 end, vec3 color, float width) {
   vec3 ray = end - start;
   float length = glm::length(ray);

   SceneObject<LaserVertex>& laser = addObject(laserObjects, laserPipeline,
      addObjectIndex<LaserVertex>(laserObjects.size(), {
         {{ width / 2, 0.0f, -width / 2         }, {1.0f, 0.5f }},
         {{-width / 2, 0.0f, -width / 2         }, {0.0f, 0.5f }},
         {{-width / 2, 0.0f, 0.0f               }, {0.0f, 0.0f }},
         {{ width / 2, 0.0f, 0.0f               }, {1.0f, 0.0f }},
         {{ width / 2, 0.0f, -length + width / 2}, {1.0f, 0.51f}},
         {{-width / 2, 0.0f, -length + width / 2}, {0.0f, 0.51f}},
         {{ width / 2, 0.0f, -length,           }, {1.0f, 1.0f }},
         {{-width / 2, 0.0f, -length            }, {0.0f, 1.0f }}
      }), {
          0, 1, 2, 0, 2, 3,
          4, 5, 1, 4, 1, 0,
          6, 7, 5, 6, 5, 4
      }, objects_laserPipeline, {
         mat4(1.0f),
         color,
         false
      });

   float xAxisRotation = asin(ray.y / length);
   float yAxisRotation = atan2(-ray.x, -ray.z);

   vec3 normal(rotate(mat4(1.0f), yAxisRotation, vec3(0.0f, 1.0f, 0.0f)) *
            rotate(mat4(1.0f), xAxisRotation, vec3(1.0f, 0.0f, 0.0f)) *
            vec4(0.0f, 1.0f, 0.0f, 1.0f));

   // To project point P onto line AB:
   // projection = A + dot(AP,AB) / dot(AB,AB) * AB
   vec3 projOnLaser = start + glm::dot(this->cam_pos - start, ray) / (length * length) * ray;
   vec3 laserToCam = this->cam_pos - projOnLaser;

   float zAxisRotation = -atan2(glm::dot(glm::cross(normal, laserToCam), glm::normalize(ray)), glm::dot(normal, laserToCam));

   laser.targetAsteroid = nullptr;

   laser.model_base =
      rotate(mat4(1.0f), zAxisRotation, vec3(0.0f, 0.0f, 1.0f));

   laser.model_transform =
      translate(mat4(1.0f), start) *
      rotate(mat4(1.0f), yAxisRotation, vec3(0.0f, 1.0f, 0.0f)) *
      rotate(mat4(1.0f), xAxisRotation, vec3(1.0f, 0.0f, 0.0f));
}

void VulkanGame::translateLaser(size_t index, const vec3& translation) {
   SceneObject<LaserVertex>& laser = laserObjects[index];

   // TODO: A lot of the values calculated here can be calculated once and saved when the laser is created,
   // and then re-used here

   vec3 start = vec3(laser.model_transform * vec4(0.0f, 0.0f, 0.0f, 1.0f));
   vec3 end = vec3(laser.model_transform * vec4(0.0f, 0.0f, laser.vertices[6].pos.z, 1.0f));

   vec3 ray = end - start;
   float length = glm::length(ray);

   float xAxisRotation = asin(ray.y / length);
   float yAxisRotation = atan2(-ray.x, -ray.z);

   vec3 normal(rotate(mat4(1.0f), yAxisRotation, vec3(0.0f, 1.0f, 0.0f)) *
      rotate(mat4(1.0f), xAxisRotation, vec3(1.0f, 0.0f, 0.0f)) *
      vec4(0.0f, 1.0f, 0.0f, 1.0f));

   // To project point P onto line AB:
   // projection = A + dot(AP,AB) / dot(AB,AB) * AB
   vec3 projOnLaser = start + glm::dot(cam_pos - start, ray) / (length*length) * ray;
   vec3 laserToCam = cam_pos - projOnLaser;

   float zAxisRotation = -atan2(glm::dot(glm::cross(normal, laserToCam), glm::normalize(ray)), glm::dot(normal, laserToCam));

   laser.model_base = rotate(mat4(1.0f), zAxisRotation, vec3(0.0f, 0.0f, 1.0f));
   laser.model_transform = translate(mat4(1.0f), translation) * laser.model_transform;
}

void VulkanGame::updateLaserTarget(size_t index) {
   SceneObject<LaserVertex>& laser = laserObjects[index];

   // TODO: A lot of the values calculated here can be calculated once and saved when the laser is created,
   // and then re-used here

   vec3 start = vec3(laser.model_transform * vec4(0.0f, 0.0f, 0.0f, 1.0f));
   vec3 end = vec3(laser.model_transform * vec4(0.0f, 0.0f, laser.vertices[6].pos.z, 1.0f));

   vec3 intersection(0.0f), closestIntersection(0.0f);
   SceneObject<ModelVertex>* closestAsteroid = nullptr;
   unsigned int closestAsteroidIndex = -1;

   for (int i = 0; i < asteroidObjects.size(); i++) {
      if (!objects_asteroidPipeline.get(i).deleted &&
          getLaserAndAsteroidIntersection(asteroidObjects[i], start, end, intersection)) {
         // TODO: Implement a more generic algorithm for testing the closest object by getting the distance between the points
         // TODO: Also check which intersection is close to the start of the laser. This would make the algorithm work
         // regardless of which way -Z is pointing
         if (closestAsteroid == nullptr || intersection.z > closestIntersection.z) {
            // TODO: At this point, find the real intersection of the laser with one of the asteroid's sides
            closestAsteroid = &asteroidObjects[i];
            closestIntersection = intersection;
            closestAsteroidIndex = i;
         }
      }
   }

   float width = laser.vertices[0].pos.x - laser.vertices[1].pos.x;

   if (laser.targetAsteroid != closestAsteroid) {
      if (laser.targetAsteroid != nullptr) {
         if (index == leftLaserIdx && leftLaserEffect != nullptr) {
            leftLaserEffect->deleted = true;
         } else if (index == rightLaserIdx && rightLaserEffect != nullptr) {
            rightLaserEffect->deleted = true;
         }
      }

      EffectOverTime<SSBO_Asteroid>* eot = nullptr;

      if (closestAsteroid != nullptr) {
         // TODO: Figure out if I should use a smart pointer instead
         eot = new EffectOverTime(objects_asteroidPipeline, closestAsteroidIndex, offset_of(&SSBO_Asteroid::hp),
                                  curTime, -20.0f);
         effects.push_back(eot);
      }

      if (index == leftLaserIdx) {
         leftLaserEffect = eot;
      } else if (index == rightLaserIdx) {
         rightLaserEffect = eot;
      }

      laser.targetAsteroid = closestAsteroid;
   }

   // Make the laser go past the end of the screen if it doesn't hit anything
   float length = closestAsteroid == nullptr ? 5.24f : glm::length(closestIntersection - start);

   laser.vertices[4].pos.z = -length + width / 2;
   laser.vertices[5].pos.z = -length + width / 2;
   laser.vertices[6].pos.z = -length;
   laser.vertices[7].pos.z = -length;

   // TODO: Consider if I want to set a flag and do this update in updateScene() instead
   updateObjectVertices(laserPipeline, laser, index);
}

// TODO: Determine if I should pass start and end by reference or value since they don't get changed
// Probably use const reference
bool VulkanGame::getLaserAndAsteroidIntersection(SceneObject<ModelVertex>& asteroid, vec3& start, vec3& end,
                                                 vec3& intersection) {
   /*
   ### LINE EQUATIONS ###
   x = x1 + u * (x2 - x1)
   y = y1 + u * (y2 - y1)
   z = z1 + u * (z2 - z1)

   ### SPHERE EQUATION ###
   (x - x3)^2 + (y - y3)^2 + (z - z3)^2 = r^2

   ### QUADRATIC EQUATION TO SOLVE ###
   a*u^2 + b*u + c = 0
   WHERE THE CONSTANTS ARE
   a = (x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2
   b = 2*( (x2 - x1)*(x1 - x3) + (y2 - y1)*(y1 - y3) + (z2 - z1)*(z1 - z3) )
   c = x3^2 + y3^2 + z3^2 + x1^2 + y1^2 + z1^2 - 2(x3*x1 + y3*y1 + z3*z1) - r^2

   u = (-b +- sqrt(b^2 - 4*a*c)) / 2a

   If the value under the root is >= 0, we got an intersection
   If the value > 0, there are two solutions. Take the one closer to 0, since that's the
   one closer to the laser start point
   */

   vec3& center = asteroid.center;

   float a = pow(end.x - start.x, 2) + pow(end.y - start.y, 2) + pow(end.z - start.z, 2);
   float b = 2 * ((start.x - end.x) * (start.x - center.x) + (end.y - start.y) * (start.y - center.y) +
            (end.z - start.z) * (start.z - center.z));
   float c = pow(center.x, 2) + pow(center.y, 2) + pow(center.z, 2) + pow(start.x, 2) + pow(start.y, 2) +
            pow(start.z, 2) - 2 * (center.x * start.x + center.y * start.y + center.z * start.z) -
            pow(asteroid.radius, 2);
   float discriminant = pow(b, 2) - 4 * a * c;

   if (discriminant >= 0.0f) {
      // In this case, the negative root will always give the point closer to the laser start point
      float u = (-b - sqrt(discriminant)) / (2 * a);

      // Check that the intersection is within the line segment corresponding to the laser
      if (0.0f <= u && u <= 1.0f) {
         intersection = start + u * (end - start);
         return true;
      }
   }

   return false;
}

void VulkanGame::addExplosion(mat4 model_mat, float duration, float cur_time) {
   vector<ExplosionVertex> vertices;
   vertices.reserve(EXPLOSION_PARTICLE_COUNT);

   float particle_start_time = 0.0f;

   for (int i = 0; i < EXPLOSION_PARTICLE_COUNT; i++) {
      float randx = ((float)rand() / (float)RAND_MAX) - 0.5f;
      float randy = ((float)rand() / (float)RAND_MAX) - 0.5f;

      vertices.push_back({ vec3(randx, randy, 0.0f), particle_start_time});

      particle_start_time += .01f;
      // TODO: Get this working
      // particle_start_time += 1.0f * EXPLOSION_PARTICLE_COUNT / duration
   }

   // Fill the indices with the the first EXPLOSION_PARTICLE_COUNT ints
   vector<uint16_t> indices(EXPLOSION_PARTICLE_COUNT);
   iota(indices.begin(), indices.end(), 0);

   SceneObject<ExplosionVertex>& explosion = addObject(explosionObjects, explosionPipeline,
      addObjectIndex(explosionObjects.size(), vertices), indices, objects_explosionPipeline, {
         mat4(1.0f),
         cur_time,
         duration,
         false
      });

   explosion.model_base = model_mat;
   explosion.model_transform = mat4(1.0f);
}

void VulkanGame::recreateSwapChain() {
   if (vkDeviceWaitIdle(device) != VK_SUCCESS) {
      throw runtime_error("failed to wait for device!");
   }

   cleanupSwapChain();

   createSwapChain();
   createImageViews();

   // The depth buffer does need to be recreated with the swap chain since its dimensions depend on the window size
   // and resizing the window is a common reason to recreate the swapchain
   VulkanUtils::createDepthImage(device, physicalDevice, resourceCommandPool, findDepthFormat(), swapChainExtent,
                                 depthImage, graphicsQueue);

   createRenderPass();
   createCommandPools();
   createFramebuffers();
   createCommandBuffers();
   createSyncObjects();

   createBufferSet(uniforms_modelPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_modelPipeline);

   uniforms_modelPipeline.map(uniformBuffers_modelPipeline.memory, device);

   createBufferSet(objects_modelPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_modelPipeline);

    objects_modelPipeline.map(objectBuffers_modelPipeline.memory, device);

   modelPipeline.updateRenderPass(renderPass);
   modelPipeline.createPipeline("shaders/model-vert.spv", "shaders/model-frag.spv");
   modelPipeline.createDescriptorPool(swapChainImages.size());
   modelPipeline.createDescriptorSets(swapChainImages.size());

   createBufferSet(uniforms_shipPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_shipPipeline);

   uniforms_shipPipeline.map(uniformBuffers_shipPipeline.memory, device);

   createBufferSet(objects_shipPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_shipPipeline);

    objects_shipPipeline.map(objectBuffers_shipPipeline.memory, device);

   shipPipeline.updateRenderPass(renderPass);
   shipPipeline.createPipeline("shaders/ship-vert.spv", "shaders/ship-frag.spv");
   shipPipeline.createDescriptorPool(swapChainImages.size());
   shipPipeline.createDescriptorSets(swapChainImages.size());

   createBufferSet(uniforms_asteroidPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_asteroidPipeline);

   uniforms_asteroidPipeline.map(uniformBuffers_asteroidPipeline.memory, device);

   createBufferSet(objects_asteroidPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_asteroidPipeline);

    objects_asteroidPipeline.map(objectBuffers_asteroidPipeline.memory, device);

   asteroidPipeline.updateRenderPass(renderPass);
   asteroidPipeline.createPipeline("shaders/asteroid-vert.spv", "shaders/asteroid-frag.spv");
   asteroidPipeline.createDescriptorPool(swapChainImages.size());
   asteroidPipeline.createDescriptorSets(swapChainImages.size());

   createBufferSet(uniforms_laserPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_laserPipeline);

   uniforms_laserPipeline.map(uniformBuffers_laserPipeline.memory, device);

   createBufferSet(objects_laserPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_laserPipeline);

    objects_laserPipeline.map(objectBuffers_laserPipeline.memory, device);

   laserPipeline.updateRenderPass(renderPass);
   laserPipeline.createPipeline("shaders/laser-vert.spv", "shaders/laser-frag.spv");
   laserPipeline.createDescriptorPool(swapChainImages.size());
   laserPipeline.createDescriptorSets(swapChainImages.size());

   createBufferSet(uniforms_explosionPipeline.memorySize(),
                   VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   uniformBuffers_explosionPipeline);

   uniforms_explosionPipeline.map(uniformBuffers_explosionPipeline.memory, device);

   createBufferSet(objects_explosionPipeline.memorySize(),
                   VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT
                   | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                   objectBuffers_explosionPipeline);

    objects_explosionPipeline.map(objectBuffers_explosionPipeline.memory, device);

   explosionPipeline.updateRenderPass(renderPass);
   explosionPipeline.createPipeline("shaders/explosion-vert.spv", "shaders/explosion-frag.spv");
   explosionPipeline.createDescriptorPool(swapChainImages.size());
   explosionPipeline.createDescriptorSets(swapChainImages.size());

   imageIndex = 0;
}

void VulkanGame::cleanupSwapChain() {
   VulkanUtils::destroyVulkanImage(device, depthImage);

   for (VkFramebuffer framebuffer : swapChainFramebuffers) {
      vkDestroyFramebuffer(device, framebuffer, nullptr);
   }

   for (uint32_t i = 0; i < swapChainImageCount; i++) {
      vkFreeCommandBuffers(device, commandPools[i], 1, &commandBuffers[i]);
      vkDestroyCommandPool(device, commandPools[i], nullptr);
   }

   modelPipeline.cleanup();

   uniforms_modelPipeline.unmap(uniformBuffers_modelPipeline.memory, device);

   for (size_t i = 0; i < uniformBuffers_modelPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, uniformBuffers_modelPipeline.buffers[i], nullptr);
      vkFreeMemory(device, uniformBuffers_modelPipeline.memory[i], nullptr);
   }

   objects_modelPipeline.unmap(objectBuffers_modelPipeline.memory, device);

   for (size_t i = 0; i < objectBuffers_modelPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, objectBuffers_modelPipeline.buffers[i], nullptr);
      vkFreeMemory(device, objectBuffers_modelPipeline.memory[i], nullptr);
   }

   shipPipeline.cleanup();

   uniforms_shipPipeline.unmap(uniformBuffers_shipPipeline.memory, device);

   for (size_t i = 0; i < uniformBuffers_shipPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, uniformBuffers_shipPipeline.buffers[i], nullptr);
      vkFreeMemory(device, uniformBuffers_shipPipeline.memory[i], nullptr);
   }

   objects_shipPipeline.unmap(objectBuffers_shipPipeline.memory, device);

   for (size_t i = 0; i < objectBuffers_shipPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, objectBuffers_shipPipeline.buffers[i], nullptr);
      vkFreeMemory(device, objectBuffers_shipPipeline.memory[i], nullptr);
   }

   asteroidPipeline.cleanup();

   uniforms_asteroidPipeline.unmap(uniformBuffers_asteroidPipeline.memory, device);

   for (size_t i = 0; i < uniformBuffers_asteroidPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, uniformBuffers_asteroidPipeline.buffers[i], nullptr);
      vkFreeMemory(device, uniformBuffers_asteroidPipeline.memory[i], nullptr);
   }

   objects_asteroidPipeline.unmap(objectBuffers_asteroidPipeline.memory, device);

   for (size_t i = 0; i < objectBuffers_asteroidPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, objectBuffers_asteroidPipeline.buffers[i], nullptr);
      vkFreeMemory(device, objectBuffers_asteroidPipeline.memory[i], nullptr);
   }

   laserPipeline.cleanup();

   uniforms_laserPipeline.unmap(uniformBuffers_laserPipeline.memory, device);

   for (size_t i = 0; i < uniformBuffers_laserPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, uniformBuffers_laserPipeline.buffers[i], nullptr);
      vkFreeMemory(device, uniformBuffers_laserPipeline.memory[i], nullptr);
   }

   objects_laserPipeline.unmap(objectBuffers_laserPipeline.memory, device);

   for (size_t i = 0; i < objectBuffers_laserPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, objectBuffers_laserPipeline.buffers[i], nullptr);
      vkFreeMemory(device, objectBuffers_laserPipeline.memory[i], nullptr);
   }

   explosionPipeline.cleanup();

   uniforms_explosionPipeline.unmap(uniformBuffers_explosionPipeline.memory, device);

   for (size_t i = 0; i < uniformBuffers_explosionPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, uniformBuffers_explosionPipeline.buffers[i], nullptr);
      vkFreeMemory(device, uniformBuffers_explosionPipeline.memory[i], nullptr);
   }

   objects_explosionPipeline.unmap(objectBuffers_explosionPipeline.memory, device);

   for (size_t i = 0; i < objectBuffers_explosionPipeline.buffers.size(); i++) {
      vkDestroyBuffer(device, objectBuffers_explosionPipeline.buffers[i], nullptr);
      vkFreeMemory(device, objectBuffers_explosionPipeline.memory[i], nullptr);
   }

   for (size_t i = 0; i < swapChainImageCount; i++) {
      vkDestroySemaphore(device, imageAcquiredSemaphores[i], nullptr);
      vkDestroySemaphore(device, renderCompleteSemaphores[i], nullptr);
      vkDestroyFence(device, inFlightFences[i], nullptr);
   }

   vkDestroyRenderPass(device, renderPass, nullptr);

   for (VkImageView imageView : swapChainImageViews) {
      vkDestroyImageView(device, imageView, nullptr);
   }

   vkDestroySwapchainKHR(device, swapChain, nullptr);
}

void VulkanGame::renderMainScreen(int width, int height) {
   {
      int padding = 4;
      ImGui::SetNextWindowPos(vec2(-padding, -padding), ImGuiCond_Once);
      ImGui::SetNextWindowSize(vec2(width + 2 * padding, height + 2 * padding), ImGuiCond_Always);
      ImGui::Begin("WndMain", nullptr,
         ImGuiWindowFlags_NoTitleBar |
         ImGuiWindowFlags_NoResize |
         ImGuiWindowFlags_NoMove);

      ButtonImGui btn("New Game");

      ImGui::InvisibleButton("", vec2(10, height / 6));
      if (btn.draw((width - btn.getWidth()) / 2)) {
         goToScreen(&VulkanGame::renderGameScreen);
      }

      ButtonImGui btn2("Quit");

      ImGui::InvisibleButton("", vec2(10, 15));
      if (btn2.draw((width - btn2.getWidth()) / 2)) {
         quitGame();
      }

      ImGui::End();
   }
}

void VulkanGame::renderGameScreen(int width, int height) {
   {
      ImGui::SetNextWindowSize(vec2(130, 65), ImGuiCond_Once);
      ImGui::SetNextWindowPos(vec2(10, 50), ImGuiCond_Once);
      ImGui::Begin("WndStats", nullptr,
         ImGuiWindowFlags_NoTitleBar |
         ImGuiWindowFlags_NoResize |
         ImGuiWindowFlags_NoMove);

      //ImGui::Text(ImGui::GetIO().Framerate);
      renderGuiValueList(valueLists["stats value list"]);

      ImGui::End();
   }

   {
      ImGui::SetNextWindowSize(vec2(250, 35), ImGuiCond_Once);
      ImGui::SetNextWindowPos(vec2(width - 260, 10), ImGuiCond_Always);
      ImGui::Begin("WndMenubar", nullptr,
         ImGuiWindowFlags_NoTitleBar |
         ImGuiWindowFlags_NoResize |
         ImGuiWindowFlags_NoMove);
      ImGui::InvisibleButton("", vec2(155, 18));
      ImGui::SameLine();
      if (ImGui::Button("Main Menu")) {
         goToScreen(&VulkanGame::renderMainScreen);
      }
      ImGui::End();
   }

   {
      ImGui::SetNextWindowSize(vec2(200, 200), ImGuiCond_Once);
      ImGui::SetNextWindowPos(vec2(width - 210, 60), ImGuiCond_Always);
      ImGui::Begin("WndDebug", nullptr,
         ImGuiWindowFlags_NoTitleBar |
         ImGuiWindowFlags_NoResize |
         ImGuiWindowFlags_NoMove);

      renderGuiValueList(valueLists["debug value list"]);

      ImGui::End();
   }
}

void VulkanGame::initGuiValueLists(map<string, vector<UIValue>>& valueLists) {
   valueLists["stats value list"] = vector<UIValue>();
   valueLists["debug value list"] = vector<UIValue>();
}

// TODO: Probably turn this into a UI widget class
void VulkanGame::renderGuiValueList(vector<UIValue>& values) {
   float maxWidth = 0.0f;
   float cursorStartPos = ImGui::GetCursorPosX();

   for (vector<UIValue>::iterator it = values.begin(); it != values.end(); it++) {
      float textWidth = ImGui::CalcTextSize(it->label.c_str()).x;

      if (maxWidth < textWidth)
         maxWidth = textWidth;
   }

   stringstream ss;

   // TODO: Possibly implement this based on gui/ui-value.hpp instead and use templates
   // to keep track of the type. This should make it a bit easier to use and maintain
   // Also, implement this in a way that's agnostic to the UI renderer.
   for (vector<UIValue>::iterator it = values.begin(); it != values.end(); it++) {
      ss.str("");
      ss.clear();

      switch (it->type) {
      case UIVALUE_INT:
         ss << it->label << ": " << *(unsigned int*)it->value;
         break;
      case UIVALUE_DOUBLE:
         ss << it->label << ": " << *(double*)it->value;
         break;
      }

      float textWidth = ImGui::CalcTextSize(it->label.c_str()).x;

      ImGui::SetCursorPosX(cursorStartPos + maxWidth - textWidth);
      //ImGui::Text("%s", ss.str().c_str());
      ImGui::Text("%s: %.1f", it->label.c_str(), *(float*)it->value);
   }
}

void VulkanGame::goToScreen(void (VulkanGame::* renderScreenFn)(int width, int height)) {
   currentRenderScreenFn = renderScreenFn;

   // TODO: Maybe just set shouldRecreateSwapChain to true instead. Check this render loop logic
   // to make sure there'd be no issues
   //recreateSwapChain();
}

void VulkanGame::quitGame() {
   done = true;
}
