#include <painlessMesh.h>
#include <Preferences.h>
#include <SPIFFS.h>

// Konstanta Mesh
#define MESH_PREFIX       "SaklarPintarMeshNet"
#define MESH_PASSWORD     "SaKlarPinTarmESHnET"
#define MESH_PORT         5555

// Konstanta Sensor/Aktuator/Indikator
#define TOUCH_INPIN       3 //4
#define RELAY_OUTPIN      1 //14
#define TOUCH_INDICATOR   6 //26
#define NET_INDICATOR     7 //32

// Konstanta Preferences
#define GATEWAY_ID        "gateID"
#define MESH_CHANNEL      "meshCH"

// Konstanta SPIFF for Scheduling/Timer
#define SCHEDULEJSON      "/schedule.json"

// Konstanta Broadcast/Single
#define GET_GATEWAY       "getGateway"
#define SEND_STATES       "sendStates"
#define SEND_RSSIS        "sendRSSIS"

// Inisialisasi library
painlessMesh  mesh;
Preferences   preferences;
Scheduler     userScheduler;

//// Konstanta Global
// WiFi and Mesh
int wifi_channel    = 0;                            // Channel WiFi untuk Mesh Network
uint32_t gateway_id = 0;                            // ID gateway untuk terhubung ke jaringan luar
SimpleList<uint32_t> nodes;                         // Node dalam jaringan mesh
// Saklar status
bool relay_state = LOW;                             // Status terkini relay
bool last_touch_state;                              // Status terakhir touch sensor
bool current_touch_state;                           // Status terkini touch sensor
// Reset Device and Touch sensor
bool is_reset_indicator = false;
int reset_indicator_count = 0;
unsigned long last_reset_blinking_time = 0;         // Waktu sebelumnya untuk berkedip
const unsigned long reset_indicator_interval = 500; // Interval kedip LED
bool can_reset_device = true;
const unsigned long reset_device_expired = 300000;  // Durasi untuk reset device sejak perangkat dinyalakan (5 menit) ??? Apakah sebaiknya ditambah durasi expirenya
unsigned long touch_start_reset = 0;                // Waktu saat sentuhan dimulai untuk melakukan reset device
const unsigned long touch_reset_duration = 5000;    // Durasi tombol ditekan untuk reset (5 detik)
// Konfigurasi untuk Scheduling/Timer dari Gateway
String current_time = "00:00";
unsigned long last_check_time = 0;
const unsigned long interval_check_time = 60000;    // 1 menit untuk interval cek waktu schedule
String last_activated_time = "";                    // Variabel penanda apakah lampu sudah dinyalakan untuk waktu itu


//// Fungsi Prototipe
// Callback
void receivedCallback(const uint32_t &from, const String &msg);
void newConnectionCallback(uint32_t nodeId);
void changedConnectionCallback();
void nodeTimeAdjustedCallback(int32_t offset);
void nodeDelayReceivedCallback(uint32_t nodeId, int32_t delay);
// Send Messages
void sendStateToGateway();
void sendGatewayId(uint32_t toNode);
void sendBroadcastMsg(String msg);
void sendSingleMsg(uint32_t toNode, String msg);
// System and Indicator
void scanMeshNetwork();
void handleResetIndicator();
bool readJsonFile(const char* path, DynamicJsonDocument& doc);
bool writeJsonFile(const char* path, const JsonDocument& doc);


