#include <TVout.h>
#include <TVoutfonts/fontALL.h>
#include "constants.h"
#include "utils.h"
#include "logo.h"
#include "room_image.h"
#include "dark_scenary.h"
#include "marco_icon.h"
#include "polo_icon.h"

TVout tv;

// Game
int current_game_state = 0;
char game_name[] = "Neutron games";
bool show_sinalization_found = false;
int sinalization_found_time = 0;

int eyeX = 0;
int eyeY = 0;
int maxHeight = 0;
int maxWidth = 0;

// Marco polo
int marco_polo_timer = 0;
int marco_polo_used = MARCO_POLO_CHANCES;
bool showing_marco_polo = false;

// Timing
int timer = GAME_DURATION;
int one_second = 1000;
int milli_seconds = 0;

// Positions
int possible_positions[5][4] = {
  // Is enabled?    X  Y  Id
  { ENEMY_DISABLED, 0, 0, 0 },
  { ENEMY_DISABLED, 0, 0, 0 },
  { ENEMY_DISABLED, 0, 0, 0 },
  { ENEMY_DISABLED, 0, 0, 0 },
  { ENEMY_DISABLED, 0, 0, 0 },
};

int ALL_CONTROLS[5] = {
  RIGHT_MOVEMENT,
  LEFT_MOVEMENT,
  UP_MOVEMENT,
  DOWN_MOVEMENT,
  ACTION
};

// Collision controls
bool colliding = false;
int enemy_id_collided = 0;
int enemies_found = 0;

void setup() {
  randomSeed(analogRead(A0));

  configure_buttons();
  configure_libraries();
  configure_debugger();
  configure_screen();

  // Starts the eye in the center
  eyeX = maxWidth / 2;
  eyeY = maxHeight - 10;

  generate_enemies_positions();

  start_screen();
  // win_screen();
  // game_over_screen();
}

// OBS: don't use Serial.print inside a loop, for some reason its breaks
void loop() {
  if (current_game_state == WIN)
  {
    win_screen();
    return;
  }

  if (current_game_state == LOST)
  {
    game_over_screen();
    return;
  }

  tv.delay_frame(1);
  tv.clear_screen();

  draw_game_delimiters();

  control_game_time();
  draw_all_enemies();
  draw_eyes();

  // if (!has_pressed_any_button())
  //   return;
  
  show_debug_info();

  verify_if_has_collided_with_enemy();
  activate_marco_polo();

  if (show_sinalization_found) {
    show_sinalization();
  }
}

void configure_buttons() {
  pinMode(RIGHT_MOVEMENT, INPUT_PULLUP);
  pinMode(LEFT_MOVEMENT, INPUT_PULLUP);
  pinMode(UP_MOVEMENT, INPUT_PULLUP);
  pinMode(DOWN_MOVEMENT, INPUT_PULLUP);
  pinMode(ACTION, INPUT_PULLUP);
}

void configure_libraries() {
  tv.begin(NTSC, SCREEN_WIDTH, SCREEN_HEIGHT);
}

void configure_screen() {
  maxWidth = tv.hres() - 10;
  maxHeight = tv.vres() - 3;
}

void configure_debugger() {
  Serial.begin(9650);
}

void draw_game_delimiters() {
  tv.bitmap((tv.hres() - dark_scenary[0]) - 10, (tv.vres() - dark_scenary[1]) / 2, dark_scenary);
}

void start_screen() {
  current_game_state = IN_START_SCREEN;

  bool pressed = false;
  int center_point = 0;

  draw_game_delimiters();
  center_point = x_position_to_center(game_name, maxWidth);

  show_text(tv, 30, 30, game_name, font4x6);
  // tv.bitmap(3, 20, logo);
  show_text(tv, 10, maxHeight - 20, "By Astha", font4x6);
  show_text(tv, maxWidth - 40, maxHeight - 20, "FATEC AM", font4x6);

  do {
    pressed = has_pressed_any_button();
  } while (!pressed);

  tv.delay_frame(1);
  tv.clear_screen();

  current_game_state = PLAYING;

  scenery();
}

void game_over_screen() {
  bool pressed = false;
  char you_lose[] = "You lost";

  draw_game_delimiters();

  show_text(tv, 20, 10, you_lose);
  show_text(tv, 40, 35, ":)");
  show_text(tv, 25, maxHeight - 35, "Press any key", font4x6);
  show_text(tv, 25, maxHeight - 25, "to play again", font4x6);

  do {
    pressed = has_pressed_any_button();
  } while (!pressed);

  tv.delay_frame(1);
  tv.clear_screen();

  reset_game();
  scenery();
}

