/*
 * Project 14: ESP32 Wi-Fi + Bluetooth (BLE) Coexistence
 * Run a Wi-Fi web server and a BLE GATT server at the same time. Both read the
 * same DHT11 sensor and both control the same LED, kept in sync with each other.
 *
 * Pin map:
 *   GPIO 4  -> DHT11 data (VCC to 3.3V, GND to GND)
 *   GPIO 26 -> LED (through 220 ohm resistor to GND)
 *
 * Libraries: ESPAsyncWebServer, AsyncTCP, DHT sensor library, Adafruit Unified Sensor,
 *            BLEDevice (ships with the ESP32 Arduino core, no extra install)
 * Serial: 115200 baud
 */

// Import required libraries
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

#define DHTPIN 4          // Digital pin connected to the DHT sensor
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

#define LED_PIN 26

// Same three IDs, just the last hex digit changed: easy to tell apart while learning.
#define SERVICE_UUID      "12345678-1234-5678-1234-56789abcdef0"
#define SENSOR_CHAR_UUID  "12345678-1234-5678-1234-56789abcdef1"
#define LED_CHAR_UUID     "12345678-1234-5678-1234-56789abcdef2"
#define BLE_DEVICE_NAME   "ESP32-Kit-P14"

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
const char* PARAM_INPUT_1 = "state";

// Shared state: both the web server and the BLE server read and write these.
bool ledState = false;
String lastTempStr = "--";
String lastHumStr = "--";

BLECharacteristic *sensorChar;
BLECharacteristic *ledChar;
bool bleClientConnected = false;

// Non-blocking sensor timer (Project 4's millis() pattern)
unsigned long lastSensorRead = 0;
const unsigned long SENSOR_INTERVAL = 3000;

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 2.4rem;}
    p {font-size: 1.6rem;}
    body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
    .switch {position: relative; display: inline-block; width: 120px; height: 68px}
    .switch input {display: none}
    .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px}
    .slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
    input:checked+.slider {background-color: #2196F3}
    input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
    .badge {display:inline-block; background:#4527a0; color:#fff; font-size:.9rem; padding:4px 12px; border-radius:16px; margin-bottom:10px;}
  </style>
</head>
<body>
  <div class="badge">Wi-Fi + Bluetooth, both live</div>
  <h2>ESP32 Coexistence Demo</h2>
  %BUTTONPLACEHOLDER%
  <p>Temperature: <span id="temperature">%TEMPERATURE%</span> &deg;C</p>
  <p>Humidity: <span id="humidity">%HUMIDITY%</span> %</p>
  <p style="font-size:1rem;color:#666;">The same LED and the same sensor are also reachable over BLE. Connect with a BLE app (e.g. nRF Connect) while this page stays open, and watch both update.</p>
<script>
function toggleCheckbox(element) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/update?state=" + (element.checked ? 1 : 0), true);
  xhr.send();
}
setInterval(function () {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      var on = this.responseText == "1";
      document.getElementById("output").checked = on;
      document.getElementById("outputState").innerHTML = on ? "On" : "Off";
    }
  };
  xhttp.open("GET", "/state", true);
  xhttp.send();
}, 1000);
setInterval(function () {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById("temperature").innerHTML = this.responseText;
    }
  };
  xhttp.open("GET", "/temperature", true);
  xhttp.send();
}, 3000);
setInterval(function () {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById("humidity").innerHTML = this.responseText;
    }
  };
  xhttp.open("GET", "/humidity", true);
  xhttp.send();
}, 3000);
</script>
</body>
</html>
)rawliteral";

// Replaces placeholders with live values in the web page
String processor(const String& var){
  if (var == "BUTTONPLACEHOLDER") {
    String checked = ledState ? "checked" : "";
    return "<h4>LED - GPIO 26 - State <span id=\"outputState\">" + String(ledState ? "On" : "Off") +
           "</span></h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"output\" " +
           checked + "><span class=\"slider\"></span></label>";
  }
  if (var == "TEMPERATURE") return lastTempStr;
  if (var == "HUMIDITY") return lastHumStr;
  return String();
}

