/*
 * Main game module with entry point and game loop.
 *
 * The game module sets up the window and GL context and defers the core game
 * logic to a plugin.
 */
#define _GNU_SOURCE 200112L // For readlink()

#include "game.h"
#include "plugins/plugin.h"

#include <gfx/render_backend.h>
#include <gfx/scene/camera.h>
#include <gfx/scene/object.h>

#include <error.h>
#include <log/log.h>
#include <math/camera.h>
#include <plugin.h>

#include <assert.h>
#include <stdbool.h>
#include <stdio.h>

#include <linux/limits.h>

#include <unistd.h>

#undef _GNU_SOURCE

// Plugin to load if no plugin is provided.
static const char* DEFAULT_PLUGIN = "texture_view";

bool game_new(Game* game, int argc, const char** argv) {
  assert(game);

  // Syntax: game [plugin] <plugin args>
  //
  // Here we consume the [plugin] arg so that plugins receive the remainder
  // args starting from 0.
  game->argc = argc - 1;
  game->argv = argv + 1;

  char exe_path_buf[NAME_MAX] = {0};
  if (readlink("/proc/self/exe", exe_path_buf, sizeof(exe_path_buf)) == -1) {
    LOGE("readlink(/proc/self/exe) failed");
    goto cleanup;
  }

  // Replace the last / with a null terminator to remove the exe file from the
  // path. This gets the file's parent directory.
  *strrchr(exe_path_buf, '/') = 0;

  const mstring exe_dir      = mstring_make(exe_path_buf);
  const mstring plugins_path = mstring_concat_cstr(exe_dir, "/src/plugins");

  if (!(game->plugin_engine = new_plugin_engine(
            &(PluginEngineDesc){.plugins_dir = mstring_cstr(&plugins_path)}))) {
    goto cleanup;
  }

  const char* plugin = argc > 1 ? argv[1] : DEFAULT_PLUGIN;
  if (!(game->plugin = load_plugin(game->plugin_engine, plugin))) {
    goto cleanup;
  }

  if (!(game->gfx = gfx_init())) {
    goto cleanup;
  }
  if (!(game->scene = gfx_make_scene())) {
    goto cleanup;
  }
  if (!(game->camera = gfx_make_camera())) {
    goto cleanup;
  }

  if (plugin_resolve(game->plugin, plugin_init, "init")) {
    void* plugin_state = plugin_call(game->plugin, plugin_init, "init", game);
    if (!plugin_state) {
      goto cleanup;
    }
    set_plugin_state(game->plugin, plugin_state);
  }

  if (plugin_resolve(game->plugin, plugin_boot, "boot")) {
    void* plugin_state = get_plugin_state(game->plugin);
    bool  boot_success =
        plugin_call(game->plugin, plugin_boot, "boot", plugin_state, game);
    if (!boot_success) {
      goto cleanup;
    }
  }

  return true;

cleanup:
  LOGE("Gfx error: %s", get_error());
  game_end(game);
  return false;
}

void game_end(Game* game) {
  assert(game);
  if (game->gfx) {
    gfx_destroy(&game->gfx);
  }
  if (game->plugin) {
    delete_plugin(&game->plugin);
  }
  if (game->plugin_engine) {
    delete_plugin_engine(&game->plugin_engine);
  }
}

void game_update(Game* game, double t, double dt) {
  plugin_engine_update(game->plugin_engine);
  if (plugin_reloaded(game->plugin) &&
      plugin_resolve(game->plugin, plugin_init, "init")) {
    void* plugin_state = plugin_call(game->plugin, plugin_init, "init", game);
    assert(plugin_state); // TODO: handle error better.
    set_plugin_state(game->plugin, plugin_state);
  }

  if (plugin_resolve(game->plugin, plugin_update, "update")) {
    // Plugin state may be null if plugin does not expose init().
    void* plugin_state = get_plugin_state(game->plugin);
    plugin_call(
        game->plugin, plugin_update, "update", plugin_state, game, t, dt);
  }
}

void game_render(const Game* game) {
  RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
  Renderer*      renderer       = gfx_get_renderer(game->gfx);

  gfx_start_frame(render_backend);

  gfx_render_scene(
      renderer,
      &(RenderSceneParams){
          .mode = RenderDefault, .scene = game->scene, .camera = game->camera});

  if (plugin_resolve(game->plugin, plugin_render, "render")) {
    // Plugin state may be null if plugin does not expose init().
    void* plugin_state = get_plugin_state(game->plugin);
    plugin_call(game->plugin, plugin_render, "render", plugin_state, game);
  }

  gfx_end_frame(render_backend);
}

void game_set_viewport(Game* game, int width, int height) {
  RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
  gfx_set_viewport(render_backend, width, height);

  const R    fovy       = 90 * TO_RAD;
  const R    aspect     = (R)width / (R)height;
  const R    near       = 0.1;
  const R    far        = 1000;
  const mat4 projection = mat4_perspective(fovy, aspect, near, far);

  Camera* camera     = gfx_get_camera_camera(game->camera);
  camera->projection = projection;
}