#include <ncurses.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define WIDTH 30
#define HEIGHT 20
#define NAPTIME 150
#define COLOR_PURPLE 8
#define MAX_SEGMENTS (WIDTH * HEIGHT)
#define INITIAL_SNAKE_LENGTH 1
#define BCHAR '.'
#define SCHAR '#'
#define SCHARUP '^'
#define SCHARDOWN 'v'
#define SCHARLEFT '<'
#define SCHARRIGHT '>'
#define ACHAR 'O'
#define BORDER '#'

// Structs for storing positions of snake head and tail segments
enum      snakeDirection {UP = 0, LEFT = 1, DOWN = 2, RIGHT = 3};
enum      axis           {X = 0, Y = 1};
const int SCHARdir[4] =  {SCHARUP, SCHARLEFT, SCHARDOWN, SCHARRIGHT};
const int xy[4] =        {Y, X, Y, X};
const int movement[4] =  {-1, -1, 1, 1};
const int limit[2] =     {WIDTH, HEIGHT};

char board[WIDTH][HEIGHT];                                  // 2d array representing board
int snakeLength = INITIAL_SNAKE_LENGTH;
enum snakeDirection direction = RIGHT;

typedef struct snakeHead {
  int Pos[2];
  enum snakeDirection localDirection;
} snakeHead;

// Global variables
snakeHead snake;                                            // Snake head
snakeHead tail[MAX_SEGMENTS];                               // Tail array
snakeHead apple;

void initBoard(void) {
  for (int i = 0; i < HEIGHT; i++)
    for (int j = 0; j < WIDTH; j++)
      board[j][i] = BCHAR;
}

void colorch(int pairno, int ch) {
  attron(COLOR_PAIR(pairno)); addch(ch); attroff(COLOR_PAIR(pairno));
  // Excessive attribute setting combined with redrawing entire playfield
  // on each frame causes flickering :-(  Keep a 'previous' copy and
  // only draw if changed.  Or much better, only draw added head and
  // removed tail character...
}

void drawBoard(void) {
  for (int i = 0; i < WIDTH + 2; i++) colorch(3, BORDER);   // Top border
  addch('\n');

  // Draw play field
  for (int i = 0; i < HEIGHT; i++) {              // Y
    colorch(3, BORDER);                           // first of line (left border)    
    for (int j = 0; j < WIDTH /* + 1 No!!! */; j++) { // X
      bool isTail = false;                        // Check if part of tail
      for (int k = 0; k < snakeLength; k++)
        if (tail[k].Pos[X] == j && tail[k].Pos[Y] == i) isTail = true;
      if (apple.Pos[X] == j && apple.Pos[Y] == i) {         // Is it an apple?
        colorch(2, board[j][i]);
      } else if (isTail) {
        colorch(1, board[j][i]);
      } else {
        addch(board[j][i]);
      }
    }
    colorch(3, BORDER);                           // last of line (right border)
    addch('\n');
  }

  for (int i = 0; i < WIDTH + 2; i++) colorch(3, BORDER);   // Bottom border
}

void drawStart(void) {
  char *title = "SNAKE";
  char *subtitle = "PRESS ANY KEY TO START";

  int titleLen = strlen(title);
  int subtitleLen = strlen(subtitle);

  snakeLength = titleLen + subtitleLen;

  initBoard();

  int start = WIDTH / 2 - (titleLen / 2);
  int start2 = WIDTH / 2 - (subtitleLen / 2);

  for (int i = 0; i < titleLen; i++) {
    board[start + i][HEIGHT / 4] = title[i];
    tail[i].Pos[X] = start + i;
    tail[i].Pos[Y] = HEIGHT / 4;
  }

  for (int i = 0; i < subtitleLen; i++) {
    board[start2 + i][HEIGHT / 4 + 2] = subtitle[i];
    tail[i + titleLen].Pos[X] = start2 + i;
    tail[i + titleLen].Pos[Y] = HEIGHT / 4 + 2;
  }

  drawBoard();                                    // Write "SNAKE"
  snakeLength = INITIAL_SNAKE_LENGTH;
}

void drawGameOver(void) {
  char *title = "GAME";
  char *subtitle = "OVER";
  int titleLen = strlen(title);
  int subtitleLen = strlen(subtitle);
  int start = WIDTH / 2 - (titleLen / 2);
  int start2 = WIDTH / 2 - (subtitleLen / 2);

  snakeLength = titleLen + subtitleLen;
  initBoard();

  for (int i = 0; i < titleLen; i++) {
    board[start + i][HEIGHT / 4] = title[i];
    tail[i].Pos[X] = start + i;
    tail[i].Pos[Y] = HEIGHT / 4;
  }

  for (int i = 0; i < subtitleLen; i++) {
    board[start2 + i][HEIGHT / 4 + 2] = subtitle[i];
    tail[i + titleLen].Pos[X] = start2 + i;
    tail[i + titleLen].Pos[Y] = HEIGHT / 4 + 2;
  }

  drawBoard();  // Write "SNAKE"
}

