// =================== PENGATURAN UTAMA (AKTIFKAN FITUR DI SINI) ===================
#define USE_BME280
#define USE_BH1750
#define USE_YL83
#define USE_CLOTH_MOISTURE
#define USE_OLED_DISPLAY
#define USE_TELEGRAM_BOT
#define USE_SERVO
// =================== PENGATURAN KONEKSI & LAYANAN ===================
#define WIFI_SSID "Si Kriting"
#define WIFI_PASS "kriting26"
#define BLYNK_TEMPLATE_ID "TMPL6U8tKrRNz"
#define BLYNK_TEMPLATE_NAME "Sistem Monitoring Cuaca"
#define BLYNK_AUTH_TOKEN "_eMb1J6U-MMz01lGCHTsE8BnK6kPRBW9"
#define BLYNK_PRINT Serial
#ifdef USE_TELEGRAM_BOT
  #define TELEGRAM_BOT_TOKEN "7764648935:AAHl4hvuLhIODVsGiupgPVPLqNtknZ0RS5k"
#endif
// =================== LIBRARY ===================
#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
#include <Wire.h>
#include "time.h"
#ifdef USE_TELEGRAM_BOT
  #include <WiFiClientSecure.h>
  #include <UniversalTelegramBot.h>
  #include <ArduinoJson.h>
  #include <LittleFS.h>
  #include <vector>
#endif
#ifdef USE_BME280
  #include <Adafruit_BME280.h>
  #include <Adafruit_Sensor.h>
#endif
#ifdef USE_BH1750
  #include <BH1750.h>
#endif
#ifdef USE_SERVO
  #include <ESP32Servo.h>
#endif
#ifdef USE_OLED_DISPLAY
  #include <Adafruit_GFX.h>
  #include <Adafruit_SSD1306.h>
#endif
// =================== PIN CONFIGURATION ===================
#define SDA_PIN 21
#define SCL_PIN 22
#ifdef USE_BH1750
  #define BH1750_CALIBRATION_FACTOR 1.35
#endif
#ifdef USE_YL83
  #define RAIN_SENSOR_PIN 35
  #define RAIN_SENSOR_DRY 4095
  #define RAIN_SENSOR_WET 1500
  #define RAIN_THRESHOLD  30
#endif
#ifdef USE_CLOTH_MOISTURE
  #define CLOTH_MOISTURE_PIN 34
#endif
#ifdef USE_SERVO
  #define SERVO_PIN 13
#endif
#ifdef USE_OLED_DISPLAY
  #define SCREEN_WIDTH 128
  #define SCREEN_HEIGHT 64
#endif
// =================== KONFIGURASI FUZZY ===================
const float CAHAYA_MENDUNG_MIN = 1000.0;
const float CAHAYA_MENDUNG_MID = 10000.0;
const float CAHAYA_MENDUNG_MAX = 60000.0;
const float CAHAYA_CERAH_MIN = 10000.0;
const float CAHAYA_CERAH_MAX = 60000.0;
const float PAKAIAN_KERING_MAX = 500.0;
const float PAKAIAN_LEMBAB_MIN = 500.0;
const float PAKAIAN_LEMBAB_MID = 1500.0;
const float PAKAIAN_LEMBAB_MAX = 2000.0;
const float PAKAIAN_BASAH_MIN = 1500.0;
const float PAKAIAN_BASAH_MAX = 2000.0;
#ifdef USE_SERVO
  const int POSISI_DALAM = 0;
  const int POSISI_TENGAH = 90;
  const int POSISI_LUAR = 180;
#endif
// =================== VIRTUAL PINS ===================
#define VPIN_TEMP V0
#define VPIN_HUM V1
#define VPIN_PRESSURE V2
#define VPIN_LUX V3
#define VPIN_RAIN_PERCENT V4
#define VPIN_CLOTH_MOISTURE V5
#define VPIN_JEMURAN_STATUS V6
#define VPIN_RAIN_TEXT V9
#define VPIN_MODE_SWITCH V10
#define VPIN_MANUAL_DALAM V11
#define VPIN_MANUAL_LUAR V12
// =================== OBJEK & VARIABEL ===================
#ifdef USE_BME280
  Adafruit_BME280 bme;
