﻿#include <TFT_eSPI.h>
#include <SPI.h>
#include <SD.h>
#include <math.h>


TFT_eSPI tft = TFT_eSPI();
SPIClass sdSPI(HSPI);


// Replace with your own calibration values if different
uint16_t calData[5] = { 325, 3521, 291, 3460, 7 };


// -------------------- Screen/Layout --------------------
const int SCREEN_W = 480;
const int SCREEN_H = 320;


const int DRAW_AREA_X = 10;
const int DRAW_AREA_Y = 10;
const int DRAW_AREA_W = 460;
const int DRAW_AREA_H = 240;


// -------------------- SD Pins --------------------------
const int SD_CS   = 5;
const int SD_MOSI = 23;
const int SD_MISO = 19;
const int SD_SCK  = 18;


// -------------------- State ----------------------------
bool sdReady = false;
bool galleryActive = false;


struct Button {
  int x;
  int y;
  int w;
  int h;
  uint16_t color;
  const char* label;
};


Button btnMode    = {  10, 270,  86, 40, TFT_BLUE,    "MODE"  };
Button btnReset   = { 104, 270,  86, 40, TFT_RED,     "RESET" };
Button btnSave    = { 198, 270,  86, 40, TFT_GREEN,   "FAV"   };
Button btnGallery = { 292, 270,  86, 40, tft.color565(255, 165, 0), "GAL"   };
Button btnSpeed   = { 386, 270,  84, 40, TFT_MAGENTA, "SPEED" };


Button btnBack    = {  10, 270,  86, 40, TFT_BLUE,    "BACK"  };
Button btnPrev    = { 104, 270,  86, 40, TFT_RED,     "PREV"  };
Button btnLoad    = { 198, 270,  86, 40, TFT_GREEN,   "LOAD"  };
Button btnNext    = { 292, 270,  86, 40, tft.color565(255, 165, 0), "NEXT"  };
Button btnRefresh = { 386, 270,  84, 40, TFT_MAGENTA, "SCAN"  };


int modeIndex = 0;
int speedIndex = 1;


const char* modeNames[]  = {
  "Pi Walker",
  "Pi Chords",
  "Pi Bloom",
  "Pi Grid",
  "Pi Spiral",
  "Pi Wave"
};


const char* speedNames[] = {
  "Slow",
  "Normal",
  "Fast"
};


// -------------------- Pi Digits ------------------------
const char piDigits[] =
"3141592653589793238462643383279502884197169399375105820974944592"
"3078164062862089986280348253421170679821480865132823066470938446"
"0955058223172535940812848111745028410270193852110555964462294895"
"4930381964428810975665933446128475648233786783165271201909145648"
"5669234603486104543266482133936072602491412737245870066063155881"
"7488152092096282925409171536436789259036001133053054882046652138"
"4146951941511609433057270365759591953092186117381932611793105118"
"5480744623799627495673518857527248912279381830119491298336733624"
"4065664308602139494639522473719070217986094370277053921717629317";


const int PI_LEN = sizeof(piDigits) - 1;
int piIndex = 0;


// -------------------- Mode State -----------------------
float walkerX;
float walkerY;
float walkerAngle = 0.0;


float bloomRotationOffset = 0.0;


const int GRID_CELL = 10;
const int GRID_START_X = 20;
const int GRID_START_Y = 60;
const int GRID_COLS = 44;
const int GRID_ROWS = 17;
int gridCellIndex = 0;


float spiralAngle = 0.0;
float spiralRadius = 2.0;
int spiralLastX = 0;
int spiralLastY = 0;
float spiralTurnJitter = 0.0;
float spiralGrowthJitter = 0.0;


int waveX = 20;
int waveLastX = 20;
int waveLastY = 140;
int waveMidY = 140;
int waveAmplitude = 70;
int waveStepSize = 6;


// -------------------- Gallery State --------------------
const int MAX_GALLERY_FILES = 200;
String galleryFiles[MAX_GALLERY_FILES];
int galleryFileCount = 0;
int galleryPage = 0;
int gallerySelectedIndex = -1;


// -------------------- Helpers --------------------------
int speedToSteps() {
  if (speedIndex == 0) return 1;
  if (speedIndex == 1) return 3;
  return 6;
}


String getPiWindow(int startIndex, int count) {
  String s = "";
  for (int i = 0; i < count; i++) {
    int idx = (startIndex + i) % PI_LEN;
    s += piDigits[idx];
  }
  return s;
}


int getNextDigit() {
  int digit = piDigits[piIndex] - '0';
  piIndex++;
  if (piIndex >= PI_LEN) piIndex = 0;
  return digit;
}


