// clang-format off
// glad must be included before any other OpenGL libraries
#include <glad/gl.h>
// clang-format on

#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#include <X11/Xlib.h>
#include <signal.h>
#include <unistd.h>

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sstream>

#include "backends/imgui_impl_glfw.h"
#include "backends/imgui_impl_opengl3.h"
#include "imgui.h"
#include "utils.h"
#include "srender.h"
#include "simulator.h"

GLFWwindow* gWindow = nullptr;
GuiInputState* gGuiInputState = nullptr;

// Rendering
srndr* gRndr = nullptr;
srview* gView = nullptr;
srcmdbuf* gRndrCmds = nullptr;

double mouse_scroll_x = 0.;
double mouse_scroll_y = 0.;

using namespace std;

static void error_callback(int error, const char* description) {
  fprintf(stderr, "Error (%d): %s\n", error, description);
}

static void opengl_error_callback(
    GLenum source,
    GLenum type,
    GLuint id,
    GLenum severity,
    GLsizei length,
    const GLchar* message,
    const void* userParam) {
  gLog(
      "OpenGL Error: %s type %0x%x, severity = 0x%x, message = %s",
      (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
      type,
      severity,
      message);
}

static void
key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
  if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
    glfwSetWindowShouldClose(window, GLFW_TRUE);
}

void signal_handler(int signo) { gLog("Received signal %d", signo); }

void mouse_scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
  mouse_scroll_x += xoffset;
  mouse_scroll_y += yoffset;
}

void handle_mouse() {
  if (!glfwGetWindowAttrib(gWindow, GLFW_FOCUSED)) {
    return;
  }

  double mouse_x, mouse_y;
  glfwGetCursorPos(gWindow, &mouse_x, &mouse_y);

  if (gGuiInputState->mouseButton) {
    gGuiInputState->mousedX = mouse_x - gGuiInputState->mouseX;
    gGuiInputState->mousedY = mouse_y - gGuiInputState->mouseY;
  } else {
    gGuiInputState->mousedX = 0;
    gGuiInputState->mousedY = 0;
  }
  gGuiInputState->mouseX = mouse_x;
  gGuiInputState->mouseY = mouse_y;
  gGuiInputState->mouseScroll = mouse_scroll_y;

  gGuiInputState->mouseButton = glfwGetMouseButton(gWindow, 0)
                                + (glfwGetMouseButton(gWindow, 1) << 1)
                                + (glfwGetMouseButton(gWindow, 2) << 2);
}

void ShowDockspace(bool open) {
  static bool opt_fullscreen = true;
  static bool opt_padding = false;
  static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_None;

  ImGuiIO& io = ImGui::GetIO();
  (void)io;

  // We are using the ImGuiWindowFlags_NoDocking flag to make the parent window not dockable into,
  // because it would be confusing to have two docking targets within each others.
  ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking;
  if (opt_fullscreen) {
    ImGuiViewport* viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(viewport->GetWorkPos());
    ImGui::SetNextWindowSize(viewport->GetWorkSize());
    ImGui::SetNextWindowViewport(viewport->ID);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse
                    | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
    window_flags |=
        ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
  } else {
    dockspace_flags &= ~ImGuiDockNodeFlags_PassthruCentralNode;
  }

  // When using ImGuiDockNodeFlags_PassthruCentralNode, DockSpace() will render our background
  // and handle the pass-thru hole, so we ask Begin() to not render a background.
  if (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode)
    window_flags |= ImGuiWindowFlags_NoBackground;

  // Important: note that we proceed even if Begin() returns false (aka window is collapsed).
  // This is because we want to keep our DockSpace() active. If a DockSpace() is inactive,
  // all active windows docked into it will lose their parent and become undocked.
  // We cannot preserve the docking relationship between an active window and an inactive docking, otherwise
  // any change of dockspace/settings would lead to windows being stuck in limbo and never being visible.
  if (!opt_padding)
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
  ImGui::Begin("DockSpace Demo", &open, window_flags);
  if (!opt_padding) ImGui::PopStyleVar();

  if (opt_fullscreen) ImGui::PopStyleVar(2);

  // DockSpace
  if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) {
    ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
    ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
  } else {
    gLog("Error: no docking not enabled");
  }

  ImGui::End();
}

void DoRender() {
  // Render Output
  ImGui::Begin("Render Output");

  // Update the view
  const ImVec2 content_avail = ImGui::GetContentRegionAvail();
  srview_set_size(gView, content_avail.x, content_avail.y);
  simd4x4f view;
  simd4x4f proj;
  simd4x4f_translation(&view, 0.1f, 0.1f, -0.5f);
  simd4f eye = simd4f_create (0.f, 3.f, 10.f, 1.f);
  simd4x4f_lookat(
      &view,
      eye,
      simd4f_create(0.f, 1.f, 0.f, 1.f),
      simd4f_create(0.f, 1.f, 0.f, 1.f));

  float view_width = content_avail.x / 50.f;
  float view_height = content_avail.y / 50.f;

  simd4x4f_ortho(
      &proj,
      -0.5f * view_width,
      0.5f * view_width,
      -0.5f * view_height,
      0.5f * view_height,
      -50.0f,
      50.0f);
  simd4x4f_perspective(&proj,
                       70.f *  (M_PI / 180.f), view_width / view_height, 0.1f, 50.f);
  srview_set_view(gView, view);
  srview_set_proj(gView, proj);

  // Populate render commands
  srcmdbuf_clear(gRndrCmds);

  srcmd rcmd;
  srcmd_clear(&rcmd);

  rcmd.type = SRndrCmdTypeFrame;
  srcmdbuf_add(gRndrCmds, &rcmd);

  simulator_draw(gRndrCmds);

  // Perform the actual render
  srndr_render(gRndr, gView, gRndrCmds);
  GLuint view_texture;
  srview_get_output_texture(gView, &view_texture);

  ImGui::Image(
      (void*)view_texture,
      content_avail,
      ImVec2(0.0f, 1.0f),
      ImVec2(1.0f, 0.0f));

  ImGui::End();
}

