/**
 * RemoteRig — ESP8266 Camera Bridge Firmware
 * ==========================================
 * Dedicated board clipped to each GoPro Hero 3.
 * 
 * ONE JOB: talk to the camera.
 *   - Connects to GoPro Wi-Fi AP (10.5.5.1)
 *   - Polls status every 30s → sends JSON over UART to ESP32
 *   - Receives commands from ESP32 over UART → executes against camera
 *   - Zero network switching, zero MQTT, zero cloud
 *
 * UART Protocol: JSON-per-line at 115200 8N1
 *   ESP8266 → ESP32: {"type":"status","battery_raw":217,...}\n
 *   ESP8266 → ESP32: {"type":"ack","cmd":"start_recording"}\n
 *   ESP8266 → ESP32: {"type":"error","msg":"..."}\n
 *   ESP32 → ESP8266: {"type":"cmd","command":"start_recording"}\n
 *
 * Hardware:
 *   - ESP8266 D1 Mini (or NodeMCU)
 *   - UART TX → ESP32 RX (GPIO 16)
 *   - UART RX → ESP32 TX (GPIO 16)
 *   - Shared GND between boards
 *   - LiPo → 3.3V buck → VIN on both boards
 */

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <LittleFS.h>

// ────────────────────────────────────────────
// Configuration (SPIFFS via LittleFS)
// ────────────────────────────────────────────

struct Config {
  String camera_ssid     = "GOPRO-BP-";
  String camera_password = "goprohero";
  String camera_ip       = "10.5.5.1";
  int    poll_interval_sec = 30;
} cfg;

bool loadConfig() {
  if (!LittleFS.begin()) { Serial.println("[CFG] LittleFS mount failed"); return false; }
  File f = LittleFS.open("/config.json", "r");
  if (!f) { Serial.println("[CFG] No config — using defaults"); return false; }

  JsonDocument doc;
  DeserializationError err = deserializeJson(doc, f);
  f.close();
  if (err) { Serial.printf("[CFG] Parse error: %s\n", err.c_str()); return false; }

  cfg.camera_ssid       = doc["camera_ssid"]     | cfg.camera_ssid;
  cfg.camera_password   = doc["camera_password"] | cfg.camera_password;
  cfg.camera_ip         = doc["camera_ip"]       | cfg.camera_ip;
  cfg.poll_interval_sec = doc["poll_interval_sec"] | cfg.poll_interval_sec;
  return true;
}

// ────────────────────────────────────────────
// Camera HTTP Client (GoPro Hero 3)
// ────────────────────────────────────────────

WiFiClient goproClient;

struct CamStatus {
  bool valid             = false;
  int  video_remaining_sec = 0;
  bool recording         = false;
  int  battery_raw       = 0;
};

CamStatus fetchStatus() {
  CamStatus s;
  
  String url = "http://" + cfg.camera_ip +
               "/bacpac/SH?t=" + cfg.camera_password + "&p=%01";
  
  HTTPClient http;
  http.useHTTP10(true);
  http.begin(goproClient, url);
  http.setTimeout(5000);
  int code = http.GET();

  if (code != 200) { http.end(); return s; }

  String raw = http.getString();
  http.end();
  if (raw.length() < 58) return s;

  const uint8_t* buf = (const uint8_t*)raw.c_str();
  s.valid             = true;
  s.video_remaining_sec = buf[25] | (buf[26] << 8);
  s.recording         = (buf[29] == 1);
  s.battery_raw       = buf[57];
  return s;
}

bool sendCommand(const String& cmd) {
  String param = (cmd == "start_recording") ? "%01" : "%00";
  String url = "http://" + cfg.camera_ip +
               "/bacpac/SH?t=" + cfg.camera_password + "&p=" + param;

  HTTPClient http;
  http.useHTTP10(true);
  http.begin(goproClient, url);
  http.setTimeout(5000);
  int code = http.GET();
  http.end();
  return (code == 200);
}

// ────────────────────────────────────────────
// UART Protocol (to ESP32)
// ────────────────────────────────────────────
// Using HardwareSerial on GPIO1/3 (D1 Mini default TX/RX)
// On D1 Mini: TX=GPIO1, RX=GPIO3 (labeled TX/RX on board)

// Send JSON line to ESP32
void sendToESP32(const JsonDocument& doc) {
  String line;
  serializeJson(doc, line);
  Serial.println(line);  // newline-terminated for framing
  Serial.flush();
}

