// 20260403 ChatGPT
// Exercise 17_Flash

#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <time.h>
#include <SPIFFS.h>

#include "tbeam_supreme_adapter.h"

#ifndef NODE_LABEL
#define NODE_LABEL "FLASH"
#endif

#ifndef RTC_I2C_ADDR
#define RTC_I2C_ADDR 0x51
#endif

#ifndef OLED_SDA
#define OLED_SDA 17
#endif
#ifndef OLED_SCL
#define OLED_SCL 18
#endif
#ifndef OLED_ADDR
#define OLED_ADDR 0x3C
#endif

static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE);
static const char *kFlashDir = "/flash_logs";
static char g_currentFilePath[64] = {0};
static File g_flashFile;
static unsigned g_flashLineNumber = 0;
static XPowersLibInterface* g_pmu = nullptr;
static bool g_hasRtc = false;
static bool g_rtcLowVoltage = false;

struct RtcDateTime {
  uint16_t year;
  uint8_t month;
  uint8_t day;
  uint8_t hour;
  uint8_t minute;
  uint8_t second;
  uint8_t weekday;
};

static void oledShowLines(const char *l1,
                          const char *l2 = nullptr,
                          const char *l3 = nullptr,
                          const char *l4 = nullptr,
                          const char *l5 = nullptr)
{
  g_oled.clearBuffer();
  g_oled.setFont(u8g2_font_5x8_tf);
  if (l1) g_oled.drawUTF8(0, 12, l1);
  if (l2) g_oled.drawUTF8(0, 24, l2);
  if (l3) g_oled.drawUTF8(0, 36, l3);
  if (l4) g_oled.drawUTF8(0, 48, l4);
  if (l5) g_oled.drawUTF8(0, 60, l5);
  g_oled.sendBuffer();
}

static size_t getFlashTotalBytes()
{
  return SPIFFS.totalBytes();
}

static size_t getFlashUsedBytes()
{
  return SPIFFS.usedBytes();
}

static size_t getFlashFreeBytes()
{
  const size_t total = getFlashTotalBytes();
  const size_t used = getFlashUsedBytes();
  return total > used ? total - used : 0;
}

static uint8_t toBcd(uint8_t v) {
  return ((v / 10U) << 4U) | (v % 10U);
}

static uint8_t fromBcd(uint8_t b) {
  return ((b >> 4U) * 10U) + (b & 0x0FU);
}

static bool isRtcDateTimeValid(const RtcDateTime& dt) {
  if (dt.year < 2020 || dt.year > 2099) return false;
  if (dt.month < 1 || dt.month > 12) return false;
  if (dt.day < 1 || dt.day > 31) return false;
  if (dt.hour > 23 || dt.minute > 59 || dt.second > 59) return false;
  return true;
}

static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) {
  Wire1.beginTransmission(RTC_I2C_ADDR);
  Wire1.write(0x02);
  if (Wire1.endTransmission(false) != 0) {
    return false;
  }

  const uint8_t need = 7;
  uint8_t got = Wire1.requestFrom((int)RTC_I2C_ADDR, (int)need);
  if (got != need) {
    return false;
  }

  uint8_t sec = Wire1.read();
  uint8_t min = Wire1.read();
  uint8_t hour = Wire1.read();
  uint8_t day = Wire1.read();
  uint8_t weekday = Wire1.read();
  uint8_t month = Wire1.read();
  uint8_t year = Wire1.read();

  lowVoltageFlag = (sec & 0x80U) != 0;
  out.second = fromBcd(sec & 0x7FU);
  out.minute = fromBcd(min & 0x7FU);
  out.hour = fromBcd(hour & 0x3FU);
  out.day = fromBcd(day & 0x3FU);
  out.weekday = fromBcd(weekday & 0x07U);
  out.month = fromBcd(month & 0x1FU);
  uint8_t yy = fromBcd(year);
  bool century = (month & 0x80U) != 0;
  out.year = century ? (1900U + yy) : (2000U + yy);

  return true;
}

static bool initRtc() {
  if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) {
    Serial.println("RTC init: PMU/i2c init failed");
    return false;
  }
  RtcDateTime now{};
  if (!rtcRead(now, g_rtcLowVoltage) || !isRtcDateTimeValid(now)) {
    Serial.println("RTC init: no valid time available");
    return false;
  }
  g_hasRtc = true;
  Serial.printf("RTC init: %04u-%02u-%02u %02u:%02u:%02u%s\r\n",
                (unsigned)now.year, (unsigned)now.month, (unsigned)now.day,
                (unsigned)now.hour, (unsigned)now.minute, (unsigned)now.second,
                g_rtcLowVoltage ? " [LOW_BATT]" : "");
  return true;
}