void win_screen() {
  bool pressed = false;

  char win_text[] = "You win!";
  char press_text[] = "Press any key";
  char play_text[] = "to play again";

  draw_game_delimiters();

  show_text(tv, 20, 10, win_text);
  show_text(tv, 40, 35, ":(");
  show_text(tv, 25, maxHeight - 35, "Press any key", font4x6);
  show_text(tv, 25, maxHeight - 25, "to play again", font4x6);

  do {
    pressed = has_pressed_any_button();
  } while (!pressed);

  tv.delay_frame(1);
  tv.clear_screen();

  reset_game();
  scenery();
}

void activate_marco_polo() {
  // If is showing the indicator and already used all marco polos, we need to await and continue showing
  // the indicators
  if (marco_polo_used == 0 && !showing_marco_polo)
    return;

  if (showing_marco_polo)
    draw_marco_polo_indicators();

  if (digitalRead(ACTION) == LOW)
    marco_polo_timer++;

  if (digitalRead(ACTION) == HIGH) {
    reset_marco_polo();
    return;
  }

  // Controls for how long the marco polo indicators (?, !) will be displayed
  if (showing_marco_polo && ((marco_polo_timer - TIME_MARCO_POLO) >= MARCO_POLO_SHOWING_TIME)) {
    // Serial.println("Reset marco polo");
    reset_marco_polo();
    return;
  }

  if (!showing_marco_polo && marco_polo_timer >= TIME_MARCO_POLO)
    marco_polo();
}

void draw_eyes() {
  // Left
  tv.draw_rect(eyeX, eyeY, 3, 4, WHITE);
  // Right
  tv.draw_rect(eyeX + 5, eyeY, 3, 4, WHITE);

  if (eyeX < (maxWidth - EYE_WIDTH - 7) && digitalRead(RIGHT_MOVEMENT) == LOW) {
    // Left iris
    tv.draw_rect(eyeX + 1, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX + 2, eyeY + 1, 1, 2, BLACK);

    // Right iris
    tv.draw_rect(eyeX + 6, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX + 7, eyeY + 1, 1, 2, BLACK);
    eyeX++;
  }

  if (eyeX > 7 && digitalRead(LEFT_MOVEMENT) == LOW) {
    // Left iris
    tv.draw_rect(eyeX + 1, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX, eyeY + 1, 1, 2, BLACK);

    // Right iris
    tv.draw_rect(eyeX + 6, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX + 5, eyeY + 1, 1, 2, BLACK);
    eyeX--;
  }

  if (eyeY > 17 && digitalRead(UP_MOVEMENT) == LOW) {
    // Left iris
    tv.draw_rect(eyeX + 1, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX + 1, eyeY, 1, 2, BLACK);

    // Right iris
    tv.draw_rect(eyeX + 6, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX + 6, eyeY, 1, 2, BLACK);
    eyeY--;
  }

  if (eyeY < (maxHeight - EYE_HEIGHT - 8) && digitalRead(DOWN_MOVEMENT) == LOW) {
    // Left iris
    tv.draw_rect(eyeX + 1, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX + 1, eyeY + 2, 1, 2, BLACK);

    // Right iris
    tv.draw_rect(eyeX + 6, eyeY + 1, 1, 2, WHITE);
    tv.draw_rect(eyeX + 6, eyeY + 2, 1, 2, BLACK);
    eyeY++;
  }
}

void show_debug_info() {
  tv.select_font(font4x6);

  // Show the current X value of the player
  tv.print(2, 5, eyeX);
  // Show the current Y value of the player
  tv.print(2, 15, eyeY);

  // Show the number of marco polo used
  tv.print(maxWidth - 12, 5, marco_polo_used);

  // Show the number of marco polo used
  tv.print(maxWidth - 12, maxHeight - 15, enemies_found);
}

void draw_all_enemies() {
  for (auto &positions : possible_positions)
  {
    int enabled = positions[0];

    if (enabled == ENEMY_DISABLED)
      continue;

    int x = positions[1];
    int y = positions[2];
    int id = positions[3];

    // In the real game the enemies will not be displayed
    // draw_enemy(x, y);
  }
}

void draw_enemy(int x, int y) {
  tv.draw_rect(x, y, ENEMY_WIDTH, ENEMY_HEIGHT, WHITE);
}


bool has_pressed_any_button() {
  int controls_length = (sizeof(ALL_CONTROLS) / sizeof(ALL_CONTROLS[0]));

  bool pressed = false;

  for (int index = 0; index < controls_length; index++) {
    if (digitalRead(ALL_CONTROLS[index]) == LOW) {
      pressed = true;
    }
  }

  return pressed;
}

