/*
  Balance connectée WiFi — ESP32 + HX711
  ----------------------------------------
  - L'ESP32 crée un point d'accès WiFi (mode AP)
  - Un portail captif redirige automatiquement le smartphone vers la page de pesée
  - La page se met à jour en temps réel via Server-Sent Events (SSE)
  - La calibration se fait depuis la page web, le facteur est sauvegardé en NVS

  Bibliothèques requises :
    - HX711 (bogde)
    - ESPAsyncWebServer (me-no-dev)
    - AsyncTCP (me-no-dev)
    - Preferences (intégrée ESP32)

  Câblage HX711 :
    DOUT → GPIO 4
    SCK  → GPIO 5
    VCC  → 3V3 (ou 5V si module compatible)
    GND  → GND
*/

#include <WiFi.h>
#include <DNSServer.h>
#include <ESPAsyncWebServer.h>
#include <Preferences.h>
#include "HX711.h"

// --- Pins HX711 ---
static const int PIN_DOUT = 4;
static const int PIN_SCK  = 5;

// --- WiFi AP ---
static const char* AP_SSID = "Balance-ESP32";
static const char* AP_PASS = "";   // réseau ouvert

// --- Objets globaux ---
HX711          scale;
DNSServer      dnsServer;
AsyncWebServer server(80);
AsyncEventSource events("/events");
Preferences    prefs;

float calibrationFactor = 1.0f;   // chargé depuis NVS au démarrage

// --- Flags inter-tâches (handler HTTP → loop) ---
// AsyncWebServer tourne dans une tâche séparée ; les opérations HX711
// doivent rester dans loop() pour éviter les conflits de timing.
volatile bool  pendingTare      = false;
volatile bool  pendingCalib     = false;
volatile float pendingCalibMass = 0.0f;

