#include "plugin.h"

#include <gfx/app.h>
#include <gfx/gfx.h>
#include <gfx/renderer.h>

#include <math/mat4.h>
#include <math/vec2.h>
#include <math/vec4.h>

#include <stdlib.h>

static const vec2 PAD_SIZE        = (vec2){120, 20};
static const R    PLAYER_Y_OFFSET = 50;
static const R    PLAYER_SPEED    = 800;

static const R ENEMY_SPEED = 2;

static const R BALL_SIZE  = 18;
static const R BALL_SPEED = 360; // In each dimension.

static const R EPS = (R)1e-3;

typedef struct Player {
  vec2 position;
} Player;

typedef struct Ball {
  vec2 position;
  vec2 velocity;
} Ball;

typedef struct State {
  bool   game_started;
  Player human;
  Player enemy;
  Ball   ball;
  mat4   viewProjection;
} State;

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

  State* state = calloc(1, sizeof(State));
  if (!state) {
    return false;
  }

  *pp_state = state;
  return true;

cleanup:
  free(state);
  return false;
}

void shutdown(Game* game, State* state) {
  assert(game);
  assert(state);
}

static void move_ball(Ball* ball, R dt, int width, int height) {
  assert(ball);

  const R offset = BALL_SIZE / 2;

  ball->position = vec2_add(ball->position, vec2_scale(ball->velocity, dt));

  // Right wall.
  if (ball->position.x + offset > (R)width) {
    ball->position.x = (R)width - offset - EPS;
    ball->velocity.x = -ball->velocity.x;
  }
  // Left wall.
  else if (ball->position.x - offset < 0) {
    ball->position.x = offset + EPS;
    ball->velocity.x = -ball->velocity.x;
  }
  // Top wall.
  if (ball->position.y + offset > (R)height) {
    ball->position.y = (R)height - offset - EPS;
    ball->velocity.y = -ball->velocity.y;
  }
  // Bottom wall.
  else if (ball->position.y - offset < 0) {
    ball->position.y = offset + EPS;
    ball->velocity.y = -ball->velocity.y;
  }
}

void move_enemy_player(int width, Player* player, R t) {
  const R half_width = (R)width / 2;
  const R amplitude  = half_width - (PAD_SIZE.x / 2);
  player->position.x = half_width + amplitude * sinf(t * ENEMY_SPEED);
}

void move_human_player(Player* player, R dt) {
  assert(player);

  R speed = 0;
  if (gfx_is_key_pressed('a')) {
    speed -= PLAYER_SPEED;
  }
  if (gfx_is_key_pressed('d')) {
    speed += PLAYER_SPEED;
  }

  player->position.x += speed * dt;
}

void clamp_player(Player* player, int width) {
  assert(player);

  const R offset = PAD_SIZE.x / 2;

  // Left wall.
  if (player->position.x + offset > (R)width) {
    player->position.x = (R)width - offset;
  }
  // Right wall.
  else if (player->position.x - offset < 0) {
    player->position.x = offset;
  }
}

void collide_ball(vec2 old_ball_position, const Player* player, Ball* ball) {
  assert(player);
  assert(ball);

  // Discrete but simple collision. Checks for intersection and moves the ball
  // back by a small epsilon.

  // Player bounding box.
  const vec2 player_pmin = vec2_make(
      player->position.x - PAD_SIZE.x / 2, player->position.y - PAD_SIZE.y / 2);
  const vec2 player_pmax = vec2_make(
      player->position.x + PAD_SIZE.x / 2, player->position.y + PAD_SIZE.y / 2);

  // Ball bounding box.
  const vec2 ball_pmin = vec2_make(
      ball->position.x - BALL_SIZE / 2, ball->position.y - BALL_SIZE / 2);
  const vec2 ball_pmax = vec2_make(
      ball->position.x + BALL_SIZE / 2, ball->position.y + BALL_SIZE / 2);

  // Check for intersection and update ball.
  if (!((ball_pmax.x < player_pmin.x) || (ball_pmin.x > player_pmax.x) ||
        (ball_pmax.y < player_pmin.y) || (ball_pmin.y > player_pmax.y))) {
    ball->position =
        vec2_add(old_ball_position, vec2_scale(ball->velocity, -EPS));
    ball->velocity.y = -ball->velocity.y;
  }
}

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

  // TODO: Move game width/height to GfxApp query functions?
  const vec2 old_ball_position = state->ball.position;
  move_ball(&state->ball, (R)dt, game->width, game->height);
  move_human_player(&state->human, (R)dt);
  move_enemy_player(game->width, &state->enemy, (R)t);
  clamp_player(&state->human, game->width);
  collide_ball(old_ball_position, &state->human, &state->ball);
  collide_ball(old_ball_position, &state->enemy, &state->ball);
}

static void draw_player(ImmRenderer* imm, const Player* player) {
  assert(imm);
  assert(player);

  const vec2 half_box = vec2_div(PAD_SIZE, vec2_make(2, 2));

  const vec2  pmin = vec2_sub(player->position, half_box);
  const vec2  pmax = vec2_add(player->position, half_box);
  const aabb2 box  = aabb2_make(pmin, pmax);

  gfx_imm_draw_aabb2(imm, box);
}

static void draw_ball(ImmRenderer* imm, const Ball* ball) {
  assert(imm);
  assert(ball);

  const vec2  half_box = vec2_make(BALL_SIZE / 2, BALL_SIZE / 2);
  const vec2  pmin     = vec2_sub(ball->position, half_box);
  const vec2  pmax     = vec2_add(ball->position, half_box);
  const aabb2 box      = aabb2_make(pmin, pmax);

  gfx_imm_draw_aabb2(imm, box);
}

void render(const Game* game, const State* state) {
  assert(game);
  assert(state);

  ImmRenderer* imm = gfx_get_imm_renderer(game->gfx);
  gfx_imm_start(imm);
  gfx_imm_set_view_projection_matrix(imm, &state->viewProjection);
  gfx_imm_load_identity(imm);
  gfx_imm_set_colour(imm, vec4_make(1, 1, 1, 1));
  draw_player(imm, &state->human);
  draw_player(imm, &state->enemy);
  draw_ball(imm, &state->ball);
  gfx_imm_end(imm);
}

static R clamp_to_width(int width, R x, R extent) {
  return min(x, (R)width - extent);
}

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

  state->viewProjection = mat4_ortho(0, (R)width, 0, (R)height, -1, 1);

  state->human.position.y = PLAYER_Y_OFFSET;
  state->enemy.position.y = (R)height - PLAYER_Y_OFFSET;

  if (!state->game_started) {
    state->human.position.x = (R)width / 2;
    state->enemy.position.x = (R)width / 2;

    state->ball.position =
        vec2_div(vec2_make((R)width, (R)height), vec2_make(2, 2));

    state->ball.velocity = vec2_make(BALL_SPEED, BALL_SPEED);

    state->game_started = true;
  } else {
    state->human.position.x =
        clamp_to_width(width, state->human.position.x, PAD_SIZE.x / 2);
    state->enemy.position.x =
        clamp_to_width(width, state->enemy.position.x, PAD_SIZE.x / 2);
  }
}