#include "sdl-game.hpp"

#include <array>
#include <iostream>
#include <set>

#include "IMGUI/imgui_impl_sdl.h"

#include "logger.hpp"
#include "utils.hpp"

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

using namespace std;

#define IMGUI_UNLIMITED_FRAME_RATE

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

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

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)
                     , imguiDescriptorPool(VK_NULL_HANDLE)
                     , gui(nullptr)
                     , window(nullptr)
                     , uniforms_modelPipeline()
                     , objects_modelPipeline()
                     , score(0)
                     , fps(0.0f) {
}

VulkanGame::~VulkanGame() {
}

void VulkanGame::run(int width, int height, unsigned char guiFlags) {
   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);

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

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

   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 }, 16, 24);
}

// TODO: Maybe change 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;

   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
               // END UNREVIEWED SECTION
               } else {
                  cout << "Key event detected" << endl;
               }
               break;
            case UI_EVENT_KEYUP:
               // START UNREVIEWED SECTION
               // 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;
         }
      }

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

      updateScene();

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

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

void VulkanGame::updateScene() {
   uniforms_modelPipeline.data()->view = viewMat;
   uniforms_modelPipeline.data()->proj = projMat;

   // TODO: Probably move the resizing to the VulkanBuffer class's add() method
   // Remember that we want to avoid resizing the actuall ssbo or ubo more than once per frame
   // 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);
}

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

   vkDestroySampler(device, textureSampler, nullptr);

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

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

   // Per Spec Format and View Format are expected to be the same unless VK_IMAGE_CREATE_MUTABLE_BIT was set at image creation
   // Assuming that the default behavior is without setting this bit, there is no need for separate Swapchain image and image view format
   // Additionally several new color spaces were introduced with Vulkan Spec v1.0.40,
   // hence we must make sure that a format with the mostly available color space, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, is found and used.
   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);

#ifdef IMGUI_UNLIMITED_FRAME_RATE
   vector<VkPresentModeKHR> presentModes{
      VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_FIFO_KHR
   };
#else
   vector<VkPresentModeKHR> presentModes{ VK_PRESENT_MODE_FIFO_KHR };
#endif

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

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

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

   // If min image count was not specified, request different count of images dependent on selected present mode
   if (swapChainMinImageCount == 0) {
      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 (uint32_t i = 0; i < swapChainImageViews.size(); 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;
}

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; // Set to VK_ATTACHMENT_LOAD_OP_DONT_CARE to disable clearing
   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!");
   }

   // We do not create a pipeline by default as this is also used by examples' main.cpp,
   // but secondary viewport in multi-viewport mode may want to create one with:
   //ImGui_ImplVulkan_CreatePipeline(device, g_Allocator, VK_NULL_HANDLE, g_MainWindowData.RenderPass, VK_SAMPLE_COUNT_1_BIT, &g_MainWindowData.Pipeline);
}

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;

      if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPools[i]) != VK_SUCCESS) {
         throw runtime_error("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 (size_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::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::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 });




   }

   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::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());

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

   for (uint32_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;
}