//// System and Indicator
// Fungsi untuk Scanning Mesh Network
void scanMeshNetwork() {
  // WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  bool meshNetFound = false;

  digitalWrite(NET_INDICATOR, LOW);     // Matikan LED

  while (!meshNetFound) {
    Serial.println("Node: Memulai Scanning Mesh Network...");

    digitalWrite(NET_INDICATOR, !digitalRead(NET_INDICATOR));   // Indikator Net Blinking

    int numNetworks = WiFi.scanNetworks();
    Serial.print("Node: Scanning selesai. Hasil: ");

    if (numNetworks == 0) {
      Serial.println("No Networks Found!");
    }
    else {
      Serial.printf("%d Networks Found!\n", numNetworks);
      Serial.println("No | SSID                             | RSSI | CH |");
      for (int i = 0; i < numNetworks; ++i) {
        Serial.printf("%2d | %-32.32s | %4ld | %2ld | ", i + 1, WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.channel(i));

        if (WiFi.SSID(i) == MESH_PREFIX) {
          Serial.print(" -> Found Mesh Network SSID and Channel!");

          // Simpan channel yang ditemukan
          wifi_channel = WiFi.channel(i);
          meshNetFound = true;

          // break; // Hentikan scanning setelah menemukan mesh network
        }

        Serial.println();
        delay(10);
      }
    }

    WiFi.scanDelete();
    delay(6000);      // beri delay 6 detik
  }

  digitalWrite(NET_INDICATOR, LOW);     // Matikan LED

  preferences.putInt(MESH_CHANNEL, wifi_channel);
  Serial.printf("Node: Menggunakan channel %d untuk Mesh Network.\n", wifi_channel);

  // Blink 5x Indikator Net
  for (int i = 0; i < 2; i++) {
    digitalWrite(NET_INDICATOR, LOW);
    delay(250);
    digitalWrite(NET_INDICATOR, HIGH);
    delay(750);
  }
  digitalWrite(NET_INDICATOR, LOW);     // Matikan LED
} // end scanMeshNetwork

// Fungsi untuk handle indikator pada perangkat berupa kedipan led
void handleResetIndicator() {
  if (is_reset_indicator) {
    unsigned long current_millis = millis();

    if (current_millis - last_reset_blinking_time >= reset_indicator_interval) {
      last_reset_blinking_time = current_millis;

      // Toggle LED Reset Indicator
      digitalWrite(TOUCH_INDICATOR, !digitalRead(TOUCH_INDICATOR));
      digitalWrite(NET_INDICATOR, !digitalRead(NET_INDICATOR));

      reset_indicator_count++;

      // Stop blinking after 18 toggles (9 cycles of on and off)
      if (reset_indicator_count >= 18) {
        digitalWrite(TOUCH_INDICATOR, LOW);     // Matikan LED
        digitalWrite(NET_INDICATOR, LOW);       // Matikan LED
        is_reset_indicator = false;

        delay(1000);
        ESP.restart();    // Reset board
      }
    }
  }
} // end handleResetIndicator

// Fungsi untuk membaca file JSON dan mengembalikan sebagai JsonDocument
bool readJsonFile(const char* path, DynamicJsonDocument& doc) {
  Serial.printf("Node: Read file JSON: %s", path);
  File file = SPIFFS.open(path, "r");
  if (!file || file.isDirectory()) {
    Serial.println(" - Gagal membuka file");
    return false;
  }

  DeserializationError error = deserializeJson(doc, file);
  file.close();

  if (error) {
    Serial.print(" - Gagal parsing JSON: ");
    Serial.println(error.c_str());
    return false;
  }

  Serial.println(" - JSON berhasil dibaca");
  return true;
}

// Fungsi untuk menulis JSON ke file
bool writeJsonFile(const char* path, const JsonDocument& doc) {
  Serial.printf("Node: Write file JSON: %s", path);
  File file = SPIFFS.open(path, "w");
  if (!file) {
    Serial.println(" - Gagal membuka file untuk menulis");
    return false;
  }

  if (serializeJson(doc, file) == 0) {
    Serial.println(" - Gagal menulis JSON ke file");
    file.close();
    return false;
  }

  file.close();
  Serial.println(" - JSON berhasil ditulis ke file");
  return true;
}

//// Fungsi untuk mengirim pesan ke mesh
void sendStateToGateway() {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["status"] = relay_state;          // bool status saklar

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  if (mesh.isConnected(gateway_id)) {
    mesh.sendSingle(gateway_id, jsonString);
    Serial.println("Node: Send state to Gateway");
  }
} // end sendStateToGateway

void  sendRSSIToGateway() {
  int nodeRSSI = WiFi.RSSI();

  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["rssi"]   = nodeRSSI;             // signal strength

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  if (mesh.isConnected(gateway_id)) {
    mesh.sendSingle(gateway_id, jsonString);
    Serial.println("Node: Send rssi to Gateway");
  }
} // end sendRSSIToGateway

