#include "plugin.h"

#include <gfx/app.h>
#include <gfx/asset.h>
#include <gfx/renderer.h>
#include <gfx/scene.h>
#include <gfx/util/skyquad.h>
#include <math/camera.h>
#include <math/spatial3.h>

#include <log/log.h>

#include <stdlib.h>

// 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";
static const char* GIRL =
    "/home/jeanne/Nextcloud/assets/models/girl/girl-with-ground.gltf";
static const char* BOXES =
    "/home/jeanne/Nextcloud/assets/models/boxes/boxes.gltf";

#define DEFAULT_SCENE_FILE GIRL

static const bool RenderBoundingBoxes     = false;
static const R    DefaultCameraSpeed      = (R)6.0;
static const R    DefaultMouseSensitivity = (R)(10 * TO_RAD);
static const vec3 DefaultCameraPosition   = (vec3){0, 2, 5};

typedef struct CameraCommand {
  bool CameraMoveLeft     : 1;
  bool CameraMoveRight    : 1;
  bool CameraMoveForward  : 1;
  bool CameraMoveBackward : 1;
} CameraCommand;

typedef struct CameraController {
  R camera_speed;           // Camera movement speed.
  R mouse_sensitivity;      // Controls the degree with which mouse movements
                            // rotate the camera.
  vec2 prev_mouse_position; // Mouse position in the previous frame.
  bool rotating;            // When true, subsequent mouse movements cause the
                            // camera to rotate.
} CameraController;

typedef struct State {
  Scene*           scene;
  Model*           model;
  SceneCamera*     camera;
  CameraController camera_controller;
} State;

/// Load the skyquad texture.
static const Texture* load_environment_map(Gfx* gfx) {
  assert(gfx);
  return gfx_load_texture(
      gfx, &(LoadTextureCmd){
               .origin                 = AssetFromFile,
               .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_south.bmp"),
                                          mstring_make("/assets/skybox/clouds1/clouds1_north.bmp")}
  });
}

/// Load the skyquad and return the environment light node.
static SceneNode* load_skyquad(Gfx* gfx, SceneNode* root) {
  assert(gfx);
  assert(root);

  GfxCore* gfxcore = gfx_get_core(gfx);

  const Texture* environment_map = load_environment_map(gfx);
  if (!environment_map) {
    return 0;
  }

  return gfx_setup_skyquad(gfxcore, root, environment_map);
}

/// Load the 3D scene.
/// Return the loaded model.
static Model* load_scene(Game* game, State* state, const char* scene_filepath) {
  assert(game);
  assert(game->gfx);
  assert(state);
  assert(state->scene);

  Camera* camera = gfx_get_camera_camera(state->camera);
  spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2));

  SceneNode* root           = gfx_get_scene_root(state->scene);
  SceneNode* sky_light_node = load_skyquad(game->gfx, root);
  if (!sky_light_node) {
    return 0; // test
  }

  Model* model = gfx_load_model(
      game->gfx,
      &(LoadModelCmd){
          .origin = AssetFromFile, .filepath = mstring_make(scene_filepath)});
  if (!model) {
    return 0;
  }
  SceneNode* model_node = gfx_make_model_node(model);
  if (!model_node) {
    return 0;
  }
  gfx_set_node_parent(model_node, sky_light_node);

  gfx_log_node_hierarchy(root);

  return model;
}

bool init(Game* game, State** pp_state) {
  assert(game);

  // Usage: <scene file>
  const char* scene_filepath =
      game->argc > 1 ? game->argv[1] : DEFAULT_SCENE_FILE;

  State* state = calloc(1, sizeof(State));
  if (!state) {
    goto cleanup;
  }

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

  state->model = load_scene(game, state, scene_filepath);
  if (!state->model) {
    goto cleanup;
  }

  Anima* anima = gfx_get_model_anima(state->model);
  if (anima) {
    gfx_play_animation(
        anima, &(AnimationPlaySettings){.name = "Walk", .loop = true});
    // TODO: Interpolate animations.
    /*gfx_play_animation(
        anima,
        &(AnimationPlaySettings){.name = "Jumping-jack-lower", .loop = true});
    gfx_play_animation(
        anima, &(AnimationPlaySettings){
                   .name = "Jumping-jack-arms-mid", .loop = true});*/
  }

  spatial3_set_position(
      &gfx_get_camera_camera(state->camera)->spatial, DefaultCameraPosition);

  state->camera_controller.camera_speed      = DefaultCameraSpeed;
  state->camera_controller.mouse_sensitivity = DefaultMouseSensitivity;

  *pp_state = state;
  return true;

cleanup:
  shutdown(game, state);
  if (state) {
    free(state);
  }
  return false;
}

void shutdown(Game* game, State* state) {
  assert(game);
  if (state) {
    gfx_destroy_camera(&state->camera);
    gfx_destroy_scene(&state->scene);
    // State freed by plugin engine.
  }
}

