// 20260214 ChatGPT
// $Id$
// $HeadURL$

#include <Arduino.h>
#include <stdarg.h>
#include <FS.h>
#include <SD.h>
#include <SPI.h>
#include "driver/gpio.h"          // gpio_get_level()
#include "tbeam_supreme_adapter.h"

// -------------------------
// Configuration toggles
// -------------------------
#define ENABLE_SD_RAIL_CYCLE    1   // Power-cycle AXP2101 BLDO1 (SD rail) at boot.
#define ENABLE_PIN_DUMPS        1   // Log SPI pin logic levels at key points (NON-INTRUSIVE).
#define STARTUP_SERIAL_DELAY_MS 5000

// -------------------------
// Globals
// -------------------------
static SPIClass sdSpiH(HSPI);
static SPIClass sdSpiF(FSPI);
static SPIClass* g_sdSpi = nullptr;
static const char* g_sdBusName = "none";
static uint32_t g_sdFreq = 0;
static XPowersLibInterface* g_pmu = nullptr;

static const char* kRootTestFile   = "/Exercise_05_test.txt";
static const char* kNestedDir      = "/test/testsub1/testsubsub1";
static const char* kNestedTestFile = "/test/testsub1/testsubsub1/Exercise_05_test.txt";
static const char* kPayload        = "This is a test";

enum class WatchState : uint8_t {
  UNKNOWN = 0,
  ABSENT,
  MOUNTED
};

static WatchState g_watchState = WatchState::UNKNOWN;
static uint8_t g_presentVotes = 0;
static uint8_t g_absentVotes = 0;
static uint32_t g_lastPollMs = 0;
static uint32_t g_lastFullScanMs = 0;
static uint32_t g_lastPeriodicActionMs = 0;

static const uint32_t kPollIntervalAbsentMs  = 1000;
static const uint32_t kPollIntervalMountedMs = 2000;
static const uint32_t kFullScanIntervalMs    = 10000;
static const uint32_t kPeriodicActionMs      = 15000;

static const uint8_t  kVotesToPresent = 2;
static const uint8_t  kVotesToAbsent  = 5;     // More votes needed to declare absent to prevent false removes.
static const uint32_t kStartupWarmupMs = 1500; // Allow PMU and SD rail to stabilize.

static uint32_t g_logSeq = 0;

// -------------------------
// Logging helpers
// -------------------------
static void logf(const char* fmt, ...) {
  char msg[192];
  va_list args;
  va_start(args, fmt);
  vsnprintf(msg, sizeof(msg), fmt, args);
  va_end(args);
  Serial.printf("[%10lu][%06lu] %s\r\n",
                (unsigned long)millis(),
                (unsigned long)g_logSeq++,
                msg);
}

static void dumpPmu(const char* tag, XPowersLibInterface* pmu) {
  if (!pmu) {
    logf("PMU(%s): pmu=null", tag);
    return;
  }

  bool bldo1 = pmu->isPowerChannelEnable(XPOWERS_BLDO1);
  int vbus = pmu->getVbusVoltage();
  int batt = pmu->getBattVoltage();

  logf("PMU(%s): BLDO1(SD)=%s  VBUS=%dmV  VBAT=%dmV",
       tag, bldo1 ? "ON" : "OFF", vbus, batt);
}

// IMPORTANT: this function MUST NOT modify pin modes (regression cause).
static void dumpSdPins(const char* tag) {
#if ENABLE_PIN_DUMPS
  const gpio_num_t CS   = (gpio_num_t)tbeam_supreme::sdCs();
  const gpio_num_t SCK  = (gpio_num_t)tbeam_supreme::sdSck();
  const gpio_num_t MISO = (gpio_num_t)tbeam_supreme::sdMiso();
  const gpio_num_t MOSI = (gpio_num_t)tbeam_supreme::sdMosi();

  int cs   = gpio_get_level(CS);
  int sck  = gpio_get_level(SCK);
  int miso = gpio_get_level(MISO);
  int mosi = gpio_get_level(MOSI);

  logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d", tag, cs, sck, miso, mosi);
#else
  (void)tag;
#endif
}

