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

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

#include <sys/ioctl.h>

#include <time.h>
#include <sys/time.h>

#include <string.h>
#include <ctype.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 ACTUAL_WIDTH ((WIDTH * 2) + 2)
#define ACTUAL_HEIGHT (HEIGHT + 2)


#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 {
  selected position[MAX_LENGTH];
  uint16_t length;
} snake;



void update_field(snake my_snake, char board[HEIGHT][WIDTH][STRING_LEN], selected fruit);
selected new_fruit(char board[HEIGHT][WIDTH][STRING_LEN]);

bool draw_menu(main_menu menu, struct winsize window_size);

void draw_board(char board[HEIGHT][WIDTH][STRING_LEN], struct winsize window_size);
void draw_pause(struct winsize window_size);

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] = 1;

  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 timeval start;
  gettimeofday(&start, NULL);


  /* 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;


    /* Check for window size */
    
    struct timeval current;
    gettimeofday(&current, NULL);

    uint64_t time_diff = (current.tv_sec - start.tv_sec) * 1000000 + (current.tv_usec - start.tv_usec);

    if (time_diff >= SPEED * 1000) {
      
      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;
      }
    }
  }



  /* initialisation of the game */

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

  {
    uint16_t i;
    
    my_snake.position[0].y = HEIGHT >> 1;
    my_snake.position[0].x = WIDTH >> 1;
    for (i = 1; i < my_snake.length; i++) {
      my_snake.position[i].y = -1;
      my_snake.position[i].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 (current_size.ws_row < ACTUAL_HEIGHT || current_size.ws_col < ACTUAL_WIDTH) {
    too_small = true;
    paused = true;

    printf("\033[2J\033[HToo small\n");
    printf("Min width: %i & Min height: %i\n", ACTUAL_WIDTH, ACTUAL_WIDTH);
  } else {
    draw_board(board, current_size);
  }

  direction shift = RIGHT;
  direction tmp_direction = shift;


  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') {
            paused = !paused;
        }
      }

      if (paused)
        draw_pause(current_size);
    }


    /* game logic */

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

    uint64_t time_diff = (current.tv_sec - start.tv_sec) * 1000000 + (current.tv_usec - start.tv_usec);

    if (time_diff >= SPEED * 1000) {
     
      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;
        
        if (current_size.ws_row < ACTUAL_HEIGHT || current_size.ws_col < ACTUAL_WIDTH) {
          too_small = true;
          
          printf("\033[2J\033[HToo small\n");
          printf("Min width: %i & Min height: %i\n", ACTUAL_WIDTH, ACTUAL_WIDTH);
        } else {
          too_small = false;
          
          draw_board(board, current_size);
        }
      }

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

        uint16_t i;
        for (i = my_snake.length; i > 0; i--) {
          my_snake.position[i].y = my_snake.position[i - 1].y;
          my_snake.position[i].x = my_snake.position[i - 1].x;
        }

        switch (shift) {
          case UP:
            my_snake.position[0].y = ((my_snake.position[0].y + HEIGHT - 1) % HEIGHT);
            break;
          case DOWN:
            my_snake.position[0].y = ((my_snake.position[0].y + 1) % HEIGHT);
            break;
          case LEFT:
            my_snake.position[0].x = ((my_snake.position[0].x + WIDTH - 1) % WIDTH);
            break;
          case RIGHT:
            my_snake.position[0].x = ((my_snake.position[0].x + 1) % WIDTH);
            break;
          default:
            goto end_of_program;
            break;
        }
      
        uint8_t head_y = my_snake.position[0].y;
        uint8_t head_x = my_snake.position[0].x;

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

          if (
            x == head_x && 
            y == head_y
          ) {
            printf("\n\nHAHA LOST!!!\n\n");
            goto end_of_program;
          }
        }

        update_field(my_snake, board, fruit);

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


end_of_program:

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

  return 0;
}





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].x;
    int16_t y = my_snake.position[i].y;
    if (x >= 0 && y >= 0) {
      if (!i) {
        if (x == (my_snake.position[i + 1].x + 1) % WIDTH) {
          strcpy(board[y][x], "\033[33m═\033[31mX\033[0m");
        } else {
          strcpy(board[y][x], "\033[31m X\033[0m");
        }
      } else if (i == my_snake.length - 1) {
        if (x != my_snake.position[i - 1].x) {
          strcpy(board[y][x], "\033[33m══\033[0m"); 
        } else {
          strcpy(board[y][x], "\033[33m ║\033[0m"); 
        }
      } else {
        if (
          x != my_snake.position[i - 1].x && 
          x != my_snake.position[i + 1].x
        ) {
          strcpy(board[y][x], "\033[33m══\033[0m"); 
        } else if (
          y != my_snake.position[i - 1].y && 
          y != my_snake.position[i + 1].y
        ) {
          strcpy(board[y][x], "\033[33m ║\033[0m");
        } else if (
          (y == (my_snake.position[i - 1].y + 1) % HEIGHT && 
          x == (my_snake.position[i + 1].x + 1) % WIDTH) ||
           (y == (my_snake.position[i + 1].y + 1) % HEIGHT && 
          x == (my_snake.position[i - 1].x + 1) % WIDTH)
        ) {
          strcpy(board[y][x], "\033[33m═╝\033[0m");
        } else if (
          (y == (my_snake.position[i - 1].y + (HEIGHT - 1)) % HEIGHT && 
          x == (my_snake.position[i + 1].x + 1) % WIDTH) ||
           (y == (my_snake.position[i + 1].y + HEIGHT - 1) % HEIGHT && 
          x == (my_snake.position[i - 1].x + 1) % WIDTH)
        ) {
          strcpy(board[y][x], "\033[33m═╗\033[0m");
        } else if (
          (y == (my_snake.position[i - 1].y + 1) % HEIGHT && 
          x == (my_snake.position[i + 1].x + (WIDTH - 1)) % WIDTH) ||
           (y == (my_snake.position[i + 1].y + 1) % HEIGHT && 
          x == (my_snake.position[i - 1].x + (WIDTH - 1)) % WIDTH)
        ) {
          strcpy(board[y][x], "\033[33m ╚\033[0m");
        } else if (
          (y == (my_snake.position[i - 1].y + (HEIGHT - 1)) % HEIGHT && 
          x == (my_snake.position[i + 1].x + (WIDTH - 1)) % WIDTH) ||
           (y == (my_snake.position[i + 1].y + (HEIGHT - 1)) % HEIGHT && 
          x == (my_snake.position[i - 1].x + (WIDTH - 1)) % WIDTH)
        ) {
          strcpy(board[y][x], "\033[33m ╔\033[0m");
        }
      }
    }
  }
  
  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 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;
}


void draw_board (char board[HEIGHT][WIDTH][STRING_LEN], struct winsize window_size) {
  
  uint8_t i, j; 

  uint8_t vertical_buffer = (window_size.ws_row - ACTUAL_HEIGHT) >> 1;
  uint8_t horizontal_buffer = (window_size.ws_col - ACTUAL_WIDTH) >> 1;

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

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

  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("+");
  fflush(stdout);
}


void draw_pause(struct winsize window_size) {

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

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


  const uint8_t going_up = (ACTUAL_HEIGHT + height) >> 1;
  const uint8_t going_right = (ACTUAL_WIDTH - width) >> 1;

  const uint8_t horizontal_buffer = (window_size.ws_col - ACTUAL_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;
}
