#include "opengl-game.hpp"

#include <iostream>

#include "consts.hpp"
#include "logger.hpp"

#include "utils.hpp"

using namespace std;

OpenGLGame::OpenGLGame() {
   gui = nullptr;
   window = nullptr;
}

OpenGLGame::~OpenGLGame() {
}

void OpenGLGame::run(int width, int height, unsigned char guiFlags) {
#ifdef NDEBUG
   cout << "DEBUGGING IS OFF" << endl;
#else
   cout << "DEBUGGING IS ON" << endl;
#endif

   cout << "OpenGL Game" << endl;

   // TODO: Refactor the logger api to be more flexible,
   // esp. since gl_log() and gl_log_err() have issues printing anything besides stirngs
   restart_gl_log();
   gl_log("starting GLFW\n%s", glfwGetVersionString());

   open_log();
   get_log() << "starting GLFW" << endl;
   get_log() << glfwGetVersionString() << endl;

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

   initOpenGL();
   mainLoop();
   cleanup();

   close_log();
}

// TODO: Make some more initi functions, or call this initUI if the
// amount of things initialized here keeps growing
bool OpenGLGame::initWindow(int width, int height, unsigned char guiFlags) {
   // TODO: Put all fonts, textures, and images in the assets folder
   gui = new GameGui_GLFW();

   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;
      return RTWO_ERROR;
   }
   cout << "GUI init succeeded" << endl;

   window = (GLFWwindow*) gui->createWindow("OpenGL 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;
   }

   viewport = { 0, 0, gui->getWindowWidth(), gui->getWindowHeight() };

   cout << "Target window size: (" << width << ", " << height << ")" << endl;
   cout << "Actual window size: (" << viewport.width << ", " << viewport.height << ")" << endl;

   return RTWO_SUCCESS;
}

void OpenGLGame::initOpenGL() {
   glfwMakeContextCurrent(window);
   glViewport(0, 0, gui->getWindowWidth(), gui->getWindowHeight());

   glewExperimental = GL_TRUE;
   glewInit();

   if (GLEW_KHR_debug) {
      cout << "FOUND GLEW debug extension" << endl;
      glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
      glDebugMessageCallback((GLDEBUGPROC)opengl_debug_callback, nullptr);
      cout << "Bound debug callback" << endl;
   } else {
      cout << "OpenGL debug message callback is not supported" << endl;
   }

   // Setup Dear ImGui binding
   IMGUI_CHECKVERSION();
   ImGui::CreateContext();
   ImGuiIO& io = ImGui::GetIO(); (void)io;
   //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;  // Enable Keyboard Controls
   //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;   // Enable Gamepad Controls
   ImGui_ImplGlfwGL3_Init(window, true);

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

   // The glfw event handlers have to be bound after ImGui is initialized.
   // Otherwise, it seems they get overridden by ImGui
   ((GameGui_GLFW*)gui)->bindEventHandlers();

   graphicsPipelines.push_back(GraphicsPipeline_OpenGL(viewport));

   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&SceneObject::points));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&SceneObject::colors));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&SceneObject::normals));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_OBJECT_VARYING, 1, GL_UNSIGNED_INT,
      offset_of(&SceneObject::ubo_offset));

   graphicsPipelines.back().createPipeline("gl-shaders/ship.vert", "gl-shaders/ship.frag");

   graphicsPipelines.push_back(GraphicsPipeline_OpenGL(viewport));

   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&SceneObject::points));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&SceneObject::colors));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&SceneObject::normals));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_OBJECT_VARYING, 1, GL_UNSIGNED_INT,
      offset_of(&SceneObject::ubo_offset));

   graphicsPipelines.back().createPipeline("gl-shaders/asteroid.vert", "gl-shaders/asteroid.frag");

   graphicsPipelines.push_back(GraphicsPipeline_OpenGL(viewport));

   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&SceneObject::points));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 2, GL_FLOAT,
      offset_of(&SceneObject::texcoords));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_OBJECT_VARYING, 1, GL_UNSIGNED_INT,
      offset_of(&SceneObject::ubo_offset));

   graphicsPipelines.back().createPipeline("gl-shaders/laser.vert", "gl-shaders/laser.frag");

   graphicsPipelines.push_back(GraphicsPipeline_OpenGL(viewport));

   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 3, GL_FLOAT,
      offset_of(&ParticleEffect::particleVelocities));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_POINT_VARYING, 1, GL_FLOAT,
      offset_of(&ParticleEffect::particleTimes));
   graphicsPipelines.back().addVaryingAttribute(ATTRIB_OBJECT_VARYING, 1, GL_UNSIGNED_INT,
      offset_of(&ParticleEffect::ubo_offset));

   graphicsPipelines.back().createPipeline("gl-shaders/explosion.vert", "gl-shaders/explosion.frag");

   cout << "Created " << graphicsPipelines.size() << " graphics pipelines" << endl;
}

