#include <gfx/app.h>

#include <glad/glad.h>

#include <GLFW/glfw3.h>
#include <log/log.h>
#include <timer.h>

#include <assert.h>
#include <stdlib.h>

/// Application state.
typedef struct GfxApp {
  GfxAppState*    app_state;
  GfxAppCallbacks callbacks;
  int             max_fps;
  double          update_delta_time;
  GLFWwindow*     window;
} GfxApp;

/// Storing the application state in a global variable so that we can call the
/// application's callbacks from GLFW callbacks.
static GfxApp g_gfx_app;

/// Called by GLFW when the window is resized.
static void on_resize(GLFWwindow* window, int width, int height) {
  (*g_gfx_app.callbacks.resize)(g_gfx_app.app_state, width, height);
}

/// Run the application's main loop.
static void loop(GfxApp* app) {
  assert(app);
  assert(app->window);

  const time_delta min_frame_time =
      app->max_fps > 0 ? sec_to_time_delta(1.0 / (double)(app->max_fps)) : 0;
  const time_delta update_dt   = sec_to_time_delta(app->update_delta_time);
  time_delta       time        = 0;
  time_delta       time_budget = 0;
  Timer            timer       = timer_make();

  // Warm up the update to initialize the application's state.
  (*app->callbacks.update)(
      app->app_state, time_delta_to_sec(time), time_delta_to_sec(update_dt));

  // Warm up the rendering before entering the main loop. A renderer can
  // compile shaders and do other initialization the first time it renders a
  // scene.
  (*app->callbacks.render)(app->app_state);
  glfwSwapBuffers(app->window);

  timer_start(&timer);
  while (!glfwWindowShouldClose(app->window)) {
    timer_tick(&timer);
    time_budget += timer.delta_time;

    while (time_budget >= update_dt) {
      (*app->callbacks.update)(
          app->app_state, time_delta_to_sec(time),
          time_delta_to_sec(update_dt));

      time += update_dt;
      time_budget -= update_dt;
    }

    (*app->callbacks.render)(app->app_state);
    glfwSwapBuffers(app->window);
    glfwPollEvents();

    const time_point frame_end  = time_now();
    const time_delta frame_time = time_diff(timer.last_tick, frame_end);
    if ((min_frame_time > 0) && (frame_time < min_frame_time)) {
      time_sleep(min_frame_time - frame_time);
    }
  }
}

bool gfx_app_run(const GfxAppDesc* desc, const GfxAppCallbacks* callbacks) {
  assert(desc);
  assert(callbacks);

  bool success = false;

  g_gfx_app.app_state         = desc->app_state;
  g_gfx_app.callbacks         = *callbacks;
  g_gfx_app.max_fps           = desc->max_fps;
  g_gfx_app.update_delta_time = desc->update_delta_time;
  g_gfx_app.window            = 0;

  if (!glfwInit()) {
    LOGE("glfwInit() failed");
    return false;
  }

  const int major = 4;
  const int minor = 4;
  glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, major);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, minor);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
  // TODO: Test antialiasing later on.
  // glfwWindowHint(GLFW_SAMPLES, 4);

  const char* title = desc->title ? desc->title : "Gfx Application";

  g_gfx_app.window =
      glfwCreateWindow(desc->width, desc->height, title, NULL, NULL);
  if (!g_gfx_app.window) {
    LOGE("glfwCreateWindow() failed");
    goto cleanup;
  }
  glfwMakeContextCurrent(g_gfx_app.window);

  // Request adaptive sync if supported.
  glfwSwapInterval(-1);

  // Load GL before calling the application init clalback.
  if (!gladLoadGL()) {
    LOGE("Failed loading glad!");
    return 0;
  }

  // Initialize the application's state before setting any callbacks.
  if (!(*g_gfx_app.callbacks.init)(
          g_gfx_app.app_state, desc->argc, desc->argv)) {
    LOGE("Failed to initialize application");
    goto cleanup;
  }

  // Trigger an initial resize for convenience.
  (*g_gfx_app.callbacks.resize)(g_gfx_app.app_state, desc->width, desc->height);

  // Set GLFW callbacks now that the application has been initialized.
  glfwSetWindowSizeCallback(g_gfx_app.window, on_resize);

  loop(&g_gfx_app);

  (*g_gfx_app.callbacks.shutdown)(g_gfx_app.app_state);

  success = true;

cleanup:
  if (g_gfx_app.window) {
    glfwDestroyWindow(g_gfx_app.window);
  }
  glfwTerminate();
  return success;
}

void gfx_app_get_mouse_position(double* x, double* y) {
  glfwGetCursorPos(g_gfx_app.window, x, y);
}

static int to_glfw_mouse_button(MouseButton button);

bool gfx_app_is_mouse_button_pressed(MouseButton button) {
  return glfwGetMouseButton(g_gfx_app.window, to_glfw_mouse_button(button)) ==
         GLFW_PRESS;
}

static int to_glfw_key(Key key);

bool gfx_app_is_key_pressed(Key key) {
  return glfwGetKey(g_gfx_app.window, to_glfw_key(key)) == GLFW_PRESS;
}

static int to_glfw_mouse_button(MouseButton button) {
  switch (button) {
  case LMB:
    return GLFW_MOUSE_BUTTON_LEFT;
  case RMB:
    return GLFW_MOUSE_BUTTON_RIGHT;
  case MMB:
    return GLFW_MOUSE_BUTTON_MIDDLE;
  }
}

static int to_glfw_key(Key key) {
  switch (key) {
  case KeyA:
    return GLFW_KEY_A;
  case KeyB:
    return GLFW_KEY_B;
  case KeyC:
    return GLFW_KEY_C;
  case KeyD:
    return GLFW_KEY_D;
  case KeyE:
    return GLFW_KEY_E;
  case KeyF:
    return GLFW_KEY_F;
  case KeyG:
    return GLFW_KEY_G;
  case KeyH:
    return GLFW_KEY_H;
  case KeyI:
    return GLFW_KEY_I;
  case KeyJ:
    return GLFW_KEY_J;
  case KeyK:
    return GLFW_KEY_K;
  case KeyL:
    return GLFW_KEY_L;
  case KeyM:
    return GLFW_KEY_M;
  case KeyN:
    return GLFW_KEY_N;
  case KeyO:
    return GLFW_KEY_O;
  case KeyP:
    return GLFW_KEY_P;
  case KeyQ:
    return GLFW_KEY_Q;
  case KeyR:
    return GLFW_KEY_R;
  case KeyS:
    return GLFW_KEY_S;
  case KeyT:
    return GLFW_KEY_T;
  case KeyU:
    return GLFW_KEY_U;
  case KeyV:
    return GLFW_KEY_V;
  case KeyW:
    return GLFW_KEY_W;
  case KeyX:
    return GLFW_KEY_X;
  case KeyY:
    return GLFW_KEY_Y;
  case KeyZ:
    return GLFW_KEY_Z;
  }
}