#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <Preferences.h>
#include <cstring>

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R2, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ 9, /* data=*/ 8);
Preferences preferences;

#define BTNU 0
#define BTNR 1
#define BTND 3
#define BTNL 10
#define BUZZER 7
const uint8_t ledcChannel = 0;
const uint16_t SCREEN_W = 128;
const uint16_t SCREEN_H = 64;
const uint8_t HUD_H = 8;
const uint8_t CELL = 4;
const uint8_t GRID_W = SCREEN_W / CELL;
const uint8_t GRID_H = (SCREEN_H - HUD_H) / CELL;
const uint16_t MAX_SNAKE = GRID_W * GRID_H;

const uint16_t BASE_STEP_MS = 170;
const uint16_t MIN_STEP_MS = 85;
const uint16_t SPEEDUP_PER_FOOD_MS = 5;
const uint16_t SPRINT_STEP_MS = 50;
const uint8_t SPRINT_HOLD_FRAMES = 5;

const uint16_t MIN_TONE_FREQ = 500;
const uint16_t MAX_TONE_FREQ = 2000;
const uint16_t MENU_DEBOUNCE_MS = 150;
const uint16_t NAME_ENTRY_DWELL_MS = 350;
const uint8_t LEADERBOARD_SIZE = 5;
const uint8_t NAME_LENGTH = 8;
const uint8_t LEADERBOARD_BOARDS = 3; // Easy, Medium, Hard

// Difficulty presets and custom bounds
const uint16_t DIFF_EASY_MS = 200;
const uint16_t DIFF_MEDIUM_MS = BASE_STEP_MS;
const uint16_t DIFF_HARD_MS = 130;
const uint16_t CUSTOM_MIN_MS = 85;
const uint16_t CUSTOM_MAX_MS = 300;
// Difficulty offsets and preset speedup-per-food values
const int16_t DIFF_OFFSET_EASY = 30;
const int16_t DIFF_OFFSET_MED = 0;
const int16_t DIFF_OFFSET_HARD = -40;
const uint16_t DIFF_SPEEDUP_EASY = 3;
const uint16_t DIFF_SPEEDUP_MED = SPEEDUP_PER_FOOD_MS;
const uint16_t DIFF_SPEEDUP_HARD = 7;

struct Cell {
  int8_t x;
  int8_t y;
};

enum Direction : uint8_t { UP, RIGHT, DOWN, LEFT };
enum GameState : uint8_t { START, PLAYING, GAME_OVER, SETTINGS, LEADERBOARD, NAME_ENTRY };

struct LeaderboardEntry {
  char name[NAME_LENGTH + 1];
  uint16_t score;
};

GameState gameState = START;
// toneFreq == 0 means buzzer OFF; otherwise valid range is 500..2000 in 100 Hz steps
uint16_t toneFreq = 1200;
bool isSprinting = false;
uint8_t sprintHoldU = 0, sprintHoldR = 0, sprintHoldD = 0, sprintHoldL = 0;
// settingsMenuIndex removed; using settingsScrollOffset and dynamic list
uint32_t lastSettingsInputMs = 0;
// Settings UI state: scroll offset, total items. Selector stays in middle of visible window.
uint8_t settingsScrollOffset = 0;
const uint8_t SETTINGS_VISIBLE = 3; // visible rows

// Difficulty selection
uint8_t difficultyPreset = 1; // 0=Easy,1=Medium,2=Hard,3=Custom
uint16_t customStepMs = BASE_STEP_MS;
// Sprint enable/disable (settings)
bool sprintAllowed = true;
// Custom advanced params
uint16_t customSpeedupPerFood = SPEEDUP_PER_FOOD_MS;
uint16_t customMinStepMs = MIN_STEP_MS;
uint16_t customMaxStepMs = DIFF_MEDIUM_MS * 2;

// Calculate dynamic settings total depending on whether custom step row is present
uint8_t settingsTotal() {
  // If custom difficulty selected, include extra rows: custom start, speedup, min, max
  if (difficultyPreset == 3) return 10; // blank, buzzer, sprint, difficulty, customStart, customSpeedup, customMin, customMax, back, blank
  return 6; // blank, buzzer, sprint, difficulty, back, blank
}

uint8_t leaderboardBoardIndexForDifficulty(uint8_t difficulty) {
  return (difficulty < LEADERBOARD_BOARDS) ? difficulty : 1;
}