void OpenGLGame::mainLoop() {
   UIEvent uiEvent;
   bool quit = false;

   while (!quit) {
      gui->processEvents();

      while (gui->pollEvent(&uiEvent)) {
         GameEvent& e = uiEvent.event;

         switch (e.type) {
            case UI_EVENT_QUIT:
               cout << "Quit event detected" << endl;
               quit = true;
               break;
            case UI_EVENT_KEYDOWN:
               if (e.key.keycode == GLFW_KEY_ESCAPE) {
                  quit = true;
               } else {
                  cout << "Key event detected" << endl;
               }
               break;
            case UI_EVENT_WINDOWRESIZE:
               cout << "Window resize event detected" << endl;
               viewport.width = e.windowResize.width;
               viewport.height = e.windowResize.height;
               break;
            default:
               cout << "Unhandled UI event: " << e.type << endl;
         }
      }

      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

      // Anton's book suggests placing this here, after glClear(). Check it's impact on framerate
      // TODO: This doesn't seem to work correctly when in the loop. DO some research
      // max viewport dims are clamped to glGet(GL_MAX_VIEWPORT_DIMS)
      //glViewport(0, 0, gui->getWindowWidth(), gui->getWindowHeight());

      renderScene();
      renderUI();

      glfwSwapBuffers(window);
   }
}

void OpenGLGame::renderScene() {

}

void OpenGLGame::renderUI() {
   ImGui_ImplGlfwGL3_NewFrame();

   {
      int padding = 4;
      ImGui::SetNextWindowPos(ImVec2(-padding, -padding), ImGuiCond_Once);
      ImGui::SetNextWindowSize(ImVec2(gui->getWindowWidth() + 2 * padding, gui->getWindowHeight() + 2 * padding), ImGuiCond_Always);
      ImGui::Begin("WndMain", NULL,
         ImGuiWindowFlags_NoTitleBar |
         ImGuiWindowFlags_NoResize |
         ImGuiWindowFlags_NoMove);

      ImGui::InvisibleButton("", ImVec2(10, 80));
      ImGui::InvisibleButton("", ImVec2(285, 18));
      ImGui::SameLine();
      ImGui::Button("New Game");

      ImGui::InvisibleButton("", ImVec2(10, 15));
      ImGui::InvisibleButton("", ImVec2(300, 18));
      ImGui::SameLine();
      ImGui::Button("Quit");

      ImGui::End();
   }

   ImGui::Render();
   ImGui_ImplGlfwGL3_RenderDrawData(ImGui::GetDrawData());
}

void OpenGLGame::cleanup() {
   ImGui_ImplGlfwGL3_Shutdown();
   ImGui::DestroyContext();

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

void APIENTRY opengl_debug_callback(
   GLenum source,
   GLenum type,
   GLuint id,
   GLenum severity,
   GLsizei length,
   const GLchar* message,
   const void* userParam
) {
   string strMessage(message);

   // TODO: Use C++ strings directly and see if there are other ways to clean
   // this function up
   char source_str[2048];
   char type_str[2048];
   char severity_str[2048];

   switch (source) {
      case 0x8246:
         strcpy(source_str, "API");
         break;
      case 0x8247:
         strcpy(source_str, "WINDOW_SYSTEM");
         break;
      case 0x8248:
         strcpy(source_str, "SHADER_COMPILER");
         break;
      case 0x8249:
         strcpy(source_str, "THIRD_PARTY");
         break;
      case 0x824A:
         strcpy(source_str, "APPLICATION");
         break;
      case 0x824B:
         strcpy(source_str, "OTHER");
         break;
      default:
         strcpy(source_str, "undefined");
         break;
   }

   switch (type) {
      case 0x824C:
         strcpy(type_str, "ERROR");
         break;
      case 0x824D:
         strcpy(type_str, "DEPRECATED_BEHAVIOR");
         break;
      case 0x824E:
         strcpy(type_str, "UNDEFINED_BEHAVIOR");
         break;
      case 0x824F:
         strcpy(type_str, "PORTABILITY");
         break;
      case 0x8250:
         strcpy(type_str, "PERFORMANCE");
         break;
      case 0x8251:
         strcpy(type_str, "OTHER");
         break;
      case 0x8268:
         strcpy(type_str, "MARKER");
         break;
      case 0x8269:
         strcpy(type_str, "PUSH_GROUP");
         break;
      case 0x826A:
         strcpy(type_str, "POP_GROUP");
         break;
      default:
         strcpy(type_str, "undefined");
         break;
   }
   switch (severity) {
      case 0x9146:
         strcpy(severity_str, "HIGH");
         break;
      case 0x9147:
         strcpy(severity_str, "MEDIUM");
         break;
      case 0x9148:
         strcpy(severity_str, "LOW");
         break;
      case 0x826B:
         strcpy(severity_str, "NOTIFICATION");
         break;
      default:
         strcpy(severity_str, "undefined");
         break;
   }

   if (string(severity_str) != "NOTIFICATION") {
      cout << "OpenGL Error!!!" << endl;
      cout << "Source: " << string(source_str) << endl;
      cout << "Type: " << string(type_str) << endl;
      cout << "Severity: " << string(severity_str) << endl;
      cout << strMessage << endl;
   }
}