// -------------------------
// Power + bus conditioning
// -------------------------
static void forceSpiDeselected() {
  pinMode(tbeam_supreme::sdCs(), OUTPUT);
  digitalWrite(tbeam_supreme::sdCs(), HIGH);

  pinMode(tbeam_supreme::imuCs(), OUTPUT);
  digitalWrite(tbeam_supreme::imuCs(), HIGH);
}

static bool initPmuForSdPower() {
  bool ok = tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial);
  if (!ok) {
    logf("ERROR: PMU init failed");
    return false;
  }

  logf("PMU adapter: AXP2101 ready, BLDO1(SD)=%s",
       g_pmu && g_pmu->isPowerChannelEnable(XPOWERS_BLDO1) ? "ON" : "OFF");
  return true;
}

static void cycleSdRail(XPowersLibInterface* pmu,
                        uint32_t off_ms = 250,
                        uint32_t on_settle_ms = 600) {
#if ENABLE_SD_RAIL_CYCLE
  if (!pmu) {
    logf("SD rail cycle skipped: pmu=null");
    return;
  }

  dumpPmu("pre-sd-cycle", pmu);

  // Ensure the card is NOT selected while power is unstable.
  forceSpiDeselected();
  dumpSdPins("pre-sd-cycle");

  pmu->disablePowerOutput(XPOWERS_BLDO1);
  delay(off_ms);

  pmu->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
  pmu->enablePowerOutput(XPOWERS_BLDO1);
  delay(on_settle_ms);

  dumpPmu("post-sd-cycle", pmu);
  dumpSdPins("post-sd-cycle");
#else
  (void)pmu; (void)off_ms; (void)on_settle_ms;
#endif
}

// -------------------------
// SD helpers
// -------------------------
static const char* cardTypeToString(uint8_t type) {
  switch (type) {
    case CARD_MMC:  return "MMC";
    case CARD_SD:   return "SDSC";
    case CARD_SDHC: return "SDHC/SDXC";
    default:        return "UNKNOWN";
  }
}

static bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) {
  SD.end();
  bus.end();
  delay(10);

  forceSpiDeselected();

  bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs());

  // SD SPI "idle clocks" ritual: CS HIGH + >= 74 clocks with MOSI high (0xFF).
  digitalWrite(tbeam_supreme::sdCs(), HIGH);
  delay(2);
  for (int i = 0; i < 10; i++) {
    bus.transfer(0xFF); // 80 clocks total
  }
  delay(2);

  dumpSdPins("after-idle-clocks");

  if (verbose) {
    logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz);
  }

  if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
    if (verbose) logf("SD: mount failed (possible non-FAT format, power, or bus issue)");
    return false;
  }

  uint8_t cardType = SD.cardType();
  if (cardType == CARD_NONE) {
    SD.end();
    return false;
  }

  g_sdSpi = &bus;
  g_sdBusName = busName;
  g_sdFreq = hz;
  return true;
}

static bool mountPreferred(bool verbose) {
  return tryMountWithBus(sdSpiH, "HSPI", 400000, verbose);
}

static bool mountCardFullScan() {
  const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};

  for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
    if (tryMountWithBus(sdSpiH, "HSPI", freqs[i], true)) {
      logf("SD: card detected and mounted");
      return true;
    }
  }
  for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
    if (tryMountWithBus(sdSpiF, "FSPI", freqs[i], true)) {
      logf("SD: card detected and mounted");
      return true;
    }
  }

  logf("SD: begin() failed on all bus/frequency attempts");
  logf("    likely card absent, bad format, pin mismatch, or hardware issue");
  return false;
}