uint8_t activeLeaderboardBoardIndex() {
  return leaderboardBoardIndexForDifficulty(difficultyPreset);
}

// NAME_ENTRY: require L+R release before next select
bool lrComboReleased = true;
LeaderboardEntry leaderboard[LEADERBOARD_BOARDS][LEADERBOARD_SIZE];
char pendingName[NAME_LENGTH + 1] = {0};
uint16_t pendingScore = 0;
uint8_t pendingLeaderboardBoard = 0;
uint8_t keyboardRow = 0;
uint8_t keyboardCol = 0;
Cell snake[MAX_SNAKE];
uint16_t snakeLen = 0;
Cell food = {0, 0};
Direction currentDir = RIGHT;
Direction nextDir = RIGHT;
uint16_t score = 0;
uint16_t highScore = 0;
uint32_t lastStepMs = 0;
uint32_t stateChangeMs = 0;
uint32_t inputIgnoreUntilMs = 0; // ignore button input until this millis() (used after name-entry OK)

void resetGame();

bool cellEquals(const Cell &first, const Cell &second) {
  return first.x == second.x && first.y == second.y;
}

bool isSnakeCell(const Cell &target) {
  for (uint16_t index = 0; index < snakeLen; ++index) {
    if (cellEquals(snake[index], target)) return true;
  }
  return false;
}

void initLeaderboardDefaults() {
  for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
    for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
      strncpy(leaderboard[board][index].name, "--------", NAME_LENGTH);
      leaderboard[board][index].name[NAME_LENGTH] = '\0';
      leaderboard[board][index].score = 0;
    }
  }
}

void loadLeaderboardBoard(uint8_t board) {
  char key[16];
  snprintf(key, sizeof(key), "leader%u", board);
  const size_t loadedBytes = preferences.getBytes(key, leaderboard[board], sizeof(leaderboard[board]));
  if (loadedBytes != sizeof(leaderboard[board])) {
    for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
      strncpy(leaderboard[board][index].name, "--------", NAME_LENGTH);
      leaderboard[board][index].name[NAME_LENGTH] = '\0';
      leaderboard[board][index].score = 0;
    }
  }
}

void saveLeaderboardBoard(uint8_t board) {
  char key[16];
  snprintf(key, sizeof(key), "leader%u", board);
  preferences.putBytes(key, leaderboard[board], sizeof(leaderboard[board]));
}

void loadHighScore() {
  preferences.begin("snake", true);
  const bool buzzerEnabledLegacy = preferences.getBool("buzzerEnabled", true);
  toneFreq = preferences.getUShort("toneFreq", 1200);
  difficultyPreset = preferences.getUShort("diffPreset", 1);
  customStepMs = preferences.getUShort("customStepMs", BASE_STEP_MS);
  customSpeedupPerFood = preferences.getUShort("customSpeedup", SPEEDUP_PER_FOOD_MS);
  customMinStepMs = preferences.getUShort("customMin", MIN_STEP_MS);
  customMaxStepMs = preferences.getUShort("customMax", DIFF_MEDIUM_MS * 2);
  sprintAllowed = preferences.getBool("sprintAllowed", true);
  initLeaderboardDefaults();
  for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
    loadLeaderboardBoard(board);
  }
  if (toneFreq != 0 && (toneFreq < MIN_TONE_FREQ || toneFreq > MAX_TONE_FREQ || ((toneFreq - MIN_TONE_FREQ) % 100) != 0)) {
    toneFreq = 1200;
  }
  if (!buzzerEnabledLegacy) {
    toneFreq = 0;
  }
  highScore = (difficultyPreset < 3) ? leaderboard[activeLeaderboardBoardIndex()][0].score : 0;
  preferences.end();
}

void saveHighScore() {
  preferences.begin("snake", false);
  preferences.putUShort("toneFreq", toneFreq);
  preferences.putUShort("diffPreset", difficultyPreset);
  preferences.putUShort("customStepMs", customStepMs);
  preferences.putUShort("customSpeedup", customSpeedupPerFood);
  preferences.putUShort("customMin", customMinStepMs);
  preferences.putUShort("customMax", customMaxStepMs);
  preferences.putBool("sprintAllowed", sprintAllowed);
  for (uint8_t board = 0; board < LEADERBOARD_BOARDS; ++board) {
    saveLeaderboardBoard(board);
  }
  if (difficultyPreset < 3) {
    preferences.putUShort("highScore", leaderboard[activeLeaderboardBoardIndex()][0].score);
  } else {
    preferences.putUShort("highScore", 0);
  }
  preferences.end();
}