// Sets the LED and pushes the new state into the BLE characteristic, so a BLE
// client reading it afterward (from either source of a toggle) sees the truth.
void setLed(bool on, const char* source){
  ledState = on;
  digitalWrite(LED_PIN, ledState ? HIGH : LOW);
  ledChar->setValue(ledState ? "1" : "0");
  Serial.printf("[%s]    LED -> %s\n", source, ledState ? "ON" : "OFF");
}

// BLE: a phone wrote to the LED characteristic
class LedCallbacks: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *characteristic) override {
    String value = characteristic->getValue().c_str();
    if (value.length() > 0) {
      setLed(value[0] == '1', "BLE");
    }
  }
};

// BLE: track whether a client is connected, so notify() is only called when it matters
class ServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* bleServer) override {
    bleClientConnected = true;
    Serial.println("[BLE]    Client connected");
  }
  void onDisconnect(BLEServer* bleServer) override {
    bleClientConnected = false;
    Serial.println("[BLE]    Client disconnected, advertising again");
    bleServer->startAdvertising();
  }
};

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

  Serial.println();
  Serial.println("==============================================");
  Serial.println(" Project 14: ESP32 Wi-Fi + Bluetooth Coexistence");
  Serial.println("==============================================");
  Serial.print  ("DHT11 data -> GPIO "); Serial.println(DHTPIN);
  Serial.print  ("LED        -> GPIO "); Serial.println(LED_PIN);

  dht.begin();
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  // Connect to Wi-Fi
  Serial.print("Connecting to Wi-Fi: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("Wi-Fi connected!");
  Serial.print("Open this address in your browser:  http://");
  Serial.println(WiFi.localIP());
  Serial.println("----------------------------------------------");

  // Bring up BLE alongside the Wi-Fi connection that is already running
  BLEDevice::init(BLE_DEVICE_NAME);
  BLEServer *bleServer = BLEDevice::createServer();
  bleServer->setCallbacks(new ServerCallbacks());
  BLEService *service = bleServer->createService(SERVICE_UUID);

  sensorChar = service->createCharacteristic(
    SENSOR_CHAR_UUID,
    BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
  );
  sensorChar->addDescriptor(new BLE2902());   // lets a client subscribe to notifications
  sensorChar->setValue("--,--");

  ledChar = service->createCharacteristic(
    LED_CHAR_UUID,
    BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE
  );
  ledChar->setValue("0");
  ledChar->setCallbacks(new LedCallbacks());

  service->start();
  BLEAdvertising *advertising = BLEDevice::getAdvertising();
  advertising->addServiceUUID(SERVICE_UUID);
  advertising->start();

  Serial.print  ("BLE advertising as: "); Serial.println(BLE_DEVICE_NAME);
  Serial.println("Scan for it in a BLE app (e.g. nRF Connect) while the Wi-Fi page stays open.");
  Serial.println("----------------------------------------------");

  // Web routes
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });
  server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", lastTempStr.c_str());
  });
  server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", lastHumStr.c_str());
  });
  // GET <ESP_IP>/state  (polled once a second to stay in sync with BLE toggles)
  server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", ledState ? "1" : "0");
  });
  // GET <ESP_IP>/update?state=<0|1>  (from the web toggle)
  server.on("/update", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request->hasParam(PARAM_INPUT_1)) {
      String inputMessage = request->getParam(PARAM_INPUT_1)->value();
      setLed(inputMessage.toInt() == 1, "WEB");
    }
    request->send(200, "text/plain", "OK");
  });

  server.begin();
}

void loop(){
  // Read the DHT11 on a timer, never inside a request handler: one reading feeds
  // both the web page's poll and the BLE notification below.
  if (millis() - lastSensorRead >= SENSOR_INTERVAL) {
    lastSensorRead = millis();

    float t = dht.readTemperature();
    float h = dht.readHumidity();
    bool ok = true;
    if (isnan(t)) { Serial.println("[SENSOR] Failed to read temperature!"); ok = false; }
    else lastTempStr = String(t, 1);
    if (isnan(h)) { Serial.println("[SENSOR] Failed to read humidity!"); ok = false; }
    else lastHumStr = String(h, 1);
    if (ok) Serial.printf("[SENSOR] Temp: %.1f C  Hum: %.1f %%\n", t, h);

    String payload = lastTempStr + "," + lastHumStr;
    sensorChar->setValue(payload.c_str());
    if (bleClientConnected) sensorChar->notify();
  }
}
