#include "snake.h"

int main(int argc, char *args[]) {

  /* command argument handling */

  if (argc > 1) {
    if (argc > 2) {
      printf("Too many parameters\n");
      goto help_info;
    }

    if (!strcmp(args[1], "-h") || !strcmp(args[1], "--help")) {

      printf("This is a small TUI snake game.\n\n");
      printf("Controls: Arrow keys/WASD to control the snake.\n");
      printf("          P to pause.\n");
      printf("          Q to quit.\n");

      return 0;
    }

    printf("Argument \"%s\" not recognized\n", args[1]);

  help_info:
    printf("Try \"-h\" or \"--help\" for more information\n");
    return 1;
  }

  /* Setting up terminal with termios */

  struct termios old, new;

  tcgetattr(STDIN_FILENO, &old);

  new = old;

  new.c_lflag &= ~(ECHO | ICANON);
  new.c_cc[VMIN] = 0;
  new.c_cc[VTIME] = 0;

  tcsetattr(STDIN_FILENO, TCSANOW, &new);

  srand(time(NULL));

  printf("\033[?25l");

  struct winsize init_size, current_size;
  ioctl(STDOUT_FILENO, TIOCGWINSZ, &init_size);
  current_size = init_size;
  bool too_small = false;


  struct timespec mili100 = {0, 1000000 * 100};


  /* Animation at the start */
  if (!start_animation())
    goto end_of_program;

  /* Menu */

  bool menu = true;
  bool setting = false;
  bool change_menu = true;

  main_menu selected_menu = NEW_GAME;
  settings selected_setting = HEIGHT_S;

  speed game_speed = AVERAGE;

  selected game_size = {WIDTH, HEIGHT};

  while (menu) {

    bool pressed_enter = false;
    bool changed_selected = false;

    direction arrow = NO_DIRECTION;

    char string[STRING_LEN] = {};
    ssize_t length_string = read(STDIN_FILENO, string, sizeof(string) - 1);


    ioctl(STDOUT_FILENO, TIOCGWINSZ, &current_size);

    if (current_size.ws_col != init_size.ws_col ||
        current_size.ws_row != init_size.ws_row) {
      init_size = current_size;
      changed_selected = true;
    }


    if (length_string > 0) {
      string[length_string] = '\0';

      arrow = read_direction(string, false);

      if (string[0] == '\n')
        pressed_enter = true;
      
      if (toupper(string[0]) == 'Q')
        goto end_of_program;
    }

    if (!setting) {
      
      if (!too_small) {
        switch (arrow) {
          case UP:
            selected_menu = (selected_menu + (MAIN_MENU - 1)) % MAIN_MENU;
            changed_selected = true;
            break;
          case DOWN:
            selected_menu = (selected_menu + 1) % MAIN_MENU;
            changed_selected = true;
            break;
          default:
            break;
        }
      }

      if (changed_selected || change_menu) {
        too_small = draw_menu(selected_menu, init_size);
        change_menu = false;
      }

      if (pressed_enter && !too_small) {
        switch (selected_menu) {
        case NEW_GAME:
          menu = false;
          break;
        case SETTINGS:
          setting = true;
          change_menu = true;
          break;
        default:
          tcsetattr(STDIN_FILENO, TCSANOW, &old);
          printf("\033[?25h\n\n");


          printf("\033[32m");
          return 0;
          break;
        }
      }
    } else {

      char *setting_string[] = {
        "  HEIGHT  ",
        "  WIDTH   ",
        "  SPEED   ",
        "   BACK   "
      };

      char *speed_char[] = {
        "  VERY FAST  ",
        "    FAST     ",
        "   AVERAGE   ",
        "    SLOW     ",
        "  VERY SLOW  "
      };

      uint8_t width = (uint8_t) strlen(setting_string[0]);
      uint8_t width_setting = (uint8_t) strlen(speed_char[0]);


      if (changed_selected || change_menu) {
              
        uint8_t i, j;

        printf("\033[2J\033[H");
      
         
        for (i = 0; i < SETTINGS_MENU; i++) {
      
          printf("+");
          for (j = 0; j < width; j++)
            printf("-");
          printf("+");

          if (i != BACK_S) {
            printf ("  +");

            for (j = 0; j < width_setting; j++)
              printf("-");
            printf("+");
          }

          printf("\n|");

          if (i == selected_setting)
            printf("\033[32m");

          printf("%s\033[0m", setting_string[i]);

          printf("|");

          if (i != BACK_S) {
            printf("  |");
            if (selected_setting == i)
              printf("\033[32m");

            switch (i) {
              case SPEED_S:
                printf("%s", speed_char[game_speed]);
                break;

              case WIDTH_S:
              case HEIGHT_S:
              for (j = 0; j < (width_setting - 3) >> 1; j++)
                printf(" ");
              printf("%3d", (i == WIDTH_S) ? game_size.x : game_size.y);
              for (j = 0; j < width_setting - ((width_setting - 3) >> 1) - 3; j++)
                printf(" ");

            }
            printf("\033[0m");
            printf("|");
          }

          printf("\n");

          if (i != HEIGHT_S) {
          
            printf("+");
            for (j = 0; j < width; j++)
              printf("-");
            printf("+");

            if (i != BACK_S) {
              printf ("  +");

              for (j = 0; j < width_setting; j++)
                printf("-");
              printf("+");
            }

            if (i < SETTINGS_MENU - 1)
              printf("\n");
          }
        }
        fflush(stdout);

        change_menu = false;
      }


      if (arrow != NO_DIRECTION) {

        switch (arrow) {
          case UP:
            selected_setting = (selected_setting + (SETTINGS_MENU - 1)) % SETTINGS_MENU;
            break;
          case DOWN:
            selected_setting = (selected_setting + 1) % SETTINGS_MENU;
            break;
          case LEFT:
            switch (selected_setting) {
              case SPEED_S:
                if (game_speed < SPEED_L - 1)
                  game_speed++;
                break;
              case WIDTH_S:
                if (game_size.x > MIN_SIZE)
                  game_size.x--;
                break;
              case HEIGHT_S:
                if (game_size.y > MIN_SIZE)
                  game_size.y--;
                break;
              default:
                break;
            }
            break;
          case RIGHT:
            switch (selected_setting) {
              case SPEED_S:
                if (game_speed)
                  game_speed--;
                break;
              case WIDTH_S:
                if (game_size.x < MAX_SIZE)
                  game_size.x++;
                break;
              case HEIGHT_S:
                if (game_size.y < MAX_SIZE)
                  game_size.y++;
                break;
              default:
                break;
            }
            break;
          default:
            break;
        }
        change_menu = true;

      }

      if (pressed_enter) {
        switch (selected_setting) {
          case BACK_S:
            setting = false;
            break;
          default:
            break;
        }
        change_menu = true;
      }
      pressed_enter = false;
    }

  nanosleep(&mili100, NULL);
  }

  /* initialisation of the game */

  snake my_snake;
  my_snake.length = STARTING_LENGTH;

  my_snake.position = malloc((my_snake.length + 1) * sizeof(snake_part));

  direction shift = RIGHT;
  direction tmp_direction = shift;

  {
    uint16_t i;

    my_snake.position[0].coords.y = game_size.y >> 1;
    my_snake.position[0].coords.x = game_size.x >> 1;
    my_snake.position[0].directions[0] = shift;
    my_snake.position[0].directions[1] = shift;
    for (i = 1; i <= my_snake.length; i++) {
      my_snake.position[i].coords.y = -1;
      my_snake.position[i].coords.x = -1;
    }
  }

  board_state **board = malloc(game_size.y * sizeof(board_state*));
  board_state *values = calloc(game_size.y * game_size.x, sizeof(board_state));

  if(board == NULL || values == NULL)
    goto end_of_program;

  {
    uint8_t i;

    for (i = 0; i < game_size.y; i++)
      board[i] = values + i * game_size.x;
  }

  update_field(my_snake, game_size, board, true);

  selected fruit = new_fruit(game_size, board);

  bool paused = false;

  if ((too_small = draw_board(game_size, board, current_size, true, false)))
    paused = true;


  bool run = true;

  struct timespec milispeed = {0, 1000000 * (SPEED + game_speed * 25)};

  /* start of the actual game loop */

  while (run) {

    /* input handling  */

    
      char string[STRING_LEN] = {};
      ssize_t length_string = read(STDIN_FILENO, string, sizeof(string) - 1);

      if (length_string > 0) {
        string[length_string] = '\0';

        if (!too_small) {
          if (toupper(string[0]) == 'P' || string[0] == '\n') {
            paused = !paused;
            if (paused)
              draw_board(game_size, board, current_size, true, paused);
          }

          if (!paused)
            tmp_direction = read_direction(string, true);
        }

        if (toupper(string[0]) == 'Q')
          run = false;
      }
    

    /* game logic */

    /*
    struct timeval current;
    gettimeofday(&current, NULL);*/

    if (/*time_diff >= SPEED * 1000*/ true) {

      ioctl(STDOUT_FILENO, TIOCGWINSZ, &current_size);

      if (current_size.ws_col != init_size.ws_col ||
          current_size.ws_row != init_size.ws_row) {
        init_size = current_size;

        paused = true;
        too_small = draw_board(game_size, board, current_size, true, paused);
      }

      if (!paused) {

        if (tmp_direction != NO_DIRECTION &&
            ((shift + (DIRECTIONS >> 1)) % DIRECTIONS) != tmp_direction) {
          shift = tmp_direction;
        }

        uint8_t update = update_snake(game_size ,&my_snake, shift);

        uint8_t head_y = my_snake.position[0].coords.y;
        uint8_t head_x = my_snake.position[0].coords.x;

        
        update_field(my_snake, game_size, board, false);

        if (head_y == fruit.y && head_x == fruit.x) {
          my_snake.length++;
          snake_part *tmp = realloc(my_snake.position, (my_snake.length + 1) * sizeof(snake_part));
          if (tmp == NULL)
            goto end_free;
          my_snake.position = tmp;
          fruit = new_fruit(game_size, board);
        }

        draw_board(game_size, board, current_size, false, false);

        switch (update) {
          case 0: 
            printf("\nERROR\n");
            goto end_free;
            break;
          case 2:
            printf("\nHAHA LOST, LOSER");
            goto end_free;
            break;
          default:
            break;
        }

      }
    }
    nanosleep(&milispeed, NULL);
  }

end_free:

    free(my_snake.position);
    free(board);
    free(values);

end_of_program:

  tcsetattr(STDIN_FILENO, TCSANOW, &old);
  printf("\033[?25h\n\n");


  printf("\033[32m");
  return 0;
}