bool qualifiesForLeaderboard(uint16_t scoreValue) {
  if (difficultyPreset >= 3) return false;
  const uint8_t board = activeLeaderboardBoardIndex();
  return scoreValue > leaderboard[board][LEADERBOARD_SIZE - 1].score ||
         leaderboard[board][LEADERBOARD_SIZE - 1].name[0] == '\0' ||
         leaderboard[board][LEADERBOARD_SIZE - 1].score == 0;
}

void syncHighScoreFromLeaderboard(uint8_t board) {
  highScore = leaderboard[board][0].score;
}

void insertLeaderboardEntry(uint8_t board, const char *name, uint16_t scoreValue) {
  LeaderboardEntry entry = {};
  strncpy(entry.name, name, NAME_LENGTH);
  entry.name[NAME_LENGTH] = '\0';
  entry.score = scoreValue;

  uint8_t insertAt = LEADERBOARD_SIZE;
  for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
    if (scoreValue > leaderboard[board][index].score) {
      insertAt = index;
      break;
    }
  }

  if (insertAt == LEADERBOARD_SIZE) {
    return;
  }

  for (int index = LEADERBOARD_SIZE - 1; index > static_cast<int>(insertAt); --index) {
    leaderboard[board][index] = leaderboard[board][index - 1];
  }
  leaderboard[board][insertAt] = entry;
  syncHighScoreFromLeaderboard(board);
}

void prepareNameEntry(uint16_t scoreValue) {
  pendingScore = scoreValue;
  memset(pendingName, 0, sizeof(pendingName));
  keyboardRow = 0;
  keyboardCol = 0;
  pendingLeaderboardBoard = activeLeaderboardBoardIndex();
  gameState = NAME_ENTRY;
}

void confirmNameEntry() {
  if (pendingName[0] == '\0') {
    strncpy(pendingName, "PLAYER", NAME_LENGTH);
    pendingName[NAME_LENGTH] = '\0';
  }
  insertLeaderboardEntry(pendingLeaderboardBoard, pendingName, pendingScore);
  saveHighScore();
  resetGame();
  gameState = LEADERBOARD;
  // Prevent accidental immediate input if OK was still held: ignore inputs for 500ms
  inputIgnoreUntilMs = millis() + 500;
  lastSettingsInputMs = millis();
}

const char *keyboardKeyLabel(uint8_t row, uint8_t col) {
  static const char *layout[4][7] = {
    {"A", "B", "C", "D", "E", "F", "G"},
    {"H", "I", "J", "K", "L", "M", "N"},
    {"O", "P", "Q", "R", "S", "T", "U"},
    {"V", "W", "X", "Y", "Z", "DEL", "OK"}
  };
  return layout[row][col];
}

void moveKeyboardCursor(int8_t rowDelta, int8_t colDelta) {
  keyboardRow = (keyboardRow + 4 + rowDelta) % 4;
  keyboardCol = (keyboardCol + 7 + colDelta) % 7;
}

void applyKeyboardSelection() {
  const char *label = keyboardKeyLabel(keyboardRow, keyboardCol);
  if (strcmp(label, "DEL") == 0) {
    if (strlen(pendingName) > 0) {
      pendingName[strlen(pendingName) - 1] = '\0';
    }
  } else if (strcmp(label, "OK") == 0) {
    confirmNameEntry();
  } else if (strlen(pendingName) < NAME_LENGTH) {
    const size_t currentLength = strlen(pendingName);
    pendingName[currentLength] = label[0];
    pendingName[currentLength + 1] = '\0';
  }
}

void toneMs(uint16_t hz, uint16_t durationMs) {
  if (toneFreq == 0) return;
  ledcWriteTone(ledcChannel, hz);
  delay(durationMs);
  ledcWriteTone(ledcChannel, 0);
}

uint16_t buzzerCycleUp(uint16_t current) {
  if (current == 0) return MIN_TONE_FREQ;
  if (current < MAX_TONE_FREQ) return current + 100;
  return 0;
}

uint16_t buzzerCycleDown(uint16_t current) {
  if (current == 0) return MAX_TONE_FREQ;
  if (current > MIN_TONE_FREQ) return current - 100;
  return 0;
}