static void printCardInfo() {
  uint8_t cardType = SD.cardType();
  uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL);
  uint64_t totalMB    = SD.totalBytes() / (1024ULL * 1024ULL);
  uint64_t usedMB     = SD.usedBytes() / (1024ULL * 1024ULL);

  logf("SD type: %s", cardTypeToString(cardType));
  logf("SD size: %llu MB", cardSizeMB);
  logf("FS total: %llu MB", totalMB);
  logf("FS used : %llu MB", usedMB);
  logf("SPI bus: %s @ %lu Hz", g_sdBusName, (unsigned long)g_sdFreq);
}

static bool ensureDirRecursive(const char* path) {
  String full(path);
  if (!full.startsWith("/")) full = "/" + full;

  int start = 1;
  while (start > 0 && start < (int)full.length()) {
    int slash = full.indexOf('/', start);
    String partial = (slash < 0) ? full : full.substring(0, slash);

    if (!SD.exists(partial.c_str())) {
      logf("Creating directory: %s", partial.c_str());
      if (!SD.mkdir(partial.c_str())) {
        logf("ERROR: mkdir failed for %s", partial.c_str());
        return false;
      }
    }

    if (slash < 0) break;
    start = slash + 1;
  }
  return true;
}

static bool rewriteFile(const char* path, const char* payload) {
  if (SD.exists(path)) {
    logf("WARNING: %s exists ... erasing", path);
    if (!SD.remove(path)) {
      logf("ERROR: failed to erase %s", path);
      return false;
    }
  }

  File f = SD.open(path, FILE_WRITE);
  if (!f) {
    logf("ERROR: failed to create %s", path);
    return false;
  }

  size_t wrote = f.println(payload);
  f.close();

  if (wrote == 0) {
    logf("ERROR: write failed for %s", path);
    return false;
  }

  logf("Wrote file: %s", path);
  return true;
}

static void permissionsDemo(const char* path) {
  logf("Permissions demo:");
  logf("  SD/FAT does not support Unix permissions (chmod/chown).");
  logf("  Access control is by open mode (FILE_READ/FILE_WRITE).");

  File r = SD.open(path, FILE_READ);
  if (!r) {
    logf("  Could not open %s as FILE_READ", path);
    return;
  }

  logf("  FILE_READ open succeeded, size=%u bytes", (unsigned)r.size());
  size_t writeInReadMode = r.print("attempt write while opened read-only");
  if (writeInReadMode == 0) {
    logf("  As expected, write via FILE_READ handle was blocked.");
  } else {
    logf("  NOTE: write via FILE_READ returned %u (unexpected)", (unsigned)writeInReadMode);
  }
  r.close();
}

static bool verifyMountedCard() {
  File root = SD.open("/", FILE_READ);
  if (!root) return false;
  root.close();
  return true;
}

static void runCardWorkflow() {
  printCardInfo();

  if (!rewriteFile(kRootTestFile, kPayload)) {
    logf("Watcher action: root file write failed");
    return;
  }
  if (!ensureDirRecursive(kNestedDir)) {
    logf("Watcher action: directory creation failed");
    return;
  }
  if (!rewriteFile(kNestedTestFile, kPayload)) {
    logf("Watcher action: nested file write failed");
    return;
  }

  permissionsDemo(kRootTestFile);
}

// -------------------------
// Watcher state transitions
// -------------------------
static void setStateMounted() {
  if (g_watchState != WatchState::MOUNTED) {
    logf("EVENT: card inserted/mounted");
    runCardWorkflow();
    g_lastPeriodicActionMs = millis();
  }
  g_watchState = WatchState::MOUNTED;
}

static void setStateAbsent() {
  if (g_watchState == WatchState::MOUNTED) {
    logf("EVENT: card removed/unavailable");
  } else if (g_watchState != WatchState::ABSENT) {
    logf("EVENT: no card detected");
  }
  SD.end();
  g_watchState = WatchState::ABSENT;
}