static bool getRtcTimestamp(char *out, size_t outSize) {
  if (!g_hasRtc) {
    return false;
  }
  RtcDateTime now{};
  bool low = false;
  if (!rtcRead(now, low) || !isRtcDateTimeValid(now)) {
    return false;
  }
  g_rtcLowVoltage = low;
  snprintf(out, outSize, "%04u-%02u-%02u %02u:%02u:%02u",
           now.year,
           now.month,
           now.day,
           now.hour,
           now.minute,
           now.second);
  return true;
}

static void getTimestamp(char *out, size_t outSize)
{
  if (getRtcTimestamp(out, outSize)) {
    return;
  }

  const time_t now = time(nullptr);
  if (now > 1700000000) {
    struct tm tmNow;
    localtime_r(&now, &tmNow);
    snprintf(out, outSize, "%04d-%02d-%02d %02d:%02d:%02d",
             tmNow.tm_year + 1900,
             tmNow.tm_mon + 1,
             tmNow.tm_mday,
             tmNow.tm_hour,
             tmNow.tm_min,
             tmNow.tm_sec);
    return;
  }

  const uint32_t sec = millis() / 1000;
  const uint32_t hh = sec / 3600;
  const uint32_t mm = (sec % 3600) / 60;
  const uint32_t ss = sec % 60;
  snprintf(out, outSize, "uptime %02u:%02u:%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss);
}

static void getFilenameTimestamp(char *out, size_t outSize)
{
  if (g_hasRtc) {
    RtcDateTime now{};
    bool low = false;
    if (rtcRead(now, low) && isRtcDateTimeValid(now)) {
      snprintf(out, outSize, "%04u%02u%02u_%02u%02u",
               now.year,
               now.month,
               now.day,
               now.hour,
               now.minute);
      return;
    }
  }

  const time_t now = time(nullptr);
  if (now > 1700000000) {
    struct tm tmNow;
    localtime_r(&now, &tmNow);
    snprintf(out, outSize, "%04d%02d%02d_%02d%02d",
             tmNow.tm_year + 1900,
             tmNow.tm_mon + 1,
             tmNow.tm_mday,
             tmNow.tm_hour,
             tmNow.tm_min);
    return;
  }

  const uint32_t sec = millis() / 1000;
  const uint32_t hh = sec / 3600;
  const uint32_t mm = (sec % 3600) / 60;
  const uint32_t ss = sec % 60;
  snprintf(out, outSize, "uptime_%02u%02u%02u", (unsigned)hh, (unsigned)mm, (unsigned)ss);
}

static String getNewFlashFilePath()
{
  char baseName[64];
  getFilenameTimestamp(baseName, sizeof(baseName));

  char candidate[96];
  snprintf(candidate, sizeof(candidate), "%s/%s.log", kFlashDir, baseName);
  if (!SPIFFS.exists(candidate)) {
    return String(candidate);
  }

  int suffix = 1;
  do {
    snprintf(candidate, sizeof(candidate), "%s/%s-%d.log", kFlashDir, baseName, suffix);
    suffix += 1;
  } while (SPIFFS.exists(candidate));

  return String(candidate);
}

static bool ensureFlashDirectory()
{
  if (SPIFFS.exists(kFlashDir)) {
    return true;
  }
  if (!SPIFFS.mkdir(kFlashDir)) {
    Serial.printf("Warning: failed to create %s\r\n", kFlashDir);
    return false;
  }
  return true;
}

static bool openCurrentFlashFile(bool truncate = false)
{
  if (g_flashFile) {
    g_flashFile.close();
  }

  if (truncate) {
    g_flashFile = SPIFFS.open(g_currentFilePath, FILE_WRITE);
  } else {
    g_flashFile = SPIFFS.open(g_currentFilePath, FILE_APPEND);
  }

  if (!g_flashFile) {
    Serial.printf("ERROR: cannot open %s\r\n", g_currentFilePath);
    return false;
  }
  return true;
}

static bool createFlashLogFile()
{
  if (!ensureFlashDirectory()) {
    return false;
  }

  String path = getNewFlashFilePath();
  path.toCharArray(g_currentFilePath, sizeof(g_currentFilePath));

  g_flashFile = SPIFFS.open(g_currentFilePath, FILE_WRITE);
  if (!g_flashFile) {
    Serial.printf("ERROR: could not create %s\r\n", g_currentFilePath);
    return false;
  }

  const char *header = "FLASH log file created\r\n";
  g_flashFile.print(header);
  g_flashFile.flush();
  g_flashLineNumber = 0;
  return true;
}

static void appendFlashTimestampLine()
{
  if (!g_flashFile) {
    return;
  }

  char timestamp[32];
  getTimestamp(timestamp, sizeof(timestamp));

  char line[96];
  const int written = snprintf(line, sizeof(line), "%u, %s\r\n", g_flashLineNumber + 1, timestamp);
  if (written <= 0) {
    return;
  }

  const size_t lineLen = (size_t)written;
  if (g_flashFile.write(reinterpret_cast<const uint8_t *>(line), lineLen) != lineLen) {
    Serial.println("Warning: flash write failed");
    return;
  }
  g_flashFile.flush();
  g_flashLineNumber += 1;
}

static void printFlashStatus()
{
  const size_t total = getFlashTotalBytes();
  const size_t used = getFlashUsedBytes();
  const size_t freeBytes = getFlashFreeBytes();

  Serial.printf("FLASH total=%u used=%u free=%u\r\n",
                (unsigned)total, (unsigned)used, (unsigned)freeBytes);

  char line1[32];
  char line2[32];
  char line3[32];
  char line4[32];
  char line5[32];

  snprintf(line1, sizeof(line1), "Exercise 17 Flash");
  snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL);
  snprintf(line3, sizeof(line3), "Free: %u KB", (unsigned)(freeBytes / 1024U));
  snprintf(line4, sizeof(line4), "Used: %u KB", (unsigned)(used / 1024U));
  snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_flashLineNumber);

  oledShowLines(line1, line2, line3, line4, line5);
}