uint16_t colorForDigit(int digit) {
  switch (digit) {
    case 0: return TFT_RED;
    case 1: return tft.color565(255, 140, 0);
    case 2: return TFT_YELLOW;
    case 3: return TFT_GREEN;
    case 4: return TFT_CYAN;
    case 5: return TFT_BLUE;
    case 6: return TFT_MAGENTA;
    case 7: return tft.color565(255, 105, 180);
    case 8: return TFT_WHITE;
    case 9: return TFT_LIGHTGREY;
    default: return TFT_WHITE;
  }
}


uint16_t randomBrightColor() {
  uint8_t r = random(80, 255);
  uint8_t g = random(80, 255);
  uint8_t b = random(80, 255);
  return tft.color565(r, g, b);
}


void drawButton(Button b) {
  tft.fillRoundRect(b.x, b.y, b.w, b.h, 8, b.color);
  tft.drawRoundRect(b.x, b.y, b.w, b.h, 8, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, b.color);
  tft.setTextDatum(MC_DATUM);
  tft.drawString(b.label, b.x + b.w / 2, b.y + b.h / 2, 2);
}


bool touchedButton(Button b, uint16_t tx, uint16_t ty) {
  return (tx >= b.x && tx <= (b.x + b.w) && ty >= b.y && ty <= (b.y + b.h));
}


// -------------------- UI -------------------------------
void drawButtons() {
  tft.fillRect(0, 260, SCREEN_W, 60, TFT_BLACK);
  drawButton(btnMode);
  drawButton(btnReset);
  drawButton(btnSave);
  drawButton(btnGallery);
  drawButton(btnSpeed);
}


void drawGalleryButtons() {
  tft.fillRect(0, 260, SCREEN_W, 60, TFT_BLACK);
  drawButton(btnBack);
  drawButton(btnPrev);
  drawButton(btnLoad);
  drawButton(btnNext);
  drawButton(btnRefresh);
}


void drawStatusBar() {
  tft.fillRect(12, 12, 456, 42, TFT_BLACK);


  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(TL_DATUM);
  tft.drawString(modeNames[modeIndex], 18, 14, 2);


  String rightText = "Speed: ";
  rightText += speedNames[speedIndex];
  if (!sdReady) rightText += "  SD:OFF";
  tft.drawRightString(rightText, 462, 14, 2);


  String indexText = "Index: " + String(piIndex);
  String digitText = "Digits: " + getPiWindow(piIndex, 10);


  tft.drawString(indexText, 18, 34, 2);
  tft.drawRightString(digitText, 462, 34, 2);
}


void drawFrame() {
  tft.fillScreen(TFT_BLACK);
  tft.drawRoundRect(DRAW_AREA_X, DRAW_AREA_Y, DRAW_AREA_W, DRAW_AREA_H, 10, TFT_WHITE);
  drawStatusBar();
  drawButtons();
}


void showToast(const String &msg, uint16_t bg) {
  tft.fillRoundRect(120, 110, 240, 40, 8, bg);
  tft.drawRoundRect(120, 110, 240, 40, 8, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, bg);
  tft.setTextDatum(MC_DATUM);
  tft.drawString(msg, 240, 130, 2);
  delay(700);
  drawStatusBar();
}


// -------------------- Guides ---------------------------
void resetWalker() {
  walkerX = DRAW_AREA_X + DRAW_AREA_W / 2;
  walkerY = DRAW_AREA_Y + DRAW_AREA_H / 2;
  walkerAngle = 0.0;
}


void drawChordGuide() {
  int cx = DRAW_AREA_X + DRAW_AREA_W / 2;
  int cy = DRAW_AREA_Y + 135;
  int r = 85;


  tft.drawCircle(cx, cy, r, TFT_DARKGREY);


  for (int i = 0; i < 10; i++) {
    float angle = ((360.0 / 10.0) * i - 90.0) * 0.0174532925;
    int px = cx + cos(angle) * r;
    int py = cy + sin(angle) * r;


    tft.fillCircle(px, py, 2, TFT_DARKGREY);
    tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
    tft.setTextDatum(MC_DATUM);
    tft.drawString(String(i), cx + cos(angle) * (r + 14), cy + sin(angle) * (r + 14), 2);
  }
}


