#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

#define TFT_DC 9
#define TFT_CS 10
#define VERT_PIN A0
#define HORZ_PIN A1
#define SEL_PIN  2

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
int const blockSize = 15;
int const displayHeight = 20;
int const displayWidth = 10;

/*
The actual grid is represented by the even coordinates, 
while the middle points are represented by the odd coordinates.
*/

class Point {
public:
  int x;
  int y;
  Point(int x = 0, int y = 0) {
    this->x = x;
    this->y = y;
  }
  Point rotate90ccw() {
    return Point(-y, x);
  }
  Point rotate90cw() const {
      return Point(y, -x);
  }
  Point operator + (Point const oth) const {
    return Point(x + oth.x, y + oth.y);
  }  
  Point operator - (Point const oth) const {
    return Point(x - oth.x, y - oth.y);
  }
  Point operator * (int scale) const {
    return Point(x * scale, y * scale);
  }
  bool gameValid(uint16_t (&board)[displayWidth][displayHeight]) {
    return (x % 2 == 0) && (y % 2 == 0) &&
          0 <= x && (x / 2) < displayWidth &&
          0 <= y && (y / 2) < displayHeight &&
          board[x / 2][y / 2] == 0;
  }

  void debug() {
    Serial.print("Point: ");
    Serial.print(x);
    Serial.print(" ");
    Serial.println(y);
  }
};

//Darken 565 color was generated by ChatGPT, so no clu
uint16_t darken_565_color(uint16_t color, float factor = 0.1) {
    // Extract the red, green, and blue components
    uint16_t red = (color >> 11) & 0x1F;
    uint16_t green = (color >> 5) & 0x3F;
    uint16_t blue = color & 0x1F;

    // Darken each component
    red = (uint16_t)(red * (1.0f - factor));
    green = (uint16_t)(green * (1.0f - factor));
    blue = (uint16_t)(blue * (1.0f - factor));

    // Clamp values to ensure they are within valid range
    red = (red > 0x1F) ? 0x1F : red;
    green = (green > 0x3F) ? 0x3F : green;
    blue = (blue > 0x1F) ? 0x1F : blue;

    // Reassemble the color
    return (red << 11) | (green << 5) | blue;
}

void displayBlock(int x, int y, int color) {
  tft.fillRect(x * blockSize, y * blockSize,
                blockSize, blockSize,
                darken_565_color(color, 0.3));

  tft.fillRect(x * blockSize + 1, y * blockSize + 1,
                blockSize - 2, blockSize - 2,
                color);
}

