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

#include "../include/player.h"
#include "../include/const.h"
#include "../include/tile.h"
#include "../include/util.h"
#include "../include/entity.h"
#include "../include/effect.h"

player_t *player_create(pos_t pos) {
  player_t *player = malloc(sizeof(player_t));
  *player = (player_t) {
    .pos = pos,
    .health = PLAYER_MAX_HEALTH,
    .max_health = PLAYER_MAX_HEALTH,
    .on_ground = false,
    .velocity = 0.0,
    .gravity = 0.0,
    .dir = 1,
    .sneaking = false,
    .shooting = false,
    .damage_timer = 0,
    .fall_height = 0.0,
    .held_enemy = NULL,
    .moving = false,
    .timer_walking = fz_timer_create(9, 4),
    .timer_sneaking = fz_timer_create(9, 8),
    .target_entity = NULL,
    .shooting_distance = PLAYER_SHOOTING_RANGE,
  };
  return player;
}

void player_update(player_t *player, game_t *game) {
  float move = 0.0;
  bool go_down = false;
  bool sneaking = false;
  bool holding = false;
  player->shooting = false;
  player->moving = false;
  tile_t *left_ground_tile = tile_get(game, player->pos.x + 0.2 * TILE_WIDTH, player->pos.y + PLAYER_HEIGHT + 1);
  tile_t *right_ground_tile = tile_get(game, player->pos.x + PLAYER_WIDTH - 0.2 * TILE_WIDTH, player->pos.y + PLAYER_HEIGHT + 1);
  // game over
  if (player->health <= 0) {
    game->defeat = true;
  }
  if (IsKeyDown(KEY_A) && !IsKeyDown(KEY_D)) {
    move = -3.0;
    player->dir = -1;
    player->moving = true;
  }
  if (IsKeyDown(KEY_D) && !IsKeyDown(KEY_A)) {
    move = 3.0;
    player->dir = 1;
    player->moving = true;
  }
  if (IsKeyDown(KEY_S)) {
    sneaking = true;
  }
  if (IsKeyDown(KEY_S) && IsKeyDown(KEY_SPACE)) {
    if (
      (left_ground_tile == NULL || !tile_solid(left_ground_tile)) &&
      (right_ground_tile == NULL || !tile_solid(right_ground_tile))
    ) {
      go_down = true;
    }
  }
  if (IsKeyPressed(KEY_SPACE)) {
    if (player->on_ground && !player->sneaking && !go_down) {
      player->velocity = 13.0;
    }
  }
  if (IsKeyDown(KEY_ENTER)) {
    player->shooting = true;
  }
  if (IsKeyPressed(KEY_W)) {
    holding = true;
  }
  // detect on ground
  bool on_ground = false;
  if (!(player->velocity > 0.0)) {
    if (!go_down) {
      for (int i = 0; i < game->tiles_len; i++) {
        tile_t *tile = game->tiles[i];
        if (tile_ground(tile)) {
          if (
            player->pos.x + PLAYER_WIDTH > tile->pos.x &&
            player->pos.x < tile->pos.x + TILE_WIDTH
          ) {
            float tolerance = 4.0;
            if (player->gravity > tolerance) {
              tolerance = player->gravity;
            }
            if (fabs(player->pos.y + PLAYER_HEIGHT - tile->pos.y) < tolerance) {
              player->pos.y = tile->pos.y - PLAYER_HEIGHT;
              on_ground = true;
              if (player->fall_height >= 1.5 * TILE_HEIGHT) {
                effect_play(player->pos, EFFECT_FALL, game);
              }
              // fall damage
              if (player->fall_height >= 6 * TILE_HEIGHT) {
                if (player->damage_timer == 0) {
                  if (player->health > 0) {
                    player->health--;
                    player->damage_timer = 80;
                    player->velocity = 6.0;
                    if (player->held_enemy != NULL) {
                      player->held_enemy = NULL;
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    for (int i = 0; i < game->entities_len; i++) {
      if (game->entities[i].type == ENTITY_ENEMY) {
        enemy_t *enemy = game->entities[i].enemy;
        if (enemy->frozen) {
          if (
            player->pos.x + PLAYER_WIDTH > enemy->pos.x &&
            player->pos.x < enemy->pos.x + ENEMY_WIDTH
          ) {
            if (fabs(player->pos.y + PLAYER_HEIGHT - enemy->pos.y) < 4.0) {
              player->pos.y = enemy->pos.y - PLAYER_HEIGHT;
              on_ground = true;
              if (player->sneaking && player->fall_height >= 2 * TILE_HEIGHT) {
                effect_play(enemy->pos, EFFECT_BREAK, game);
                effect_play(enemy->pos, EFFECT_PARTICLE, game);
                enemy_kill(enemy, game);
                i--;
                player->velocity = 6.0;
              }
            }
          }
        }
      }
    }
  }
  // detect ceiling
  if (player->velocity > 0.0) {
    for (int i = 0; i < game->tiles_len; i++) {
      tile_t *tile = game->tiles[i];
      if (tile_solid(tile)) {
        if (
          player->pos.x + PLAYER_WIDTH - 0.2 * TILE_WIDTH > tile->pos.x &&
          player->pos.x + 0.2 * TILE_WIDTH < tile->pos.x + TILE_WIDTH
        ) {
          float tolerance = 4.0;
          if (player->velocity > tolerance) {
            tolerance = player->velocity;
          }
          if (fabs(player->pos.y - (tile->pos.y + 0.2 * TILE_HEIGHT)) < tolerance) {
            player->pos.y = tile->pos.y + 0.2 * TILE_HEIGHT;
            player->velocity = 0.0;
          }
        }
      }
    }
  }
  // detect wall
  if (fabs(move) > 0.0) {
    for (int i = 0; i < game->tiles_len; i++) {
      tile_t *tile = game->tiles[i];
      if (tile_wall(tile)) {
        if (
          player->pos.y + PLAYER_HEIGHT > tile->pos.y &&
          player->pos.y < tile->pos.y + TILE_HEIGHT
        ) {
          float tolerance = 4.0;
          if (move > tolerance) {
            tolerance = player->velocity;
          }
          if (player->dir > 0) {
            if (fabs((player->pos.x + PLAYER_WIDTH - 0.2 * TILE_WIDTH) - tile->pos.x) < tolerance) {
              player->pos.x = tile->pos.x - (PLAYER_WIDTH - 0.2 * TILE_WIDTH);
            }
          }
          else {
            if (fabs((player->pos.x + 0.2 * TILE_WIDTH) - (tile->pos.x + TILE_WIDTH)) < tolerance) {
              player->pos.x = tile->pos.x + TILE_WIDTH - 0.2 * TILE_WIDTH;
            }
          }
        }
      }
    }
  }
  player->on_ground = on_ground;
  player->sneaking = sneaking;
  // handle move
  if (player->sneaking || player->shooting) {
    player->pos.x += 0.5 * move;
  }
  else {
    player->pos.x += move;
  }
  // handle map border
  if (player->pos.x < 0.0) {
    player->pos.x = 0.0;
  }
  if (player->pos.x + PLAYER_WIDTH > TILE_WIDTH * game->level->width) {
    player->pos.x = TILE_WIDTH * game->level->width - PLAYER_WIDTH;
  }
  // handle jump
  if (player->velocity > 0.0) {
    player->pos.y -= player->velocity;
    player->velocity -= 1.1;
  }
  if (player->on_ground) {
    player->gravity = 0.0;
    player->fall_height = 0.0;
  }
  else {
    player->pos.y += player->gravity;
    player->fall_height += player->gravity;
    if (player->gravity < PLAYER_GRAVITY) {
      player->gravity += 0.2;
    }
    else {
      player->gravity = PLAYER_GRAVITY;
    }
  }
  if (player->damage_timer > 0) {
    player->damage_timer--;
  }
  // detect shooting
  if (player->shooting) {
    // determine target
    float shooting_range = 2 * TILE_WIDTH;
    rect_t shooting_rect = {
      .x = player->dir > 0 ? player->pos.x + PLAYER_WIDTH : player->pos.x - shooting_range,
      .y = player->sneaking ? player->pos.y + 8 * SCALE : player->pos.y + 7 * SCALE,
      .width = shooting_range,
      .height = SCALE,
    };
    float closest_distance = shooting_range;
    entity_t *closest_entity = NULL;
    bool target_in_range = false;
    for (int i = 0; i < game->entities_len; i++) {
      entity_t *entity = &game->entities[i];
      // ignore held enemies
      if (entity->type == ENTITY_ENEMY && player->held_enemy == entity->enemy) {
        continue;
      }
      // ignore frozen gates
      if (entity->type == ENTITY_GATE && entity->gate->frozen) {
        continue;
      }
      // ignore non-bad entities
      if (!entity->is_bad) {
        continue;
      }
      // ignore bosses
      if (entity->type == ENTITY_BOSS) {
        continue;
      }
      rect_t entity_rect = entity_get_rect(entity);
      if (rect_collide(shooting_rect, entity_rect)) {
        float entity_x = entity_rect.x + entity_rect.width / 2.0;
        float entity_distance = fabs(entity_x - (player->dir > 0 ? player->pos.x + PLAYER_WIDTH : player->pos.x));
        if (player->target_entity != NULL && player->target_entity == entity) {
          target_in_range = true;
          // if target entity set distance to that
          player->shooting_distance = entity_distance;
        }
        if (entity_distance < closest_distance) {
          closest_distance = entity_distance;
          closest_entity = entity;
        }
      }
    }
    if (closest_entity != NULL) {
      if (!target_in_range) {
        // if no target entity use closest distance
        player->shooting_distance = closest_distance;
        player->target_entity = closest_entity;
      }
    }
    else {
      player->target_entity = NULL;
      player->shooting_distance = PLAYER_SHOOTING_RANGE;
    }
    // detect shooting wall/solid
    for (int i = 0; i < game->tiles_len; i++) {
      tile_t *tile = game->tiles[i];
      if (tile_wall(tile) || tile_solid(tile)) {
        rect_t tile_rect = { tile->pos.x, tile->pos.y, TILE_WIDTH, TILE_HEIGHT };
        if (rect_collide(shooting_rect, tile_rect)) {
          if (player->dir > 0) {
            // clear target if it stands behind a wall
            if (player->target_entity == NULL) {
              player->shooting_distance = tile->pos.x - (player->pos.x + PLAYER_WIDTH);
            }
            else {
              rect_t rect = entity_get_rect(player->target_entity);
              if (rect.x > tile->pos.x) {
                player->target_entity = NULL;
                player->shooting_distance = tile->pos.x - (player->pos.x + PLAYER_WIDTH);
              }
            }
          }
          else {
            // clear target if it stands behind a wall
            if (player->target_entity == NULL) {
              player->shooting_distance = player->pos.x - (tile->pos.x + TILE_WIDTH);
            }
            else {
              rect_t rect = entity_get_rect(player->target_entity);
              if (rect.x + rect.width < tile->pos.x + TILE_WIDTH) {
                player->target_entity = NULL;
                player->shooting_distance = player->pos.x - (tile->pos.x + TILE_WIDTH);
              }
            }
          }
        }
      }
    }
  }
  else {
    player->target_entity = NULL;
    player->shooting_distance = PLAYER_SHOOTING_RANGE;
  }
  // detect holding
  if (holding) {
    bool picked_up = false;
    for (int i = 0; i < game->entities_len; i++) {
      if (game->entities[i].type == ENTITY_ENEMY) {
        enemy_t *enemy = game->entities[i].enemy;
        if (enemy->frozen) {
          float tolerance = 0.0;
          if (
            player->pos.x <= enemy->pos.x + ENEMY_WIDTH - tolerance &&
            player->pos.x + PLAYER_WIDTH >= enemy->pos.x + tolerance &&
            player->pos.y <= enemy->pos.y + ENEMY_HEIGHT - tolerance &&
            player->pos.y + PLAYER_HEIGHT >= enemy->pos.y + tolerance 
          ) {
            if (player->held_enemy == NULL) {
              player->held_enemy = enemy;
              picked_up = true;
            }
          }
        }
      }
    }
    if (!picked_up && player->held_enemy != NULL) {
      player->held_enemy = NULL;
    }
  }
  if (player->held_enemy != NULL) {
    player->held_enemy->pos = (pos_t) {
      .x = player->pos.x,
      .y = player->pos.y - (player->sneaking ? 0.3 : 0.5) * TILE_HEIGHT,
    };
  }
  // detect door entering
  if (holding) {
    rect_t player_rect = { player->pos.x, player->pos.y, PLAYER_WIDTH, PLAYER_HEIGHT };
    for (int i = 0; i < game->entities_len; i++) {
      if (game->entities[i].type == ENTITY_DOOR) {
        door_t *door = game->entities[i].door;
        rect_t door_rect = entity_get_rect(&game->entities[i]);
        if (rect_collide(player_rect, door_rect)) {
          if (door_unlock(door, game)) {
            game->door = door;
            game->victory = true;
          }
          else {
            PlaySound(game->assets.locked);
          }
        }
      }
    }
  }
  // detect damage
  rect_t player_rect = {
    .x = player->pos.x,
    .y = player->pos.y,
    .width = PLAYER_WIDTH,
    .height = PLAYER_HEIGHT,
  };
  for (int i = 0; i < game->entities_len; i++) {
    entity_t *entity = &game->entities[i];
    if (entity->is_bad) {
      // ignore gates
      if (entity->type == ENTITY_GATE) {
        continue;
      }
      // ignore frozen enemies
      if (entity->type == ENTITY_ENEMY && entity->enemy->frozen) {
        continue;
      }
      rect_t entity_rect = entity_get_rect(entity);
      if (rect_collide_threshold(entity_rect, player_rect, PLAYER_WIDTH / 4.0)) {
        if (player->damage_timer == 0) {
          if (player->health > 0) {
            player->health--;
            player->damage_timer = 80;
            player->velocity = 8.0;
            if (player->pos.x + PLAYER_WIDTH / 2.0 > entity_rect.x + entity_rect.width / 2.0) {
              player->pos.x += 20.0;
            }
            else {
              player->pos.x -= 20.0;
            }
            if (player->held_enemy != NULL) {
              player->held_enemy = NULL;
            }
          }
        }
      }
    }
  }
  // fall out of map
  if (player->pos.y > TILE_HEIGHT * game->level->height) {
    if (player->health > 0) {
      player->health = 0;
    }
  }
  // update timer
  if (player->on_ground && player->moving) {
    fz_timer_update(player->timer_walking);
    fz_timer_update(player->timer_sneaking);
  }
  else {
    fz_timer_reset(player->timer_walking);
    fz_timer_reset(player->timer_sneaking);
  }
}

void player_draw(player_t *player, game_t *game) {
  pos_t pos = pos_snap(player->pos);
  int frame = (player->sneaking || player->shooting)
    ? fz_timer_get(player->timer_sneaking)
    : fz_timer_get(player->timer_walking);
  if (player->damage_timer / 4 % 2 == 0) {
    if (player->sneaking) {
      if (player->dir > 0) {
        DrawTextureRec(game->assets.entities, texture_rect(frame, 14, PLAYER_WIDTH, PLAYER_HEIGHT), pos, WHITE);
      }
      else {
        DrawTextureRec(game->assets.entities, texture_rect(frame, 15, PLAYER_WIDTH, PLAYER_HEIGHT), pos, WHITE);
      }
    }
    else {
      if (player->dir > 0) {
        DrawTextureRec(game->assets.entities, texture_rect(frame, 12, PLAYER_WIDTH, PLAYER_HEIGHT), pos, WHITE);
      }
      else {
        DrawTextureRec(game->assets.entities, texture_rect(frame, 13, PLAYER_WIDTH, PLAYER_HEIGHT), pos, WHITE);
      }
    }
    if (player->shooting) {
      float y = player->sneaking ? pos.y + 8 * SCALE : pos.y + 7 * SCALE;
      if (player->dir > 0) {
        DrawRectangle(pos.x + PLAYER_WIDTH, y, player->shooting_distance, 1 * SCALE, BLUE);
      }
      else {
        DrawRectangle(pos.x - player->shooting_distance, y, player->shooting_distance, 1 * SCALE, BLUE);
      }
    }
  }
}

void player_free(player_t *player) {
  fz_timer_free(player->timer_walking);
  fz_timer_free(player->timer_sneaking);
  free(player);
}
