// https://wokwi.com/projects/378469495159511041
// https://forum.arduino.cc/t/snake-game-on-8-8-leds-not-fully-working/1176943

# include <FastLED.h>

// board size, new snake size, snake speed, colors

# define SIZEX  16              // square matrix 16 x 16
const byte initSnakeSize = 5;   // birth length
const unsigned int startSpeed = 347;     // game time constant old man speed.

CRGB crgbAPPLE = CRGB(255, 0, 0);     // Couleur de la pomme (rouge)
CRGB crgbSNAKE = CRGB(100, 255, 0);
CRGB crgbTRAIL = CRGB(0x30, 0x20, 0x10);

# define gameMaxInt 0x10   // power of two is matrix dimension, this bit is over/underflow

# define DATA_PIN 2        // Broche de données du carré de LED
# define NUM_LEDS (SIZEX * SIZEX)       // Nombre de LEDs (sizex square)

# define RESET_SW 8     // Broche pour le bouton du réinitialisation
# define LED_INDEX 12      // Indice de la LED de départ du Snake

CRGB leds[NUM_LEDS];

// colour constants
//CRGB crgbAPPLE = CRGB(255, 0, 0);     // Couleur de la pomme (rouge)
CRGB crgbBLACK = CRGB(0, 0, 0);       // Éteindre toutes les LEDs
CRGB crgbSCORET = CRGB(255, 242, 242);// score text color
CRGB crgbSCOREX = CRGB(10, 20, 30);  // score background colour
//CRGB crgbSNAKE = CRGB(100, 255, 0);
//CRGB crgbTRAIL = CRGB(10, 20, 30);

// pins left right up down
# define LEFT   3
# define RIGHT  4
# define UP     6
# define DOWN   5

const unsigned char jPin[4] = {LEFT, RIGHT, UP, DOWN};

int snakeLength = initSnakeSize;   // Longueur initiale du Snake

bool kFreebie;                     // one-time free to eat pass

// ring buffer for snake pixel coordonnées
byte snakeYX[NUM_LEDS];

//...
int headIdx;                // maintained as head of snake
int tailIdx;                // temporary is just H + L -1, so may not even need it
const int iMask = 0xff;     // mask for snake number math 0..255
// const int long kF = 4;         // shift for multiply by SIZEX

unsigned char appleYX;   // old school, appleY;        // Coordonnées de la pomme
int direction = 1;         // Direction du Snake (0: haut, 1: droite, 2: bas, 3: gauche)

int speed = startSpeed;       // Vitesse de déplacement du Snake (plus la valeur est basse, plus il est rapide)

unsigned long lastMoveTime; // Dernier moment où le Snake a bougé
bool gameOverFlag = false;  // Drapeau pour indiquer la fin du jeu
int applesEaten;            // Nombre de pommes mangées
bool restartGame = false;   // Drapeau pour indiquer le redémarrage du jeu

volatile unsigned long randomNuber;  // don't optimise me away

void setup() {
  Serial.begin(115200);
  Serial.println("no flying blind!\n");

  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
  FastLED.show();
// let's have the same gane every time for awhile  randomSeed(analogRead(0)); // Initialisation du générateur de nombres aléatoires
  lastMoveTime = millis();

  for (unsigned char tt = 0; tt < 4; tt++)
    pinMode(jPin[tt], INPUT_PULLUP);

  pinMode(RESET_SW, INPUT_PULLUP);

  resetGame();
}

bool collide;

void loop() {
  if (gameOverFlag) {
    // Afficher le nombre de pommes mangées en rouge
    displayScore();

    // Attendre que le bouton du joystick soit enfoncé pour redémarrer le jeu
    while (digitalRead(RESET_SW) == HIGH)
      ; // hang out until

    Serial.println("BUTTON");
    resetGame(); // need to RESET game
    return;
  }

  unsigned long currentTime = millis();
  if (currentTime - lastMoveTime >= speed) {
//    Serial.println(randomNuber);   // don't optimize me
collide = false;
    moveSnake();
    lastMoveTime = currentTime;
  }

  checkCollision();

  if (!digitalRead(LEFT)) {
    randomNuber += random(1000);
    changeDirection(3);
//    Serial.println("LEFT");
  }
 
  if (!digitalRead(RIGHT)) {
    randomNuber += random(1000);
//    Serial.println("RIGHT");
    changeDirection(1);    
  }

  if (!digitalRead(UP)) {
    randomNuber += random(1000);
    changeDirection(0);
//    Serial.println("UP");
  }

  if (!digitalRead(DOWN)) {
    randomNuber += random(1000);
    changeDirection(2);
//    Serial.println("DOWN");
  }

  // Si restartGame est vrai, réinitialise le jeu
  if (restartGame) {
    resetGame();
  }

  FastLED.show();
}