// Send status update
void sendStatus(const CamStatus& s) {
  JsonDocument doc;
  doc["type"]               = "status";
  doc["valid"]              = s.valid;
  doc["battery_raw"]        = s.battery_raw;
  doc["video_remaining_sec"] = s.video_remaining_sec;
  doc["recording"]          = s.recording;
  doc["online"]             = s.valid;
  doc["uptime_ms"]          = millis();
  sendToESP32(doc);
}

// Send acknowledgment
void sendAck(const String& cmd) {
  JsonDocument doc;
  doc["type"] = "ack";
  doc["cmd"]  = cmd;
  sendToESP32(doc);
}

// Send error
void sendError(const String& msg) {
  JsonDocument doc;
  doc["type"]  = "error";
  doc["msg"]   = msg;
  sendToESP32(doc);
}

// ────────────────────────────────────────────
// Command handling (from ESP32 over UART)
// ────────────────────────────────────────────

void handleCommand(const JsonDocument& doc) {
  String cmd = doc["command"] | "";
  
  if (cmd == "start_recording" || cmd == "stop_recording") {
    bool ok = sendCommand(cmd);
    if (ok) {
      sendAck(cmd);
    } else {
      sendError("Camera unreachable — command failed");
    }
  } else if (cmd == "ping") {
    JsonDocument pong;
    pong["type"] = "pong";
    pong["uptime_ms"] = millis();
    sendToESP32(pong);
  } else {
    sendError("Unknown command: " + cmd);
  }
}

// ────────────────────────────────────────────
// UART line reader (non-blocking)
// ────────────────────────────────────────────

String serialLine;

bool readLine(String& line) {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n') {
      line = serialLine;
      serialLine = "";
      return true;
    }
    if (c != '\r') serialLine += c;
  }
  return false;
}

// ────────────────────────────────────────────
// LED
// ────────────────────────────────────────────

const int LED = LED_BUILTIN; // active-low on ESP8266 D1 Mini

void ledOn()  { digitalWrite(LED, LOW); }
void ledOff() { digitalWrite(LED, HIGH); }

// ────────────────────────────────────────────
// Setup
// ────────────────────────────────────────────

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("\n[BRIDGE] ESP8266 Camera Bridge v1.0");

  pinMode(LED, OUTPUT);
  ledOff();

  loadConfig();

  // Connect to GoPro AP — this is the ONLY network we touch
  Serial.printf("[WIFI] Connecting to camera AP: %s\n", cfg.camera_ssid.c_str());
  WiFi.mode(WIFI_STA);
  WiFi.begin(cfg.camera_ssid.c_str(), cfg.camera_password.c_str());

  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500); Serial.print("."); attempts++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\n[WIFI] Connected. IP: %s\n", WiFi.localIP().toString().c_str());
    ledOn(); // Solid = connected
  } else {
    Serial.println("\n[WIFI] FAILED — will retry in loop");
  }
}

// ────────────────────────────────────────────
// Main Loop — poll camera, relay over UART
// ────────────────────────────────────────────

void loop() {
  unsigned long now = millis();
  static unsigned long lastPoll = 0;
  static unsigned long lastWiFiRetry = 0;
  static bool cameraOnline = false;

  // ── Wi-Fi reconnection ──
  if (WiFi.status() != WL_CONNECTED && now - lastWiFiRetry > 10000) {
    lastWiFiRetry = now;
    Serial.println("[WIFI] Reconnecting...");
    WiFi.reconnect();
  }

  // ── Poll camera ──
  if (now - lastPoll > (unsigned long)(cfg.poll_interval_sec * 1000)) {
    lastPoll = now;

    if (WiFi.status() == WL_CONNECTED) {
      CamStatus s = fetchStatus();
      
      if (s.valid && !cameraOnline) {
        cameraOnline = true;
        ledOn();
      } else if (!s.valid && cameraOnline) {
        cameraOnline = false;
        ledOff();
      }
      
      sendStatus(s);
    } else {
      // Offline — send empty status so ESP32 knows we're alive but camera is down
      CamStatus s;
      sendStatus(s);
    }
  }

  // ── Read commands from ESP32 over UART ──
  String line;
  if (readLine(line)) {
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, line);
    if (!err) {
      String type = doc["type"] | "";
      if (type == "cmd") {
        handleCommand(doc);
      }
      // Ignore other message types — they're for the ESP32
    }
  }

  // ── LED blink when offline ──
  if (!cameraOnline) {
    static unsigned long lastBlink = 0;
    if (now - lastBlink > 500) {
      lastBlink = now;
      digitalWrite(LED, !digitalRead(LED));
    }
  }
}
