#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 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 {
  UP, DOWN, LEFT, RIGHT
} direction;

typedef enum {
  EMPTY, SNAKE, HEAD, FRUIT
} field_content;

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, field_content board[HEIGHT][WIDTH], selected fruit);
selected new_fruit(field_content board[HEIGHT][WIDTH]);

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

direction read_direction (char* string, direction shift);



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

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


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


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

  {
    uint16_t i;
    
    my_snake.position[0].x = HEIGHT >> 1;
    my_snake.position[0].y = 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};
  field_content board[HEIGHT][WIDTH] = {0};
  
  update_field(my_snake, board, fruit);
  
  fruit = new_fruit(board);
  update_field(my_snake, board, fruit);

  bool paused = false;


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

  bool too_small = 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;


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


  bool run = true;

  while (run) {


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

      if (length_string > 0) {
        string[length_string] = '\0';
    
        if (!paused)
          tmp_direction = read_direction(string, shift);

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

         if (toupper(string[0]) == 'P') {
            paused = !paused;
        }
      }

      if (paused)
        draw_pause(current_size);
    }


    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;
        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;
        }
      
        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, field_content board[HEIGHT][WIDTH], selected fruit) {
  
  uint8_t i, j;

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

  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)
      board[y][x] = (i == 0) ? HEAD : SNAKE;
  }
  
  if (fruit.x >= 0 && fruit.y >= 0)
    board[fruit.y][fruit.x] = FRUIT;
}


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

  uint8_t x;
  uint8_t y;

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

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

  selected fruit = {x, y};

  return fruit;
}


void draw_board (field_content board[HEIGHT][WIDTH], 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++) {

      switch (board[i][j]) {
        case EMPTY:
          printf("  ");
          break;
        case SNAKE:
          printf("\033[31m x\033[0m");
          break;
        case HEAD:
          printf("\033[32m X\033[0m");
          break;
        case FRUIT:
          printf("\033[34m O\033[0m");
          break;
      }
    }
    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, direction shift) {

  if (strlen(string) >= 3) {
    if (string[0] == '\033') {
      if (string[1] == '[') {
       
        switch (string[2]) {
          case RIGHT_CODE:
            if (shift != LEFT)
              return RIGHT;
            break;
          case LEFT_CODE:
            if (shift != RIGHT)
              return LEFT;
            break;
          case UP_CODE:
            if (shift != DOWN)
              return UP;
            break;
          case DOWN_CODE:
            if (shift != UP)
              return DOWN;
            break;
        }          
      }
    }
  }


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

  return shift;
}