void moveSnake() {
  // Déplacer le Snake en ajoutant une nouvelle tête dans la direction actuelle
  int newHeadYX = snakeYX[headIdx];

//Serial.print(newHeadYX, 16); Serial.print(" "); 
  switch (direction) {
  case 0: // Haut
    if ((newHeadYX & 0xf0) != 0) newHeadYX -= 0x10;
    else collide = true;
    break;

  case 1: // Droite
    if ((newHeadYX & 0xf) != 0xf) newHeadYX++;
    else collide = true;
    break;

  case 2: // Bas
    if ((newHeadYX & 0xf0) != 0xf0) newHeadYX += 0x10;
    else collide = true;
    break;

  case 3: // Gauche
    if ((newHeadYX & 0xf) != 0) newHeadYX--;
    else collide = true;
    break;
  }
//  collide = false;
//Serial.print(newHeadYX, 16); Serial.print(" ");   

/* now the snake element are in a ring buffer, so no need to shuffle
    // Déplacer le corps du Snake
    for (int i = snakeLength - 1; i > 0; i--) {
      snakeX[i] = snakeX[i - 1];
      snakeY[i] = snakeY[i - 1];
    }
*/  

  // Mettre à jour la position de la tête du Snake
  headIdx--; headIdx &= iMask; // snake grows to the left (-), and extends right (+)
  snakeYX[headIdx] = newHeadYX;


  // Vérifier si le Snake a mangé une pomme

  kFreebie = false;
  if (snakeYX[headIdx] == appleYX) {
    snakeLength++;
    spawnApple();

    kFreebie = true;

    // Accélérer légèrement le Snake
    if (speed > 20) {
      // speed -= 5;
    }
  }

  // Afficher le Snake et la pomme
//  clearBoard();
  drawSnake();
  drawApple();
}

void printSnake()
{
  for (unsigned char sl = 0; sl < snakeLength; sl++) {
    int theIdx = (headIdx + sl) & iMask;
    Serial.print(snakeYX[theIdx], 16); Serial.print(" ");
  }
  Serial.println("");
}

void checkCollision() {
  // Vérifier si le Snake a heurté les bords
//  if (snakeX[headIdx] & gameMaxInt || snakeY[headIdx] & gameMaxInt) {
if (collide) {
    Serial.println("game over hit a wall");
    gameOver();
    return;
  }

  // Vérifier si le Snake a mangé son propre corps
  for (int i = 1; i < snakeLength; i++) {
    int theIdx = (i + headIdx) & iMask;
    if (snakeYX[theIdx] == snakeYX[headIdx]) {
      if (!kFreebie) {
        kFreebie = false;   // but should have been resetted?
        Serial.println("game over snake bite");



        gameOver();
      }
    }
  }
}

void spawnApple() {
  applesEaten += 3 + random(5); // seems only fair

if (0) {  // testing eating the apple on the snake
  int xIdx = (headIdx + 3) & iMask;
  appleYX = snakeYX[xIdx];

  Serial.print("            new apple @ "); Serial.println(appleYX);

  return;
}

// sometimes aim the apple onto the snake but not its head
  if (random(1000) > 777) { // (random(1000) > 100) { hasn't worked well 
    int randomSnake = random(1, snakeLength - 1);
    int theIdx = (headIdx + randomSnake) & iMask;
    appleYX = snakeYX[theIdx];

  }
// and sometimes totally random, including the snake's head...
  else {
    appleYX = random(0, NUM_LEDS);

  }
}

// minimum draw
void drawSnake() {

//  Serial.print("gonna draw the head and undraw the (old?) tail... ");

//  Serial.print(snakeX[headIdx]); Serial.print(",");
//  Serial.print(snakeY[headIdx]); Serial.print("   ");
//  Serial.println();

  int thePixel = snakeYX[headIdx];
//  int thePixel = snakeY[headIdx] * SIZEX + snakeX[headIdx];
  leds[thePixel] = crgbSNAKE; // Couleur du Snake (vert)

  int theIdx = (headIdx + snakeLength) & iMask;
  thePixel = snakeYX[theIdx];
//  thePixel = snakeY[theIdx] * 16 + snakeX[theIdx];
  leds[thePixel] = crgbTRAIL; // snake leaves a trail
}

void drawApple() {
  leds[appleYX ] = crgbAPPLE; // Couleur de la pomme (rouge)
}

void clearBoard() {
  fill_solid(leds, NUM_LEDS, crgbBLACK);
}

// wrecks applesEaten!
void displayScore() {
  fill_solid(leds, NUM_LEDS, crgbSCOREX);
  Serial.print("your score = "); Serial.println(applesEaten);
  //  delay(1000);  // sue me.

  if (applesEaten >= 1000) applesEaten = 999;

  int hundreds = applesEaten / 100;
  applesEaten -= 100 * hundreds;
  int tens = applesEaten / 10; // Dizaines
  int units = applesEaten - 10 * tens;

  int posX = 2;
  if (hundreds) {
    drawN(hundreds, posX, 2);
    posX += 4;
  }

  if (hundreds || tens) {
    drawN(tens, posX, 2);
    posX += 4;
  }

  drawN(units, posX, 2);

  FastLED.show();
//    delay(1300);    // get off my back
}

void gameOver() {
  gameOverFlag = true;
  clearBoard();
//  displayScore();
  FastLED.show();
  restartGame = false; // Ne pas redémarrer automatiquement
}

void resetGame() {
  snakeLength = initSnakeSize;
  direction = 1;
  speed = startSpeed;
  gameOverFlag = false;
  applesEaten = 0;

  headIdx = 0;

  // Réinitialiser la position de départ du Snake
  for (unsigned char sl = 0; sl < snakeLength; sl++) {  
    int theIdx = (headIdx + sl) & iMask;
    snakeYX[theIdx] = 0x30 | (9 - sl);
  }

  printSnake();

  // Réinitialiser la position de la pomme
  spawnApple();

  // Effacer l'affichage du score
  clearBoard();
  drawSnake0();   //once draw entire snake
  drawApple();

  // Réinitialiser la variable restartGame à faux
  restartGame = false;
}

void changeDirection(int newDirection) {
  if ((newDirection & 1) != (direction & 1))
    direction = newDirection;  // only on valid changes. here be where
}

unsigned char font[10][3] = {
  /*0*/ {0b11111, 0b10001, 0b11111,},
  /*1*/ {0b00000, 0b11111, 0b00000,},
  /*2*/ {0b11101, 0b10101, 0b10111,},
  /*3*/ {0b10101, 0b10101, 0b11111,},
  /*4*/ {0b00111, 0b00100, 0b11111,},
  /*5*/ {0b10111, 0b10101, 0b11101,},
  /*6*/ {0b11111, 0b10101, 0b11101,},
  /*7*/ {0b00001, 0b00001, 0b11111,},
  /*8*/ {0b11111, 0b10101, 0b11111,},
  /*9*/ {0b10111, 0b10101, 0b11111,},
};

void drawN(int N, int X, int Y)
{
  Y <<= 3;

  for (unsigned char colm = 0; colm < 3; colm++, X++) {
    unsigned char yT = Y;
    unsigned char theBits = font[N][colm];
    unsigned char mask = 0x1;

    for (unsigned char row = 0; row < 5; row++) {
      leds[yT + X] = (mask & theBits) ? crgbSCORET : crgbSCOREX;
      mask <<= 1;
      yT += SIZEX;
    }
  }
}

void drawSnake0() {
//  Serial.print("gonna draw  ");
//  Serial.print(headIdx); Serial.print(" -> ");

  for (int i = 0; i < snakeLength; i++) {
    int theIdx = (headIdx + i) & iMask;

//    Serial.print(snakeX[theIdx]); Serial.print(",");
//    Serial.print(snakeY[theIdx]); Serial.print("   ");

    int thePixel = snakeYX[theIdx];
    leds[thePixel] = crgbSNAKE; // Couleur du Snake (vert)
  }
//  Serial.println();
}

void eof() {}



  /* Réinitialiser la position de départ du Snake

    snakeYX[headIdx] = 0x93;

  for (unsigned char sl = 0; sl < snakeLength; sl++) {  
    int theIdx = (headIdx + sl + 1) & iMask;
    int xx = 9 - sl;
    int yy = 3;
    snakeYX[theIdx] = (yy << 4) | xx;
  }
*/

/*

void changeDirection0(int newDirection) {
  if ((newDirection == 0 && direction != 2) ||   // Nouvelle direction : haut, direction actuelle : bas
      (newDirection == 1 && direction != 3) ||   // Nouvelle direction : droite, direction actuelle : gauche
      (newDirection == 2 && direction != 0) ||   // Nouvelle direction : bas, direction actuelle : haut
      (newDirection == 3 && direction != 1)) {   // Nouvelle direction : gauche, direction actuelle : droite
    direction = newDirection;

    // this is on speed dial! do a printing state trick one day if
    //    Serial.print("change direction to "); Serial.println(direction);
  }
}
*/