#endif
#ifdef USE_BH1750
  BH1750 lightMeter;
#endif
#ifdef USE_SERVO
  Servo jemuranServo;
#endif
#ifdef USE_OLED_DISPLAY
  Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
#endif
// Sensor Data
float temp = 0.0, hum = 0.0, pressure = 0.0, lux = 0.0;
int rainValue = 0, rainPercent = 0, clothMoisture = 0;
// Fuzzy Variabel
float F_pakaian_kering=0, F_pakaian_lembab=0, F_pakaian_basah=0;
float F_cahaya_gelap=0, F_cahaya_mendung=0, F_cahaya_cerah=0;
float F_hujan_tidak=0, F_hujan_ya=0;
// Status
String statusPakaian = "Init";
String statusCahaya = "Init";
int posisiServoSaatIni = POSISI_DALAM;
bool notifHujanTerkirim = false;
bool notifKeringTerkirim = true;
bool modeOtomatis = true;
// Timer
unsigned long previousSensorReadMillis = 0;
unsigned long previousTelegramMillis = 0;
const long sensorReadInterval = 5000;
const long telegramInterval = 2000; // Diperlambat dikit biar aman
#ifdef USE_TELEGRAM_BOT
  WiFiClientSecure client_telegram;
  UniversalTelegramBot bot(TELEGRAM_BOT_TOKEN, client_telegram);
  std::vector<String> subscribed_chat_ids;
  const char* chat_id_db_path = "/chat_ids.json";
#endif
// =================== PROTOTIPE ===================
void readAllSensors();
void fuzzifyInputs();
void handleOtomasiJemuran();
void sendDataToBlynk();
void updateOledDisplay();
void checkRainAndNotify();
String getRainIntensity(int percent);
String getPosisiJemuranText(int posisi);
float fuzzy_linear_down(float value, float min, float max);
float fuzzy_linear_up(float value, float min, float max);
float fuzzy_triangle(float value, float min, float mid, float max);
#ifdef USE_TELEGRAM_BOT
  void initLittleFS();
  void loadChatIDs();
  void saveChatIDs();
  void handleNewMessages();
  void sendTelegramNotification(const String &message);