void spawnFood() {
  Cell candidate;
  do {
    candidate.x = random(0, GRID_W);
    candidate.y = random(0, GRID_H);
  } while (isSnakeCell(candidate));
  food = candidate;
}

void resetGame() {
  score = 0;
  currentDir = RIGHT;
  nextDir = RIGHT;
  gameState = START;
  stateChangeMs = millis();

  snakeLen = 3;
  const int8_t centerX = GRID_W / 2;
  const int8_t centerY = GRID_H / 2;
  snake[0] = {centerX, centerY};
  snake[1] = {static_cast<int8_t>(centerX - 1), centerY};
  snake[2] = {static_cast<int8_t>(centerX - 2), centerY};

  spawnFood();
  lastStepMs = millis();
}

bool anyButtonPressed() {
  return digitalRead(BTNU) == LOW ||
         digitalRead(BTNR) == LOW ||
         digitalRead(BTND) == LOW ||
         digitalRead(BTNL) == LOW;
}

void updateDirectionFromButtons() {
  if (digitalRead(BTNU) == LOW && currentDir != DOWN) {
    nextDir = UP;
    sprintHoldU++;
  } else {
    sprintHoldU = 0;
  }
  
  if (digitalRead(BTNR) == LOW && currentDir != LEFT) {
    nextDir = RIGHT;
    sprintHoldR++;
  } else {
    sprintHoldR = 0;
  }
  
  if (digitalRead(BTND) == LOW && currentDir != UP) {
    nextDir = DOWN;
    sprintHoldD++;
  } else {
    sprintHoldD = 0;
  }
  
  if (digitalRead(BTNL) == LOW && currentDir != RIGHT) {
    nextDir = LEFT;
    sprintHoldL++;
  } else {
    sprintHoldL = 0;
  }

  isSprinting = sprintAllowed && ((sprintHoldU >= SPRINT_HOLD_FRAMES) || 
                (sprintHoldR >= SPRINT_HOLD_FRAMES) ||
                (sprintHoldD >= SPRINT_HOLD_FRAMES) || 
                (sprintHoldL >= SPRINT_HOLD_FRAMES));
}

uint16_t stepIntervalMs() {
  if (isSprinting) return SPRINT_STEP_MS;
  int32_t baseMs = BASE_STEP_MS;
  uint16_t curSpeedup = SPEEDUP_PER_FOOD_MS;
  uint16_t minStep = MIN_STEP_MS;
  uint16_t maxStep = BASE_STEP_MS * 2;
  switch (difficultyPreset) {
    case 0:
      baseMs = BASE_STEP_MS + DIFF_OFFSET_EASY;
      curSpeedup = DIFF_SPEEDUP_EASY;
      break;
    case 1:
      baseMs = BASE_STEP_MS + DIFF_OFFSET_MED;
      curSpeedup = DIFF_SPEEDUP_MED;
      break;
    case 2:
      baseMs = BASE_STEP_MS + DIFF_OFFSET_HARD;
      curSpeedup = DIFF_SPEEDUP_HARD;
      break;
    case 3:
      baseMs = customStepMs;
      curSpeedup = customSpeedupPerFood;
      minStep = customMinStepMs;
      maxStep = customMaxStepMs;
      break;
  }

  if (baseMs < (int32_t)minStep) baseMs = minStep;
  if (baseMs > (int32_t)maxStep) baseMs = maxStep;

  const uint32_t speedDrop = (uint32_t)score * (uint32_t)curSpeedup;
  if ((uint32_t)baseMs <= (uint32_t)minStep + speedDrop) return minStep;
  int32_t result = (int32_t)baseMs - (int32_t)speedDrop;
  if (result < (int32_t)minStep) return minStep;
  return (uint16_t)result;
}

Cell nextHeadCell() {
  Cell head = snake[0];
  switch (currentDir) {
    case UP:    --head.y; break;
    case RIGHT: ++head.x; break;
    case DOWN:  ++head.y; break;
    case LEFT:  --head.x; break;
  }
  return head;
}

bool outOfBounds(const Cell &cell) {
  return cell.x < 0 || cell.x >= GRID_W || cell.y < 0 || cell.y >= GRID_H;
}

bool hitsSnakeBody(const Cell &cell) {
  for (uint16_t index = 0; index < snakeLen; ++index) {
    if (cellEquals(snake[index], cell)) return true;
  }
  return false;
}

