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

#include <termios.h>
#include <sys/ioctl.h>
#include <sys/stat.h>

#include <time.h>
#include <unistd.h>

#include <errno.h>


/* TETRIS */

#define FIELD_WIDTH 10
#define FIELD_HEIGHT 21

#define BORDER_WIDTH 2
#define TOP_GAP 1

#define FULL_WIDTH (FIELD_WIDTH + BORDER_WIDTH * 2)
#define FULL_HEIGHT (FIELD_HEIGHT + BORDER_WIDTH)

typedef enum {
  EMPTY, I_PIECE, J_PIECE, L_PIECE, O_PIECE, S_PIECE, T_PIECE, Z_PIECE, TETRIS
} tetromino;

typedef enum {
  TOP, RIGHT, BOTTOM, LEFT
} direction;

typedef struct {
  tetromino name;
  bool piece[4][4];
  int8_t x;
  int8_t y;
  uint8_t size;
  direction rotation;
  uint8_t positions;
} piece;

typedef struct {
  int8_t x;
  int8_t y;
} coord;

tetromino playfield[FULL_HEIGHT][FULL_WIDTH] = {};


void init_field();

void draw_board(piece current_piece, bool *blink, bool is_piece);

piece init_piece(tetromino new_piece);

coord find_direction(piece current_piece, uint8_t x, uint8_t y);
  

int main () {
  

  /* Setting up the seed */
  
  srand(time(NULL));


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

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

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

  /* Init */

  init_field();


  /* Init the first piece */

  bool is_piece = false;

  piece current_piece = {};


  uint16_t i;

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

    int8_t move = 0;
    bool down = false;
    bool draw = false;
    bool rotate = false;

    int8_t j, k;

    if (!is_piece) {
      current_piece = init_piece((rand() % Z_PIECE) + 1);
      is_piece = true;
      draw = true;
      i = 0;
    }


    /* Take care of the input */


    char buf[10] = {};

    if (read(STDIN_FILENO, buf, sizeof(buf)) > 0) {
      if (buf[0] == 'q') {
        break;
      }

      if (buf[0] == '\033' && buf[1] == '[') {
        
        switch (buf[2]) {
          case 'A':
            rotate = true;
            break;
          case 'B':
            down = true;
            break;
          case 'C':
            move = 1;
            break;
          case 'D':
            move = -1;
            break;
          default:
            break;
        }
      }
    }


    /* Move piece */

    if (move != 0) {
      if (
        current_piece.x + move >= 0 && 
        current_piece.x + move + current_piece.size <= FULL_WIDTH
      ) {
       
        bool is_valid = true;

        for (j = 0; j < current_piece.size; j++) {
          for (k = 0; k < current_piece.size; k++) {

            coord points = find_direction(current_piece, k, j);

            if (
              current_piece.piece[j][k]  && 
              playfield[current_piece.y + points.y][current_piece.x + points.x + move] != EMPTY
            ) {
              is_valid = false;
            }
          }
        }

        if (is_valid) {
          current_piece.x += move;
          draw = true;
        }
      }
    }


    /* Piece rotates */
    
    if (rotate && current_piece.positions > 1) {
      
      piece rotated = current_piece;

      rotated.rotation = (current_piece.rotation + 1) % (current_piece.positions);

      bool is_valid = true;

      for(j = 0; j < rotated.size; j++) {
        for (k = 0; k < rotated.size; k++) {

          coord points = find_direction(rotated, k, j);

          if (
            rotated.piece[j][k] &&
            playfield[rotated.y + points.y][rotated.x + points.x] != EMPTY
          ) {
            is_valid = false;
          }
        }
      } 
      
      if (is_valid) {
        current_piece = rotated;
        draw = true;
      }

    }
    

    /* Piece falls */

    if (i >= 100 || down) {
      current_piece.y++;
      i = 0;
      draw = true;
    }


    /* When a piece is stuck */

    for(j = 0; j < current_piece.size; j++) {
      for (k = 0; k < current_piece.size; k++) {
      
        coord points = find_direction(current_piece, k, j);

        uint8_t x = current_piece.x + points.x;
        uint8_t y = current_piece.y + points.y;

        if (
          y >= FULL_HEIGHT || 
          (playfield[y][x] != EMPTY && current_piece.piece[j][k])
        ) {

          uint8_t l, m;

          for (l = 0; l < current_piece.size; l++) {
            for (m = 0; m < current_piece.size; m++) {
              
              if (current_piece.piece[l][m]) {
                
                coord solid = find_direction(current_piece, m, l);

                playfield[current_piece.y - 1 + solid.y][current_piece.x + solid.x] = current_piece.name;
              }
            }
          }
          is_piece = false;
          draw = true;
        }
      }
    }

    /* Looks if there is a tetris */

    bool blink[FIELD_HEIGHT] = {};
    bool is_tetris = false;

    for (j = FIELD_HEIGHT - 1; j >= 0; j--) {

      uint8_t blocked = 0;

      for (k = 0; k < FIELD_WIDTH; k++) {
        if (playfield[j][k + BORDER_WIDTH] != EMPTY)
          blocked++;
      }

      if (blocked == 10) {
        
        blink[j] = true;
        is_tetris = true;  
      }
    }

    if (is_tetris) {
      for (j = 0; j < 4; j++) {
        draw_board(current_piece, (j % 2) ? NULL : blink, is_piece);
        usleep(200000);
      }

      for (j = 0; j < FIELD_HEIGHT; j++) {
        if (blink[j]) {

          for (k = j; k >= 0; k--) {
            int8_t l;
          
            for (l = 0; l < FIELD_WIDTH; l++) {
              playfield[k][l + BORDER_WIDTH] = playfield[k - 1][l + BORDER_WIDTH];
            }
          }
        }
      }

    }


    /* Looks for game over */

    for (j = 0; j < FIELD_WIDTH; j++) {
      if (playfield[TOP_GAP][j + BORDER_WIDTH] > 0) {
        draw_board(current_piece, NULL, is_piece);
        sleep(2);
        printf("\033[2J\033[H");
        printf("HAHA LOSER\n");
        sleep(1);
        goto end;
      }
    }


    if (draw)
      draw_board(current_piece, NULL, is_piece);
    usleep(10000);
  }

