// 20260217 ChatGPT
// $Id$
// $HeadURL$

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

#include "tbeam_supreme_adapter.h"

#ifndef OLED_SDA
#define OLED_SDA 17
#endif

#ifndef OLED_SCL
#define OLED_SCL 18
#endif

#ifndef OLED_ADDR
#define OLED_ADDR 0x3C
#endif

#ifndef RTC_I2C_ADDR
#define RTC_I2C_ADDR 0x51
#endif

#ifndef GPS_BAUD
#define GPS_BAUD 9600
#endif

static const uint32_t kSerialDelayMs = 5000;
static const uint32_t kReportIntervalMs = 30000;

static XPowersLibInterface* g_pmu = nullptr;
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
static HardwareSerial g_gpsSerial(1);

static uint32_t g_logSeq = 0;
static uint32_t g_lastReportMs = 0;
static uint32_t g_gpsBaud = GPS_BAUD;

static char g_gpsLine[128];
static size_t g_gpsLineLen = 0;

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

struct GpsState {
  bool sawAnySentence = false;
  uint8_t satsUsed = 0;
  uint8_t satsInView = 0;

  bool hasValidUtc = false;
  uint16_t utcYear = 0;
  uint8_t utcMonth = 0;
  uint8_t utcDay = 0;
  uint8_t utcHour = 0;
  uint8_t utcMinute = 0;
  uint8_t utcSecond = 0;
};

static GpsState g_gps;

static void logf(const char* fmt, ...) {
  char msg[220];
  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 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 uint8_t fromBcd(uint8_t b) {
  return ((b >> 4U) * 10U) + (b & 0x0FU);
}

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();
  (void)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.month = fromBcd(month & 0x1FU);
  uint8_t yy = fromBcd(year);
  bool century = (month & 0x80U) != 0;
  out.year = century ? (1900U + yy) : (2000U + yy);
  return true;
}

static String formatRtcNow() {
  RtcDateTime now{};
  bool lowV = false;
  if (!rtcRead(now, lowV)) {
    return "RTC read failed";
  }

  char buf[48];
  snprintf(buf,
           sizeof(buf),
           "RTC %04u-%02u-%02u %02u:%02u:%02u%s",
           (unsigned)now.year,
           (unsigned)now.month,
           (unsigned)now.day,
           (unsigned)now.hour,
           (unsigned)now.minute,
           (unsigned)now.second,
           lowV ? " !LOWV" : "");
  return String(buf);
}

static bool parseUInt2(const char* s, uint8_t& out) {
  if (!s || !isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
    return false;
  }
  out = (uint8_t)((s[0] - '0') * 10 + (s[1] - '0'));
  return true;
}

static void parseGga(char* fields[], int count) {
  if (count <= 7) {
    return;
  }
  int sats = atoi(fields[7]);
  if (sats >= 0 && sats <= 255) {
    g_gps.satsUsed = (uint8_t)sats;
  }
}

static void parseGsv(char* fields[], int count) {
  if (count <= 3) {
    return;
  }
  int sats = atoi(fields[3]);
  if (sats >= 0 && sats <= 255) {
    g_gps.satsInView = (uint8_t)sats;
  }
}

static void parseRmc(char* fields[], int count) {
  if (count <= 9) {
    return;
  }

  const char* utc = fields[1];
  const char* status = fields[2];
  const char* date = fields[9];

  if (!status || status[0] != 'A') {
    return;
  }

  if (!utc || strlen(utc) < 6 || !date || strlen(date) < 6) {
    return;
  }

  uint8_t hh = 0, mm = 0, ss = 0;
  uint8_t dd = 0, mo = 0, yy = 0;
  if (!parseUInt2(utc + 0, hh) || !parseUInt2(utc + 2, mm) || !parseUInt2(utc + 4, ss)) {
    return;
  }
  if (!parseUInt2(date + 0, dd) || !parseUInt2(date + 2, mo) || !parseUInt2(date + 4, yy)) {
    return;
  }

  g_gps.utcHour = hh;
  g_gps.utcMinute = mm;
  g_gps.utcSecond = ss;
  g_gps.utcDay = dd;
  g_gps.utcMonth = mo;
  g_gps.utcYear = (uint16_t)(2000U + yy);
  g_gps.hasValidUtc = true;
}

static void processNmeaLine(char* line) {
  if (!line || line[0] != '$') {
    return;
  }

  g_gps.sawAnySentence = true;

  char* star = strchr(line, '*');
  if (star) {
    *star = '\0';
  }

  char* fields[24] = {0};
  int count = 0;
  char* saveptr = nullptr;
  char* tok = strtok_r(line, ",", &saveptr);
  while (tok && count < 24) {
    fields[count++] = tok;
    tok = strtok_r(nullptr, ",", &saveptr);
  }

  if (count <= 0 || !fields[0]) {
    return;
  }

  const char* header = fields[0];
  size_t n = strlen(header);
  if (n < 6) {
    return;
  }

  const char* type = header + (n - 3);
  if (strcmp(type, "GGA") == 0) {
    parseGga(fields, count);
  } else if (strcmp(type, "GSV") == 0) {
    parseGsv(fields, count);
  } else if (strcmp(type, "RMC") == 0) {
    parseRmc(fields, count);
  }
}

static void pollGpsSerial() {
  while (g_gpsSerial.available() > 0) {
    char c = (char)g_gpsSerial.read();
    if (c == '\r') {
      continue;
    }
    if (c == '\n') {
      if (g_gpsLineLen > 0) {
        g_gpsLine[g_gpsLineLen] = '\0';
        processNmeaLine(g_gpsLine);
        g_gpsLineLen = 0;
      }
      continue;
    }

    if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) {
      g_gpsLine[g_gpsLineLen++] = c;
    } else {
      g_gpsLineLen = 0;
    }
  }
}

