#include <Arduino.h>
#include <TBeamClock.h>
#include <TBeamDisplay.h>
#include <TBeamLogger.h>
#include <TBeamStorage.h>

#ifndef BOARD_ID
#define BOARD_ID "UNKNOWN"
#endif

#ifndef NODE_LABEL
#define NODE_LABEL "Unknown"
#endif

namespace {

tbeam::TBeamDisplay display;
tbeam::TBeamClock clockService(Wire1);
tbeam::TBeamStorage storage(Serial);
tbeam::TBeamLogger Log;

uint32_t sampleSeq = 0;
uint32_t lastSampleMs = 0;
uint32_t lastPageMs = 0;
uint8_t page = 0;

void formatRtc(char* out, size_t outSize) {
  if (!clockService.ready()) {
    strlcpy(out, "RTC read fail", outSize);
    return;
  }
  if (!clockService.valid()) {
    strlcpy(out, clockService.lowVoltage() ? "RTC low voltage" : "RTC invalid", outSize);
    return;
  }
  tbeam::TBeamClock::formatIsoUtc(clockService.lastRtc(), out, outSize);
}

void openDisplayLog() {
  if (!storage.ready()) {
    Serial.printf("Display log skipped: storage error=%s\r\n", storage.lastError());
    return;
  }

  bool opened = false;
  if (clockService.valid()) {
    char runId[64];
    char path[112];
    tbeam::TBeamClock::makeRunId(clockService.lastRtc(), BOARD_ID, runId, sizeof(runId));
    snprintf(path, sizeof(path), "%s/%s.csv", storage.logDir(), runId);
    opened = Log.openLog(path);
  }
  if (!opened) {
    opened = Log.openUniqueLog(BOARD_ID, ".csv");
  }
  if (!opened) {
    Serial.printf("Display log open failed: %s\r\n", storage.lastError());
    return;
  }

  Log.printf("# test: display\r\n");
  Log.printf("# board_id: %s\r\n", BOARD_ID);
  Log.printf("# node_label: %s\r\n", NODE_LABEL);
  Log.printf("# log_path: %s\r\n", Log.currentLogPath());
  Log.println("seq,millis,page,display_ready,rtc_valid,sd_ready,free_heap");
  Log.flush();
  Serial.printf("Display log opened: %s\r\n", Log.currentLogPath());
}

void drawCurrentPage() {
  char line1[32];
  char line2[32];
  char line3[32];
  char line4[32];
  char line5[32];
  char rtc[32];

  formatRtc(rtc, sizeof(rtc));

  if (page == 0) {
    snprintf(line1, sizeof(line1), "Display Test");
    snprintf(line2, sizeof(line2), "Board: %s", BOARD_ID);
    snprintf(line3, sizeof(line3), "Node: %s", NODE_LABEL);
    snprintf(line4, sizeof(line4), "OLED: %s", display.ready() ? "ready" : "fail");
    snprintf(line5, sizeof(line5), "Seq: %lu", (unsigned long)sampleSeq);
    display.showLines(line1, line2, line3, line4, line5);
  } else if (page == 1) {
    display.showStatus("Clock", rtc, clockService.valid() ? "OK" : "BAD", storage.ready() ? "SD mounted" : "SD missing");
  } else if (page == 2) {
    snprintf(line1, sizeof(line1), "Storage");
    snprintf(line2, sizeof(line2), "SD: %s", storage.ready() ? "mounted" : "missing");
    snprintf(line3, sizeof(line3), "Log: %s", Log.storageReady() ? "open" : "closed");
    snprintf(line4, sizeof(line4), "Heap: %lu", (unsigned long)ESP.getFreeHeap());
    snprintf(line5, sizeof(line5), "Millis: %lu", (unsigned long)millis());
    display.showLines(line1, line2, line3, line4, line5);
  } else {
    display.setFont(tbeam::DisplayFont::SMALL);
    snprintf(line1, sizeof(line1), "Services");
    snprintf(line2, sizeof(line2), "clock: %s", clockService.valid() ? "valid" : "bad");
    snprintf(line3, sizeof(line3), "storage: %s", storage.ready() ? "ready" : "bad");
    snprintf(line4, sizeof(line4), "logger: %s", Log.storageReady() ? "file" : "serial");
    snprintf(line5, sizeof(line5), "%s", rtc);
    display.showLines(line1, line2, line3, line4, line5, "page 4/4");
    display.setFont(tbeam::DisplayFont::NORMAL);
  }
}

}  // namespace

void setup() {
  Serial.begin(115200);
  delay(1500);
  Serial.println();
  Serial.printf("display test boot board=%s label=%s\r\n", BOARD_ID, NODE_LABEL);

  const bool displayReady = display.begin();
  Serial.printf("display_begin=%s error=%s\r\n", displayReady ? "ok" : "fail", display.lastError());
  display.showBoot("Display Test", BOARD_ID, "starting services");

  tbeam::StorageConfig storageConfig;
  storageConfig.logDir = "/logs/display";
  storageConfig.enablePinDumps = false;
  storage.begin(storageConfig);
  Log.begin(Serial, &storage);

  clockService.begin();
  openDisplayLog();
  drawCurrentPage();
}

void loop() {
  storage.update();
  clockService.update();
  Log.update();

  const uint32_t now = millis();
  if ((uint32_t)(now - lastSampleMs) >= 1000) {
    lastSampleMs = now;
    Log.printf("%lu,%lu,%u,%s,%s,%s,%lu\r\n",
               (unsigned long)sampleSeq++,
               (unsigned long)now,
               (unsigned)page,
               display.ready() ? "yes" : "no",
               clockService.valid() ? "yes" : "no",
               storage.ready() ? "yes" : "no",
               (unsigned long)ESP.getFreeHeap());
  }

  if ((uint32_t)(now - lastPageMs) >= 2500) {
    lastPageMs = now;
    page = (uint8_t)((page + 1U) % 4U);
    drawCurrentPage();
  }
}