end:

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

  tcsetattr(STDIN_FILENO, TCSANOW, &old);

  return 0;
}



void init_field() {
  uint8_t i, j;
  for(i = 0; i < FULL_HEIGHT; i++) {
    for (j = 0; j < FULL_WIDTH; j++) {
      
      if (
        j < BORDER_WIDTH || 
        j >= FULL_WIDTH - BORDER_WIDTH ||
        i >= FULL_HEIGHT - BORDER_WIDTH
      ) {
        playfield[i][j] = TETRIS; 
      } else {
        playfield[i][j] = EMPTY;
      }
    }
  }
}



#define RED "\033[38;2;255;0;0m"
#define GREEN "\033[38;2;0;255;0m"
#define BLUE "\033[38;2;0;0;255m"
#define YELLOW "\033[38;2;255;255;0m"
#define MAGENTA "\033[38;2;255;0;255m"
#define TEAL "\033[38;2;0;255;255m"
#define PURPLE "\033[38;2;127;0;255m"
#define WHITE "\033[38;2;255;255;255m"

void draw_color (tetromino current_square) {
  switch (current_square) {
    case I_PIECE:
      printf("%s", RED);
      break;
    case J_PIECE:
      printf("%s", GREEN);
      break;
    case L_PIECE:
      printf("%s", BLUE);
      break;
    case O_PIECE:
      printf("%s", YELLOW);
      break;
    case S_PIECE:
      printf("%s", MAGENTA);
      break;
    case T_PIECE:
      printf("%s", TEAL);
      break;
    case Z_PIECE:
      printf("%s", PURPLE);
      break;
  } 
}


void draw_board(piece current_piece, bool *blink, bool is_piece) {

  uint8_t i, j;


  printf("\033[2J\033[H");
  printf("┏");
  for (i = 0; i < FIELD_WIDTH; i++)
    printf("━━");
  printf("┓\n");


  for (i = 0; i < FIELD_HEIGHT; i++) {
    
    printf("┃");

    for (j = 0; j < FIELD_WIDTH; j++) {
      
      if ( blink != NULL && blink[i]) {
  
        printf("%s", WHITE);
      } else {


        tetromino current_square = current_square = playfield[i][j + BORDER_WIDTH];

        if (is_piece) {

          uint8_t k, l;
          
          for (k = 0; k < current_piece.size; k++) {
            for (l = 0; l < current_piece.size; l++) {

              coord points = find_direction(current_piece, l, k);

              if (
                i == current_piece.y + points.y && 
                j == current_piece.x + points.x - BORDER_WIDTH && 
                current_piece.piece[k][l]) 
              {
                current_square = current_piece.name;
              }
            }
          }
        }

        if (current_square <= EMPTY || current_square > Z_PIECE) {
          printf("· ");
          continue;
        }

        draw_color(current_square);
      }
        
      printf("██\033[0m");
    }
    printf("┃\n");
  }

  printf("┗");
  for (i = 0; i < FIELD_WIDTH; i++)
    printf("━━");
  printf("┛\n");

}


