#include "game.h"

#include <gfx/error.h>
#include <gfx/render_backend.h>
#include <gfx/scene/light.h>
#include <gfx/scene/material.h>
#include <gfx/scene/mesh.h>
#include <gfx/scene/node.h>
#include <gfx/scene/object.h>
#include <gfx/util/geometry.h>
#include <gfx/util/ibl.h>
#include <gfx/util/scene.h>
#include <gfx/util/shader.h>
#include <gfx/util/skyquad.h>
#include <gfx/util/texture.h>

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

#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h> // usleep; TODO Remove.

// Paths to various scene files.
static const char* BOX     = "/assets/models/box.gltf";
static const char* SUZANNE = "/assets/models/suzanne.gltf";
static const char* SPONZA =
    "/assets/glTF-Sample-Models/2.0/Sponza/glTF/Sponza.gltf";
static const char* FLIGHT_HELMET =
    "/assets/glTF-Sample-Models/2.0/FlightHelmet/glTF/FlightHelmet.gltf";
static const char* DAMAGED_HELMET =
    "/assets/glTF-Sample-Models/2.0/DamagedHelmet/glTF/DamagedHelmet.gltf";

#define DEFAULT_SCENE_FILE DAMAGED_HELMET

static const char* CLOUDS1_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp";

static ShaderProgram* load_shader(
    RenderBackend* render_backend, const char* view_mode) {
  ShaderProgram* shader = 0;
  if (strcmp(view_mode, "debug") == 0) {
    shader = gfx_make_debug3d_shader(render_backend);
  } else if (strcmp(view_mode, "normals") == 0) {
    shader = gfx_make_view_normals_shader(render_backend);
  } else if (strcmp(view_mode, "normal_mapped_normals") == 0) {
    shader = gfx_make_view_normal_mapped_normals_shader(render_backend);
  } else if (strcmp(view_mode, "tangents") == 0) {
    shader = gfx_make_view_tangents_shader(render_backend);
  } else {
    shader = gfx_make_cook_torrance_shader(render_backend);
  }
  return shader;
}

/// Loads the skyquad texture.
static Texture* load_environment_map(RenderBackend* render_backend) {
  return gfx_load_texture(
      render_backend,
      &(LoadTextureCmd){
          .origin                 = TextureFromFile,
          .type                   = LoadCubemap,
          .colour_space           = sRGB,
          .filtering              = NearestFiltering,
          .mipmaps                = false,
          .data.cubemap.filepaths = {
                                     mstring_make("/assets/skybox/clouds1/clouds1_east.bmp"),
                                     mstring_make("/assets/skybox/clouds1/clouds1_west.bmp"),
                                     mstring_make("/assets/skybox/clouds1/clouds1_up.bmp"),
                                     mstring_make("/assets/skybox/clouds1/clouds1_down.bmp"),
                                     mstring_make("/assets/skybox/clouds1/clouds1_north.bmp"),
                                     mstring_make("/assets/skybox/clouds1/clouds1_south.bmp")}
  });
}

/// Creates an object to render the skyquad in the background.
static SceneNode* make_skyquad_object_node(
    Game* game, const Texture* environment_map) {
  assert(game);

  SceneObject* skyquad_object =
      gfx_make_skyquad(game->gfx, game->scene, environment_map);
  if (!skyquad_object) {
    return 0;
  }
  SceneNode* skyquad_node = gfx_make_object_node(skyquad_object);
  if (!skyquad_node) {
    return 0;
  }
  gfx_set_node_parent(skyquad_node, gfx_get_scene_root(game->scene));
  return skyquad_node;
}

/// Creates an environment light.
static SceneNode* make_environment_light(
    Game* game, const Texture* environment_light) {
  assert(game);

  Light* light = gfx_make_light(&(LightDesc){
      .type  = EnvironmentLightType,
      .light = (EnvironmentLightDesc){.environment_map = environment_light}});
  if (!light) {
    return 0;
  }
  SceneNode* light_node = gfx_make_light_node(light);
  if (!light_node) {
    return 0;
  }
  gfx_set_node_parent(light_node, gfx_get_scene_root(game->scene));
  return light_node;
}

/// Loads the skyquad and returns the SceneNode with the environment light.
static bool load_skyquad(Game* game, SceneNode** node) {
  assert(game);
  assert(node);

  Texture* environment_map = load_environment_map(game->render_backend);
  if (!environment_map) {
    return false;
  }

  make_skyquad_object_node(game, environment_map);
  *node = make_environment_light(game, environment_map);

  return true;
}

