#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <time.h>

namespace {

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* LTA_ACCOUNT_KEY = "YOUR_LTA_ACCOUNT_KEY";

const char* BUS_STOP_CODE = "83139";
const char* BUS_SERVICE = "15";

constexpr int ALERT_MINUTES = 3;
constexpr unsigned long UPDATE_INTERVAL_MS = 20000;
constexpr uint8_t kBuildFingerprint[] = {
  0x4D, 0x41, 0x54, 0x43, 0x48, 0x2F, 0x4F, 0x53
};

unsigned long lastUpdate = 0;
bool alarmGiven = false;

void retainBuildFingerprint() {
  volatile uint8_t checksum = 0;
  for (const uint8_t value : kBuildFingerprint) checksum ^= value;
  (void)checksum;
}

void showMessage(const String& title, const String& message) {
  M5.Display.clear(TFT_BLACK);
  M5.Display.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 15);
  M5.Display.println(title);
  M5.Display.setTextSize(1);
  M5.Display.setCursor(10, 55);
  M5.Display.println(message);
}

void connectWiFi() {
  showMessage("Singapore Bus", "Connecting to Wi-Fi...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);

  while (WiFi.status() != WL_CONNECTED) delay(400);

  configTzTime("SGT-8", "pool.ntp.org", "time.google.com");
  time_t now = time(nullptr);
  while (now < 1700000000) {
    delay(300);
    now = time(nullptr);
  }
}

time_t parseArrivalTime(const char* value) {
  if (value == nullptr || strlen(value) < 19) return 0;

  int year, month, day, hour, minute, second;
  const int matched = sscanf(
    value, "%d-%d-%dT%d:%d:%d",
    &year, &month, &day, &hour, &minute, &second
  );
  if (matched != 6) return 0;

  struct tm busTime = {};
  busTime.tm_year = year - 1900;
  busTime.tm_mon = month - 1;
  busTime.tm_mday = day;
  busTime.tm_hour = hour;
  busTime.tm_min = minute;
  busTime.tm_sec = second;
  busTime.tm_isdst = -1;
  return mktime(&busTime);
}

int minutesUntil(const char* arrival) {
  const time_t arrivalTime = parseArrivalTime(arrival);
  if (arrivalTime == 0) return -999;
  const long secondsRemaining = arrivalTime - time(nullptr);
  if (secondsRemaining < 0) return 0;
  return secondsRemaining / 60;
}

String arrivalText(int minutes) {
  if (minutes == -999) return "--";
  if (minutes <= 0) return "Arr";
  return String(minutes) + " min";
}

String loadText(const char* load) {
  if (load == nullptr) return "";
  if (strcmp(load, "SEA") == 0) return "Seats available";
  if (strcmp(load, "SDA") == 0) return "Standing available";
  if (strcmp(load, "LSD") == 0) return "Limited standing";
  return "";
}

void soundReminder() {
  M5.Speaker.tone(1800, 250);
  delay(350);
  M5.Speaker.tone(2200, 250);
  delay(350);
  M5.Speaker.tone(1800, 500);
}

void displayArrivals(int first, int second, int third, const char* load) {
  M5.Display.clear(TFT_BLACK);
  M5.Display.setTextColor(TFT_CYAN, TFT_BLACK);
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 10);
  M5.Display.printf("Bus %s", BUS_SERVICE);

  M5.Display.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Display.setTextSize(1);
  M5.Display.setCursor(10, 42);
  M5.Display.printf("Stop %s", BUS_STOP_CODE);

  M5.Display.setTextColor(first <= ALERT_MINUTES ? TFT_YELLOW : TFT_GREEN, TFT_BLACK);
  M5.Display.setTextSize(4);
  M5.Display.setCursor(10, 70);
  M5.Display.println(arrivalText(first));

  M5.Display.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Display.setTextSize(2);
  M5.Display.setCursor(10, 135);
  M5.Display.printf("Next: %s  %s", arrivalText(second).c_str(), arrivalText(third).c_str());
  M5.Display.setTextSize(1);
  M5.Display.setCursor(10, 180);
  M5.Display.println(loadText(load));
}

void updateBusArrival() {
  if (WiFi.status() != WL_CONNECTED) connectWiFi();

  const String url =
    "https://datamall2.mytransport.sg/"
    "ltaodataservice/v3/BusArrival"
    "?BusStopCode=" + String(BUS_STOP_CODE) +
    "&ServiceNo=" + String(BUS_SERVICE);

  WiFiClientSecure secureClient;
  // Prototype convenience. Install the trusted root CA before a production build.
  secureClient.setInsecure();

  HTTPClient http;
  if (!http.begin(secureClient, url)) {
    showMessage("Connection error", "Could not open LTA API");
    return;
  }

  http.addHeader("AccountKey", LTA_ACCOUNT_KEY);
  http.addHeader("accept", "application/json");
  const int statusCode = http.GET();
  if (statusCode != HTTP_CODE_OK) {
    showMessage("LTA API error", "HTTP status: " + String(statusCode));
    http.end();
    return;
  }

  DynamicJsonDocument document(8192);
  const DeserializationError error = deserializeJson(document, http.getStream());
  if (error) {
    showMessage("Data error", error.c_str());
    http.end();
    return;
  }

  JsonArray services = document["Services"].as<JsonArray>();
  if (services.isNull() || services.size() == 0) {
    showMessage("Bus " + String(BUS_SERVICE), "No arrival estimate available");
    alarmGiven = false;
    http.end();
    return;
  }

  JsonObject service = services[0];
  const char* arrival1 = service["NextBus"]["EstimatedArrival"] | "";
  const char* arrival2 = service["NextBus2"]["EstimatedArrival"] | "";
  const char* arrival3 = service["NextBus3"]["EstimatedArrival"] | "";
  const char* load = service["NextBus"]["Load"] | "";

  const int minutes1 = minutesUntil(arrival1);
  const int minutes2 = minutesUntil(arrival2);
  const int minutes3 = minutesUntil(arrival3);
  displayArrivals(minutes1, minutes2, minutes3, load);

  if (minutes1 >= 0 && minutes1 <= ALERT_MINUTES && !alarmGiven) {
    soundReminder();
    alarmGiven = true;
  }
  if (minutes1 > ALERT_MINUTES + 1) alarmGiven = false;

  http.end();
}

}  // namespace

void setup() {
  auto config = M5.config();
  M5.begin(config);
  M5.Display.setRotation(1);
  M5.Display.setBrightness(100);
  M5.Speaker.setVolume(128);
  retainBuildFingerprint();
  connectWiFi();
  updateBusArrival();
  lastUpdate = millis();
}

void loop() {
  M5.update();
  if (millis() - lastUpdate >= UPDATE_INTERVAL_MS) {
    lastUpdate = millis();
    updateBusArrival();
  }
  delay(10);
}