static void startGpsUart(uint32_t baud) {
  g_gpsSerial.end();
  delay(20);
  g_gpsSerial.setRxBufferSize(1024);
  g_gpsSerial.begin(baud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
  g_gpsBaud = baud;
}

static bool collectGpsTraffic(uint32_t windowMs) {
  uint32_t start = millis();
  size_t bytesSeen = 0;
  while ((uint32_t)(millis() - start) < windowMs) {
    while (g_gpsSerial.available() > 0) {
      (void)g_gpsSerial.read();
      bytesSeen++;
    }
    pollGpsSerial();
    delay(2);
  }
  return bytesSeen > 0 || g_gps.sawAnySentence;
}

static bool probeGpsAtBaud(uint32_t baud) {
  startGpsUart(baud);
  logf("Probing GPS at %lu baud...", (unsigned long)baud);
  if (collectGpsTraffic(700)) {
    return true;
  }

  // Try common query/wake commands used by MTK/L76K and related chipsets.
  g_gpsSerial.write("$PCAS06,0*1B\r\n");     // Request module SW text
  g_gpsSerial.write("$PMTK605*31\r\n");      // MTK firmware query
  g_gpsSerial.write("$PQTMVERNO*58\r\n");    // Quectel LC86 query
  g_gpsSerial.write("$PMTK353,1,1,1,1,1*2A\r\n");
  g_gpsSerial.write("$PMTK314,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\r\n");

  return collectGpsTraffic(1200);
}

static void initialGpsProbe() {
  const uint32_t bauds[] = {GPS_BAUD, 115200, 38400, 57600, 19200};
  for (size_t i = 0; i < sizeof(bauds) / sizeof(bauds[0]); ++i) {
    if (probeGpsAtBaud(bauds[i])) {
      logf("GPS traffic detected at %lu baud", (unsigned long)g_gpsBaud);
      return;
    }
  }
  logf("No GPS traffic detected during startup probe");
}

static uint8_t bestSatelliteCount() {
  return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView;
}

static void showTryingMessage() {
  String rtc = formatRtcNow();
  oledShowLines("Trying to locate",
                "satellites",
                g_gps.sawAnySentence ? "NMEA seen: yes" : "NMEA seen: no",
                rtc.c_str());
  logf("Trying to locate satellites. %s", rtc.c_str());
}

static void showStatusMessage() {
  uint8_t sats = bestSatelliteCount();

  if (g_gps.hasValidUtc) {
    char line2[40];
    char line3[28];
    snprintf(line2,
             sizeof(line2),
             "GPS UTC %04u-%02u-%02u",
             (unsigned)g_gps.utcYear,
             (unsigned)g_gps.utcMonth,
             (unsigned)g_gps.utcDay);
    snprintf(line3,
             sizeof(line3),
             "%02u:%02u:%02u  sats:%u",
             (unsigned)g_gps.utcHour,
             (unsigned)g_gps.utcMinute,
             (unsigned)g_gps.utcSecond,
             (unsigned)sats);
    oledShowLines("GPS lock acquired",
                  line2,
                  line3,
                  g_gps.sawAnySentence ? "NMEA seen: yes" : "NMEA seen: no");
    logf("GPS lock acquired. %s  sats=%u", line3, (unsigned)sats);
    return;
  }

  String rtc = formatRtcNow();
  oledShowLines("Take me outside",
                "No GPS time/sats yet",
                g_gps.sawAnySentence ? "NMEA seen: yes" : "NMEA seen: no",
                rtc.c_str());
  logf("Take me outside. sats=%u, has_utc=%s, nmea_seen=%s. %s",
       (unsigned)sats,
       g_gps.hasValidUtc ? "yes" : "no",
       g_gps.sawAnySentence ? "yes" : "no",
       rtc.c_str());
}

void setup() {
  Serial.begin(115200);
  delay(kSerialDelayMs);

  Serial.println("\r\n==================================================");
  Serial.println("Exercise 10: Simple GPS (No SD)");
  Serial.println("==================================================");

  if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) {
    logf("PMU init failed");
  }

  Wire.begin(OLED_SDA, OLED_SCL);
  g_oled.setI2CAddress(OLED_ADDR << 1);
  g_oled.begin();
  oledShowLines("Simple GPS", "Booting...");

#ifdef GPS_1PPS_PIN
  pinMode(GPS_1PPS_PIN, INPUT);
#endif

#ifdef GPS_WAKEUP_PIN
  // Keep wake pin in a neutral state similar to Meshtastic behavior.
  pinMode(GPS_WAKEUP_PIN, INPUT);
#endif

  startGpsUart(GPS_BAUD);
  logf("GPS UART started: RX=%d TX=%d baud=%lu", GPS_RX_PIN, GPS_TX_PIN, (unsigned long)g_gpsBaud);
  initialGpsProbe();

  showTryingMessage();
  g_lastReportMs = millis();
}

void loop() {
  pollGpsSerial();

  uint32_t now = millis();
  if ((uint32_t)(now - g_lastReportMs) >= kReportIntervalMs) {
    g_lastReportMs = now;

    showTryingMessage();
    uint32_t start = millis();
    while ((uint32_t)(millis() - start) < 2000) {
      pollGpsSerial();
      delay(5);
    }

    showStatusMessage();
  }
}
