#define _POSIX_C_SOURCE 199309L

#include <termios.h>
#include <unistd.h>

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include <sys/ioctl.h>

#include <time.h>

#include <ctype.h>
#include <string.h>

#define RIGHT_CODE 'C'
#define LEFT_CODE 'D'
#define UP_CODE 'A'
#define DOWN_CODE 'B'

#define WIDTH 20
#define HEIGHT 20

#define STRING_LEN 20

#define SPEED 100 /* in ms */

#define MAX_LENGTH 1000
#define STARTING_LENGTH 2

typedef enum { NEW_GAME, SETTINGS, EXIT, MAIN_MENU } main_menu;

typedef enum {
  NO_DIRECTION = -1,
  UP = 0,
  LEFT = 1,
  DOWN = 2,
  RIGHT = 3,
  DIRECTIONS = 4
} direction;

typedef struct selected {
  int16_t x;
  int16_t y;
} selected;

typedef struct snake_part {
  selected coords;
  direction directions[2];
} snake_part;

typedef struct snake {
  snake_part position[MAX_LENGTH];
  uint16_t length;
} snake;


uint8_t update_snake (snake *my_snake, direction shift);
void update_field(snake my_snake, char board[HEIGHT][WIDTH][STRING_LEN],
                  selected fruit);
selected new_fruit(char board[HEIGHT][WIDTH][STRING_LEN]);

bool start_animation();
bool draw_menu(main_menu menu, struct winsize window_size);
bool draw_board(char board[HEIGHT][WIDTH][STRING_LEN],
  struct winsize window_size, bool redraw, bool paused);
void draw_pause(struct winsize window_size, 
  uint8_t field_width,uint8_t field_height);