void sendGatewayId(uint32_t toNode) {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"]     = mesh.getNodeId();     // uint32_t
  jsonDoc["gatewayId"]  = gateway_id;           // Id Gateway

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendSingle(toNode, jsonString);
  Serial.printf("Node: Send Id Gateway to %u\n", toNode);
} // end sendGatewayId

void sendBroadcastMsg(String msg) {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["msg"]    = msg;                  // Isi pesan
  jsonDoc["bc"]     = true;                 // Pesan adalah broadcast

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendBroadcast(jsonString);
  Serial.println("Node: Send broadcast msg");
} // end sendBroadcastMsg

void sendSingleMsg(uint32_t toNode, String msg) {
  StaticJsonDocument<200> jsonDoc;
  jsonDoc["nodeId"] = mesh.getNodeId();     // uint32_t
  jsonDoc["msg"]    = msg;                  // Isi pesan
  jsonDoc["bc"]     = false;                // Pesan adalah single

  String jsonString;
  serializeJson(jsonDoc, jsonString);

  mesh.sendSingle(toNode, jsonString);
  Serial.printf("Node: Send single msg to %u\n", toNode);
} // end sendSingleMsg

//// Mesh Callback
// Fungsi untuk menerima pesan dari mesh
void receivedCallback(const uint32_t &from, const String &msg) {
  Serial.printf("Node: MESH Received -> From: %u, Msg = %s\n", from, msg.c_str());

  // Buat objek JSON
  JsonDocument jsonDoc;     // Pakai definisi abstrak biar nilai memori menyesuaikan
  DeserializationError error = deserializeJson(jsonDoc, msg);

  // Cek apakah parsing JSON berhasil
  if (error) {
    Serial.println("Node: Failed to parse MESH JSON!");
    return;
  }

  // Ambil "nodeId" (wajib ada dalam setiap pesan)
  uint32_t nodeId = jsonDoc["nodeId"];
  if (!nodeId) {
    Serial.println("Node: Missing MESH nodeId!");
    return;
  }

  Serial.printf("Node: Getting message from nodeId: %u\n", nodeId);

  // Cek apakah ada field "gatewayId" dan simpan ke Preferences
  if (jsonDoc.containsKey("gatewayId")) {
    uint32_t gatewayId = jsonDoc["gatewayId"];
    gateway_id = gatewayId;
    preferences.putUInt(GATEWAY_ID, gatewayId);
    Serial.printf("Node: Save %u as Gateway\n", gatewayId);
  }

  // Cek apakah ada field "status" dari Gateway
  if (nodeId == gateway_id && jsonDoc.containsKey("status")) {
    bool state = jsonDoc["status"];

    relay_state = state;
    digitalWrite(RELAY_OUTPIN, relay_state);

    // Kirim status saklar ke gateway
    sendStateToGateway();
    Serial.printf("Node: Change switch state to %s from Gateway\n", state ? "ON" : "OFF");
  }

  // Cek apakah ada field "schedule" dari Gateway
  if (nodeId == gateway_id && jsonDoc.containsKey("schedule")) {
    // Serial.print("Node: Update Schedule == ");
    // serializeJsonPretty(jsonDoc, Serial);
    // Serial.println();

    JsonDocument scheduleList;
    scheduleList = jsonDoc["schedule"].as<JsonArray>();

    writeJsonFile(SCHEDULEJSON, scheduleList);
    sendSingleMsg(gateway_id, "Update Schedule Berhasil");
    Serial.println("Node: Update Schedule Timer from Gateway");
  }

  // Cek apakah ada field "msg" untuk pesan broadcast
  if (jsonDoc.containsKey("bc")) {
    bool bcMsg = jsonDoc["bc"];
    if (bcMsg) {  // Pesan Broadcast
      String msg = jsonDoc["msg"];

      if (msg == GET_GATEWAY) {
        if (gateway_id != 0) {      // cek jika tidak bernilai 0 maka kirimkan Gateway id pada node tersebut
          sendGatewayId(nodeId);
        }
      }

      if (nodeId == gateway_id && msg == SEND_STATES) {
        sendStateToGateway();
      }
      else if (nodeId == gateway_id && msg == SEND_RSSIS) {
        sendRSSIToGateway();
      }
      else if (nodeId == gateway_id) {      // message berupa current time dari gateway
        current_time = msg;
        Serial.printf("Node: Get current_time %s from gateway\n", current_time);
      }

      /////// if apa lagi
    }
    else {        // Pesan Single
      String msg = jsonDoc["msg"];

      if (nodeId == gateway_id && msg == SEND_STATES) {
        sendStateToGateway();
      }
      else if (nodeId == gateway_id && msg == SEND_RSSIS) {
        sendRSSIToGateway();
      }

      /////// if apa lagi
    }
  }
} // end receivedCallback