uint8_t update_snake (selected game_size, snake *my_snake, direction shift) {
  
  my_snake->position[0].directions[1] = my_snake->position[0].directions[0];
  my_snake->position[0].directions[0] = shift;

  uint16_t i;
  for (i = my_snake->length; i > 0; i--)
    my_snake->position[i] = my_snake->position[i - 1];

  switch (shift) {
  case UP:
    my_snake->position[0].coords.y =
        ((my_snake->position[0].coords.y + game_size.y - 1) % game_size.y);
    break;
  case DOWN:
    my_snake->position[0].coords.y = ((my_snake->position[0].coords.y + 1) % game_size.y);
    break;
  case LEFT:
    my_snake->position[0].coords.x =((my_snake->position[0].coords.x + game_size.x - 1) % game_size.x);
    break;
  case RIGHT:
    my_snake->position[0].coords.x = ((my_snake->position[0].coords.x + 1) % game_size.x);
    break;
  default:
    return 0;
    break;
  }

  uint8_t head_y = my_snake->position[0].coords.y;
  uint8_t head_x = my_snake->position[0].coords.x;

  for (i = 1; i < my_snake->length; i++) {
    int16_t x = my_snake->position[i].coords.x;
    int16_t y = my_snake->position[i].coords.y;

    if (x == head_x && y == head_y)
      return 2;
  }


  return 1;
}