void runGameStep() {
  currentDir = nextDir;
  const Cell newHead = nextHeadCell();

  if (outOfBounds(newHead) || hitsSnakeBody(newHead)) {
    stateChangeMs = millis();
    if (qualifiesForLeaderboard(score)) {
      prepareNameEntry(score);
    } else {
      gameState = GAME_OVER;
    }
    toneMs(330, 120);
    toneMs(220, 180);
    return;
  }

  const bool ateFood = cellEquals(newHead, food);
  const uint16_t oldLen = snakeLen;
  uint16_t newLen = oldLen;
  if (ateFood && snakeLen < MAX_SNAKE) newLen = oldLen + 1;

  for (uint16_t index = newLen - 1; index > 0; --index) {
    snake[index] = snake[index - 1];
  }
  snake[0] = newHead;
  snakeLen = newLen;

  if (ateFood) {
    const uint16_t pointsEarned = isSprinting ? 2 : 1;
    score += pointsEarned;
    toneMs(toneFreq, 35);
    spawnFood();
  }
}

void drawGame() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_5x8_tr);

  char hudText[32];
  if (isSprinting) {
    snprintf(hudText, sizeof(hudText), "S:%u  B:%u       [SPRINT]", score, highScore);
  } else {
    snprintf(hudText, sizeof(hudText), "S:%u  B:%u", score, highScore);
  }
  u8g2.drawStr(0, 7, hudText);

  for (uint16_t index = 0; index < snakeLen; ++index) {
    const int16_t x = snake[index].x * CELL;
    const int16_t y = HUD_H + snake[index].y * CELL;
    u8g2.drawBox(x, y, CELL, CELL);
  }

  const int16_t foodX = food.x * CELL;
  const int16_t foodY = HUD_H + food.y * CELL;
  u8g2.drawFrame(foodX, foodY, CELL, CELL);

  u8g2.sendBuffer();
}

void drawStartScreen() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(32, 20, "SNAKE");
  
  u8g2.setFont(u8g2_font_4x6_tr);
  u8g2.drawStr(26, 40, "START: UP");
  u8g2.drawStr(20, 50, "SETTINGS: RIGHT");
  if (difficultyPreset == 3) {
    u8g2.drawStr(4, 60, "NOT AVAILABLE IN CUSTOM");
  } else {
    u8g2.drawStr(14, 60, "LEADERBOARD: LEFT");
  }
  
  u8g2.sendBuffer();
}

void drawGameOverScreen() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(28, 20, "GAME OVER");
  
  u8g2.setFont(u8g2_font_5x8_tr);
  char scoreText[24];
  snprintf(scoreText, sizeof(scoreText), "Score: %u", score);
  u8g2.drawStr(28, 35, scoreText);
  
  char bestText[24];
  snprintf(bestText, sizeof(bestText), "Best: %u", highScore);
  u8g2.drawStr(28, 48, bestText);
  
  u8g2.drawStr(18, 62, "Press any button");
  
  u8g2.sendBuffer();
}

void drawLeaderboardScreen() {
  const uint8_t board = activeLeaderboardBoardIndex();
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  // char title[28];
  const char *difficultyNames[3] = {"EASY", "MED ", "HARD"};
  // snprintf(title, sizeof(title), "LEADERBOARD - %s", difficultyNames[board]);
  u8g2.drawStr(6, 10, "LEADERBOARD");

  char ch[2] = {0, 0}; // Reusable 1-char string buffer

  if (board == 1) {
    ch[0] = difficultyNames[board][0]; u8g2.drawStr(93, 26, ch);
    ch[0] = difficultyNames[board][1]; u8g2.drawStr(98, 36, ch);
    ch[0] = difficultyNames[board][2]; u8g2.drawStr(103, 46, ch);

  } else {
    ch[0] = difficultyNames[board][0]; u8g2.drawStr(90, 22, ch);
    ch[0] = difficultyNames[board][1]; u8g2.drawStr(95, 32, ch);
    ch[0] = difficultyNames[board][2]; u8g2.drawStr(100, 42, ch);
    ch[0] = difficultyNames[board][3]; u8g2.drawStr(105, 52, ch);
  };

  u8g2.setFont(u8g2_font_5x8_tr);
  for (uint8_t index = 0; index < LEADERBOARD_SIZE; ++index) {
    char line[24];
    snprintf(line, sizeof(line), "%u. %-8s %3u", index + 1, leaderboard[board][index].name, leaderboard[board][index].score);
    u8g2.drawStr(4, 20 + index * 8, line);
  }

  u8g2.setFont(u8g2_font_4x6_tr);
  u8g2.drawStr(85, 64, "Back ----->");
  u8g2.sendBuffer();
}