class Tetromino {
public: 
  Point center;
  int blockCount;
  Point* blocks;
  uint16_t color;
  Tetromino(Point center, int blockCount, Point* blocks, uint16_t color){
    this->center = center;
    this->blockCount = blockCount;
    this->blocks = blocks;
    this->color = color;
  }
  Tetromino(int type) {
    if (type == 0) 
      (*this) = Tetromino({1, 1}, 4, new Point[4]{Point(1, 1), Point(-1, 1), Point(1, -1), Point(-1, -1)}, ILI9341_YELLOW);
    else if (type == 1) {
      (*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(2, -2)}, ILI9341_ORANGE);
    } else if(type == 2) {
      (*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(-2, -2)}, ILI9341_BLUE);
    } else if(type == 3) {
      (*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(4, 0)}, ILI9341_CYAN);
    } else if(type == 4){
      (*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(0, -2), Point(2, -2)}, ILI9341_GREEN);
    } else if (type == 5) {
      (*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(0, -2)}, ILI9341_PURPLE);
    } else if (type == 6) {
      (*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, -2), Point(2, 0), Point(0, -2)}, ILI9341_RED);
    }
  }

  void display(uint16_t (&board)[displayWidth][displayHeight]) {
    for (int i = 0; i < blockCount; i++) {
      if ((blocks[i] + center).gameValid(board) == true) {
        displayBlock((center.x + blocks[i].x) / 2 , (center.y + blocks[i].y) / 2, color);
      }
    }
  }
  void undisplay(uint16_t (&board)[displayWidth][displayHeight]) {
    for(int i = 0; i < blockCount; i++) {
      if((blocks[i] + center).gameValid(board) == true) {
        displayBlock((center.x + blocks[i].x) / 2 , (center.y + blocks[i].y) / 2, 0);
      }
    }
  }
  
  bool checkObject(uint16_t (&board)[displayWidth][displayHeight]) {
    for(int i = 0; i < blockCount; i++ ) {
      if((blocks[i] + center).gameValid(board) == false)
        return false;
    }
    return true;
  }

  void transform(Point oth) {
    this->center.x += oth.x;
    this->center.y += oth.y;
  }

  //Transform restricted based on game rules.
  
  bool gameTransform(Point oth, uint16_t (&board)[displayWidth][displayHeight]) {
    this->undisplay(board);
    this->transform(oth);
    bool verdict = true;
    if(!this->checkObject(board)) {
      this->transform(Point(0, 0)-oth);
      verdict = false;
    }
    this->display(board);
    return verdict;
  }

  void rotate90ccw() {
    for(int i = 0; i < blockCount; i++) {
      blocks[i] = blocks[i].rotate90ccw();
    }
  }
  
  void rotate90cw() {
    for(int i = 0; i < blockCount; i++) {
      blocks[i] = blocks[i].rotate90cw();
    }
  }

  void gameRotate(uint16_t (&board)[displayWidth][displayHeight]) {
    this->undisplay(board);
    this->rotate90ccw();
    if (!this->checkObject(board)) {
      this->rotate90cw();
    }
    this->display(board);
  }

  void solidify(uint16_t (&board)[displayWidth][displayHeight]) {
    for (int i = 0; i < blockCount; i++) {
      Point block = blocks[i] + center;
      if ((blocks[i] + center).gameValid(board) == true) {
        board[block.x / 2][block.y / 2] = color;
      }
    }
  }  

  void debug() {
    Serial.println("Debug state:");
    center.debug();
    Serial.print("blockCount: ");
    Serial.println(blockCount);
    for(int i = 0; i < blockCount; i++) {
      blocks[i].debug();
    }
    Serial.print("color: ");
    Serial.println(color);
  }
};


/*
//Taken straight from the library for easy reference.
// Color definitions
#define ILI9341_BLACK 0x0000       ///<   0,   0,   0
#define ILI9341_NAVY 0x000F        ///<   0,   0, 123
#define ILI9341_DARKGREEN 0x03E0   ///<   0, 125,   0
#define ILI9341_DARKCYAN 0x03EF    ///<   0, 125, 123
#define ILI9341_MAROON 0x7800      ///< 123,   0,   0
#define ILI9341_PURPLE 0x780F      ///< 123,   0, 123
#define ILI9341_OLIVE 0x7BE0       ///< 123, 125,   0
#define ILI9341_LIGHTGREY 0xC618   ///< 198, 195, 198
#define ILI9341_DARKGREY 0x7BEF    ///< 123, 125, 123
#define ILI9341_BLUE 0x001F        ///<   0,   0, 255
#define ILI9341_GREEN 0x07E0       ///<   0, 255,   0
#define ILI9341_CYAN 0x07FF        ///<   0, 255, 255
#define ILI9341_RED 0xF800         ///< 255,   0,   0
#define ILI9341_MAGENTA 0xF81F     ///< 255,   0, 255
#define ILI9341_YELLOW 0xFFE0      ///< 255, 255,   0
#define ILI9341_WHITE 0xFFFF       ///< 255, 255, 255
#define ILI9341_ORANGE 0xFD20      ///< 255, 165,   0
#define ILI9341_GREENYELLOW 0xAFE5 ///< 173, 255,  41
#define ILI9341_PINK 0xFC18        ///< 255, 130, 198
*/