void drawGridGuide() {
  for (int c = 0; c <= GRID_COLS; c++) {
    int x = GRID_START_X + c * GRID_CELL;
    tft.drawLine(x, GRID_START_Y, x, GRID_START_Y + GRID_ROWS * GRID_CELL, TFT_DARKGREY);
  }


  for (int r = 0; r <= GRID_ROWS; r++) {
    int y = GRID_START_Y + r * GRID_CELL;
    tft.drawLine(GRID_START_X, y, GRID_START_X + GRID_COLS * GRID_CELL, y, TFT_DARKGREY);
  }
}


void resetSpiral() {
  int cx = DRAW_AREA_X + DRAW_AREA_W / 2;
  int cy = DRAW_AREA_Y + 140;


  spiralAngle = random(0, 360);
  spiralRadius = random(2, 8);
  spiralTurnJitter = random(-20, 21) / 10.0;
  spiralGrowthJitter = random(-10, 11) / 100.0;


  spiralLastX = cx + cos((spiralAngle - 90.0) * 0.0174532925) * spiralRadius;
  spiralLastY = cy + sin((spiralAngle - 90.0) * 0.0174532925) * spiralRadius;
}


void resetWave() {
  waveX = 20;
  waveLastX = 20;
  waveMidY = random(110, 181);
  waveAmplitude = random(45, 91);
  waveStepSize = random(4, 8);
  waveLastY = waveMidY;
}


void resetMode() {
  piIndex = 0;
  bloomRotationOffset = 0.0;
  gridCellIndex = 0;


  drawFrame();


  if (modeIndex == 0) {
    resetWalker();
    tft.fillCircle((int)walkerX, (int)walkerY, 2, TFT_WHITE);
  } else if (modeIndex == 1) {
    drawChordGuide();
  } else if (modeIndex == 3) {
    drawGridGuide();
  } else if (modeIndex == 4) {
    resetSpiral();
  } else if (modeIndex == 5) {
    resetWave();
  }
}


// -------------------- Drawing Modes --------------------
void stepPiWalker() {
  int digit = getNextDigit();


  walkerAngle += digit * 36.0;
  float radiansAngle = walkerAngle * 0.0174532925;
  float stepSize = 6.0;


  float newX = walkerX + cos(radiansAngle) * stepSize;
  float newY = walkerY + sin(radiansAngle) * stepSize;


  if (newX < DRAW_AREA_X + 2 || newX > DRAW_AREA_X + DRAW_AREA_W - 2 ||
      newY < DRAW_AREA_Y + 50 || newY > DRAW_AREA_Y + DRAW_AREA_H - 2) {
    walkerAngle += 180.0;
    radiansAngle = walkerAngle * 0.0174532925;
    newX = walkerX + cos(radiansAngle) * stepSize;
    newY = walkerY + sin(radiansAngle) * stepSize;
  }


  tft.drawLine((int)walkerX, (int)walkerY, (int)newX, (int)newY, colorForDigit(digit));
  walkerX = newX;
  walkerY = newY;
}


void stepPiChords() {
  int digitA = getNextDigit();
  int digitB = getNextDigit();


  int cx = DRAW_AREA_X + DRAW_AREA_W / 2;
  int cy = DRAW_AREA_Y + 135;
  int r = 85;


  float angleA = ((360.0 / 10.0) * digitA - 90.0) * 0.0174532925;
  float angleB = ((360.0 / 10.0) * digitB - 90.0) * 0.0174532925;


  int x1 = cx + cos(angleA) * r;
  int y1 = cy + sin(angleA) * r;
  int x2 = cx + cos(angleB) * r;
  int y2 = cy + sin(angleB) * r;


  tft.drawLine(x1, y1, x2, y2, colorForDigit(digitA));
}


void stepPiBloom() {
  int digit = getNextDigit();


  int cx = DRAW_AREA_X + DRAW_AREA_W / 2;
  int cy = DRAW_AREA_Y + DRAW_AREA_H / 2 + 10;


  float angle = (digit * 36.0 + bloomRotationOffset) * 0.0174532925;
  int len = 30 + (digit * 12);


  int x2 = cx + cos(angle) * len;
  int y2 = cy + sin(angle) * len;


  tft.drawLine(cx, cy, x2, y2, randomBrightColor());
  tft.fillCircle(x2, y2, 2 + (digit % 3), colorForDigit(digit));


  bloomRotationOffset += 7.0;
  if (bloomRotationOffset >= 360.0) bloomRotationOffset -= 360.0;
}