#endif
// =================== SETUP ===================
void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println("\nSistem Otomasi Jemuran...");
  #ifdef USE_TELEGRAM_BOT
    client_telegram.setInsecure();
    initLittleFS();
    loadChatIDs();
  #endif
  Wire.begin(SDA_PIN, SCL_PIN);
  // Init Sensor (dengan Error Handling)
  #ifdef USE_BME280
    if (!bme.begin(0x76)) Serial.println("BME280 Gagal!");
  #endif
  #ifdef USE_BH1750
    if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) Serial.println("BH1750 Gagal!");
  #endif
  #ifdef USE_YL83
    pinMode(RAIN_SENSOR_PIN, INPUT);
  #endif
  #ifdef USE_CLOTH_MOISTURE
    pinMode(CLOTH_MOISTURE_PIN, INPUT);
  #endif
  #ifdef USE_SERVO
    jemuranServo.attach(SERVO_PIN, 500, 2500);
    jemuranServo.write(POSISI_DALAM);
  #endif
  #ifdef USE_OLED_DISPLAY
    if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) Serial.println("OLED Gagal");
    display.clearDisplay(); display.setTextColor(SSD1306_WHITE);
    display.setTextSize(1); display.setCursor(0,0); 
    display.println("Booting Sistem..."); display.display();
  #endif
  // WiFi & Blynk
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.print("Konek WiFi");
  int timeout = 0;
  while (WiFi.status() != WL_CONNECTED && timeout < 20) {
    delay(500); Serial.print("."); timeout++;
  }
  if(WiFi.status() == WL_CONNECTED) {
    Serial.println(" OK");
    Blynk.config(BLYNK_AUTH_TOKEN);
    Blynk.connect();
  } else {
    Serial.println(" Gagal WiFi");
  }
}
// =================== LOOP ===================
void loop() {
  if(WiFi.status() == WL_CONNECTED) Blynk.run();
  unsigned long currentMillis = millis();
  // --- Loop Sensor (5 Detik) ---
  if (currentMillis - previousSensorReadMillis >= sensorReadInterval) {
    previousSensorReadMillis = currentMillis;
    readAllSensors();
    fuzzifyInputs();
    if (modeOtomatis) handleOtomasiJemuran();
    if(WiFi.status() == WL_CONNECTED) sendDataToBlynk();
    #ifdef USE_OLED_DISPLAY
      updateOledDisplay();
    #endif
    checkRainAndNotify();
  }
  // --- Loop Telegram (2 Detik) ---
  #ifdef USE_TELEGRAM_BOT
    if (currentMillis - previousTelegramMillis >= telegramInterval) {
      previousTelegramMillis = currentMillis;
      if(WiFi.status() == WL_CONNECTED) handleNewMessages();
    }
  #endif
}
// =================== IMPLEMENTASI FUNGSI ===================
// 1. Baca Sensor
void readAllSensors() {
  #ifdef USE_BME280
    temp = bme.readTemperature(); 
    hum = bme.readHumidity(); 
    pressure = bme.readPressure() / 100.0F;
  #endif
  #ifdef USE_BH1750
    float rawLux = lightMeter.readLightLevel();
    if (rawLux >= 0) lux = rawLux * BH1750_CALIBRATION_FACTOR;
  #endif
  #ifdef USE_YL83
    rainValue = analogRead(RAIN_SENSOR_PIN);
    rainPercent = map(rainValue, RAIN_SENSOR_WET, RAIN_SENSOR_DRY, 100, 0);
    rainPercent = constrain(rainPercent, 0, 100);
  #endif
  #ifdef USE_CLOTH_MOISTURE
    clothMoisture = analogRead(CLOTH_MOISTURE_PIN);
  #endif
}
// 2. Fuzzifikasi
void fuzzifyInputs() {
  // Hujan (Biner)
  F_hujan_ya = (rainPercent > RAIN_THRESHOLD) ? 1.0 : 0.0;
  F_hujan_tidak = (rainPercent <= RAIN_THRESHOLD) ? 1.0 : 0.0;
  // Kain (Kering - Lembab - Basah)
  F_pakaian_kering = fuzzy_linear_down(clothMoisture, 0.0, PAKAIAN_KERING_MAX);
  F_pakaian_lembab = fuzzy_triangle(clothMoisture, PAKAIAN_LEMBAB_MIN, PAKAIAN_LEMBAB_MID, PAKAIAN_LEMBAB_MAX);
  F_pakaian_basah = fuzzy_linear_up(clothMoisture, PAKAIAN_BASAH_MIN, PAKAIAN_BASAH_MAX);
  if (F_pakaian_kering > 0.5) statusPakaian = "Kering";
  else if (F_pakaian_basah > 0.5) statusPakaian = "Basah";
  else statusPakaian = "Lembab";
  // Cahaya (Gelap - Mendung - Cerah)
  F_cahaya_gelap = fuzzy_linear_down(lux, 0.0, CAHAYA_MENDUNG_MIN);
  F_cahaya_mendung = fuzzy_triangle(lux, CAHAYA_MENDUNG_MIN, CAHAYA_MENDUNG_MID, CAHAYA_MENDUNG_MAX);
  F_cahaya_cerah = fuzzy_linear_up(lux, CAHAYA_CERAH_MIN, CAHAYA_CERAH_MAX);
  if (F_cahaya_gelap > 0.7) statusCahaya = "Gelap";
  else if (F_cahaya_cerah > 0.7) statusCahaya = "Cerah";
  else statusCahaya = "Mendung";
}
// 3. Logika Fuzzy
void handleOtomasiJemuran() {
  #if defined(USE_SERVO)
    float R1 = F_hujan_ya;
    float R2 = F_pakaian_kering;
    float R3 = min(F_hujan_tidak, min(max(F_pakaian_basah, F_pakaian_lembab), F_cahaya_gelap));
    float R4 = min(F_hujan_tidak, min(max(F_pakaian_basah, F_pakaian_lembab), F_cahaya_mendung));
    float R5 = min(F_hujan_tidak, min(F_pakaian_basah, F_cahaya_cerah));
    float R6 = min(F_hujan_tidak, min(F_pakaian_lembab, F_cahaya_cerah));
    float num = (R1*POSISI_DALAM) + (R2*POSISI_DALAM) + (R3*POSISI_DALAM) + 
                (R4*POSISI_TENGAH) + (R5*POSISI_LUAR) + (R6*POSISI_TENGAH);
    float den = R1 + R2 + R3 + R4 + R5 + R6;
    int target = POSISI_DALAM;
    if (den > 0) target = (int)(num / den);
    // Notifikasi Kering
    if (F_pakaian_kering > 0.8 && !notifKeringTerkirim) {
      if (posisiServoSaatIni != POSISI_DALAM) {
        sendTelegramNotification("✅ Info: Jemuran kering. Ditarik masuk.");
        Blynk.logEvent("jemuran_kering");
      }
      notifKeringTerkirim = true;
    } else if (F_pakaian_kering < 0.2) {
      notifKeringTerkirim = false;
    }
    // Gerakkan Servo
    if (target != posisiServoSaatIni) {
      jemuranServo.write(target);
      posisiServoSaatIni = target;
    }
  #endif
}
// 4. Fungsi Helper Fuzzy
float fuzzy_linear_down(float val, float min, float max) {
  if (val <= min) return 1.0;
  if (val >= max) return 0.0;
  return (max - val) / (max - min);
}
float fuzzy_linear_up(float val, float min, float max) {
  if (val <= min) return 0.0;
  if (val >= max) return 1.0;
  return (val - min) / (max - min);
}
float fuzzy_triangle(float val, float min, float mid, float max) {
  if (val <= min || val >= max) return 0.0;
  if (val == mid) return 1.0;
  if (val < mid) return (val - min) / (mid - min);
  return (max - val) / (max - mid);
}
// 5. Blynk & OLED Helper
void sendDataToBlynk() {
  Blynk.virtualWrite(VPIN_TEMP, temp);
  Blynk.virtualWrite(VPIN_HUM, hum);
  Blynk.virtualWrite(VPIN_LUX, lux);
  Blynk.virtualWrite(VPIN_RAIN_PERCENT, rainPercent);
  Blynk.virtualWrite(VPIN_RAIN_TEXT, getRainIntensity(rainPercent));
  Blynk.virtualWrite(VPIN_CLOTH_MOISTURE, clothMoisture);
  Blynk.virtualWrite(VPIN_JEMURAN_STATUS, getPosisiJemuranText(posisiServoSaatIni));
  Blynk.virtualWrite(VPIN_MODE_SWITCH, modeOtomatis);
}
#ifdef USE_OLED_DISPLAY
void updateOledDisplay() {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.printf("T:%.1f H:%.0f%%", temp, hum);
  display.setCursor(0, 12);
  display.printf("L:%.0f (%s)", lux, statusCahaya.c_str());
  display.setCursor(0, 24);
  display.printf("R:%d%% (%s)", rainPercent, (rainPercent > RAIN_THRESHOLD) ? "Y" : "N");
  display.setCursor(0, 36);
  display.printf("C:%d (%s)", clothMoisture, statusPakaian.c_str());
  display.setCursor(0, 48);
  display.printf("S:%s", getPosisiJemuranText(posisiServoSaatIni).c_str());
  display.display();
}
#endif
void checkRainAndNotify() {
  #ifdef USE_TELEGRAM_BOT
  if (F_hujan_ya && !notifHujanTerkirim) {
    sendTelegramNotification(" HUJAN! Jemuran ditarik masuk.");
    notifHujanTerkirim = true;
  } else if (F_hujan_tidak) {
    notifHujanTerkirim = false;
  }
  #endif
}
String getRainIntensity(int percent) {
  if (percent < 1) return "Kering";
  if (percent < 30) return "Gerimis";
  return "Hujan";
}
String getPosisiJemuranText(int posisi) {
  if (posisi < 45) return "DLM";
  if (posisi < 135) return "TGH";
  return "LUAR";
}
// 6. Telegram & Blynk Control
#ifdef USE_TELEGRAM_BOT
void sendTelegramNotification(const String &message) {
  for (const auto& id : subscribed_chat_ids) bot.sendMessage(id, message, "");
}
// LittleFS
void initLittleFS() { if(!LittleFS.begin(true)) Serial.println("FS Error"); }
void loadChatIDs() {
  File file = LittleFS.open(chat_id_db_path, "r");
  if (file) {
    StaticJsonDocument<512> doc; // Kecilkan buffer JSON
    if (deserializeJson(doc, file) == DeserializationError::Ok) {
      JsonArray array = doc.as<JsonArray>();
      for (JsonVariant v : array) subscribed_chat_ids.push_back(v.as<String>());
    }
    file.close();
  }
}
void saveChatIDs() {
  File file = LittleFS.open(chat_id_db_path, "w");
  if (file) {
    StaticJsonDocument<512> doc;
    JsonArray array = doc.to<JsonArray>();
    for (const auto& id : subscribed_chat_ids) array.add(id);
    serializeJson(doc, file);
    file.close();
  }
}
void handleNewMessages() {
  int num = bot.getUpdates(bot.last_message_received + 1);
  while (num) {
    for (int i = 0; i < num; i++) {
      String chat_id = bot.messages[i].chat_id;
      String text = bot.messages[i].text;      
      if (text == "/start") {
        bool exists = false;
        for (const auto& id : subscribed_chat_ids) if (id == chat_id) exists = true;
        if (!exists) { subscribed_chat_ids.push_back(chat_id); saveChatIDs(); }
        bot.sendMessage(chat_id, "Bot Aktif!", "");
      } 
      else if (text == "/status") {
        readAllSensors(); // Refresh data
        fuzzifyInputs();
        String msg = "Status:\n";
        msg += "T: " + String(temp, 1) + "C | H: " + String(hum, 0) + "%\n";
        msg += "L: " + String(lux, 0) + "lx (" + statusCahaya + ")\n";
        msg += "R: " + String(rainPercent) + "%\n";
        msg += "C: " + String(clothMoisture) + " (" + statusPakaian + ")\n";
        msg += "Pos: " + getPosisiJemuranText(posisiServoSaatIni);
        bot.sendMessage(chat_id, msg, "");
      }
    }
    num = bot.getUpdates(bot.last_message_received + 1);
  }
}
#endif
// Blynk Write Handlers
BLYNK_WRITE(VPIN_MODE_SWITCH) { modeOtomatis = (param.asInt() == 1); }
BLYNK_WRITE(VPIN_MANUAL_LUAR) { if(param.asInt() && !modeOtomatis) { jemuranServo.write(POSISI_LUAR); posisiServoSaatIni=POSISI_LUAR; } }
BLYNK_WRITE(VPIN_MANUAL_DALAM) { if(param.asInt() && !modeOtomatis) { jemuranServo.write(POSISI_DALAM); posisiServoSaatIni=POSISI_DALAM; } }