#include "asset_cache.h"
#include "forces.h"
#include "polygon.h"
#include "scene.h"
#include "sdl_wrapper.h"

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define CIRCLE_POINTS 40

#define MAX ((vector_t){.x = 80.0, .y = 80.0})

#define N_ROWS 11
#define ROW_SPACING 3.6
#define COL_SPACING 3.5
#define WALL_ANGLE atan2(ROW_SPACING, COL_SPACING / 2)
#define WALL_LENGTH hypot(MAX.x / 2, MAX.y)

#define PEG_RADIUS 0.5
#define BALL_RADIUS 1.0
#define DROP_INTERVAL 1 // s
#define PEG_ELASTICITY 0.3
#define BALL_ELASTICITY 0.7
#define WALL_WIDTH 1.0
#define DELTA_X 1.0
#define DROP_Y (MAX.y - 3.0)
#define START_VELOCITY ((vector_t){.x = 0.0, .y = -8.0})

#define BALL_MASS 2.0

#define BALL_COLOR ((rgb_color_t){1, 0, 0})
#define PEG_COLOR ((rgb_color_t){0, 1, 0})
#define WALL_COLOR ((rgb_color_t){0, 0, 1})

#define G 6.67E-11          // N m^2 / kg^2
#define M 6E24              // kg
#define g 9.8               // m / s^2
#define R (sqrt(G * M / g)) // m

typedef enum {
  BALL,
  FROZEN,
  WALL, // or peg
  GRAVITY
} body_type_t;

body_type_t *make_type_info(body_type_t type) {
  body_type_t *info = malloc(sizeof(*info));
  *info = type;
  return info;
}

body_type_t get_type(body_t *body) {
  return *(body_type_t *)body_get_info(body);
}

/** Generates a random number between 0 and 1 */
double rand_double(void) { return (double)rand() / RAND_MAX; }

/** Constructs a rectangle with the given dimensions centered at (0, 0) */
list_t *rect_init(double width, double height) {
  vector_t half_width = {.x = width / 2, .y = 0.0},
           half_height = {.x = 0.0, .y = height / 2};
  list_t *rect = list_init(4, free);
  vector_t *v = malloc(sizeof(*v));
  *v = vec_add(half_width, half_height);
  list_add(rect, v);
  v = malloc(sizeof(*v));
  *v = vec_subtract(half_height, half_width);
  list_add(rect, v);
  v = malloc(sizeof(*v));
  *v = vec_negate(*(vector_t *)list_get(rect, 0));
  list_add(rect, v);
  v = malloc(sizeof(*v));
  *v = vec_subtract(half_width, half_height);
  list_add(rect, v);
  return rect;
}

/** Constructs a circles with the given radius centered at (0, 0) */
list_t *circle_init(double radius) {
  list_t *circle = list_init(CIRCLE_POINTS, free);
  double arc_angle = 2 * M_PI / CIRCLE_POINTS;
  vector_t point = {.x = radius, .y = 0.0};
  for (size_t i = 0; i < CIRCLE_POINTS; i++) {
    vector_t *v = malloc(sizeof(*v));
    *v = point;
    list_add(circle, v);
    point = vec_rotate(point, arc_angle);
  }
  return circle;
}

/** Computes the center of the peg in the given row and column */
vector_t get_peg_center(size_t row, size_t col) {
  vector_t center = {.x = MAX.x / 2 + (col - row * 0.5) * COL_SPACING,
                     .y = MAX.y - (row + 1) * ROW_SPACING};
  return center;
}

/** Creates an Earth-like mass to accelerate the balls */
void add_gravity_body(scene_t *scene) {
  // Will be offscreen, so shape is irrelevant
  list_t *gravity_ball = rect_init(1, 1);
  body_t *body = body_init_with_info(gravity_ball, M, WALL_COLOR,
                                     make_type_info(GRAVITY), free);

  // Move a distnace R below the scene
  vector_t gravity_center = {.x = MAX.x / 2, .y = -R};
  body_set_centroid(body, gravity_center);
  scene_add_body(scene, body);
}

/** Creates a ball with the given starting position and velocity */
body_t *get_ball(vector_t center, vector_t velocity) {
  list_t *shape = circle_init(BALL_RADIUS);
  body_t *ball = body_init_with_info(shape, BALL_MASS, BALL_COLOR,
                                     make_type_info(BALL), free);

  body_set_centroid(ball, center);
  body_set_velocity(ball, velocity);

  return ball;
}

/** Collision handler to freeze a ball when it collides with a frozen body */
void freeze(body_t *ball, body_t *target, vector_t axis, void *aux,
            double force_const) {
  // Skip body if it was already frozen
  if (body_is_removed(ball))
    return;

  // Replace the ball with a frozen version
  body_remove(ball);
  body_t *frozen = get_ball(body_get_centroid(ball), VEC_ZERO);
  *((body_type_t *)body_get_info(frozen)) = FROZEN;
  scene_t *scene = aux;
  scene_add_body(scene, frozen);

  // Make other falling bodies freeze when they collide with this body
  size_t body_count = scene_bodies(scene);
  for (size_t i = 0; i < body_count; i++) {
    body_t *body = scene_get_body(scene, i);
    if (get_type(body) == BALL) {
      create_collision(scene, body, frozen, (collision_handler_t)freeze, scene,
                       0);
    }
  }
}