void drawNameEntryScreen() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_5x8_tr);
  u8g2.drawStr(2, 8, "NEW RECORD - TYPE NAME");

  char scoreLine[24];
  snprintf(scoreLine, sizeof(scoreLine), "Score: %u", pendingScore);
  u8g2.drawStr(2, 16, scoreLine);

  char nameLine[24];
  snprintf(nameLine, sizeof(nameLine), "Name: %-8s", pendingName);
  u8g2.drawStr(2, 24, nameLine);

  u8g2.setFont(u8g2_font_5x8_tr);
  const uint8_t gridTop = 30;
  const uint8_t cellW = 18;
  const uint8_t cellH = 8;

  for (uint8_t row = 0; row < 4; ++row) {
    for (uint8_t col = 0; col < 7; ++col) {
      const uint8_t x = col * cellW;
      const uint8_t y = gridTop + row * cellH;
      const char *label = keyboardKeyLabel(row, col);
      const bool selected = (row == keyboardRow && col == keyboardCol);
      if (selected) {
        u8g2.drawBox(x + 1, y - 7, cellW - 2, cellH - 1);
        u8g2.setDrawColor(0);
      }
      u8g2.drawStr(x + 5, y, label);
      if (selected) {
        u8g2.setDrawColor(1);
      }
    }
  }

  u8g2.setFont(u8g2_font_4x6_tr);
  u8g2.drawStr(2, 62, "Move: arrows   Select: L+R");
  u8g2.sendBuffer();
}

void drawSettingsScreen() {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(26, 16, "SETTINGS");
  
  u8g2.setFont(u8g2_font_5x8_tr);
  // Render a small scroll window (3 rows) with selector fixed in the middle
  const uint8_t total = settingsTotal();
  for (uint8_t row = 0; row < SETTINGS_VISIBLE; ++row) {
    const uint8_t itemIndex = settingsScrollOffset + row;
    const uint8_t y = 28 + row * 10;
    bool highlight = (row == (SETTINGS_VISIBLE / 2));
    if (highlight) {
      u8g2.drawBox(4, y - 8, 120, 10);
      u8g2.setDrawColor(0);
    }

    char line[40] = {0};
    if (itemIndex == 0 || itemIndex == (total - 1)) {
      // padding
    } else {
      if (total == 6) {
        // 0 blank,1 buzzer,2 sprint,3 difficulty,4 back,5 blank
        if (itemIndex == 1) {
          if (toneFreq == 0) snprintf(line, sizeof(line), "Buzzer: OFF");
          else snprintf(line, sizeof(line), "Buzzer: %u Hz", toneFreq);
        } else if (itemIndex == 2) {
          snprintf(line, sizeof(line), "Sprinting: %s", sprintAllowed ? "ON" : "OFF");
        } else if (itemIndex == 3) {
          const char *names[4] = {"EASY", "MED", "HARD", "CUSTOM"};
          snprintf(line, sizeof(line), "Difficulty: %s", names[difficultyPreset]);
        } else if (itemIndex == 4) {
          snprintf(line, sizeof(line), "Back (save)");
        }
      } else {
        // total == 10 => 0 blank,1 buzzer,2 sprint,3 difficulty,4 customStart,5 customSpeedup,6 customMin,7 customMax,8 back,9 blank
        if (itemIndex == 1) {
          if (toneFreq == 0) snprintf(line, sizeof(line), "Buzzer: OFF");
          else snprintf(line, sizeof(line), "Buzzer: %u Hz", toneFreq);
        } else if (itemIndex == 2) {
          snprintf(line, sizeof(line), "Sprinting: %s", sprintAllowed ? "ON" : "OFF");
        } else if (itemIndex == 3) {
          const char *names[4] = {"EASY", "MED", "HARD", "CUSTOM"};
          snprintf(line, sizeof(line), "Difficulty: %s", names[difficultyPreset]);
        } else if (itemIndex == 4) {
          snprintf(line, sizeof(line), " Starting speed: %ums", customStepMs);
        } else if (itemIndex == 5) {
          snprintf(line, sizeof(line), " Speedup: %u ms/food", customSpeedupPerFood);
        } else if (itemIndex == 6) {
          snprintf(line, sizeof(line), " Min: %ums", customMinStepMs);
        } else if (itemIndex == 7) {
          snprintf(line, sizeof(line), " Max: %ums", customMaxStepMs);
        } else if (itemIndex == 8) {
          snprintf(line, sizeof(line), "Back (save)");
        }
      }
    }

    u8g2.drawStr(8, y, line);
    if (highlight) {
      u8g2.setDrawColor(1);
    }
  }

  // Help line (small)
  u8g2.setFont(u8g2_font_4x6_tr);
  u8g2.drawStr(2, 64, "UP/DN: Move  L/R: Select/Edit");
  
  u8g2.sendBuffer();
}