void stepPiGrid() {
  int digit = getNextDigit();


  int col = gridCellIndex % GRID_COLS;
  int row = gridCellIndex / GRID_COLS;


  int x = GRID_START_X + col * GRID_CELL + 1;
  int y = GRID_START_Y + row * GRID_CELL + 1;


  tft.fillRect(x, y, GRID_CELL - 1, GRID_CELL - 1, colorForDigit(digit));


  gridCellIndex++;
  if (gridCellIndex >= GRID_COLS * GRID_ROWS) {
    gridCellIndex = 0;
    drawFrame();
    drawGridGuide();
  }
}


void stepPiSpiral() {
  int digit = getNextDigit();


  int cx = DRAW_AREA_X + DRAW_AREA_W / 2;
  int cy = DRAW_AREA_Y + 140;


  spiralAngle += (digit * 8.0) + spiralTurnJitter;
  spiralRadius += 0.8 + (digit * 0.12) + spiralGrowthJitter;


  int x = cx + cos((spiralAngle - 90.0) * 0.0174532925) * spiralRadius;
  int y = cy + sin((spiralAngle - 90.0) * 0.0174532925) * spiralRadius;


  tft.drawLine(spiralLastX, spiralLastY, x, y, colorForDigit(digit));


  spiralLastX = x;
  spiralLastY = y;


  if (x < DRAW_AREA_X + 2 || x > DRAW_AREA_X + DRAW_AREA_W - 2 ||
      y < DRAW_AREA_Y + 50 || y > DRAW_AREA_Y + DRAW_AREA_H - 2) {
    resetSpiral();
  }
}


void stepPiWave() {
  int digit = getNextDigit();


  int y = waveMidY + sin((waveX + digit * 12) * 0.05) * (waveAmplitude - digit * 4);
  y = constrain(y, DRAW_AREA_Y + 55, DRAW_AREA_Y + DRAW_AREA_H - 5);


  tft.drawLine(waveLastX, waveLastY, waveX, y, colorForDigit(digit));


  waveLastX = waveX;
  waveLastY = y;
  waveX += waveStepSize;


  if (waveX > DRAW_AREA_X + DRAW_AREA_W - 5) {
    resetWave();
    drawFrame();
  }
}


void stepCurrentMode() {
  int steps = speedToSteps();


  for (int i = 0; i < steps; i++) {
    if (modeIndex == 0) stepPiWalker();
    else if (modeIndex == 1) stepPiChords();
    else if (modeIndex == 2) stepPiBloom();
    else if (modeIndex == 3) stepPiGrid();
    else if (modeIndex == 4) stepPiSpiral();
    else if (modeIndex == 5) stepPiWave();
  }


  drawStatusBar();
}


// -------------------- SD/Gallery -----------------------
bool initSD() {
  sdSPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
  return SD.begin(SD_CS, sdSPI);
}


void scanGallery() {
  galleryFileCount = 0;
  galleryPage = 0;
  gallerySelectedIndex = -1;


  if (!sdReady) return;


  File root = SD.open("/");
  if (!root) return;


  File file = root.openNextFile();
  while (file && galleryFileCount < MAX_GALLERY_FILES) {
    String name = file.name();
    name.toUpperCase();
    if (!file.isDirectory() && name.endsWith(".TXT")) {
      galleryFiles[galleryFileCount++] = String(file.name());
    }
    file.close();
    file = root.openNextFile();
  }
}


void saveFavorite() {
  if (!sdReady) {
    showToast("SD not ready", TFT_RED);
    return;
  }


  int n = 1;
  String filename;
  do {
    filename = "/PI_" + String(n) + ".TXT";
    n++;
  } while (SD.exists(filename) && n < 1000);


  File f = SD.open(filename, FILE_WRITE);
  if (!f) {
    showToast("Save failed", TFT_RED);
    return;
  }


  f.println("mode=" + String(modeIndex));
  f.println("speed=" + String(speedIndex));
  f.println("piIndex=" + String(piIndex));
  f.close();


  scanGallery();
  showToast("Saved " + filename, TFT_GREEN);
}


void loadSelectedFavorite() {
  if (!sdReady) {
    showToast("SD not ready", TFT_RED);
    return;
  }


  if (gallerySelectedIndex < 0 || gallerySelectedIndex >= galleryFileCount) {
    showToast("No file selected", TFT_RED);
    return;
  }


  File f = SD.open(galleryFiles[gallerySelectedIndex]);
  if (!f) {
    showToast("Open failed", TFT_RED);
    return;
  }


  while (f.available()) {
    String line = f.readStringUntil('\n');
    line.trim();


    if (line.startsWith("mode=")) modeIndex = line.substring(5).toInt();
    if (line.startsWith("speed=")) speedIndex = line.substring(6).toInt();
    if (line.startsWith("piIndex=")) piIndex = line.substring(8).toInt();
  }
  f.close();


  galleryActive = false;
  resetMode();
  showToast("Loaded", TFT_GREEN);
}