direction read_direction(char *string, bool wasd_enabled);

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 * SPEED};


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

  /* Menu */

  bool menu = true;

  main_menu selected_menu = NEW_GAME;

  too_small = draw_menu(selected_menu, init_size);

  while (menu) {

    bool pressed_enter = false;
    bool changed_selected = false;

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

    direction arrow = NO_DIRECTION;


    if (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;
        changed_selected = true;
      }
    }

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

      arrow = read_direction(string, false);

      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 (toupper(string[0]) == 'Q')
        goto end_of_program;

      if (string[0] == '\n')
        pressed_enter = true;
    }

    if (changed_selected)
      too_small = draw_menu(selected_menu, init_size);

    if (pressed_enter) {
      switch (selected_menu) {
      case NEW_GAME:
        menu = false;
        break;
      default:
        goto end_of_program;
        break;
      }
    }
    nanosleep(&mili100, NULL);
  }

  /* initialisation of the game */

  snake my_snake = {};
  my_snake.length = STARTING_LENGTH;

  direction shift = RIGHT;
  direction tmp_direction = shift;

  {
    uint16_t i;

    my_snake.position[0].coords.y = HEIGHT >> 1;
    my_snake.position[0].coords.x = WIDTH >> 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;
    }
  }

  selected fruit = {-1, -1};
  char board[HEIGHT][WIDTH][STRING_LEN] = {};

  update_field(my_snake, board, fruit);

  fruit = new_fruit(board);
  update_field(my_snake, board, fruit);

  bool paused = false;

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


  bool run = true;

  /* start of the actual game loop */

  while (run) {

    /* input handling  */

    if (!too_small) {
      char string[1000] = {};
      ssize_t length_string = read(STDIN_FILENO, string, sizeof(string) - 1);

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

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

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

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

    /* 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(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(&my_snake, shift);

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

        update_field(my_snake, board, fruit);

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

        if (head_y == fruit.y && head_x == fruit.x) {
          my_snake.length++;
          fruit = new_fruit(board);
        }

        update_field(my_snake, board, fruit);
        draw_board(board, current_size, false, false);
      }
    }
    nanosleep(&mili100, NULL);
  }

end_of_program:

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

  return 0;
}


uint8_t update_snake (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 + HEIGHT - 1) % HEIGHT);
    break;
  case DOWN:
    my_snake->position[0].coords.y = ((my_snake->position[0].coords.y + 1) % HEIGHT);
    break;
  case LEFT:
    my_snake->position[0].coords.x =((my_snake->position[0].coords.x + WIDTH - 1) % WIDTH);
    break;
  case RIGHT:
    my_snake->position[0].coords.x = ((my_snake->position[0].coords.x + 1) % WIDTH);
    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, char board[HEIGHT][WIDTH][STRING_LEN],
                  selected fruit) {

  uint8_t i, j;

  for (i = 0; i < HEIGHT; i++) {
    for (j = 0; j < WIDTH; j++) {
      strcpy(board[i][j], "  ");
    }
  }

  for (i = 0; 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) {
      if (!i) {
        if (my_snake.position[i].directions[0] == RIGHT) {
          strcpy(board[y][x], "\033[33m═\033[31mX\033[0m");
        } else {
          strcpy(board[y][x], "\033[31m X\033[0m");
        }
      } else {

        char *snake_bit[DIRECTIONS][DIRECTIONS] = {
          [UP][UP] = " ║",
          [DOWN][DOWN] = " ║",
          
          [RIGHT][RIGHT] = "══",
          [LEFT][LEFT] = "══",
          
          [DOWN][RIGHT] = "═╗",
          [LEFT][UP] = "═╗",
          
          [UP][RIGHT] = "═╝",
          [LEFT][DOWN] = "═╝",
          
          [DOWN][LEFT] = " ╔",
          [RIGHT][UP] = " ╔",
          
          [UP][LEFT] = " ╚",
          [RIGHT][DOWN] = " ╚"
        };

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

        snprintf(board[y][x], sizeof(board[y][x]),"\033[33m%s\033[0m",snake_bit[first][second]);
      }
    }
  }

  if (fruit.x >= 0 && fruit.y >= 0)
    strcpy(board[fruit.y][fruit.x], "\033[32m O\033[0m");
}

selected new_fruit(char board[HEIGHT][WIDTH][STRING_LEN]) {
  bool found_space = false;

  uint8_t x;
  uint8_t y;

  while (!found_space) {
    x = rand() % WIDTH;
    y = rand() % HEIGHT;

    if (!strcmp(board[y][x], "  "))
      found_space = true;
  }

  selected fruit = {x, y};

  return fruit;
}

bool start_animation() {  

  struct winsize current_size;
  ioctl(STDOUT_FILENO, TIOCGWINSZ, &current_size);

  struct timespec mili10 = {0, 100000 * SPEED};

  char *snake[3][7] = {
    {
      "           ________  ",
      "         _/ _      \\_",
      "________/  (_)       ",
      "\\       \\            ",
      "/_______/   _        ",
      "        \\_ (_)      _",
      "          \\________/ "
    },
    {
      "        ",
      "________",
      "\\      /",
      " \\    / ",
      "  \\  /  ",
      "___\\/___",
      "        ",
    },
    {
      "         ",
      "__        ",
      "  \\_____ ",
      "        \\",
      "   _____/",
      "__/      ",
      "         "
    }
  };

  uint8_t width_snake[3] = {};

  {
    uint8_t i;

    for (i = 0; i < 3; i++)
      width_snake[i] = (uint8_t) strlen(snake[i][0]);
  }

  uint8_t middle;

  uint8_t height = 7;
  uint8_t width = width_snake[0] + width_snake[1] + width_snake[2];

  for (middle = 1; width < current_size.ws_col; middle++)
    width += width_snake[1];

  int32_t end = current_size.ws_col + width;



  while (end >= 0) {

    ioctl(STDOUT_FILENO, TIOCGWINSZ, &current_size);

    int32_t start = end - width; 

    int32_t i, j;
 
    char string[1000] = {};
    ssize_t length_string = read(STDIN_FILENO, string, sizeof(string) - 1);

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


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

    if (string[0] == '\n')
      break;
    }

    printf("\033[2J\033[H");

    for (j = 0; j < ((height > current_size.ws_row) ? current_size.ws_row : height) ; j++) {

    for (i = 0; i < current_size.ws_col; i++) {
      int32_t index = i - start;

      if (index >= 0 && index < width) {
        if (index < width_snake[0]) {
          printf("%c", snake[0][j][index]);
        } else if (index < width_snake[0] + width_snake[1] * middle) {
          printf("%c", snake[1][j][(index - width_snake[0]) % width_snake[1]]);
        } else {
          printf("%c", snake[2][j][index - (width_snake[0] + width_snake[1] * middle)]);
        }
      } else {
        printf(" ");
      }
    }      
    
      printf("\n");

    }
    fflush(stdout);
    
    end--;

    nanosleep(&mili10, NULL);
  }
  return true;
}

bool draw_menu(main_menu menu, struct winsize window_size) {

  char *menu_string[] = {"NEW GAME", "SETTINGS", "EXIT"};

  const uint8_t width_small = 8;
  const uint8_t height_small = 5;

  char *menu_string_big[][6] = {
      {"███╗   ██╗███████╗██╗    ██╗     ██████╗  █████╗ ███╗   ███╗███████╗",
       "████╗  ██║██╔════╝██║    ██║    ██╔════╝ ██╔══██╗████╗ ████║██╔════╝",
       "██╔██╗ ██║█████╗  ██║ █╗ ██║    ██║  ███╗███████║██╔████╔██║█████╗  ",
       "██║╚██╗██║██╔══╝  ██║███╗██║    ██║   ██║██╔══██║██║╚██╔╝██║██╔══╝  ",
       "██║ ╚████║███████╗╚███╔███╔╝    ╚██████╔╝██║  ██║██║ ╚═╝ ██║███████╗",
       "╚═╝  ╚═══╝╚══════╝ ╚══╝╚══╝      ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝╚══════╝"},
      {"███████╗███████╗████████╗████████╗██╗███╗   ██╗ ██████╗ ███████╗",
       "██╔════╝██╔════╝╚══██╔══╝╚══██╔══╝██║████╗  ██║██╔════╝ ██╔════╝",
       "███████╗█████╗     ██║      ██║   ██║██╔██╗ ██║██║  ███╗███████╗",
       "╚════██║██╔══╝     ██║      ██║   ██║██║╚██╗██║██║   ██║╚════██║",
       "███████║███████╗   ██║      ██║   ██║██║ ╚████║╚██████╔╝███████║",
       "╚══════╝╚══════╝   ╚═╝      ╚═╝   ╚═╝╚═╝  ╚═══╝ ╚═════╝ ╚══════╝"},
      {"███████╗██╗  ██╗██╗████████╗", "██╔════╝╚██╗██╔╝██║╚══██╔══╝",
       "█████╗   ╚███╔╝ ██║   ██║   ", "██╔══╝   ██╔██╗ ██║   ██║   ",
       "███████╗██╔╝ ██╗██║   ██║   ", "╚══════╝╚═╝  ╚═╝╚═╝   ╚═╝   "}};

  const uint8_t height_line = 6;
  const uint8_t height_space = 2;

  const uint8_t width_text[] = {68, 64, 28};

  const uint8_t width_big = width_text[0] /*strlen(menu_string_big[0][0])*/;
  const uint8_t height_big =
      height_line * MAIN_MENU + height_space * (MAIN_MENU - 1);

  printf("\033[2J\033[H");

  if (window_size.ws_row < height_small || window_size.ws_col < width_small) {
    printf("Too small\n");
    return true;
  }

  uint8_t i, j, k;

  if (window_size.ws_row >= height_big && window_size.ws_col >= width_big) {

    uint8_t vertical_buffer = (window_size.ws_row - height_big) >> 1;

    for (i = 0; i < vertical_buffer; i++)
      printf("\n");

    for (i = 0; i < MAIN_MENU; i++) {

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

      uint8_t horizontal_buffer = (window_size.ws_col - width_text[i]) >> 1;

      for (j = 0; j < height_line; j++) {

        for (k = 0; k < horizontal_buffer; k++)
          printf(" ");

        printf("%s", menu_string_big[i][j]);

        if (j != height_line - 1)
          printf("\n");
      }

      printf("\033[0m");

      if (i != MAIN_MENU - 1)
        printf("\n\n");
    }

  } else {

    uint8_t vertical_buffer = (window_size.ws_row - height_small) >> 1;

    for (i = 0; i < vertical_buffer; i++)
      printf("\n");

    for (i = 0; i < MAIN_MENU; i++) {
      if (i == menu)
        printf("\033[32m");

      uint8_t horizontal_buffer =
          (window_size.ws_col - strlen(menu_string[i])) >> 1;

      for (j = 0; j < horizontal_buffer; j++)
        printf(" ");

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

      if (i != MAIN_MENU - 1)
        printf("\n\n");
    }
  }

  fflush(stdout);
  return false;
}

bool draw_board(char board[HEIGHT][WIDTH][STRING_LEN],
                struct winsize window_size, bool redraw, bool paused) {

  uint8_t i, j;

  uint8_t actual_height = HEIGHT + 2;
  uint8_t actual_width = WIDTH * 2 + 2;

  if (actual_width > window_size.ws_col || actual_height > window_size.ws_row) {

    printf("\033[2J\033[HToo small\n");
    printf("Min width: %i & Min height: %i\n", actual_height, actual_width);

    return true;
  }

  uint8_t vertical_buffer = (window_size.ws_row - actual_height) >> 1;
  uint8_t horizontal_buffer = (window_size.ws_col - actual_width) >> 1;

  if (redraw) {
    printf("\033[2J\033[H");
    for (i = 0; i < vertical_buffer; i++)
      printf("\n");
  } else {
    for (i = 0; i < actual_height - 1; i++)
      printf("\033[%c", UP_CODE);
    printf("\r");
  }

  for (i = 0; i < horizontal_buffer; i++)
    printf(" ");
  printf("+");
  for (i = 0; i < WIDTH; i++)
    printf("--");
  printf("+\n");

  for (i = 0; i < HEIGHT; i++) {

    for (j = 0; j < horizontal_buffer; j++)
      printf(" ");

    printf("|");

    for (j = 0; j < WIDTH; j++)
      printf("%s", board[i][j]);

    printf("|\n");
  }

  for (i = 0; i < horizontal_buffer; i++)
    printf(" ");
  printf("+");
  for (i = 0; i < WIDTH; i++)
    printf("--");
  printf("+");

  if (paused)
    draw_pause(window_size, actual_width, actual_height);

  fflush(stdout);

  return false;
}

void draw_pause(struct winsize window_size, uint8_t field_width,
                uint8_t field_height) {

  const uint8_t height = 5;
  const uint8_t width = 14;

  const char *pause_menu[] = {
    "+------------+", 
    "|            |",
    "|   PAUSED   |",
    "|            |",
    "+------------+"
  };

  const uint8_t going_up = (field_height + height) >> 1;
  const uint8_t going_right = (field_width - width) >> 1;

  const uint8_t horizontal_buffer = (window_size.ws_col - field_width) >> 1;

  uint8_t i, j;

  for (i = 0; i < going_up; i++)
    printf("\033[%c", UP_CODE);
  printf("\r");

  for (i = 0; i < height; i++) {
    for (j = 0; j < horizontal_buffer; j++)
      printf("\033[%c", RIGHT_CODE);
    for (j = 0; j < going_right; j++)
      printf("\033[%c", RIGHT_CODE);

    printf("\033[37m%s\033[0m\033[%c\r", pause_menu[i], DOWN_CODE);
  }

  for (i = 0; i < going_up - height; i++)
    printf("\033[%c", DOWN_CODE);
  printf("\r");

  fflush(stdout);
}

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;
}
