/*
 * main.cpp — ESP32 e-ink Display Firmware
 *
 * Connects to WiFi + MQTT broker, subscribes to display/text and display/clear,
 * renders rolling 3-line text on a Waveshare 7.5" V2 (800x480) e-ink display.
 *
 * Required libraries (platformio.ini):
 *   knolleary/PubSubClient
 *   bblanchon/ArduinoJson
 *   zinggjm/GxEPD2
 *   adafruit/Adafruit GFX Library
 */

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeSansBold24pt7b.h>
#include <Fonts/FreeSans9pt7b.h>

// ── User config — edit before flashing ───────────────────────────────────────

#define WIFI_SSID      "YOUR_WIFI_SSID"
#define WIFI_PASSWORD  "YOUR_WIFI_PASSWORD"
#define MQTT_HOST      "192.168.1.100"   // Windows PC IP running Mosquitto
#define MQTT_PORT      1883
#define DEVICE_ID      "display-01"

// ── Display pins (Waveshare 7.5" V2 → ESP32) ─────────────────────────────────
// CLK → GPIO 18  (SPI SCLK, wired directly — managed by library)
// DIN → GPIO 23  (SPI MOSI, wired directly — managed by library)

#define PIN_CS    5
#define PIN_DC   17
#define PIN_RST  16
#define PIN_BUSY  4

// ── MQTT topics ───────────────────────────────────────────────────────────────

#define TOPIC_TEXT   "display/text"
#define TOPIC_CLEAR  "display/clear"
#define TOPIC_STATUS "display/status"

// ── Display layout ────────────────────────────────────────────────────────────

#define DISPLAY_W       800
#define DISPLAY_H       480
#define MARGIN_X         12
#define LINE_1_BASELINE 110    // y baseline of first text line
#define LINE_SPACING    145    // pixels between baselines (generous for readability)

// ── Display object ────────────────────────────────────────────────────────────

GxEPD2_BW<GxEPD2_750_T7, GxEPD2_750_T7::HEIGHT> display(
    GxEPD2_750_T7(PIN_CS, PIN_DC, PIN_RST, PIN_BUSY)
);

// ── MQTT + WiFi ───────────────────────────────────────────────────────────────

WiFiClient   wifiClient;
PubSubClient mqtt(wifiClient);

// ── Pending render state ──────────────────────────────────────────────────────

static String pendingLines[3];
static bool   renderPending = false;

// ── Display helpers ───────────────────────────────────────────────────────────

void renderLines(const String lines[3]) {
    display.setFullWindow();
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);

        for (int i = 0; i < 3; i++) {
            if (lines[i].length() == 0) continue;

            // Speaker label lines (e.g. "[PASTOR]") use a smaller italic font
            bool isLabel = lines[i].startsWith("[") && lines[i].endsWith("]");

            if (isLabel) {
                display.setFont(&FreeSans9pt7b);
            } else {
                display.setFont(&FreeSansBold24pt7b);
            }

            display.setTextColor(GxEPD_BLACK);
            display.setCursor(MARGIN_X, LINE_1_BASELINE + i * LINE_SPACING);
            display.print(lines[i]);
        }
    } while (display.nextPage());

    Serial.println("[Display] Refreshed");
}

void clearDisplay() {
    display.setFullWindow();
    display.firstPage();
    do {
        display.fillScreen(GxEPD_WHITE);
    } while (display.nextPage());
    Serial.println("[Display] Cleared");
}

void showBootMessage() {
    String boot[3] = {"", "", "  DISPLAY READY"};
    renderLines(boot);
}

// ── MQTT callback ─────────────────────────────────────────────────────────────

void onMqttMessage(char* topic, byte* payload, unsigned int len) {
    String topicStr = String(topic);

    if (topicStr == TOPIC_CLEAR) {
        for (int i = 0; i < 3; i++) pendingLines[i] = "";
        renderPending = true;
        return;
    }

    if (topicStr == TOPIC_TEXT) {
        // Null-terminate the payload
        char buf[600];
        if (len >= sizeof(buf)) {
            Serial.println("[MQTT] Payload too large, skipped");
            return;
        }
        memcpy(buf, payload, len);
        buf[len] = '\0';

        JsonDocument doc;
        DeserializationError err = deserializeJson(doc, buf);
        if (err) {
            Serial.printf("[MQTT] JSON error: %s\n", err.c_str());
            return;
        }

        JsonArray arr = doc["lines"].as<JsonArray>();
        if (!arr) return;

        int i = 0;
        for (JsonVariant v : arr) {
            if (i >= 3) break;
            pendingLines[i++] = v.as<String>();
        }
        // Pad remaining lines
        for (; i < 3; i++) pendingLines[i] = "";

        renderPending = true;
    }
}

// ── WiFi ──────────────────────────────────────────────────────────────────────

void connectWifi() {
    if (WiFi.status() == WL_CONNECTED) return;

    Serial.printf("[WiFi] Connecting to %s", WIFI_SSID);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    unsigned long start = millis();
    while (WiFi.status() != WL_CONNECTED) {
        if (millis() - start > 30000) {
            Serial.println("\n[WiFi] Timeout — restarting");
            ESP.restart();
        }
        delay(500);
        Serial.print(".");
    }
    Serial.printf("\n[WiFi] Connected: %s\n", WiFi.localIP().toString().c_str());
}

// ── MQTT ──────────────────────────────────────────────────────────────────────

void connectMqtt() {
    while (!mqtt.connected()) {
        Serial.printf("[MQTT] Connecting to %s:%d ...\n", MQTT_HOST, MQTT_PORT);

        if (mqtt.connect(DEVICE_ID)) {
            Serial.println("[MQTT] Connected");
            mqtt.subscribe(TOPIC_TEXT);
            mqtt.subscribe(TOPIC_CLEAR);
            mqtt.publish(TOPIC_STATUS, "{\"ready\":true}");
        } else {
            Serial.printf("[MQTT] Failed (state=%d), retry in 5 s\n", mqtt.state());
            delay(5000);
        }
    }
}

// ── Arduino setup & loop ──────────────────────────────────────────────────────

void setup() {
    Serial.begin(115200);
    Serial.println("\n[Boot] Church Live Transcription Display");

    display.init(115200);
    display.setRotation(0);
    clearDisplay();
    showBootMessage();

    connectWifi();

    mqtt.setServer(MQTT_HOST, MQTT_PORT);
    mqtt.setCallback(onMqttMessage);
    mqtt.setBufferSize(600);
    mqtt.setKeepAlive(60);

    connectMqtt();
}

void loop() {
    // Maintain connectivity
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("[WiFi] Lost — reconnecting...");
        connectWifi();
    }
    if (!mqtt.connected()) {
        connectMqtt();
    }
    mqtt.loop();

    // Render outside of MQTT callback to avoid blocking the broker heartbeat
    if (renderPending) {
        renderPending = false;
        renderLines(pendingLines);
    }
}