/** Adds a ball to the scene */
void add_ball(scene_t *scene) {
  // Add the ball to the scene.
  vector_t ball_center = {.x = MAX.x / 2 + (rand_double() - 0.5) * DELTA_X,
                          .y = DROP_Y};
  body_t *ball = get_ball(ball_center, START_VELOCITY);
  size_t body_count = scene_bodies(scene);
  scene_add_body(scene, ball);

  // Add force creators with other bodies
  for (size_t i = 0; i < body_count; i++) {
    body_t *body = scene_get_body(scene, i);
    switch (get_type(body)) {
    case BALL:
      // Bounce off other balls
      create_physics_collision(scene, ball, body, BALL_ELASTICITY);
      break;
    case WALL:
      // Bounce off walls and pegs
      create_physics_collision(scene, ball, body, PEG_ELASTICITY);
      break;
    case FROZEN:
      // Freeze when hitting the ground or frozen balls
      create_collision(scene, ball, body, (collision_handler_t)freeze, scene,
                       0);
      break;
    case GRAVITY:
      // Simulate earth's gravity acting on the ball
      create_newtonian_gravity(scene, G, body, ball);
    }
  }
}

/** Adds the pegs to the scene */
void add_pegs(scene_t *scene) {
  // Add N_ROWS and N_COLS of pegs.
  for (size_t i = 1; i <= N_ROWS; i++) {
    for (size_t j = 0; j <= i; j++) {
      list_t *polygon = circle_init(PEG_RADIUS);
      body_t *body = body_init_with_info(polygon, INFINITY, PEG_COLOR,
                                         make_type_info(WALL), free);
      body_set_centroid(body, get_peg_center(i, j));
      scene_add_body(scene, body);
    }
  }
}

/** Adds the walls to the scene */
void add_walls(scene_t *scene) {
  // Add walls
  list_t *rect_list = rect_init(WALL_LENGTH, WALL_WIDTH);
  // delete below later
  polygon_t *rect = polygon_init(rect_list, VEC_ZERO, 0, 0, 0, 0);

  polygon_translate(rect, (vector_t){.x = WALL_LENGTH / 2, .y = 0.0});
  polygon_rotate(rect, WALL_ANGLE, VEC_ZERO);
  body_t *body = body_init_with_info(polygon_get_points(rect), INFINITY,
                                     WALL_COLOR, make_type_info(WALL), free);
  scene_add_body(scene, body);

  rect_list = rect_init(WALL_LENGTH, WALL_WIDTH);
  rect = polygon_init(rect_list, VEC_ZERO, 0, 0, 0, 0);
  polygon_translate(rect, (vector_t){.x = MAX.x - WALL_LENGTH / 2, .y = 0.0});
  polygon_rotate(rect, -WALL_ANGLE, (vector_t){.x = MAX.x, .y = 0.0});
  body = body_init_with_info(polygon_get_points(rect), INFINITY, WALL_COLOR,
                             make_type_info(WALL), free);
  scene_add_body(scene, body);

  // Ground is special; it freezes balls when they touch it
  rect_list = rect_init(MAX.x, WALL_WIDTH);
  rect = polygon_init(rect_list, VEC_ZERO, 0, 0, 0, 0);
  body = body_init_with_info(polygon_get_points(rect), INFINITY, WALL_COLOR,
                             make_type_info(FROZEN), free);
  body_set_centroid(body, (vector_t){.x = MAX.x / 2, .y = WALL_WIDTH / 2});
  scene_add_body(scene, body);
}

typedef struct state {
  scene_t *scene;
  double time_since_drop;
} state_t;

state_t *emscripten_init(void) {
  srand(time(NULL));
  asset_cache_init();
  // Initialize scene
  sdl_init(VEC_ZERO, MAX);
  scene_t *scene = scene_init();
  // Add elements to the scene
  add_gravity_body(scene);
  add_pegs(scene);
  add_walls(scene);
  // Repeatedly render scene
  double time_since_drop = INFINITY;

  state_t *state = malloc(sizeof(state_t));
  state->scene = scene;
  state->time_since_drop = time_since_drop;
  return state;
}

bool emscripten_main(state_t *state) {
  double dt = time_since_last_tick();
  // Add a new ball every DROP_INTERVAL seconds
  state->time_since_drop += dt;
  if (state->time_since_drop > DROP_INTERVAL) {
    add_ball(state->scene);
    state->time_since_drop = 0.0;
  }
  scene_tick(state->scene, dt);
  sdl_render_scene(state->scene, NULL);

  return false;
}

void emscripten_free(state_t *state) {
  scene_free(state->scene);
  free(state);
}