static void update_camera(
    CameraController* controller, R dt, vec2 mouse_position,
    CameraCommand command, Spatial3* camera) {
  assert(controller);
  assert(camera);

  // Translation.
  const R move_x = (R)(command.CameraMoveLeft ? -1 : 0) +
                   (R)(command.CameraMoveRight ? 1 : 0);
  const R move_y = (R)(command.CameraMoveForward ? 1 : 0) +
                   (R)(command.CameraMoveBackward ? -1 : 0);
  const vec2 translation =
      vec2_scale(vec2_make(move_x, move_y), controller->camera_speed * dt);
  spatial3_move_right(camera, translation.x);
  spatial3_move_forwards(camera, translation.y);

  // Rotation.
  if (controller->rotating) {
    const vec2 mouse_delta =
        vec2_sub(mouse_position, controller->prev_mouse_position);

    const vec2 rotation =
        vec2_scale(mouse_delta, controller->mouse_sensitivity * dt);

    spatial3_global_yaw(camera, -rotation.x);
    spatial3_pitch(camera, -rotation.y);
  }

  // Update controller state.
  controller->prev_mouse_position = mouse_position;
}

void update(Game* game, State* state, double t, double dt) {
  assert(game);
  assert(state);
  assert(state->scene);
  assert(state->camera);

  double mouse_x, mouse_y;
  gfx_app_get_mouse_position(&mouse_x, &mouse_y);
  const vec2 mouse_position = {(R)mouse_x, (R)mouse_y};

  const CameraCommand camera_command = (CameraCommand){
      .CameraMoveLeft     = gfx_app_is_key_pressed(KeyA),
      .CameraMoveRight    = gfx_app_is_key_pressed(KeyD),
      .CameraMoveForward  = gfx_app_is_key_pressed(KeyW),
      .CameraMoveBackward = gfx_app_is_key_pressed(KeyS),
  };

  state->camera_controller.rotating = gfx_app_is_mouse_button_pressed(LMB);

  update_camera(
      &state->camera_controller, (R)dt, mouse_position, camera_command,
      &gfx_get_camera_camera(state->camera)->spatial);

  //  const vec3 orbit_point = vec3_make(0, 2, 0);
  //  Camera*    camera      = gfx_get_camera_camera(state->camera);
  //  spatial3_orbit(
  //      &camera->spatial, orbit_point,
  //      /*radius=*/5,
  //      /*azimuth=*/(R)(t * 0.5), /*zenith=*/0);
  //  spatial3_lookat(&camera->spatial, orbit_point);

  gfx_update(state->scene, state->camera, (R)t);
}

/// Render the bounding boxes of all scene objects.
static void render_bounding_boxes_rec(
    ImmRenderer* imm, const Anima* anima, const mat4* parent_model_matrix,
    const SceneNode* node) {
  assert(imm);
  assert(node);

  const mat4 model_matrix =
      mat4_mul(*parent_model_matrix, gfx_get_node_transform(node));

  const NodeType node_type = gfx_get_node_type(node);

  if (node_type == ModelNode) {
    const Model*     model = gfx_get_node_model(node);
    const SceneNode* root  = gfx_get_model_root(model);
    render_bounding_boxes_rec(imm, anima, &model_matrix, root);
  } else if (node_type == AnimaNode) {
    anima = gfx_get_node_anima(node);
  } else if (node_type == ObjectNode) {
    gfx_imm_set_model_matrix(imm, &model_matrix);

    const SceneObject* obj      = gfx_get_node_object(node);
    const Skeleton*    skeleton = gfx_get_object_skeleton(obj);

    if (skeleton) { // Animated model.
      assert(anima);
      const size_t num_joints = gfx_get_skeleton_num_joints(skeleton);
      for (size_t i = 0; i < num_joints; ++i) {
        if (gfx_joint_has_box(anima, skeleton, i)) {
          const Box box = gfx_get_joint_box(anima, skeleton, i);
          gfx_imm_draw_box3(imm, box.vertices);
        }
      }
    } else { // Static model.
      const aabb3 box = gfx_get_object_aabb(obj);
      gfx_imm_draw_aabb3(imm, box);
    }
  }

  // Render children's boxes.
  const SceneNode* child = gfx_get_node_child(node);
  while (child) {
    render_bounding_boxes_rec(imm, anima, &model_matrix, child);
    child = gfx_get_node_sibling(child);
  }
}

/// Render the bounding boxes of all scene objects.
static void render_bounding_boxes(const Game* game, const State* state) {
  assert(game);
  assert(state);

  GfxCore*     gfxcore = gfx_get_core(game->gfx);
  ImmRenderer* imm     = gfx_get_imm_renderer(game->gfx);
  assert(gfxcore);
  assert(imm);

  const mat4 id    = mat4_id();
  Anima*     anima = 0;

  gfx_set_blending(gfxcore, true);
  gfx_set_depth_mask(gfxcore, false);
  gfx_set_polygon_offset(gfxcore, -1.5f, -1.0f);

  gfx_imm_start(imm);
  gfx_imm_set_camera(imm, gfx_get_camera_camera(state->camera));
  gfx_imm_set_colour(imm, vec4_make(0.3, 0.3, 0.9, 0.1));
  render_bounding_boxes_rec(imm, anima, &id, gfx_get_scene_root(state->scene));
  gfx_imm_end(imm);

  gfx_reset_polygon_offset(gfxcore);
  gfx_set_depth_mask(gfxcore, true);
  gfx_set_blending(gfxcore, false);
}

void render(const Game* game, const State* state) {
  assert(state);
  assert(game);
  assert(game->gfx);
  assert(state->scene);
  assert(state->camera);

  Renderer* renderer = gfx_get_renderer(game->gfx);
  assert(renderer);

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

  if (RenderBoundingBoxes) {
    render_bounding_boxes(game, state);
  }
}

void resize(Game* game, State* state, int width, int height) {
  assert(game);
  assert(state);

  const R    fovy       = 60 * 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(state->camera);
  camera->projection = projection;
}