static void showHelp()
{
  Serial.println("Flash command list:");
  Serial.println("  help           - show this menu");
  Serial.println("  stat           - show flash/file state");
  Serial.println("  rtc            - show RTC time status");
  Serial.println("  list           - list files in /flash_logs");
  Serial.println("  read           - read current flash file");
  Serial.println("  clear          - clear current flash file");
  Serial.println("  write <text>   - overwrite current flash file");
  Serial.println("  append <text>  - append text to current flash file");
}

static void printFlashFileStat()
{
  Serial.printf("Current file: %s\r\n", g_currentFilePath);
  if (!SPIFFS.exists(g_currentFilePath)) {
    Serial.println("Current file missing");
    return;
  }

  File file = SPIFFS.open(g_currentFilePath, FILE_READ);
  if (!file) {
    Serial.println("Unable to open current file for stats");
    return;
  }

  Serial.printf("Size: %u bytes\r\n", (unsigned)file.size());
  Serial.printf("Lines written: %u\r\n", (unsigned)g_flashLineNumber);
  file.close();
}

static void printFlashFileContents()
{
  if (!SPIFFS.exists(g_currentFilePath)) {
    Serial.println("Current flash file does not exist");
    return;
  }

  File file = SPIFFS.open(g_currentFilePath, FILE_READ);
  if (!file) {
    Serial.println("Unable to open current flash file");
    return;
  }

  if (file.size() == 0) {
    Serial.println("Current flash file is empty");
    file.close();
    return;
  }

  Serial.print("Flash file contents: ");
  while (file.available()) {
    Serial.write(file.read());
  }
  if (file.size() > 0) {
    Serial.println();
  }
  file.close();
}

static void clearFlashFileContents()
{
  if (!SPIFFS.exists(g_currentFilePath)) {
    Serial.println("No current file to clear");
    return;
  }

  if (!openCurrentFlashFile(true)) {
    return;
  }
  g_flashFile.close();
  g_flashLineNumber = 0;
  openCurrentFlashFile(false);
  Serial.println("Current flash file cleared");
}

static void setFlashFileContent(const char *text)
{
  if (!text) {
    clearFlashFileContents();
    return;
  }

  File file = SPIFFS.open(g_currentFilePath, FILE_WRITE);
  if (!file) {
    Serial.println("Unable to overwrite current flash file");
    return;
  }

  file.print(text);
  file.close();
  openCurrentFlashFile(false);
  g_flashLineNumber = 0;
}