void newConnectionCallback(uint32_t nodeId) {
  Serial.printf("Node: New Connection, nodeId = %u\n", nodeId);
  Serial.printf("Node: New Connection, %s\n", mesh.subConnectionJson(true).c_str());

  // Blink 5x Indikator Net
  for (int i = 0; i < 5; i++) {
    digitalWrite(NET_INDICATOR, LOW);
    delay(250);
    digitalWrite(NET_INDICATOR, HIGH);
    delay(750);
  }
  digitalWrite(NET_INDICATOR, LOW);     // Matikan LED
} // end newConnectionCallback

void changedConnectionCallback() {
  Serial.printf("Node: Changed connections\n");

  nodes = mesh.getNodeList();
  Serial.printf("Node: Num nodes %d\n", nodes.size());
  Serial.printf("Node: Connection list:");
  SimpleList<uint32_t>::iterator node = nodes.begin();
  while (node != nodes.end()) {
    Serial.printf(" %u", *node);
    node++;
  }
  Serial.println();
} // end changedConnectionCallback

void nodeTimeAdjustedCallback(int32_t offset) {
  Serial.printf("Node: Adjusted time %u. Offset = %d\n", mesh.getNodeTime(), offset);
} // end nodeTimeAdjustedCallback

void nodeDelayReceivedCallback(uint32_t nodeId, int32_t delay) {
  Serial.printf("Node: Delay from node %u. Delay = %d\n", nodeId, delay);
} // end nodeDelayReceivedCallback


void setup() {
  // Inisialisasi awal
  Serial.begin(115200);
  preferences.begin("mesh-node", false);
  if (!SPIFFS.begin(true)) {
    Serial.println("Node: Failed mount SPIFFS");
    return;
  }
  WiFi.mode(WIFI_AP_STA);
  // WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://forum.arduino.cc/t/no-wifi-connect-with-esp32-c3-super-mini/1324046/13
  WiFi.setTxPower(WIFI_POWER_11dBm);
  delay(100);              // beri sedikit waktu untuk

  // Inisialisasi penggunaan pin
  pinMode(TOUCH_INPIN, INPUT);
  pinMode(RELAY_OUTPIN, OUTPUT);
  pinMode(TOUCH_INDICATOR, OUTPUT);
  pinMode(NET_INDICATOR, OUTPUT);

  current_touch_state = digitalRead(TOUCH_INPIN);     // Mengambil status awal touch sensor

  // Memulai scanning Mesh Network untuk mencari ssid dan channel yang digunakan root device
  wifi_channel = preferences.getInt(MESH_CHANNEL, 0);
  scanMeshNetwork();

  gateway_id = preferences.getUInt(GATEWAY_ID, 0);

  // Inisialisasi mesh
  mesh.setDebugMsgTypes(ERROR | STARTUP | CONNECTION);
  // mesh.setDebugMsgTypes(ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL);
  mesh.init(MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT, WIFI_AP_STA, wifi_channel);
  mesh.onReceive(&receivedCallback);
  mesh.onNewConnection(&newConnectionCallback);
  mesh.onChangedConnections(&changedConnectionCallback);
  mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);
  mesh.onNodeDelayReceived(&nodeDelayReceivedCallback);

  // Inisialisasi pengecekan Data Schedule sudah tersedia atau belum
  if (!SPIFFS.exists(SCHEDULEJSON)) {
    File file = SPIFFS.open(SCHEDULEJSON, "w");
    if (file) {
      // Tidak menulis apa pun = file kosong
      file.close();
      Serial.println("Node: File kosong schedule.json berhasil dibuat");
    }
    else {
      Serial.println("Node: Gagal membuat file schedule.json");
    }
  }
  else {
    Serial.println("Node: File schedule.json sudah tersedia di SPIFF");
  }

  // preferences.end();
} // end setup