// -------------------------
// Arduino entry points
// -------------------------
void setup() {
  Serial.begin(115200);
  Serial.println("[WATCHER: startup]");

  // De-select SPI devices immediately.
  forceSpiDeselected();
  dumpSdPins("very-early");

  logf("Sleeping for %lu ms to allow Serial Monitor connection...", (unsigned long)STARTUP_SERIAL_DELAY_MS);
  delay(STARTUP_SERIAL_DELAY_MS);

  Serial.println();
  Serial.println("==================================================");
  Serial.println("Exercise 05: SD Card Watcher");
  Serial.println("==================================================");
  Serial.printf("Pins: CS=%d SCK=%d MISO=%d MOSI=%d\r\n",
                tbeam_supreme::sdCs(), tbeam_supreme::sdSck(),
                tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi());
  Serial.printf("PMU I2C: SDA1=%d SCL1=%d\r\n",
                tbeam_supreme::i2cSda(), tbeam_supreme::i2cScl());
  Serial.println("Note: SD must be FAT16/FAT32 for Arduino SD library.");
  Serial.println();

  initPmuForSdPower();
  dumpPmu("post-pmu-init", g_pmu);

  // Software equivalent of "remove/insert card".
  cycleSdRail(g_pmu);

  logf("Watcher: waiting %lu ms for SD rail/card stabilization", (unsigned long)kStartupWarmupMs);
  delay(kStartupWarmupMs);

  dumpSdPins("pre-warmup-mount");

  bool warmMounted = false;
  for (uint8_t i = 0; i < 3; ++i) {
    if (mountPreferred(false)) {
      warmMounted = true;
      break;
    }
    delay(200);
  }

  if (warmMounted) {
    logf("Watcher: startup warmup mount succeeded");
    setStateMounted();
  } else {
    logf("Watcher: startup warmup did not mount card");
    setStateAbsent();
  }
}

void loop() {
  const uint32_t now = millis();
  const uint32_t pollInterval =
      (g_watchState == WatchState::MOUNTED) ? kPollIntervalMountedMs : kPollIntervalAbsentMs;

  if ((uint32_t)(now - g_lastPollMs) < pollInterval) {
    delay(10);
    return;
  }
  g_lastPollMs = now;

  if (g_watchState == WatchState::MOUNTED) {
    if (verifyMountedCard()) {
      if ((uint32_t)(now - g_lastPeriodicActionMs) >= kPeriodicActionMs) {
        logf("Watcher: periodic mounted check action");
        runCardWorkflow();
        g_lastPeriodicActionMs = now;
      }
      g_presentVotes = 0;
      g_absentVotes  = 0;
      return;
    }

    // One immediate remount attempt prevents false removes on transient SPI errors.
    if (mountPreferred(false) && verifyMountedCard()) {
      g_presentVotes = 0;
      g_absentVotes  = 0;
      return;
    }

    g_absentVotes++;
    g_presentVotes = 0;
    if (g_absentVotes >= kVotesToAbsent) {
      setStateAbsent();
      g_absentVotes = 0;
    }
    return;
  }

  // ABSENT/UNKNOWN state
  bool mounted = mountPreferred(false);
  if (!mounted && (uint32_t)(now - g_lastFullScanMs) >= kFullScanIntervalMs) {
    g_lastFullScanMs = now;
    logf("Watcher: preferred probe failed, running full scan");
    mounted = mountCardFullScan();
  }

  if (mounted) {
    g_presentVotes++;
    g_absentVotes = 0;
    if (g_presentVotes >= kVotesToPresent) {
      setStateMounted();
      g_presentVotes = 0;
    }
  } else {
    g_absentVotes++;
    g_presentVotes = 0;
    if (g_absentVotes >= kVotesToAbsent) {
      setStateAbsent();
      g_absentVotes = 0;
    }
  }
}