static void appendFlashFileContent(const char *text)
{
  if (!text || text[0] == '\0') {
    return;
  }

  if (!openCurrentFlashFile(false)) {
    return;
  }

  g_flashFile.print(text);
  g_flashFile.flush();
}

static void listFlashFiles()
{
  File dir = SPIFFS.open(kFlashDir);
  if (!dir || !dir.isDirectory()) {
    Serial.printf("Unable to list files in %s\r\n", kFlashDir);
    return;
  }

  Serial.printf("Files in %s:\r\n", kFlashDir);
  File file = dir.openNextFile();
  while (file) {
    Serial.printf("  %s (%u bytes)\r\n", file.name(), (unsigned)file.size());
    file = dir.openNextFile();
  }
  dir.close();
}

static void processSerialCommand(const char *line)
{
  if (!line || line[0] == '\0') return;

  char tmp[384];
  strncpy(tmp, line, sizeof(tmp) - 1);
  tmp[sizeof(tmp) - 1] = '\0';

  char *cmd = strtok(tmp, " \t\r\n");
  if (!cmd) return;

  if (strcasecmp(cmd, "help") == 0) {
    showHelp();
    return;
  }

  if (strcasecmp(cmd, "stat") == 0) {
    printFlashFileStat();
    return;
  }

  if (strcasecmp(cmd, "rtc") == 0) {
    if (g_hasRtc) {
      char ts[32];
      if (getRtcTimestamp(ts, sizeof(ts))) {
        Serial.printf("RTC now: %s\r\n", ts);
        if (g_rtcLowVoltage) {
          Serial.println("RTC low-voltage flag is set");
        }
      } else {
        Serial.println("RTC present but time read failed");
      }
    } else {
      Serial.println("RTC unavailable");
    }
    return;
  }

  if (strcasecmp(cmd, "list") == 0) {
    listFlashFiles();
    return;
  }

  if (strcasecmp(cmd, "read") == 0) {
    printFlashFileContents();
    return;
  }

  if (strcasecmp(cmd, "clear") == 0) {
    clearFlashFileContents();
    return;
  }

  if (strcasecmp(cmd, "write") == 0 || strcasecmp(cmd, "append") == 0) {
    const char *payload = line + strlen(cmd);
    while (*payload == ' ' || *payload == '\t') payload++;
    if (strcasecmp(cmd, "write") == 0)
      setFlashFileContent(payload);
    else
      appendFlashFileContent(payload);

    Serial.printf("%s: %s\r\n", cmd, payload);
    return;
  }

  Serial.println("Unknown command (help for list)");
}

void setup()
{
  Serial.begin(115200);
  delay(800);
  Serial.println("Exercise 17_Flash boot");

  initRtc();

  if (!SPIFFS.begin(true)) {
    Serial.println("ERROR: SPIFFS mount failed");
    oledShowLines("Exercise 17_Flash", "Node: " NODE_LABEL, "SPIFFS mount FAILED");
  } else {
    Serial.println("SPIFFS mounted successfully");
    if (createFlashLogFile()) {
      Serial.printf("Current flash file: %s\r\n", g_currentFilePath);
    }
  }

  Wire.begin(OLED_SDA, OLED_SCL);
  g_oled.setI2CAddress(OLED_ADDR << 1);
  g_oled.begin();
  oledShowLines("Exercise 17_Flash", "Node: " NODE_LABEL, "Booting...");

  delay(1000);
}

void loop()
{
  static uint32_t lastMs = 0;
  const uint32_t now = millis();

  static char rxLine[384];
  static size_t rxLen = 0;

  while (Serial.available()) {
    int c = Serial.read();
    if (c <= 0) continue;
    if (c == '\r' || c == '\n') {
      if (rxLen > 0) {
        rxLine[rxLen] = '\0';
        processSerialCommand(rxLine);
        rxLen = 0;
      }
    } else if (rxLen + 1 < sizeof(rxLine)) {
      rxLine[rxLen++] = (char)c;
    }
  }

  if (now - lastMs < 1000) {
    delay(10);
    return;
  }
  lastMs = now;

  if (g_flashFile) {
    appendFlashTimestampLine();
  }
  printFlashStatus();
}