void update_field(snake my_snake, selected game_size, board_state **board, bool init) {

  uint8_t i, j;

  board_state snake_bit[DIRECTIONS][DIRECTIONS] = {
    [UP][UP] = VERTICAL,
    [DOWN][DOWN] = VERTICAL,
    
    [RIGHT][RIGHT] = HORIZONTAL,
    [LEFT][LEFT] = HORIZONTAL,
    
    [DOWN][RIGHT] = BOTTOM_LEFT,
    [LEFT][UP] = BOTTOM_LEFT,
    
    [UP][RIGHT] = TOP_LEFT,
    [LEFT][DOWN] = TOP_LEFT,
    
    [DOWN][LEFT] = BOTTOM_RIGHT,
    [RIGHT][UP] = BOTTOM_RIGHT,
          
    [UP][LEFT] = TOP_RIGHT,
    [RIGHT][DOWN] = TOP_RIGHT
  };


  if (init) {
    for (i = 0; i < game_size.y; i++) {
      for (j = 0; j < game_size.x; j++) {
        board[i][j] = AIR;
      }
    }

    for (i = 1; i < my_snake.length; i++) {
      int16_t x = my_snake.position[i].coords.x;
      int16_t y = my_snake.position[i].coords.y;
      
      if (x >= 0 && y >= 0) {

        direction first = my_snake.position[i].directions[0];
        direction second = my_snake.position[i].directions[1];

        board[y][x] = snake_bit[first][second];
      }
    }
  } else {
    
    int16_t x = my_snake.position[my_snake.length].coords.x;
    int16_t y = my_snake.position[my_snake.length].coords.y;
    
    if (x != -1 && y != -1)
      board[y][x] = AIR;

    
    x = my_snake.position[1].coords.x;
    y = my_snake.position[1].coords.y;
  
    direction first = my_snake.position[1].directions[0];
    direction second = my_snake.position[1].directions[1];
    
    if (x != -1 && y != -1)
      board[y][x] = snake_bit[first][second];
  }


  int16_t x = my_snake.position[0].coords.x;
  int16_t y = my_snake.position[0].coords.y;

  if (my_snake.position[0].directions[0] == RIGHT) {
    board[y][x] = HEAD_1;
  } else {
    board[y][x] = HEAD_2;
  }
}

selected new_fruit(selected game_size, board_state **board) {
  bool found_space = false;

  uint8_t x;
  uint8_t y;

  while (!found_space) {
    x = rand() % game_size.x;
    y = rand() % game_size.y;

    if (board[y][x] == AIR)
      found_space = true;
  }

  selected fruit = {x, y};

  board[fruit.y][fruit.x] = FRUIT;

  return fruit;
}


direction read_direction(char *string, bool wasd_enabled) {

  if (strlen(string) >= 3) {
    if (string[0] == '\033') {
      if (string[1] == '[') {

        switch (string[2]) {
        case RIGHT_CODE:
          return RIGHT;
          break;
        case LEFT_CODE:
          return LEFT;
          break;
        case UP_CODE:
          return UP;
          break;
        case DOWN_CODE:
          return DOWN;
          break;
        }
      }
    }
  }

  if (wasd_enabled) {
    switch (toupper(string[0])) {
    case 'A':
      return LEFT;
      break;
    case 'D':
      return RIGHT;
      break;
    case 'W':
      return UP;
      break;
    case 'S':
      return DOWN;
      break;
    }
  }

  return NO_DIRECTION;
}