void loop() {
  mesh.update();    // Update jaringan mesh

  last_touch_state    = current_touch_state;        // Menyimpan status terakhir touch sensor
  current_touch_state = digitalRead(TOUCH_INPIN);   // Membaca status baru touch sensor

  if (last_touch_state == LOW && current_touch_state == HIGH) {
    digitalWrite(TOUCH_INDICATOR, HIGH);
    relay_state = !relay_state;                     // Status relay beralih
    digitalWrite(RELAY_OUTPIN, relay_state);        // Mengontrol relay sesuai status peralihan

    touch_start_reset = millis();                   // Catat waktu saat sentuhan dimulai
    sendStateToGateway();                           // Kirim status saklar ke Gateway
    Serial.println("Node: Sensor pressed, send switch state to Gateway");
  }

  // Jika tombol tetap ditekan selama 5 detik
  if (can_reset_device && current_touch_state == HIGH && ((unsigned long)millis() - touch_start_reset) >= touch_reset_duration) {
    Serial.println("Node: Resetting Device Data...");

    preferences.clear();          // hapus semua key value
    if (SPIFFS.exists(SCHEDULEJSON)) {
      Serial.println("Node: File schedule.json ditemukan, menghapus...");
      if (SPIFFS.remove(SCHEDULEJSON)) {
        Serial.println("Node: File schedule.json berhasil dihapus");
      }
      else {
        Serial.println("Node: Gagal menghapus file schedule.json");
      }
    }
    else {
      Serial.println("Node: File schedule.json tidak ditemukan");
    }

    can_reset_device = false;     // Mengubah status reset device
    relay_state = LOW;            // Mematikan saklar
    digitalWrite(RELAY_OUTPIN, relay_state);

    // Mulai proses berkedip LED
    is_reset_indicator = true;
    reset_indicator_count = 0;
    last_reset_blinking_time = millis();

    touch_start_reset = 0;        // Reset waktu sentuhan
  }

  // Reset waktu sentuhan jika tombol dilepas
  if (last_touch_state == HIGH && current_touch_state == LOW) {
    digitalWrite(TOUCH_INDICATOR, LOW);   // Matikan LED
    digitalWrite(NET_INDICATOR, LOW);     // Matikan LED
    touch_start_reset = 0;                // Reset waktu sentuhan
    // Serial.println("Reset waktu sentuhan!");
  }

  // Memanggil fungsi kedip LED untuk Reset
  handleResetIndicator();

  if ((unsigned long)millis() - last_check_time >= interval_check_time) {
    last_check_time = millis();

    // cek jika bernilai 0 maka akan meminta secara broadcast Gateway id
    if (gateway_id == 0) {
      sendBroadcastMsg(GET_GATEWAY);
    }

    DynamicJsonDocument readDoc(1024);
    if (readJsonFile(SCHEDULEJSON, readDoc)) {
      // serializeJsonPretty(readDoc, Serial);
      // Serial.println();
      for (JsonObject schedule : readDoc.as<JsonArray>()) {
        String time = schedule["time"];
        String state = schedule["state"];

        if (time == current_time && last_activated_time != time) {
          Serial.printf("Node: Schedule cocok: %s → %s\n", time.c_str(), state.c_str());

          if (state == "ON") {
            relay_state = HIGH;
            digitalWrite(RELAY_OUTPIN, relay_state);
            Serial.println("Node: Switch is turned ON from Schedule, send State to Gateway");
          }
          else if (state == "OFF") {
            relay_state = LOW;
            digitalWrite(RELAY_OUTPIN, relay_state);
            Serial.println("Node: Switch is turned OFF from Schedule, send State to Gateway");
          }

          sendStateToGateway();     // Kirim status saklar ke Gateway

          last_activated_time = time;
          break;
        }
      }
    }
    else {
      Serial.println("Node: Failed to read schedule.json");
    }
  }

  // Reset device akan expired dalam waktu yang telah ditentukan hanya setelah perangkat dinyalakan
  // Hanya bisa reset device setelah alat dimatikan dan dinyalakan lagi
  if (can_reset_device && (unsigned long)millis() >= reset_device_expired) {
    can_reset_device = false;       // Mengubah status reset device
    Serial.println("Node: Reset device has expired!");
  }
} // end loop