void verify_if_has_collided_with_enemy() {
  if (digitalRead(ACTION) != LOW)
    return;

  for (auto &positions : possible_positions)
  {
    bool disable = positions[0] == ENEMY_DISABLED;

    if (disable) continue;

    int x = positions[1];
    int y = positions[2];
    int id = positions[3];

    collision_confirm(x, y, id);

    if (colliding) break;
  }

  if (!colliding)
    return;

  // if (enemy_id_collided <= 0)
  //   Serial.println("Id invalid!");

  show_sinalization_found = true;

  disable_enemy(enemy_id_collided);
  enemies_found++;

  bool find_all = found_all_enemies();

  if (find_all)
    current_game_state = WIN;
}

void collision_confirm(int enemy_x, int enemy_y, int enemy_id) {
  colliding = (eyeX + EYE_WIDTH >= enemy_x && eyeX <= enemy_x + ENEMY_WIDTH)
              && (eyeY + EYE_HEIGHT >= enemy_y && eyeY <= enemy_y + ENEMY_HEIGHT);

  if (colliding)
  {
    enemy_id_collided = enemy_id;
    return;
  }

  enemy_id_collided = 0;
}

void control_game_time() {
  int seconds = millis() - milli_seconds;
  tv.print(50, 5, timer);

  if (seconds >= one_second)
  {
    timer -= 1;
    milli_seconds = millis();
  }

  if (timer == 0)
  {
    current_game_state = LOST;
  }
}

void scenery() {
  tv.bitmap((tv.hres() - room_image[0]) - 10, (tv.vres() - room_image[1]) / 2, room_image);
  tv.delay_frame(75);
  // tv.clear_screen();
}

void marco_polo() {
  marco_polo_used--;
  showing_marco_polo = true;
  draw_marco_polo_indicators();
}

void draw_marco_polo_indicators() {
  // show_text(tv, eyeX + 3, eyeY - 6, "?", font4x6);
  tv.bitmap(eyeX - 4, eyeY - 15, marco_icon);

  for (auto &positions : possible_positions)
  {
    bool disable = positions[0] == ENEMY_DISABLED;

    if (disable) continue;

    int x = positions[1];
    int y = positions[2];

    // show_text(tv, x, y - 2, "!", font4x6);
    tv.bitmap(x - 5, y, polo_icon);
  }
}

void reset_marco_polo() {
  marco_polo_timer = 0;
  showing_marco_polo = false;
}

void disable_enemy(int enemy_id_to_disable) {
  for (auto &positions : possible_positions)
  {
    int id = positions[3];

    if (id == enemy_id_to_disable)
    {
      positions[0] = ENEMY_DISABLED;
      break;
    }
  }
}

bool found_all_enemies() {
  int internal_enemies_found_quantity = 0;
  int quantity_of_enemies = sizeof(possible_positions) / sizeof(possible_positions[0]);

  for (auto &positions : possible_positions)
  {
    if (positions[0] == ENEMY_DISABLED)
      internal_enemies_found_quantity++;
  }

  bool found_all = internal_enemies_found_quantity == quantity_of_enemies;

  return found_all;
}

void reset_game() {
  current_game_state = PLAYING;

  eyeY = maxWidth / 2;
  eyeX = maxHeight / 2;

  reset_marco_polo();
  marco_polo_used = MARCO_POLO_CHANCES;

  timer = GAME_DURATION;
  milli_seconds = 0;

  generate_enemies_positions();

  colliding = false;
  enemy_id_collided = 0;
  enemies_found = 0;

  show_sinalization_found = false;
  sinalization_found_time = 0;
}

int get_random_x_coordinate() {
  // randomSeed(analogRead(A0));
  return random(20, maxWidth - 20);
}

int get_random_y_coordinate() {
  // randomSeed(analogRead(A0));
  return random(20, maxHeight - 20);
}

void generate_enemies_positions() {
  int previous_id = 0;

  for (auto &positions : possible_positions)
  {
    previous_id++;

    positions[0] = ENEMY_ENABLED;
    positions[1] = get_random_x_coordinate();
    positions[2] = get_random_y_coordinate();
    positions[3] = previous_id;
  }
}

void show_sinalization() {
  sinalization_found_time++;

  if (sinalization_found_time >= DURATION_TO_SHOW_FOUND_SINALIZATION) {
    show_sinalization_found = false;
    sinalization_found_time = 0;
    return;
  }

  show_text(tv, eyeX + 2, eyeY - 10, ":)", font4x6);
}