/*
 * 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/gfx.h>
#include <gfx/gfx_app.h>
#include <gfx/render_backend.h>
#include <gfx/renderer.h>
#include <gfx/scene/camera.h>
#include <gfx/scene/node.h>
#include <gfx/scene/object.h>
#include <gfx/scene/scene.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 <stdlib.h>

#include <linux/limits.h>

#include <unistd.h>

#undef _GNU_SOURCE

static const int WIDTH   = 1350;
static const int HEIGHT  = 900;
static const int MAX_FPS = 60;

/// Initialize the game's plugin.
static bool init_plugin(Game* game) {
  assert(game);
  assert(game->plugin);
  // Plugin state is allowed to be null, either when the plugin does not
  // expose an init() or when init() does not initialize a state.
  if (plugin_resolve(game->plugin, plugin_init, "init")) {
    State* plugin_state = 0;
    if (!plugin_call(game->plugin, plugin_init, "init", game, &plugin_state)) {
      return false;
    }
    set_plugin_state(game->plugin, plugin_state);
  }
  return true; // Plugin does not need to expose an init().
}

/// Shutdown the game's plugin.
/// The game's plugin is allowed to be null in the call to this function.
static void shutdown_plugin(Game* game) {
  assert(game);
  if (game->plugin &&
      (plugin_resolve(game->plugin, plugin_shutdown, "shutdown"))) {
    void* plugin_state = get_plugin_state(game->plugin);
    plugin_call(game->plugin, plugin_shutdown, "shutdown", game, plugin_state);
    set_plugin_state(game->plugin, 0);
  }
}

/// Boot the game's plugin.
static bool boot_plugin(Game* game) {
  assert(game);
  assert(game->plugin);
  if (plugin_resolve(game->plugin, plugin_boot, "boot")) {
    void* plugin_state = get_plugin_state(game->plugin);
    return plugin_call(game->plugin, plugin_boot, "boot", game, plugin_state);
  }
  return true; // Plugin does not need to expose a boot().
}

/// Update the plugin's state.
static void update_plugin(Game* game, double t, double dt) {
  assert(game);
  assert(game->plugin);
  if (plugin_resolve(game->plugin, plugin_update, "update")) {
    void* plugin_state = get_plugin_state(game->plugin);
    plugin_call(
        game->plugin, plugin_update, "update", game, plugin_state, t, dt);
  }
}

/// Plugin render.
static void render_plugin(const Game* game) {
  assert(game);
  assert(game->plugin);
  if (plugin_resolve(game->plugin, plugin_render, "render")) {
    void* plugin_state = get_plugin_state(game->plugin);
    plugin_call(game->plugin, plugin_render, "render", game, plugin_state);
  }
}

/// Plugin resize.
static void resize_plugin(Game* game, int width, int height) {
  assert(game);
  assert(game->plugin);
  if (plugin_resolve(game->plugin, plugin_resize, "resize")) {
    void* plugin_state = get_plugin_state(game->plugin);
    plugin_call(
        game->plugin, plugin_resize, "resize", game, plugin_state, width,
        height);
  }
}

void app_end(Game* game);

bool app_init(const GfxAppDesc* desc, void** app_state) {
  assert(desc);

  if (desc->argc <= 1) {
    LOGE("Usage: %s <plugin> [plugin args]", desc->argv[0]);
    return false;
  }

  Game* game = calloc(1, sizeof(Game));
  if (!game) {
    LOGE("Failed to allocate game state");
    return false;
  }

  // Syntax: game <plugin> [plugin args]
  //
  // Here we consume the <plugin> arg so that plugins receive the remainder
  // args starting from 0.
  game->argc = desc->argc - 1;
  game->argv = desc->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 = desc->argv[1];
  if (!(game->plugin = load_plugin(game->plugin_engine, plugin))) {
    goto cleanup;
  }

  if (!(game->gfx = gfx_init())) {
    goto cleanup;
  }

  if (!init_plugin(game)) {
    goto cleanup;
  }
  if (!boot_plugin(game)) {
    goto cleanup;
  }

  *app_state = game;
  return true;

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

void app_end(Game* game) {
  assert(game);
  shutdown_plugin(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 app_update(Game* game, double t, double dt) {
  plugin_engine_update(game->plugin_engine);
  if (plugin_reloaded(game->plugin)) {
    shutdown_plugin(game);
    const bool result = init_plugin(game);
    assert(result); // TODO: handle error better.

    // Trigger a resize just like the initial resize that occurs when the gfx
    // application starts.
    resize_plugin(game, game->width, game->height);
  }

  update_plugin(game, t, dt);
}

void app_render(const Game* game) {
  RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
  gfx_start_frame(render_backend);
  render_plugin(game);
  gfx_end_frame(render_backend);
}

void app_resize(Game* game, int width, int height) {
  game->width  = width;
  game->height = height;

  RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
  gfx_set_viewport(render_backend, width, height);

  resize_plugin(game, width, height);
}

GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS);