#pragma once

int dataPin = 12;
int clockPin = 11;
int csPin = 10;

typedef enum MaxRegister {
    scanLimit = 0x0B,
    intensity = 0x0A,
    decode = 0x09,
    shotDown = 0x0c
} MaxRegister;

void softwareSPI(uint8_t A, uint8_t B) {  
    digitalWrite(csPin, LOW);
    shiftOut(dataPin, clockPin, MSBFIRST, A);
    shiftOut(dataPin, clockPin, MSBFIRST, B);  
    digitalWrite(csPin, HIGH);
}

void displayClear() {
  for (int i = 0; i < 8; i++) {
    softwareSPI(i, 0);
  }
}

void displayInit() {
    pinMode(clockPin, OUTPUT);
    pinMode(dataPin, OUTPUT);
    pinMode(csPin, OUTPUT);
    digitalWrite(csPin, HIGH);  
  
    // Выставляем scanLimit на максимум (таблица 8) - это количество колонок матрицы по умолчанию горела бы только одна колонка      
    softwareSPI(MaxRegister::scanLimit, 0x07);  

	  // Выставляем intensity согласно (таблице 7)  
    softwareSPI(MaxRegister::intensity, 0x0F);
  
    // отключаем decode-mode, если он вдруг включен (таблица 4)  
    softwareSPI(MaxRegister::decode, 0x00);
  
    // Очищаем дисплей. Документация про это умалчивает, но в SRAM при запуске лежит мусор  
    displayClear();
  
    // теперь выходим в рабочий режим (таблица 3) и рисуем что хотим.  
    softwareSPI(MaxRegister::shotDown, 0x01);  
}

byte leds[8] = {0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00100000, 0b01000000, 0b10000000};
byte strings[8] = {0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000};

struct Button {
  int current = 0;
  int previous = 0;
};
struct Button buttons[4];

struct BodyPart {
  int x;
  int y;
};

void moveBodyPart(struct BodyPart *bodyPart, int dx, int dy) {
  bodyPart->x = bodyPart->x + dx;
  bodyPart->y = bodyPart->y + dy;
  if (bodyPart->x == 0) {
    bodyPart->x = 8;    
  }
  if (bodyPart->x == 9) {
    bodyPart->x = 1;
  }
  if (bodyPart->y == 0) {
    bodyPart->y = 8;
  }
  if (bodyPart->y == 9) {
    bodyPart->y = 1;
  }
}

struct Snake {
  struct BodyPart body[64];
  int size = 0;
};

void moveSnake(struct Snake *snake, int direction) {
  for (int i = snake->size - 1; i >= 1; i--) {
    snake->body[i].x = snake->body[i - 1].x;
    snake->body[i].y = snake->body[i - 1].y;
  }
  switch (direction) {
    case 1:
      moveBodyPart(&snake->body[0], 0, -1);
      break;
    case 2:
      moveBodyPart(&snake->body[0], 0, 1);
      break;
    case 3:
      moveBodyPart(&snake->body[0], -1, 0);
      break;
    case 4:
      moveBodyPart(&snake->body[0], 1, 0);
      break;
  }
}

void drawSnake(struct Snake snake) {
  for (int i = 0; i < snake.size; i++) {
    high(snake.body[i].x, snake.body[i].y);
  }
}

boolean loseChecker(struct Snake snake, boolean *gameOverStatus){
  for (int i = 1; i < snake.size; i++) {
    if (snake.body[0].x == snake.body[i].x && snake.body[0].y == snake.body[i].y) {
      *gameOverStatus = true;
      break;
    }
  }
}

void generateFood(struct BodyPart *food, struct Snake snake) {
  while (true) {
    food->x = (rand() % 8) + 1;
    food->y = (rand() % 8) + 1;
    boolean foodStatus = true;
    for (int i = 0; i < snake.size; i++) {
      if (food->x == snake.body[i].x && food->y == snake.body[i].y) {
        foodStatus = false;
        break;
      }
    }
    if (foodStatus == true) {
      break;
    }
  }
}

void foodHandler(struct Snake *snake, struct BodyPart *food) {
  if (snake->body[0].x == food->x && snake->body[0].y == food->y) {
    snake->body[snake->size] = {-1, -1};
    snake->size = snake->size + 1;
    generateFood(food, *snake);
  }
}

void drawFood(struct BodyPart food) {
  high(food.x, food.y);
}

void gameOver() {
  for (int i = 1; i <= 8; i++) {
    for (int j = 1; j <= 8; j++) {
      high(i, j);
    }
  }
  while (true) {
    tone(9, 100, 109);
  }
}

void myClearDisplay() {
  for (int i = 0; i < 8; i++) {
    softwareSPI(i, 0);
    strings[i] = strings[i] & 0b00000000;
  }
  softwareSPI(8, 0);
}

void controller(int *direction) {
  buttons[0].current = digitalRead(3);
  if (buttons[0].current && buttons[0].previous == 0) {
    if (*direction != 2) {
      *direction = 1;
    }
  }
  buttons[1].current = digitalRead(4);
  if (buttons[1].current && buttons[1].previous == 0) {
    if (*direction != 1) {
      *direction = 2;
    }
  }
  buttons[2].current = digitalRead(2);
  if (buttons[2].current && buttons[2].previous == 0) {
    if (*direction != 4) {
      *direction = 3;
    }
  }
  buttons[3].current = digitalRead(5);
  if (buttons[3].current && buttons[3].previous == 0) {
    if (*direction != 3) {
      *direction = 4;
    }
  }
  buttons[0].previous = buttons[0].current;
  buttons[1].previous = buttons[1].current;
  buttons[2].previous = buttons[2].current;
  buttons[3].previous = buttons[3].current;
}

void high(int row, int col) {
  strings[row - 1] = strings[row - 1] | leds[col - 1];
  softwareSPI(row, strings[row - 1]);
}

struct Snake snake;
struct BodyPart food;
int direction = 1;
boolean gameOverStatus = false;
int frequency = 200;
unsigned long prevTime = 0;

void setup() {
  displayInit();
  pinMode(2, INPUT); // up button
  pinMode(3, INPUT); // left button
  pinMode(4, INPUT); // right button
  pinMode(5, INPUT); // down button
  pinMode(9, OUTPUT); // buzzer
  snake.body[0] = {4, 4};
  snake.size = 1;
  food = {0, 0};
  generateFood(&food, snake);
}

void loop() {
  controller(&direction);
  if (millis() - prevTime > frequency) {
    prevTime = millis();
    if (gameOverStatus) {
      gameOver();
    }
    foodHandler(&snake, &food);
    moveSnake(&snake, direction);
    loseChecker(snake, &gameOverStatus);
    myClearDisplay();
    drawFood(food);
    drawSnake(snake);
  }
}