int main(void) {
  gTimerInit();

  LoggingInit();

  if (signal(SIGUSR1, signal_handler) == SIG_ERR) {
    gLog("Error registering signal handler!");
  }

  // Initialize GLFW
  glfwSetErrorCallback(error_callback);
  glfwInit();

  const char* glsl_version = "#version 130";
  glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
  glfwWindowHint(GLFW_SAMPLES, 16);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#if __APPLE__
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

  gWindow = glfwCreateWindow(800, 600, "ViSimir", NULL, NULL);
  assert(gWindow != NULL);
  glfwMakeContextCurrent(gWindow);
  glfwSwapInterval(1);

  // Initialize OpenGL loader
  gladLoadGL(glfwGetProcAddress);

  glfwSetKeyCallback(gWindow, key_callback);
  glfwSetScrollCallback(gWindow, mouse_scroll_callback);

  std::cout << "OpenGL Version: " << glGetString(GL_VERSION) << endl;
  std::cout << "GLSL Version  : " << glGetString(GL_SHADING_LANGUAGE_VERSION)
            << endl;

  // During init, enable debug output
  //	glEnable              ( GL_DEBUG_OUTPUT );
  //	glDebugMessageCallback( (GLDEBUGPROC) opengl_error_callback, 0 );

  // imgui initialization.
  IMGUI_CHECKVERSION();
  ImGui::CreateContext();
  ImGuiIO& io = ImGui::GetIO();
  (void)io;
  io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
  //	io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;

  GuiInputState gui_input_state;
  gGuiInputState = &gui_input_state;

  ImGui_ImplGlfw_InitForOpenGL(gWindow, true);
  ImGui_ImplOpenGL3_Init(glsl_version);

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

  ImGuiStyle& style = ImGui::GetStyle();
  style.WindowRounding =
      0.0f;  // When viewports are enabled it is preferable to disable WinodwRounding
  style.Colors[ImGuiCol_WindowBg].w =
      1.0f;  // When viewports are enabled it is preferable to disable WindowBg alpha

  double frame_time_last = glfwGetTime();
  double frame_time_current = frame_time_last;
  double frame_delta_time = 0.0;
  uint64_t frame_counter = 0;

  bool show_demo_window = true;
  ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);

  gRndr = srndr_create();
  gView = srview_create();
  gRndrCmds = srcmdbuf_create(1024);
  simulator_init();

  while (!glfwWindowShouldClose(gWindow)) {
    frame_counter++;

    // Start the imgui frame such that widgets can be submitted
    handle_mouse();

    glfwPollEvents();

    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    frame_time_last = frame_time_current;
    frame_time_current = glfwGetTime();
    frame_delta_time = frame_time_current - frame_time_last;

    gTimer->mFrameTime = (float)(frame_delta_time);
    if (!gTimer->mPaused) {
      gTimer->mDeltaTime = gTimer->mFrameTime;
      gTimer->mCurrentTime = gTimer->mCurrentTime + gTimer->mDeltaTime;
    } else {
      gTimer->mDeltaTime = 0.0f;
    }

    assert(gTimer->mDeltaTime >= 0.0f);
    int width, height;
    ImGui::BeginMainMenuBar();

    if (ImGui::BeginMenu("Dialogs")) {
      ImGui::Checkbox("ImGui Demo", &show_demo_window);
      ImGui::EndMenu();
    }

    float menu_bar_height = ImGui::GetWindowHeight();
    ImGui::EndMainMenuBar();

    if (show_demo_window) ImGui::ShowDemoWindow();

    ShowDockspace(true);

    DoRender();

    simulator_update(gTimer->mDeltaTime);

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

    // Rendering
    ImGui::Render();
    int display_w, display_h;
    glfwMakeContextCurrent(gWindow);
    glfwGetFramebufferSize(gWindow, &display_w, &display_h);
    glViewport(0, 0, display_w, display_h);
    glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w);
    glClear(GL_COLOR_BUFFER_BIT);
    ImDrawData* draw_data = ImGui::GetDrawData();
    assert(draw_data != NULL);
    ImGui_ImplOpenGL3_RenderDrawData(draw_data);

    // Update and Render additional Platform Windows
    if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
      ImGui::UpdatePlatformWindows();
      ImGui::RenderPlatformWindowsDefault();
    }

    glfwMakeContextCurrent(gWindow);
    glfwSwapBuffers(gWindow);

    // Send the application to sleep if we have some time left for this frame
    double frame_target_time = 1.0 / 60.0;
    if (frame_delta_time < frame_target_time) {
      usleep((frame_target_time - frame_delta_time) * 1000000 * 0.98);
    }
  }

  gLog("Exiting application");

  srview_destroy(gView);
  srndr_destroy(gRndr);

  ImGui_ImplOpenGL3_Shutdown();
  ImGui_ImplGlfw_Shutdown();
  ImGui::DestroyContext();
  glfwTerminate();
}