void drawGalleryScreen() {
  tft.fillScreen(TFT_BLACK);
  tft.drawRoundRect(DRAW_AREA_X, DRAW_AREA_Y, DRAW_AREA_W, DRAW_AREA_H, 10, TFT_WHITE);


  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(TL_DATUM);
  tft.drawString("Gallery", 20, 18, 2);


  if (!sdReady) {
    tft.drawString("SD card not ready", 20, 50, 2);
    drawGalleryButtons();
    return;
  }


  if (galleryFileCount == 0) {
    tft.drawString("No saved files found", 20, 50, 2);
    drawGalleryButtons();
    return;
  }


  int start = galleryPage * 5;
  for (int i = 0; i < 5; i++) {
    int idx = start + i;
    int y = 50 + i * 38;
    if (idx < galleryFileCount) {
      uint16_t bg = (idx == gallerySelectedIndex) ? TFT_BLUE : TFT_BLACK;
      tft.fillRoundRect(20, y, 440, 28, 6, bg);
      tft.drawRoundRect(20, y, 440, 28, 6, TFT_WHITE);
      tft.setTextColor(TFT_WHITE, bg);
      tft.drawString(String(idx + 1) + ": " + galleryFiles[idx], 28, y + 7, 2);
    }
  }


  String pageText = "Page " + String(galleryPage + 1) + " / " + String((galleryFileCount + 4) / 5);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawRightString(pageText, 460, 18, 2);


  drawGalleryButtons();
}


int galleryListHitTest(uint16_t tx, uint16_t ty) {
  if (tx < 20 || tx > 460) return -1;


  for (int i = 0; i < 5; i++) {
    int y = 50 + i * 38;
    if (ty >= y && ty <= y + 28) {
      int idx = galleryPage * 5 + i;
      if (idx < galleryFileCount) return idx;
    }
  }
  return -1;
}


// -------------------- Touch Handling -------------------
void handleNormalTouch(uint16_t x, uint16_t y) {
  if (touchedButton(btnMode, x, y)) {
    modeIndex++;
    if (modeIndex > 5) modeIndex = 0;
    resetMode();
    delay(250);
  }
  else if (touchedButton(btnReset, x, y)) {
    resetMode();
    delay(250);
  }
  else if (touchedButton(btnSave, x, y)) {
    saveFavorite();
    delay(250);
  }
  else if (touchedButton(btnGallery, x, y)) {
    galleryActive = true;
    scanGallery();
    drawGalleryScreen();
    delay(250);
  }
  else if (touchedButton(btnSpeed, x, y)) {
    speedIndex++;
    if (speedIndex > 2) speedIndex = 0;
    drawStatusBar();
    drawButtons();
    delay(250);
  }
}


void handleGalleryTouch(uint16_t x, uint16_t y) {
  int hit = galleryListHitTest(x, y);
  if (hit >= 0) {
    gallerySelectedIndex = hit;
    drawGalleryScreen();
    delay(200);
    return;
  }


  if (touchedButton(btnBack, x, y)) {
    galleryActive = false;
    resetMode();
    delay(250);
  }
  else if (touchedButton(btnPrev, x, y)) {
    if (galleryPage > 0) galleryPage--;
    drawGalleryScreen();
    delay(250);
  }
  else if (touchedButton(btnLoad, x, y)) {
    loadSelectedFavorite();
    delay(250);
  }
  else if (touchedButton(btnNext, x, y)) {
    int maxPage = (galleryFileCount - 1) / 5;
    if (galleryPage < maxPage) galleryPage++;
    drawGalleryScreen();
    delay(250);
  }
  else if (touchedButton(btnRefresh, x, y)) {
    scanGallery();
    drawGalleryScreen();
    delay(250);
  }
}


// -------------------- Setup / Loop ---------------------
void setup() {
  Serial.begin(115200);


  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);


  randomSeed(millis());


  tft.init();
  tft.setRotation(1);
  tft.setTouch(calData);


  sdReady = initSD();


  resetWalker();
  resetSpiral();
  resetWave();


  resetMode();


  if (sdReady) {
    scanGallery();
  }
}


void loop() {
  uint16_t x, y;


  if (tft.getTouch(&x, &y)) {
    if (galleryActive) handleGalleryTouch(x, y);
    else handleNormalTouch(x, y);
  }


  if (!galleryActive) {
    stepCurrentMode();
  }


  delay(20);
}