/// Loads the 3D scene.
static bool load_scene(
    Game* game, const char* scene_filepath, const char* view_mode) {
  assert(game);

  game->camera = gfx_make_camera();
  if (!game->camera) {
    return false;
  }
  Camera* camera = gfx_get_camera_camera(game->camera);
  // Sponza.
  // spatial3_set_position(&camera->spatial, vec3_make(0, 100, 50));
  // Damaged helmet.
  spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2));

  SceneNode* sky_node = 0;
  if (!load_skyquad(game, &sky_node) || !sky_node) {
    return false;
  }

  ShaderProgram* shader = load_shader(game->render_backend, view_mode);
  if (!shader) {
    return false;
  }

  if (!gfx_load_scene(
          game->gfx, sky_node, shader,
          &(LoadSceneCmd){
              .origin = SceneFromFile, .filepath = scene_filepath})) {
    return false;
  }

  return true;
}

/// Loads a scene for debugging textures.
static bool load_texture_debugger_scene(Game* game) {
  assert(game);

  Texture* texture = gfx_load_texture(
      game->render_backend,
      &(LoadTextureCmd){
          .origin                = TextureFromFile,
          .type                  = LoadTexture,
          .filtering             = LinearFiltering,
          .mipmaps               = false,
          .data.texture.filepath = mstring_make(CLOUDS1_TEXTURE)});

  game->camera = gfx_make_camera();
  if (!game->camera) {
    return false;
  }
  Camera* camera = gfx_get_camera_camera(game->camera);
  spatial3_set_position(&camera->spatial, vec3_make(0, 0, 1));

  ShaderProgram* shader = gfx_make_view_texture_shader(game->render_backend);
  if (!shader) {
    return false;
  }

  Geometry* geometry = gfx_make_quad_11(game->render_backend);
  if (!geometry) {
    return false;
  }

  MaterialDesc material_desc = (MaterialDesc){0};
  material_desc.uniforms[0]  = (ShaderUniform){
       .type          = UniformTexture,
       .value.texture = texture,
       .name          = sstring_make("Texture")};
  material_desc.num_uniforms = 1;
  Material* material         = gfx_make_material(&material_desc);
  if (!material) {
    return false;
  }

  MeshDesc mesh_desc = (MeshDesc){0};
  mesh_desc.geometry = geometry;
  mesh_desc.material = material;
  mesh_desc.shader   = shader;
  Mesh* mesh         = gfx_make_mesh(&mesh_desc);
  if (!mesh) {
    return false;
  }

  SceneObject* object = gfx_make_object();
  if (!object) {
    return false;
  }
  gfx_add_object_mesh(object, mesh);

  SceneNode* node = gfx_make_object_node(object);
  SceneNode* root = gfx_get_scene_root(game->scene);
  gfx_set_node_parent(node, root);

  return true;
}

bool game_new(Game* game, int argc, const char** argv) {
  // TODO: getopt() to implement proper argument parsing.
  const char* view_mode      = argc > 1 ? argv[1] : "";
  const char* scene_filepath = argc > 2 ? argv[2] : DEFAULT_SCENE_FILE;

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

  game->render_backend = gfx_get_render_backend(game->gfx);
  game->renderer       = gfx_get_renderer(game->gfx);

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

  if (!load_scene(game, scene_filepath, view_mode)) {
    goto cleanup;
  }
  /*if (!load_texture_debugger_scene(game)) {
    goto cleanup;
  }*/

  return true;

cleanup:
  LOGE("Gfx error: %s", gfx_get_error());
  if (game->scene) {
    gfx_destroy_scene(game->gfx, &game->scene);
  }
  if (game->gfx) {
    gfx_destroy(&game->gfx);
  }
  return false;
}

void game_end(Game* game) { gfx_destroy(&game->gfx); }

void game_update(Game* game, double t, double dt) {
  game->elapsed += dt;
  while (game->elapsed >= 1.0) {
    // LOGD("Tick");
    usleep(1000);
    game->elapsed -= 1.0;
  }
  Camera* camera = gfx_get_camera_camera(game->camera);
  spatial3_orbit(
      &camera->spatial, vec3_make(0, 0, 0),
      /*radius=*/2,
      /*azimuth=*/t * 0.5, /*zenith=*/0);
  spatial3_lookat(&camera->spatial, vec3_make(0, 0, 0));
}

void game_render(const Game* game) {
  gfx_render_scene(
      game->renderer, game->render_backend, game->scene, game->camera);
}

void game_set_viewport(Game* game, int width, int height) {
  gfx_set_viewport(game->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;
}