#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <sys/select.h>
#include <linux/input.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <poll.h>

// New imports
#include <linux/fb.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/mman.h>
#include <time.h>

// The game state can be used to detect what happens on the playfield
#define GAMEOVER   0
#define ACTIVE     (1 << 0)
#define ROW_CLEAR  (1 << 1)
#define TILE_ADDED (1 << 2)

// First conversion to split the values of 8 bit RGB,
// then some magic bit masking stuff to convert into RGB565.
#define RGB(N) { \
  .r = (((N & 0xFF0000) >> 16) >> 3) & 0x1f, \
  .g = (((N & 0x00FF00) >> 8)  >> 2) & 0x3f, \
  .b = (((N & 0x0000FF)     )  >> 3) & 0x1f, \
}

typedef struct {
  uint8_t r : 5;
  uint8_t g : 6;
  uint8_t b : 5;
} color_t;

// If you extend this structure, either avoid pointers or adjust
// the game logic allocate/deallocate and reset the memory

typedef struct {
  bool occupied;
  // Added a color to every tile.
  color_t color;
} tile;

typedef struct {
  unsigned int x;
  unsigned int y;
} coord;

typedef struct {
  coord const grid;                     // playfield bounds
  unsigned long const uSecTickTime;     // tick rate
  unsigned long const rowsPerLevel;     // speed up after clearing rows
  unsigned long const initNextGameTick; // initial value of nextGameTick

  unsigned int tiles; // number of tiles played
  unsigned int rows;  // number of rows cleared
  unsigned int score; // game score
  unsigned int level; // game level

  tile *rawPlayfield; // pointer to raw memory of the playfield
  tile **playfield;   // This is the play field array
  unsigned int state;
  coord activeTile;                       // current tile

  unsigned long tick;         // incremeted at tickrate, wraps at nextGameTick
                              // when reached 0, next game state calculated
  unsigned long nextGameTick; // sets when tick is wrapping back to zero
                              // lowers with increasing level, never reaches 0
} gameConfig;

gameConfig game = {
                   .grid = {8, 8},
                   .uSecTickTime = 10000,
                   .rowsPerLevel = 2,
                   .initNextGameTick = 50,
};

// This is (supposed to be) monokai.
// Because of how the display works, it doesn't look correct on the sense hat,
// but at least they are clear and distinct colors.
const color_t color_scheme[] = {
  RGB(0xF92672), // red
  RGB(0xA6E22E), // green
  RGB(0xA1EFE4), // cyan
  RGB(0x66D9EF), // blue
  RGB(0xAE81FF), // violet
  RGB(0xFD5FF0), // magenta
  RGB(0xFD971F), // orange
  RGB(0xE6DB74), // yellow
};
const size_t color_scheme_n = sizeof(color_scheme) / sizeof(color_t);

const color_t black = {
  .r = 0,
  .g = 0,
  .b = 0,
};

static inline uint16_t color_to_pixel(color_t color) {
  return color.r
       | color.g << 5
       | color.b << 11;
}

// The setup of the LED matrix and the joystick was pretty similar,
// So I extracted the differences and made this unified procedure which
// are common for both of them.
static bool initializeDevice(char* dev_path,
                             char* dev_prefix,
                             char* sys_path,
                             char* dev_name,
                             bool (*handle_device)(char*)) {
  DIR* devdir = opendir(dev_path);
  if (devdir == NULL) {
    fprintf(stderr, "Could not open directory '%s'...", dev_path);
    return false;
  }

  struct dirent* file;
  while ((file = readdir(devdir))) {
    char* devx = file->d_name;
    if (strncmp(devx, dev_prefix, strlen(dev_prefix)) != 0) continue;

    //                         /sys/...        %s    dev                xx
    char* name_file = alloca(strlen(sys_path) - 2 + strlen(dev_prefix) + 2);
    sprintf(name_file, sys_path, devx);

    // read out the content of /sys/class/...../name, and compare it to the expected name
    FILE* name_fd = fopen(name_file, "r");
    char* content = alloca(strlen(dev_name));
    fgets(content, strlen(dev_name) + 1, name_fd);
    int device_name_is_matching = strncmp(content, dev_name, strlen(dev_name));
    fclose(name_fd);

    if (device_name_is_matching != 0) continue;

    // If the device name is correct, do something interesting with it.
    // If that fails, clean up the content of this proc, and forward the failure.
    if (!handle_device(devx)) goto exitError;
    return true;
  }

  fprintf(stderr, "Could not find device with name %s. Is it plugged in?\n", dev_name);
  
  exitError:
  closedir(devdir);
  return false;
}

uint16_t* framebuffer = NULL;
struct fb_fix_screeninfo fixed_info;
struct fb_var_screeninfo variable_info;
int fb_size = sizeof(color_t) * 8 * 8;