// --- Page HTML embarquée ---
static const char HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Balance</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, Arial, sans-serif;
      background: #f5f5f5;
      padding: 20px;
      min-height: 100vh;
    }
    .card {
      background: white;
      border-radius: 20px;
      padding: 40px 30px 32px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.09);
      max-width: 400px;
      margin: 0 auto 20px;
      text-align: center;
    }
    h1 { color: #555; font-size: 1.1em; font-weight: 500; margin-bottom: 28px; }
    h2 { color: #666; font-size: 1em; font-weight: 600; margin-bottom: 18px; text-align: left; }
    .weight { font-size: 5em; font-weight: 700; color: #1a1a2e; line-height: 1; }
    .unit   { font-size: 1.3em; color: #888; margin-top: 8px; }
    .dot {
      display: inline-block; width: 10px; height: 10px;
      border-radius: 50%; background: #ccc;
      margin-right: 6px; vertical-align: middle;
      transition: background 0.3s;
    }
    .dot.live { background: #27ae60; }
    .status { font-size: 0.82em; color: #aaa; margin-top: 24px; }

    /* Calibration */
    .steps { text-align: left; }
    .step  { margin-bottom: 20px; }
    .step p { font-size: 0.9em; color: #555; margin-bottom: 8px; }
    .row   { display: flex; gap: 8px; align-items: center; }
    input[type=number] {
      flex: 1; padding: 10px 12px; border: 1px solid #ddd;
      border-radius: 10px; font-size: 1em; outline: none;
    }
    input[type=number]:focus { border-color: #3498db; }
    button {
      padding: 10px 18px; border: none; border-radius: 10px;
      font-size: 0.95em; font-weight: 600; cursor: pointer;
      transition: opacity 0.2s;
    }
    button:active { opacity: 0.7; }
    .btn-tare  { background: #ecf0f1; color: #555; }
    .btn-calib { background: #3498db; color: white; }
    .fb {
      font-size: 0.82em; margin-top: 6px; min-height: 1.2em;
      color: #27ae60;
    }
    .fb.err { color: #e74c3c; }
    .factor { font-size: 0.8em; color: #bbb; margin-top: 10px; text-align: right; }
  </style>
</head>
<body>

  <!-- Affichage poids -->
  <div class="card">
    <h1>Balance connectée</h1>
    <div class="weight" id="w">—</div>
    <div class="unit">grammes</div>
    <div class="status">
      <span class="dot" id="dot"></span>
      <span id="status">Connexion…</span>
    </div>
  </div>

  <!-- Calibration -->
  <div class="card">
    <h2>Calibration</h2>
    <div class="steps">

      <div class="step">
        <p>① Vider la balance, puis mettre à zéro :</p>
        <button class="btn-tare" onclick="doTare()">Tare (zéro)</button>
        <div class="fb" id="fb-tare"></div>
      </div>

      <div class="step">
        <p>② Poser une masse connue et entrer sa valeur :</p>
        <div class="row">
          <input type="number" id="mass" placeholder="Masse en grammes" min="1" step="any">
          <button class="btn-calib" onclick="doCalib()">Calibrer</button>
        </div>
        <div class="fb" id="fb-calib"></div>
      </div>

    </div>
    <div class="factor" id="factor"></div>
  </div>

  <script>
    const wEl      = document.getElementById('w');
    const dotEl    = document.getElementById('dot');
    const statEl   = document.getElementById('status');
    const fbTare   = document.getElementById('fb-tare');
    const fbCalib  = document.getElementById('fb-calib');
    const factorEl = document.getElementById('factor');

    // --- SSE : mise à jour temps réel ---
    const src = new EventSource('/events');

    src.addEventListener('weight', e => {
      wEl.textContent = parseFloat(e.data).toFixed(1);
      dotEl.classList.add('live');
      statEl.textContent = 'Mis à jour ' + new Date().toLocaleTimeString();
    });

    src.addEventListener('info', e => {
      const msg = e.data;
      if (msg === 'tare_ok') {
        fbTare.className = 'fb';
        fbTare.textContent = '✓ Tare effectuée';
      } else if (msg.startsWith('calib_ok:')) {
        const f = parseFloat(msg.split(':')[1]);
        fbCalib.className = 'fb';
        fbCalib.textContent = '✓ Calibration sauvegardée';
        factorEl.textContent = 'Facteur : ' + f.toFixed(2);
      }
    });

    src.onopen  = () => { dotEl.classList.add('live');    statEl.textContent = 'Connecté'; };
    src.onerror = () => { dotEl.classList.remove('live'); statEl.textContent = 'Reconnexion…'; };

    // --- Actions calibration ---
    function doTare() {
      fbTare.className = 'fb';
      fbTare.textContent = 'En cours…';
      fetch('/tare', { method: 'POST' }).catch(() => {
        fbTare.className = 'fb err';
        fbTare.textContent = 'Erreur réseau';
      });
    }

    function doCalib() {
      const mass = parseFloat(document.getElementById('mass').value);
      if (!mass || mass <= 0) {
        fbCalib.className = 'fb err';
        fbCalib.textContent = 'Entrer une masse valide';
        return;
      }
      fbCalib.className = 'fb';
      fbCalib.textContent = 'En cours…';
      const body = new URLSearchParams({ mass: mass.toString() });
      fetch('/calibrate', { method: 'POST', body }).catch(() => {
        fbCalib.className = 'fb err';
        fbCalib.textContent = 'Erreur réseau';
      });
    }
  </script>
</body>
</html>
)rawliteral";

// --- Portail captif ---
static void handleCaptiveRedirect(AsyncWebServerRequest* req) {
  req->redirect("http://192.168.4.1/");
}

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

  // --- Chargement facteur de calibration depuis NVS ---
  prefs.begin("balance", true);   // lecture seule
  calibrationFactor = prefs.getFloat("calib", 1.0f);
  prefs.end();
  Serial.printf("Facteur de calibration : %.2f%s\n",
                calibrationFactor,
                calibrationFactor == 1.0f ? " (défaut — calibration requise)" : " (chargé)");

  // --- HX711 ---
  scale.begin(PIN_DOUT, PIN_SCK);
  if (!scale.is_ready()) {
    Serial.println("HX711 non détecté — vérifier câblage.");
  }
  scale.set_scale();
  scale.tare(20);
  scale.set_scale(calibrationFactor);
  Serial.println("HX711 prêt.");

  // --- WiFi AP ---
  WiFi.mode(WIFI_AP);
  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.printf("AP démarré — SSID : %s — IP : %s\n", AP_SSID,
                WiFi.softAPIP().toString().c_str());

  // --- DNS captif ---
  dnsServer.start(53, "*", WiFi.softAPIP());

  // --- Routes HTTP ---
  server.on("/", HTTP_GET, [](AsyncWebServerRequest* req) {
    req->send_P(200, "text/html", HTML);
  });

  // Tare
  server.on("/tare", HTTP_POST, [](AsyncWebServerRequest* req) {
    pendingTare = true;
    req->send(200, "text/plain", "OK");
  });

  // Calibration
  server.on("/calibrate", HTTP_POST, [](AsyncWebServerRequest* req) {
    if (req->hasParam("mass", true)) {
      float m = req->getParam("mass", true)->value().toFloat();
      if (m > 0.0f) {
        pendingCalibMass = m;
        pendingCalib     = true;
        req->send(200, "text/plain", "OK");
      } else {
        req->send(400, "text/plain", "Masse invalide");
      }
    } else {
      req->send(400, "text/plain", "Parametre mass manquant");
    }
  });

  // Portail captif Android / iOS
  server.on("/generate_204",              HTTP_GET, handleCaptiveRedirect);
  server.on("/connecttest.txt",           HTTP_GET, handleCaptiveRedirect);
  server.on("/redirect",                  HTTP_GET, handleCaptiveRedirect);
  server.on("/hotspot-detect.html",       HTTP_GET, handleCaptiveRedirect);
  server.on("/library/test/success.html", HTTP_GET, handleCaptiveRedirect);
  server.onNotFound(handleCaptiveRedirect);

  // --- SSE ---
  events.onConnect([](AsyncEventSourceClient* client) {
    client->send("0.0", "weight", millis(), 500);
  });
  server.addHandler(&events);

  server.begin();
  Serial.println("Serveur web démarré.");
}

void loop() {
  dnsServer.processNextRequest();

  // --- Tare ---
  if (pendingTare) {
    pendingTare = false;
    scale.set_scale();          // remet le facteur à 1 avant la tare
    scale.tare(20);
    scale.set_scale(calibrationFactor);
    events.send("tare_ok", "info", millis());
    Serial.println("Tare effectuée.");
  }

  // --- Calibration ---
  if (pendingCalib) {
    pendingCalib = false;
    scale.set_scale(1.0f);           // facteur neutre pour lire la valeur brute
    float raw    = scale.get_value(20);
    float factor = raw / pendingCalibMass;
    calibrationFactor = factor;
    scale.set_scale(calibrationFactor);

    prefs.begin("balance", false);   // écriture
    prefs.putFloat("calib", calibrationFactor);
    prefs.end();

    Serial.printf("Calibration : brut=%.0f  masse=%.1fg  facteur=%.2f\n",
                  raw, pendingCalibMass, calibrationFactor);

    String msg = "calib_ok:" + String(calibrationFactor, 2);
    events.send(msg.c_str(), "info", millis());
  }

  // --- Lecture et diffusion du poids ---
  if (scale.is_ready()) {
    float grams = scale.get_units(5);
    events.send(String(grams, 1).c_str(), "weight", millis());
    Serial.printf("Poids : %.1f g\n", grams);
  }

  delay(500);
}