void initSnake(void) {
  snake.Pos[X] = WIDTH / 2;  snake.Pos[Y] = HEIGHT / 2;
}

//void drawSnakeHeadOnBoard(void) {               // Draw head onto board array - UNUSED
//  board[snake.Pos[X]][snake.Pos[Y]] = SCHAR;
//}

void drawSnakeHeadOnBoardDirectional(void) {
  board[snake.Pos[X]][snake.Pos[Y]] = SCHARdir[direction];  // Once you've simplified the original function
                                                  // it is more obvious that the entire switch statement
                                                  // did nothing because it was immediately overwritten:
  // board[snake.Pos[X]][snake.Pos[Y]] = SCHAR;   // Draw head onto board array - Bug: redundant.
}

void moveSnakeHead(void) {                        // Update x and y coordinates of snake based on direction
  snake.localDirection = direction;
  snake.Pos[xy[direction]] = (snake.Pos[xy[direction]] + movement[direction] + limit[xy[direction]]) % limit[xy[direction]];
}

void storeTailPos(void) {                         // Take position of snake head and store in tail array
  for (int i = snakeLength; i >= 0; i--) tail[i] = tail[i - 1];       // Shift array +1
  tail[0] = snake;                                // Store current position of head at position 0
}

void eraseTail(void) {
  for (int i = 0; i < snakeLength; i++) board[tail[i].Pos[X]][tail[i].Pos[Y]] = BCHAR;
}

void drawTailOnBoard(void) {
  for (int i = 0; i < snakeLength; i++) {
    // board[tail[i].Pos[X]][tail[i].Pos[Y]] = SCHAR;   // Bug: redundant.
    board[tail[i].Pos[X]][tail[i].Pos[Y]] = SCHARdir[tail[i].localDirection];
  }
}

void generateApple(void) {                        // Random x and y values based on width and height
  apple.Pos[X] = rand() % WIDTH; apple.Pos[Y] = rand() % HEIGHT;
}

void appleCheck(void) {                           // If snake head touches the apple
  if (snake.Pos[X] == apple.Pos[X] && snake.Pos[Y] == apple.Pos[Y]) {
    snakeLength++; generateApple();
  }
}

bool collisionCheck(void) {
  for (int i = 1; i < snakeLength; i++)
    if (snake.Pos[X] == tail[i].Pos[X] && snake.Pos[Y] == tail[i].Pos[Y]) return false;
  return true;
}

void getInput(void) {
  int ch = getch();
  if (ch != ERR) switch (ch) {
    case KEY_UP:     if (direction != DOWN)  direction = UP;    break;
    case KEY_DOWN:   if (direction != UP)    direction = DOWN;  break;
    case KEY_LEFT:   if (direction != RIGHT) direction = LEFT;  break;
    case KEY_RIGHT:  if (direction != LEFT)  direction = RIGHT; break;
  }
}

int main(void) {
  initscr();                                      // Start curses mode
  noecho();                                       // Hide keystrokes
  start_color();                                  // Color mode
  // Define colors
  init_color(COLOR_PURPLE, /*R*/500, /*G*/0, /*B*/500);
  init_pair(1, COLOR_YELLOW, COLOR_BLACK);
  init_pair(2, COLOR_RED, COLOR_BLACK);
  init_pair(3, /*COLOR_PURPLE*/COLOR_YELLOW, COLOR_BLACK);  // Doesn't work.

  drawStart();                                    // Start screen
  getch();
  clear();

  keypad(stdscr, TRUE);                           // Allow for key input

  initBoard();
  generateApple();

  bool game = true;                               // Game still going on?

  // Make getch() non blocking, will return ERR if no input
  nodelay(stdscr, TRUE);

  curs_set(0);                                    // Hide cursor

  while (game) {                                  // Main game loop
    getInput();
    moveSnakeHead();
    appleCheck();
    storeTailPos();                               // Update tail position
    drawSnakeHeadOnBoardDirectional();
    drawTailOnBoard();
    board[apple.Pos[X]][apple.Pos[Y]] = ACHAR;    // Add apple
    drawBoard();
    eraseTail();
    game = collisionCheck();
    refresh();
    napms(NAPTIME - (snakeLength * 2));           // Nap
    clear();
  }

  drawGameOver();
  refresh();
  napms(20000);
  getch();

  endwin();                                       // Stop curses mode
  curs_set(1);                                    // Show cursor

  return 0;
}