// Initialization for the led matrix.
// This creates a file handler for the framebuffer device,
// memory maps it to an array of the same size,
// and then closes the file handler.
static bool led_matrix_callback(char* fbx) {
  printf("Found SenseHat framebuffer: %s\n", fbx);

  char devfbx[9];
  sprintf(devfbx, "/dev/%s", fbx);
  int framebuffer_fd = open(devfbx, O_RDWR);
  if (framebuffer_fd == -1) {
    fprintf(stderr, "Could not open SenseHat framebuffer...\n");
    return false;
  }

  if ( ioctl(framebuffer_fd, FBIOGET_FSCREENINFO, &fixed_info) != 0
    || ioctl(framebuffer_fd, FBIOGET_VSCREENINFO, &variable_info) != 0
     ) {
    fprintf(stderr, "Could not get screen info for %s...\n", devfbx);
    return false;
  };

  // I think this will probably break if it for some reason turns out not to be 16 * 8 * 8,
  // but I'll leave the line here nonetheless.
  fb_size = fixed_info.line_length * variable_info.xres * variable_info.yres;
  printf("Screen size: %i * %i = %i\n", variable_info.xres, variable_info.yres, fb_size);

  framebuffer = (uint16_t*) mmap(0, fb_size, PROT_WRITE, MAP_SHARED, framebuffer_fd, 0);
  if (framebuffer == -1) {
    fprintf(stderr, "Could not create framebuffer mapping...\n");
    return false;
  }
  close(framebuffer_fd);
  return true;
}

int joystick_fd = -1;

// Initialization for the joystick
// This creates a file handler for the joystick device,
// in order for it to be continuously read during the game.
static bool joystick_callback(char* eventx) {
  printf("Found SenseHat joystick: %s\n", eventx);
  
  char deveventx[17];
  sprintf(deveventx, "/dev/input/%s", eventx);
  
  joystick_fd = open(deveventx, O_RDONLY);
  if (joystick_fd == -1) {
    fprintf(stderr, "Could not open SenseHat joystick...\n");
    return false;
  }

  return true;
}

// This function is called on the start of your application
// Here you can initialize what ever you need for your task
// return false if something fails, else true
static bool initializeSenseHat() {
  // Set randomness seed to current time, in order for the colors to
  // become (virtually) different every time you run the program.
  srand(time(0));

  initializeDevice(
    "/dev",
    "fb",
    "/sys/class/graphics/%s/name",
    "RPi-Sense FB",
    led_matrix_callback
  );

  initializeDevice(
    "/dev/input",
    "event",
    "/sys/class/input/%s/device/name",
    "Raspberry Pi Sense HAT Joystick",
    joystick_callback
  );

  return (framebuffer != NULL && joystick_fd != -1);
}

// This function is called when the application exits
// Here you can free up everything that you might have opened/allocated
static void freeSenseHat() {
  munmap(framebuffer, fb_size);
  close(joystick_fd);
}

// This function should return the key that corresponds to the joystick press
// KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, with the respective direction
// and KEY_ENTER, when the the joystick is pressed
// !!! when nothing was pressed you MUST return 0 !!!
int readSenseHatJoystick() {
  struct pollfd poll_fd = {
    .fd = joystick_fd,
    .events = POLLIN,
  };

  int new_input_available = poll(&poll_fd, 1, 0);
  if (!new_input_available) return 0;

  struct input_event event;
  read(joystick_fd, &event, sizeof(event));
  
  // If the event value is not 'playing',
  // it means that the joystick was released back to the center,
  // which is an event we don't really care about.
  if (event.value != FF_STATUS_PLAYING) return 0;

  switch (event.code) {
    case KEY_UP:
    case KEY_DOWN:
    case KEY_LEFT:
    case KEY_RIGHT:
    case KEY_ENTER:
      return event.code;
    default:
      return 0;
  }
}

// This function should render the gamefield on the LED matrix. It is called
// every game tick. The parameter playfieldChanged signals whether the game logic
// has changed the playfield
static void renderSenseHatMatrix(bool const playfieldChanged) {
  if (!playfieldChanged) return;
  for (int i = 0; i < variable_info.xres * variable_info.yres; i++)
    framebuffer[i] = color_to_pixel(game.rawPlayfield[i].occupied ? game.rawPlayfield[i].color : black);
}


// The game logic uses only the following functions to interact with the playfield.
// if you choose to change the playfield or the tile structure, you might need to
// adjust this game logic <> playfield interface

static inline void newTile(coord const target) {
  game.playfield[target.y][target.x].occupied = true;
  game.playfield[target.y][target.x].color = color_scheme[rand() % color_scheme_n];
}

static inline void copyTile(coord const to, coord const from) {
  memcpy((void *) &game.playfield[to.y][to.x], (void *) &game.playfield[from.y][from.x], sizeof(tile));
}

