#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <EEPROM.h>
#include "bitmaps.h"

// Pin definitions
#define UP_BUTTON_PIN 5
#define DOWN_BUTTON_PIN 2
#define LEFT_BUTTON_PIN 4
#define RIGHT_BUTTON_PIN 3

// Constantes du jeu
const int kScreenWidth = 128, kScreenHeight = 64, kGameWidth = 64, kGameHeight = 32, kMaxLength = 464, kStartLength = 6;

// SSD1306 display object
Adafruit_SSD1306 lcd(kScreenWidth, kScreenHeight, &Wire, -1);

// Class button functionality
class PushButton {
    unsigned char last_state, is_down, pin;

    public:
      // Constructor to initialize pin and set mode to INPUT
      PushButton(int pin) : last_state(0), is_down(0), pin(pin) {
        pinMode(pin, INPUT);
      }

      // Update button state
      void update() {
        int state = digitalRead(pin);
        
        if (state != last_state && state == HIGH) {
            is_down = true;
        }
        last_state = state;
      }

      // Get the current state of the button
      bool isPressed() {
        bool down = is_down;
        is_down = false;
        return down;
      }
};

// Initialize buttons with preprocessor pin definitions
PushButton up_button(UP_BUTTON_PIN), down_button(DOWN_BUTTON_PIN), left_button(LEFT_BUTTON_PIN), right_button(RIGHT_BUTTON_PIN);

// Structure to represent a position on the screen
struct Position {
    signed char x, y;

    bool operator==(const Position& other) const {
        return x == other.x && y == other.y;
    }

    Position& operator+=(const Position& other) {
        x += other.x;
        y += other.y;
        return *this;
    }
};

// Function to draw a square at a given position
void drawSquare(Position pos, int color = WHITE) {
    lcd.fillRect(pos.x * 2, pos.y * 2, 2, 2, color);
}

// Function to test if a position is occupied
bool isPositionOccupied(Position pos) {
    return lcd.getPixel(pos.x * 2, pos.y * 2);
}

// Array to represent directions (up, right, down, left)
const Position kDirections[4] = {
    {0, -1}, {1, 0}, {0, 1}, {-1, 0}
};

// Structure to represent the player (snake)
struct Player {
    Position pos;
    unsigned char tail[kMaxLength];
    unsigned char direction;
    int size, moved;

    // Constructor to reset player state
    Player() {
        reset();
    }

    // Reset player to initial state
    void reset() {
        pos = {32, 16};
        direction = 1; // Start moving right
        size = kStartLength;
        memset(tail, 0, sizeof(tail));
        moved = 0;
    }

    // Turn the player left
    void turnLeft() {
        direction = (direction + 3) % 4;
    }

    // Turn the player right
    void turnRight() {
        direction = (direction + 1) % 4;
    }

    // Turn the player up
    void turnUp() {
        direction = 0;
    }

    // Turn the player down
    void turnDown() {
        direction = 2;
    }

    // Update the player's position and tail
    void update() {
        
        // Shift tail positions
        for (int i = kMaxLength - 1; i > 0; --i) {
            tail[i] = tail[i] << 2 | ((tail[i - 1] >> 6) & 3);
        }

        tail[0] = tail[0] << 2 | ((direction + 2) % 4);
        
        // Move the player
        pos += kDirections[direction];
        if (moved < size) {
            moved++;
        }

    }

    // Render the player on the screen
    void render() const {
        drawSquare(pos);
        
        if (moved < size) {
            return;
        }
        
        Position tailpos = pos;
        
        for (int i = 0; i < size; ++i) {
            tailpos += kDirections[(tail[(i >> 2)] >> ((i & 3) * 2)) & 3];
        }
        drawSquare(tailpos, BLACK);
    }
};

// Initialize player
Player player;

// Structure to represent an item (food) on the screen
struct Item {
    Position pos;

    // Spawn the item at a random position
    void spawn() {
        pos.x = random(1, 63);
        pos.y = random(1, 31);
    }

    // Render the item on the screen
    void render() const {
        drawSquare(pos);
    }
};

// Initialize item
Item item;

// Function to wait for user input
void waitForInput() {
    do {
        right_button.update();
        left_button.update();
        up_button.update();
        down_button.update();
    } while (!right_button.isPressed() && !left_button.isPressed() && !up_button.isPressed() && !down_button.isPressed());
}

// Function to display "Push to start" message
void displayPushToStart() {
    lcd.setCursor(26, 57);
    lcd.print(F("Commencer "));
}

// Function to flash the screen
void flashScreen() {
    lcd.invertDisplay(true);
    delay(100);
    lcd.invertDisplay(false);
    delay(200);
}

// Function to play the introduction screen
void playIntro() {
    lcd.clearDisplay();
    lcd.drawBitmap(18, 0, kSplashScreen, 92, 56, WHITE);
    displayPushToStart();
    lcd.display();
    waitForInput();
    flashScreen();
}

// Function to play the game over screen
void displayGameOver() {
    flashScreen();
    lcd.clearDisplay();
    //lcd.drawBitmap(4, 0, kGameOver, 124, 38, WHITE);
    
    int score = player.size - kStartLength;
    lcd.setCursor(28, 2);
    lcd.print(F("Bg ! Game over "));
    lcd.setCursor(26, 34);
    lcd.print(F("Score: "));
    lcd.print(score);
    
    int hiscore;
    EEPROM.get(0, hiscore);
    
    if (score > hiscore) {
        EEPROM.put(0, score);
        hiscore = score;
        lcd.setCursor(4, 44);
        // lcd.print(F("NEW"));
    }
    
    lcd.setCursor(26, 44);
    // lcd.print(F("Hi-Score: "));
    // lcd.print(hiscore);
    displayPushToStart();
    lcd.display();
    waitForInput();
}

// Function to reset the game state
void resetGame() {
    lcd.clearDisplay();
    // Draw game borders
    for (char x = 0; x < kGameWidth; ++x) {
        drawSquare({x, 0});
        drawSquare({x, 31});
    }
    for (char y = 0; y < kGameHeight; ++y) {
        drawSquare({0, y});
        drawSquare({63, y});
    }
    player.reset();
    item.spawn();
}

// Function to update the game state
void updateGame() {
    player.update();
    if (player.pos == item.pos) {
        player.size++;
        item.spawn();
    } else if (isPositionOccupied(player.pos)) {
        displayGameOver();
        resetGame();
    }
}

// Function to handle user input
void handleInput() {
    right_button.update();
    if (right_button.isPressed()) {
        player.turnRight();
    }
    left_button.update();
    if (left_button.isPressed()) {
        player.turnLeft();
    }
    up_button.update();
    if (up_button.isPressed()) {
        player.turnUp();
    }
    down_button.update();
    if (down_button.isPressed()) {
        player.turnDown();
    }
}

// Function to render the game state on the screen
void renderGame() {
    player.render();
    item.render();
    lcd.display();
}

// Setup function, runs once at startup
void setup() {

    lcd.begin(SSD1306_SWITCHCAPVCC, 0x3C);

    lcd.setTextColor(WHITE);
    playIntro();
    resetGame();
}

// Main game loop, runs repeatedly
void loop() {
    handleInput();
    updateGame();
    renderGame();
    delayMicroseconds(150);
}