void setup() {
  pinMode(BTNU, INPUT_PULLUP);
  pinMode(BTNR, INPUT_PULLUP);
  pinMode(BTND, INPUT_PULLUP);
  pinMode(BTNL, INPUT_PULLUP);
  pinMode(BUZZER, OUTPUT);

  ledcSetup(ledcChannel, 2000, 8);
  ledcAttachPin(BUZZER, ledcChannel);

  u8g2.begin();
  randomSeed(micros());
  loadHighScore();
  resetGame();
}

void loop() {
  updateDirectionFromButtons();

  switch (gameState) {
    case START: {
      const uint32_t now = millis();

      if (now >= inputIgnoreUntilMs && digitalRead(BTNL) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
        if (difficultyPreset < 3) {
          lastSettingsInputMs = now;
          gameState = LEADERBOARD;
        }
      } else if (now >= inputIgnoreUntilMs && digitalRead(BTNR) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
        lastSettingsInputMs = now;
        gameState = SETTINGS;
        // prepare scroll so the first selectable item (Buzzer) is under the selector
        settingsScrollOffset = 0;
      } else if (now >= inputIgnoreUntilMs && digitalRead(BTNU) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
        // Reset the game state before starting to avoid resuming a dead snake
        resetGame();
        // Now enter playing state
        lastSettingsInputMs = now;
        delay(60);
        isSprinting = false;
        sprintHoldU = sprintHoldR = sprintHoldD = sprintHoldL = 0;
        gameState = PLAYING;
        stateChangeMs = now;
        lastStepMs = now;
      }

      drawStartScreen();
      break;
    }

    case SETTINGS: {
      const uint32_t now = millis();

      // Debounce: only process input after a short delay
      if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
        if (digitalRead(BTNU) == LOW) {
          // UP: scroll up (if possible)
          if (settingsScrollOffset > 0) settingsScrollOffset--;
          lastSettingsInputMs = now;
        } else if (digitalRead(BTND) == LOW) {
          // DOWN: scroll down (if possible)
          if (settingsScrollOffset + SETTINGS_VISIBLE < settingsTotal()) settingsScrollOffset++;
          lastSettingsInputMs = now;
        } else if (digitalRead(BTNL) == LOW) {
          // LEFT: edit focused item
          const uint8_t focused = settingsScrollOffset + (SETTINGS_VISIBLE / 2);
          const uint8_t total = settingsTotal();
          if (focused == 1) {
            // Buzzer cycle backward: OFF <- 500 <- ... <- 2000
            toneFreq = buzzerCycleDown(toneFreq);
          } else if (focused == 2) {
            // Sprint toggle
            sprintAllowed = !sprintAllowed;
          } else if (focused == 3) {
            // Cycle difficulty presets backward
            difficultyPreset = (difficultyPreset + 3) % 4;
            if (difficultyPreset < 3) {
              highScore = leaderboard[activeLeaderboardBoardIndex()][0].score;
            } else {
              highScore = 0;
            }
          } else if (focused >= 4 && focused <= 7 && total == 10) {
            // Custom fields (decrease)
            if (focused == 4) {
              if (customStepMs > CUSTOM_MIN_MS) customStepMs -= 10;
            } else if (focused == 5) {
              if (customSpeedupPerFood > 1) customSpeedupPerFood -= 1;
            } else if (focused == 6) {
              if (customMinStepMs > CUSTOM_MIN_MS) customMinStepMs -= 5;
            } else if (focused == 7) {
              if (customMaxStepMs > customMinStepMs + 5) customMaxStepMs -= 5;
            }
          } else if ((focused == 4 && total == 6) || (focused == 8 && total == 10)) {
            // Back
            saveHighScore();
            gameState = START;
            resetGame();
          }
          lastSettingsInputMs = now;
        } else if (digitalRead(BTNR) == LOW) {
          // RIGHT: edit focused item
          const uint8_t focused = settingsScrollOffset + (SETTINGS_VISIBLE / 2);
          const uint8_t total = settingsTotal();
          if (focused == 1) {
            // Buzzer cycle forward: OFF -> 500 -> ... -> 2000 -> OFF
            toneFreq = buzzerCycleUp(toneFreq);
          } else if (focused == 2) {
            // Sprint toggle (also allow right to toggle)
            sprintAllowed = !sprintAllowed;
          } else if (focused == 3) {
            // Cycle difficulty presets forward
            difficultyPreset = (difficultyPreset + 1) % 4;
            if (difficultyPreset < 3) {
              highScore = leaderboard[activeLeaderboardBoardIndex()][0].score;
            } else {
              highScore = 0;
            }
          } else if (focused >= 4 && focused <= 7 && total == 10) {
            // Custom fields (increase)
            if (focused == 4) {
              if (customStepMs < CUSTOM_MAX_MS) customStepMs += 10;
            } else if (focused == 5) {
              if (customSpeedupPerFood < 255) customSpeedupPerFood += 1;
            } else if (focused == 6) {
              if (customMinStepMs < customMaxStepMs - 5) customMinStepMs += 5;
            } else if (focused == 7) {
              if (customMaxStepMs < CUSTOM_MAX_MS) customMaxStepMs += 5;
            }
          } else if ((focused == 4 && total == 6) || (focused == 8 && total == 10)) {
            // Back
            saveHighScore();
            gameState = START;
            resetGame();
          }
          lastSettingsInputMs = now;
        }
      }

      drawSettingsScreen();
      break;
    }

    case LEADERBOARD: {
      const uint32_t now = millis();
      if (now >= inputIgnoreUntilMs && digitalRead(BTNR) == LOW && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
        lastSettingsInputMs = now;
        gameState = START;
        settingsScrollOffset = 0;
      }

      drawLeaderboardScreen();
      break;
    }

    case NAME_ENTRY: {
      const uint32_t now = millis();

      // Require both L+R to be released before allowing another combo press
      if (digitalRead(BTNL) == HIGH && digitalRead(BTNR) == HIGH) {
        lrComboReleased = true;
      }

      // Check L+R combo first (takes priority over movement)
      if (now >= inputIgnoreUntilMs && digitalRead(BTNL) == LOW && digitalRead(BTNR) == LOW && lrComboReleased && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
        // latch until both released to avoid repeats
        applyKeyboardSelection();
        lrComboReleased = false;
        lastSettingsInputMs = now;
      } else if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS)) {
        // Then check individual movement buttons
        if (digitalRead(BTNU) == LOW) {
          moveKeyboardCursor(-1, 0);
          lastSettingsInputMs = now;
        } else if (digitalRead(BTND) == LOW) {
          moveKeyboardCursor(1, 0);
          lastSettingsInputMs = now;
        } else if (digitalRead(BTNL) == LOW) {
          moveKeyboardCursor(0, -1);
          lastSettingsInputMs = now;
        } else if (digitalRead(BTNR) == LOW) {
          moveKeyboardCursor(0, 1);
          lastSettingsInputMs = now;
        }
      }

      drawNameEntryScreen();
      break;
    }

    case PLAYING: {
      const uint32_t now = millis();
      if (now - lastStepMs >= stepIntervalMs()) {
        lastStepMs = now;
        runGameStep();
      }
      drawGame();
      break;
    }

    case GAME_OVER: {
      const uint32_t now = millis();
      // Any button press returns to start/reset the game (debounced)
      if (now >= inputIgnoreUntilMs && (now - lastSettingsInputMs >= MENU_DEBOUNCE_MS) && anyButtonPressed()) {
        lastSettingsInputMs = now;
        delay(60);
        isSprinting = false;
        sprintHoldU = sprintHoldR = sprintHoldD = sprintHoldL = 0;
        resetGame();
      }
      drawGameOverScreen();
      break;
    }
  }

  delay(8);
}