static inline void copyRow(unsigned int const to, unsigned int const from) {
  memcpy((void *) &game.playfield[to][0], (void *) &game.playfield[from][0], sizeof(tile) * game.grid.x);
}

static inline void resetTile(coord const target) {
  memset((void *) &game.playfield[target.y][target.x], 0, sizeof(tile));
}

static inline void resetRow(unsigned int const target) {
  memset((void *) &game.playfield[target][0], 0, sizeof(tile) * game.grid.x);
}

static inline bool tileOccupied(coord const target) {
  return game.playfield[target.y][target.x].occupied;
}

static inline bool rowOccupied(unsigned int const target) {
  for (unsigned int x = 0; x < game.grid.x; x++) {
    coord const checkTile = {x, target};
    if (!tileOccupied(checkTile)) {
      return false;
    }
  }
  return true;
}


static inline void resetPlayfield() {
  for (unsigned int y = 0; y < game.grid.y; y++) {
    resetRow(y);
  }
}

// Below here comes the game logic. Keep in mind: You are not allowed to change how the game works!
// that means no changes are necessary below this line! And if you choose to change something
// keep it compatible with what was provided to you!

bool addNewTile() {
  game.activeTile.y = 0;
  game.activeTile.x = (game.grid.x - 1) / 2;
  if (tileOccupied(game.activeTile))
    return false;
  newTile(game.activeTile);
  return true;
}

bool moveRight() {
  coord const newTile = {game.activeTile.x + 1, game.activeTile.y};
  if (game.activeTile.x < (game.grid.x - 1) && !tileOccupied(newTile)) {
    copyTile(newTile, game.activeTile);
    resetTile(game.activeTile);
    game.activeTile = newTile;
    return true;
  }
  return false;
}

bool moveLeft() {
  coord const newTile = {game.activeTile.x - 1, game.activeTile.y};
  if (game.activeTile.x > 0 && !tileOccupied(newTile)) {
    copyTile(newTile, game.activeTile);
    resetTile(game.activeTile);
    game.activeTile = newTile;
    return true;
  }
  return false;
}


bool moveDown() {
  coord const newTile = {game.activeTile.x, game.activeTile.y + 1};
  if (game.activeTile.y < (game.grid.y - 1) && !tileOccupied(newTile)) {
    copyTile(newTile, game.activeTile);
    resetTile(game.activeTile);
    game.activeTile = newTile;
    return true;
  }
  return false;
}


bool clearRow() {
  if (rowOccupied(game.grid.y - 1)) {
    for (unsigned int y = game.grid.y - 1; y > 0; y--) {
      copyRow(y, y - 1);
    }
    resetRow(0);
    return true;
  }
  return false;
}

void advanceLevel() {
  game.level++;
  switch(game.nextGameTick) {
  case 1:
    break;
  case 2 ... 10:
    game.nextGameTick--;
    break;
  case 11 ... 20:
    game.nextGameTick -= 2;
    break;
  default:
    game.nextGameTick -= 10;
  }
}

void newGame() {
  game.state = ACTIVE;
  game.tiles = 0;
  game.rows = 0;
  game.score = 0;
  game.tick = 0;
  game.level = 0;
  resetPlayfield();
}

void gameOver() {
  game.state = GAMEOVER;
  game.nextGameTick = game.initNextGameTick;
}


bool sTetris(int const key) {
  bool playfieldChanged = false;

  if (game.state & ACTIVE) {
    // Move the current tile
    if (key) {
      playfieldChanged = true;
      switch(key) {
      case KEY_LEFT:
        moveLeft();
        break;
      case KEY_RIGHT:
        moveRight();
        break;
      case KEY_DOWN:
        while (moveDown()) {};
        game.tick = 0;
        break;
      default:
        playfieldChanged = false;
      }
    }

    // If we have reached a tick to update the game
    if (game.tick == 0) {
      // We communicate the row clear and tile add over the game state
      // clear these bits if they were set before
      game.state &= ~(ROW_CLEAR | TILE_ADDED);

      playfieldChanged = true;
      // Clear row if possible
      if (clearRow()) {
        game.state |= ROW_CLEAR;
        game.rows++;
        game.score += game.level + 1;
        if ((game.rows % game.rowsPerLevel) == 0) {
          advanceLevel();
        }
      }

      // if there is no current tile or we cannot move it down,
      // add a new one. If not possible, game over.
      if (!tileOccupied(game.activeTile) || !moveDown()) {
        if (addNewTile()) {
          game.state |= TILE_ADDED;
          game.tiles++;
        } else {
          gameOver();
        }
      }
    }
  }

  // Press any key to start a new game
  if ((game.state == GAMEOVER) && key) {
    playfieldChanged = true;
    newGame();
    addNewTile();
    game.state |= TILE_ADDED;
    game.tiles++;
  }

  return playfieldChanged;
}

