// 20260403 ChatGPT
// Exercise 16_PSRAM - Extended Exercise 15_RAM with PSRAM support

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

#ifndef NODE_LABEL
#define NODE_LABEL "PSRAM"
#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 size_t kTmpFileCapacity = 2097152; // 2MB in PSRAM
static char *g_tmpFileBuffer = nullptr;
static size_t g_tmpFileSize = 0;
static unsigned g_tmpLineNumber = 0;

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 getAvailableRamBytes()
{
  return ESP.getFreeHeap();
}

static size_t getTotalRamBytes()
{
  return ESP.getHeapSize();
}

static size_t getPSRAMFreeBytes()
{
  size_t freeBytes = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
  if (freeBytes == 0 && ESP.getFreePsram() > 0) {
    freeBytes = ESP.getFreePsram();
  }
  return freeBytes;
}

static size_t getPSRAMTotalBytes()
{
  size_t totalBytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
  if (totalBytes == 0 && ESP.getPsramSize() > 0) {
    totalBytes = ESP.getPsramSize();
  }
  return totalBytes;
}

static void getTimestamp(char *out, size_t outSize)
{
  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 appendTimestampLine()
{
  if (!g_tmpFileBuffer) return;

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

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

  const size_t lineLen = (size_t)written;
  if (g_tmpFileSize + lineLen > kTmpFileCapacity - 1) {
    Serial.println("Warning: PSRAM log full, stopping writes");
    return;
  }

  memcpy(g_tmpFileBuffer + g_tmpFileSize, line, lineLen);
  g_tmpFileSize += lineLen;
  g_tmpLineNumber++;
}

static void printRamStatus()
{
  const size_t freeRam = getAvailableRamBytes();
  const size_t totalRam = getTotalRamBytes();
  const size_t maxAllocRam = ESP.getMaxAllocHeap();
  
  const size_t freePSRAM = getPSRAMFreeBytes();
  const size_t totalPSRAM = getPSRAMTotalBytes();

  Serial.printf("RAM  total=%u free=%u maxAlloc=%u | PSRAM total=%u free=%u\r\n", 
    (unsigned)totalRam, (unsigned)freeRam, (unsigned)maxAllocRam,
    (unsigned)totalPSRAM, (unsigned)freePSRAM);

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

  snprintf(line1, sizeof(line1), "Exercise 16 PSRAM");
  snprintf(line2, sizeof(line2), "Node: %s", NODE_LABEL);
  
  // Display format: "Free XXXKb/8.0Mbs"
  const float psramMb = totalPSRAM / (1024.0f * 1024.0f);
  const size_t ramKb = freeRam / 1024U;
  snprintf(line3, sizeof(line3), "Free %uKb/%.1fMbs", (unsigned)ramKb, psramMb);
  
  snprintf(line4, sizeof(line4), "PSRAM: %u KB", (unsigned)(freePSRAM / 1024U));
  snprintf(line5, sizeof(line5), "Lines: %u", (unsigned)g_tmpLineNumber);

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

static void showHelp()
{
  Serial.println("PSRAM command list:");
  Serial.println("  help           - show this menu");
  Serial.println("  stat           - show PSRAM buffer state");
  Serial.println("  read           - read PSRAM buffer contents");
  Serial.println("  clear          - clear PSRAM buffer contents");
  Serial.println("  write <text>   - write text to PSRAM buffer");
  Serial.println("  append <text>  - append text to PSRAM buffer");
}

static void printPSRAMFileStat()
{
  Serial.printf("PSRAM Buffer Capacity: %u bytes\r\n", (unsigned)kTmpFileCapacity);
  Serial.printf("Current Size: %u bytes\r\n", (unsigned)g_tmpFileSize);
  Serial.printf("Lines: %u\r\n", (unsigned)g_tmpLineNumber);
  Serial.printf("PSRAM Total: %u bytes (%.2f MB)\r\n", (unsigned)getPSRAMTotalBytes(), 
    getPSRAMTotalBytes() / (1024.0f * 1024.0f));
  Serial.printf("PSRAM Free: %u bytes\r\n", (unsigned)getPSRAMFreeBytes());
}

static void printPSRAMFileContents()
{
  if (!g_tmpFileBuffer) {
    Serial.println("PSRAM buffer not allocated");
    return;
  }

  if (g_tmpFileSize == 0) {
    Serial.println("PSRAM buffer is empty");
    return;
  }

  Serial.print("PSRAM contents: ");
  Serial.write((const uint8_t *)g_tmpFileBuffer, g_tmpFileSize);
  if (g_tmpFileBuffer[g_tmpFileSize - 1] != '\n')
    Serial.println();
}

static void setPSRAMFileContent(const char *text)
{
  if (!g_tmpFileBuffer) {
    Serial.println("Error: PSRAM buffer not allocated");
    return;
  }

  if (!text) {
    g_tmpFileSize = 0;
    g_tmpLineNumber = 0;
    return;
  }
  const size_t newLen = strlen(text);
  if (newLen > kTmpFileCapacity - 1) {
    Serial.printf("Error: content too large (%u/%u)\r\n", (unsigned)newLen, (unsigned)kTmpFileCapacity);
    return;
  }
  memcpy(g_tmpFileBuffer, text, newLen);
  g_tmpFileSize = newLen;
  g_tmpLineNumber = 0;
}

static void appendPSRAMFileContent(const char *text)
{
  if (!g_tmpFileBuffer) {
    Serial.println("Error: PSRAM buffer not allocated");
    return;
  }

  if (!text || text[0] == '\0') return;
  const size_t textLen = strlen(text);
  if (g_tmpFileSize + textLen > kTmpFileCapacity - 1) {
    Serial.printf("Error: append would exceed %u bytes\r\n", (unsigned)kTmpFileCapacity);
    return;
  }
  memcpy(g_tmpFileBuffer + g_tmpFileSize, text, textLen);
  g_tmpFileSize += textLen;
}

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) {
    printPSRAMFileStat();
    return;
  }

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

  if (strcasecmp(cmd, "clear") == 0) {
    g_tmpFileSize = 0;
    g_tmpLineNumber = 0;
    Serial.println("PSRAM buffer cleared");
    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)
      setPSRAMFileContent(payload);
    else
      appendPSRAMFileContent(payload);

    Serial.printf("%s: %u bytes\r\n", cmd,
                  (unsigned)g_tmpFileSize);
    return;
  }

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

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

  // Boot-time PSRAM diagnostics
  Serial.printf("Boot PSRAM size: %u bytes\r\n", (unsigned)ESP.getPsramSize());
  Serial.printf("Boot PSRAM free: %u bytes\r\n", (unsigned)ESP.getFreePsram());

  // Allocate PSRAM buffer
  g_tmpFileBuffer = (char *)heap_caps_malloc(kTmpFileCapacity, MALLOC_CAP_SPIRAM);
  if (!g_tmpFileBuffer) {
    Serial.println("ERROR: Failed to allocate PSRAM buffer!");
    oledShowLines("Exercise 16_PSRAM", "Node: " NODE_LABEL, "PSRAM alloc FAILED");
  } else {
    Serial.printf("PSRAM buffer allocated: %u bytes\r\n", (unsigned)kTmpFileCapacity);
  }

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

  delay(1000);
}

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

  // check serial commands at all times
  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_tmpFileBuffer) {
    appendTimestampLine();
  }
  printRamStatus();
}