void PrintTestPieces() {
  uint16_t board[displayWidth][displayHeight];

  for(int i = 0; i < 7; i++) {
    Tetromino tromino = Tetromino(i);
    tromino.transform({4,6 +  2 + 6 * i});
    for(int j = 0;j < 4; j++) {
      tromino.display(board);
      tromino.transform(Point(8, 0));
      tromino.rotate90ccw();
    }
  }
}

const unsigned long gameStepMillis = 1000;
const unsigned long joystickStepMillis = 50;

class Game {
public:
  uint16_t board[displayWidth][displayHeight] = {0};
  Tetromino* activeObject;

  Game() {
    activeObject = nullptr;
  }
  
  void spawnObject() {
    int col = random(1, displayWidth - 4);
    int type = random(1, 7);
    activeObject = new Tetromino(type);
    activeObject->transform(Point(col * 2, 0));
    activeObject->display(board);
    //activeObject->debug();
  }
//

  void solidify() {
    activeObject->solidify(board);
    activeObject = nullptr;
  }

  void advanceIteration() {
    unsigned long static lastTimestamp = 0;
    unsigned long currTimestamp = millis();
    if (lastTimestamp + gameStepMillis < currTimestamp) {
      lastTimestamp = currTimestamp;
      if (activeObject == nullptr) {
        spawnObject();
      } else {
        bool moveSuccesful = activeObject->gameTransform(Point(0, 2), board);

        //activeObject->debug();
        if (moveSuccesful == false) {
          solidify();
          //refreshGrid();
          checkLines();
        }
      }
    } else {
      return ;
    }
  }
 
 //why is my exxecution killed.

  void refreshGrid() {
    //tft.fillScreen(0);
    for(int i = 0; i < displayWidth; i++) 
      for(int j = 0; j < displayHeight; j++)
        displayBlock(i, j, board[i][j]);
  }
 
  int checkLines() {
    int targetLine = displayHeight - 1;
    int points = 0;
    for(int j = displayHeight - 1; 0 <= j; j--) {
      bool lineIsClear = true;
      for(int i = 0; i < displayWidth; i++) {
        lineIsClear &= (0 < board[i][j]);
      }
      if (lineIsClear == false) {
        for(int i = 0;i < displayWidth; i++) {
          if (targetLine != j) {
            board[i][targetLine] = board[i][j];
            board[i][j] = 0;
          }
        }
        targetLine--;
      } else {
        points++;
        for(int i = 0; i < displayWidth; i++)
          board[i][j] = 0;
      }
    }
    refreshGrid();
    return points;
  }

  void processInput() {
    unsigned long static lastTimestamp = 0;
    unsigned long currTimestamp = millis();
    if (lastTimestamp + joystickStepMillis < currTimestamp) {
      int vert = analogRead(VERT_PIN);
      int horz = analogRead(HORZ_PIN);
      bool selPressed = digitalRead(SEL_PIN) == LOW;
      if(horz == 0) {
        activeObject->gameTransform(Point(2, 0), board);
        lastTimestamp = currTimestamp;
      } else if (horz == 1023) {
        activeObject->gameTransform(Point(-2, 0),board);
        lastTimestamp = currTimestamp;
      }
      if (vert == 1023) {
        activeObject->gameRotate(board);
        lastTimestamp = currTimestamp;
      } else if (vert == 0){
        activeObject->gameTransform(Point(0, 2), board);
        lastTimestamp = currTimestamp;
      }

      if (selPressed == true) {
        //TODO: implement substitution of pieces.
      }
    }
  }

  void clearLine() {

  }
};

Game basegame;
void setup() {
  Serial.begin(115200);
  tft.begin();

  pinMode(VERT_PIN, INPUT);
  pinMode(HORZ_PIN, INPUT);
  pinMode(SEL_PIN, INPUT_PULLUP);

  randomSeed(millis());
  basegame = Game();
}

void loop() {
  
  basegame.advanceIteration();
  basegame.processInput();
  
}