int readKeyboard() {
  struct pollfd pollStdin = {
       .fd = STDIN_FILENO,
       .events = POLLIN
  };
  int lkey = 0;

  if (poll(&pollStdin, 1, 0)) {
    lkey = fgetc(stdin);
    if (lkey != 27)
      goto exit;
    lkey = fgetc(stdin);
    if (lkey != 91)
      goto exit;
    lkey = fgetc(stdin);
  }
 exit:
    switch (lkey) {
      case 10: return KEY_ENTER;
      case 65: return KEY_UP;
      case 66: return KEY_DOWN;
      case 67: return KEY_RIGHT;
      case 68: return KEY_LEFT;
    }
  return 0;
}

void renderConsole(bool const playfieldChanged) {
  if (!playfieldChanged)
    return;

  // Goto beginning of console
  fprintf(stdout, "\033[%d;%dH", 0, 0);
  for (unsigned int x = 0; x < game.grid.x + 2; x ++) {
    fprintf(stdout, "-");
  }
  fprintf(stdout, "\n");
  for (unsigned int y = 0; y < game.grid.y; y++) {
    fprintf(stdout, "|");
    for (unsigned int x = 0; x < game.grid.x; x++) {
      coord const checkTile = {x, y};
      fprintf(stdout, "%c", (tileOccupied(checkTile)) ? '#' : ' ');
    }
    switch (y) {
      case 0:
        fprintf(stdout, "| Tiles: %10u\n", game.tiles);
        break;
      case 1:
        fprintf(stdout, "| Rows:  %10u\n", game.rows);
        break;
      case 2:
        fprintf(stdout, "| Score: %10u\n", game.score);
        break;
      case 4:
        fprintf(stdout, "| Level: %10u\n", game.level);
        break;
      case 7:
        fprintf(stdout, "| %17s\n", (game.state == GAMEOVER) ? "Game Over" : "");
        break;
    default:
        fprintf(stdout, "|\n");
    }
  }
  for (unsigned int x = 0; x < game.grid.x + 2; x++) {
    fprintf(stdout, "-");
  }
  fflush(stdout);
}


inline unsigned long uSecFromTimespec(struct timespec const ts) {
  return ((ts.tv_sec * 1000000) + (ts.tv_nsec / 1000));
}

int main(int argc, char **argv) {
  (void) argc;
  (void) argv;
  // This sets the stdin in a special state where each
  // keyboard press is directly flushed to the stdin and additionally
  // not outputted to the stdout
  {
    struct termios ttystate;
    tcgetattr(STDIN_FILENO, &ttystate);
    ttystate.c_lflag &= ~(ICANON | ECHO);
    ttystate.c_cc[VMIN] = 1;
    tcsetattr(STDIN_FILENO, TCSANOW, &ttystate);
  }

  // Allocate the playing field structure
  game.rawPlayfield = (tile *) malloc(game.grid.x * game.grid.y * sizeof(tile));
  game.playfield = (tile**) malloc(game.grid.y * sizeof(tile *));
  if (!game.playfield || !game.rawPlayfield) {
    fprintf(stderr, "ERROR: could not allocate playfield\n");
    return 1;
  }
  for (unsigned int y = 0; y < game.grid.y; y++) {
    game.playfield[y] = &(game.rawPlayfield[y * game.grid.x]);
  }

  // Reset playfield to make it empty
  resetPlayfield();
  // Start with gameOver
  gameOver();

  if (!initializeSenseHat()) {
    fprintf(stderr, "ERROR: could not initilize sense hat\n");
    return 1;
  };

  // Clear console, render first time
  fprintf(stdout, "\033[H\033[J");
  renderConsole(true);
  renderSenseHatMatrix(true);

  while (true) {
    struct timeval sTv, eTv;
    gettimeofday(&sTv, NULL);

    int key = readSenseHatJoystick();
    if (!key)
      key = readKeyboard();
    if (key == KEY_ENTER)
      break;

    bool playfieldChanged = sTetris(key);
    renderConsole(playfieldChanged);
    renderSenseHatMatrix(playfieldChanged);

    // Wait for next tick
    gettimeofday(&eTv, NULL);
    unsigned long const uSecProcessTime = ((eTv.tv_sec * 1000000) + eTv.tv_usec) - ((sTv.tv_sec * 1000000 + sTv.tv_usec));
    if (uSecProcessTime < game.uSecTickTime) {
      usleep(game.uSecTickTime - uSecProcessTime);
    }
    game.tick = (game.tick + 1) % game.nextGameTick;
  }

  freeSenseHat();
  free(game.playfield);
  free(game.rawPlayfield);

  return 0;
}