piece init_piece(tetromino new_piece) {
  
  piece current_piece = {};

  current_piece.name = new_piece;

  current_piece.x = ((FIELD_WIDTH >> 1) - (current_piece.size >> 1));
  current_piece.y = TOP_GAP;
  current_piece.rotation = TOP;


  switch (current_piece.name) {
    case I_PIECE:
      current_piece.size = 4;
      current_piece.positions = 2;

      current_piece.piece[0][0] = false;
      current_piece.piece[0][1] = false;
      current_piece.piece[0][2] = false;
      current_piece.piece[0][3] = false;

      current_piece.piece[1][0] = true;
      current_piece.piece[1][1] = true;
      current_piece.piece[1][2] = true;
      current_piece.piece[1][3] = true;
      
      current_piece.piece[2][0] = false;
      current_piece.piece[2][1] = false;
      current_piece.piece[2][2] = false;
      current_piece.piece[2][3] = false;
      
      current_piece.piece[3][0] = false;
      current_piece.piece[3][1] = false;
      current_piece.piece[3][2] = false;
      current_piece.piece[3][3] = false;
      
      current_piece.y = 0;

      break;

    case J_PIECE:
      current_piece.size = 3;
      current_piece.positions = 4;
  
      current_piece.piece[0][0] = true;
      current_piece.piece[0][1] = false;
      current_piece.piece[0][2] = false;
      
      current_piece.piece[1][0] = true;
      current_piece.piece[1][1] = true;
      current_piece.piece[1][2] = true;

      current_piece.piece[2][0] = false;
      current_piece.piece[2][1] = false;
      current_piece.piece[2][2] = false;

      break;

    case L_PIECE:
      current_piece.size = 3;
      current_piece.positions = 4;
  
      current_piece.piece[0][0] = false;
      current_piece.piece[0][1] = false;
      current_piece.piece[0][2] = true;
      
      current_piece.piece[1][0] = true;
      current_piece.piece[1][1] = true;
      current_piece.piece[1][2] = true;

      current_piece.piece[2][0] = false;
      current_piece.piece[2][1] = false;
      current_piece.piece[2][2] = false;

      break;

    case O_PIECE:
      current_piece.size = 2;
      current_piece.positions = 1;
  
      current_piece.piece[0][0] = true;
      current_piece.piece[0][1] = true;

      current_piece.piece[1][0] = true;
      current_piece.piece[1][1] = true;
      
      break;

    case S_PIECE:
      current_piece.size = 3;
      current_piece.positions = 2;
  
      current_piece.piece[0][0] = false;
      current_piece.piece[0][1] = true;
      current_piece.piece[0][2] = true;

      current_piece.piece[1][0] = true;
      current_piece.piece[1][1] = true;
      current_piece.piece[1][2] = false;
 
      current_piece.piece[2][0] = false;
      current_piece.piece[2][1] = false;
      current_piece.piece[2][2] = false;

      break;

    case T_PIECE:
      current_piece.size = 3;
      current_piece.positions = 4;
  
      current_piece.piece[0][0] = false;
      current_piece.piece[0][1] = true;
      current_piece.piece[0][2] = false;
      
      current_piece.piece[1][0] = true;
      current_piece.piece[1][1] = true;
      current_piece.piece[1][2] = true;
      
      current_piece.piece[2][0] = false;
      current_piece.piece[2][1] = false;
      current_piece.piece[2][2] = false;

      break;
    case Z_PIECE:
      current_piece.size = 3;
      current_piece.positions = 2;
  
      current_piece.piece[0][0] = true;
      current_piece.piece[0][1] = true;
      current_piece.piece[0][2] = false;

      current_piece.piece[1][0] = false;
      current_piece.piece[1][1] = true;
      current_piece.piece[1][2] = true;

      current_piece.piece[2][0] = false;
      current_piece.piece[2][1] = false;
      current_piece.piece[2][2] = false;

      break;
    default:
      perror("Fuck you\n");
      break;
  }

  return current_piece;
}

coord find_direction(piece current_piece, uint8_t x, uint8_t y) {

  coord points = {
    x,
    y
  };

  switch (current_piece.rotation) {
    case TOP:
      /*
      x = k;
      y = j;
      */
      break;
    case RIGHT:
      points.x = y;
      points.y = current_piece.size - (x + 1);
      break;
    case BOTTOM:
      points.x = current_piece.size - (x + 1);
      points.y = current_piece.size - (y + 1);
      break;
    case LEFT:
      points.x = current_piece.size - (y + 1);
      points.y = x;
      break;
  }

  return points;
}
