#include "HX711.h"
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Firebase_ESP_Client.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WiFiManager.h>
#include <Wire.h>

#include "addons/RTDBHelper.h"
#include "addons/TokenHelper.h"

// KONFIGURASI FIREBASE 
#define API_KEY "AIzaSyDtb-5Qs13elSodm8gRJ-rGj0kornwd1BY"
#define DATABASE_URL "mylaundry-d02fb-default-rtdb.asia-southeast1.firebasedatabase.app"

// PIN HARDWARE
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define I2C_ADDRESS 0x3C
#define LOADCELL_DOUT_PIN 32
#define LOADCELL_SCK_PIN 33
#define BUTTON_PIN 27
#define BUZZER_PIN 25

// OBJEK GLOBAL
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
HX711 scale;

float calibration_factor = 103500.00;

Preferences preferences;
FirebaseData fbdo;
FirebaseData streamDo;
FirebaseAuth auth;
FirebaseConfig config;

// BERAT REALTIME & FITUR LOCK
float currentWeight = 0.000;
bool isLocked = false;
float lockedWeight = 0.000;
float lastWeight = 0.000;
unsigned long stableStartTime = 0;

// PENGATURAN SENSITIVITAS LOCK
const float TOLERANSI_STABIL = 0.005f; 
const float TOLERANSI_UNLOCK = 0.02f;  
const unsigned long WAKTU_STABIL = 2000; 

// SERIAL MONITOR
unsigned long lastSerialTime = 0;
#define SERIAL_PRINT_INTERVAL 500

// STATE DISPLAY
String strJasa = "-";
String strEst = "-";
String strTipeLayanan = "-";
String strBerat = "0.000";
String strHarga = "Rp0";

// FIREBASE PATH
String firebasePath = "/laundry";

// TEXT
int scrollPosX_Jasa = 42;
int scrollPosX_Berat = 42;
unsigned long lastScrollTime = 0;

// STREAM HEALTH CHECK
unsigned long lastStreamCheck = 0;
#define STREAM_CHECK_INTERVAL 30000

void beepBuzzer(int times) {
  for (int i = 0; i < times; i++) {
    digitalWrite(BUZZER_PIN, HIGH);
    delay(150);
    digitalWrite(BUZZER_PIN, LOW);
    delay(100);
  }
}

String getValue(String data, char separator, int index) {
  int found = 0;
  int strIndex[] = {0, -1};
  int maxIndex = data.length() - 1;
  for (int i = 0; i <= maxIndex && found <= index; i++) {
    if (data.charAt(i) == separator || i == maxIndex) {
      found++;
      strIndex[0] = strIndex[1] + 1;
      strIndex[1] = (i == maxIndex) ? i + 1 : i;
    }
  }
  return found > index ? data.substring(strIndex[0], strIndex[1]) : "";
}

void printDisplayState() {
  Serial.println("-----------------------------");
  Serial.println("[DISPLAY STATE]");
  Serial.print("  Jasa      : "); Serial.println(strJasa);
  Serial.print("  Est       : "); Serial.println(strEst);
  Serial.print("  Tipe      : "); Serial.println(strTipeLayanan);
  Serial.print("  Berat     : "); Serial.println(strBerat);
  Serial.print("  Harga     : "); Serial.println(strHarga);
  Serial.println("-----------------------------");
}

void reconnectStream() {
  Serial.println("[STREAM] Menyambung ulang stream Firebase...");
  Firebase.RTDB.endStream(&streamDo);
  delay(200);

  if (Firebase.RTDB.getString(&fbdo, firebasePath + "/status_hp")) {
    String rawData = fbdo.stringData();
    Serial.print("[STREAM] Data status_hp saat reconnect: ");
    Serial.println(rawData);
    strJasa = getValue(rawData, '|', 0); strJasa.trim();
    strEst = getValue(rawData, '|', 1); strEst.trim();
    strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim();
    strBerat = getValue(rawData, '|', 3); strBerat.trim();
    strHarga = getValue(rawData, '|', 4); strHarga.trim();
    printDisplayState();
  } else {
    Serial.println("[STREAM] Gagal baca status_hp saat reconnect");
  }

  if (Firebase.RTDB.beginMultiPathStream(&streamDo, firebasePath)) {
    Serial.println("[STREAM] Stream berhasil disambung ulang");
  } else {
    Serial.println("[STREAM] GAGAL menyambung ulang stream!");
  }
}

void setup() {
  Serial.begin(9600);
  Serial.println("\n=============================");
  Serial.println("[BOOT] Sistem mulai...");
  Wire.begin(21, 22);

  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);

  display.begin(I2C_ADDRESS, true);
  display.setTextWrap(false);
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);

  // CEK RESET WIFI
  if (digitalRead(BUTTON_PIN) == LOW) {
    Serial.println("[BOOT] Tombol ditekan saat boot — mode reset WiFi");
    display.clearDisplay();
    display.setCursor(10, 30);
    display.print("Tahan 5 detik = Reset");
    display.display();

    unsigned long pressStart = millis();
    while (digitalRead(BUTTON_PIN) == LOW && (millis() - pressStart < 5000)) {
      delay(10);
    }

    if (millis() - pressStart >= 5000) {
      Serial.println("[BOOT] WiFi direset!");
      display.clearDisplay();
      display.setCursor(10, 30);
      display.print("WiFi Direset!");
      display.setCursor(10, 45);
      display.print("Lepas Tombol...");
      display.display();
      beepBuzzer(3);

      WiFiManager wm;
      wm.resetSettings();
      delay(1000);

      while (digitalRead(BUTTON_PIN) == LOW) { delay(10); }
      delay(500);
      ESP.restart();
    }
  }

// WIFI MANAGER
  Serial.println("[WIFI] Menghubungkan ke WiFi...");
  display.clearDisplay();
  display.setCursor(0, 14); display.print("Menghubungkan WiFi");
  display.setCursor(0, 28); display.print("Jika gagal,");
  display.setCursor(0, 42); display.print("buka: laundry.net");
  display.display();

  WiFiManager wm;
  wm.setCaptivePortalEnable(true);
  wm.setHostname("laundry");
  
  wm.setConnectTimeout(15);
  wm.setConfigPortalTimeout(60); 
  bool connected = wm.autoConnect("MyLaundry");

  if (!connected) {
    Serial.println("[WIFI] GAGAL terhubung! Restart...");
    display.clearDisplay();
    display.setCursor(10, 30); display.print("WiFi Gagal!");
    display.setCursor(10, 42); display.print("Restart...");
    display.display();
    delay(3000);
    ESP.restart();
  }

  Serial.print("[WIFI] Terhubung! IP: ");
  Serial.println(WiFi.localIP());

  // FIREBASE
  Serial.println("[FIREBASE] Inisialisasi Firebase...");
  config.api_key = API_KEY;
  config.database_url = DATABASE_URL;
  config.signer.test_mode = true;
  config.token_status_callback = tokenStatusCallback;
  fbdo.setBSSLBufferSize(4096, 1024);
  Firebase.begin(&config, &auth);
  Firebase.reconnectWiFi(true);
  Serial.println("[FIREBASE] Firebase siap");

  // LOAD CELL
  Serial.println("[LOADCELL] Inisialisasi load cell...");
  scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
  scale.set_scale(calibration_factor);

  display.clearDisplay();
  display.setCursor(10, 35); display.print("Tunggu 5 detik");
  display.display();
  Serial.println("[LOADCELL] Warm-up 5 detik...");
  delay(5000);

  scale.tare();
  Serial.println("[LOADCELL] Tare selesai, siap menimbang");

  // CEK STATUS
  Serial.println("[FIREBASE] Membaca status_hp awal...");
  if (Firebase.RTDB.getString(&fbdo, firebasePath + "/status_hp")) {
    String rawData = fbdo.stringData();
    Serial.print("[FIREBASE] status_hp awal: ");
    Serial.println(rawData);
    strJasa = getValue(rawData, '|', 0); strJasa.trim();
    strEst = getValue(rawData, '|', 1); strEst.trim();
    strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim();
    strBerat = getValue(rawData, '|', 3); strBerat.trim();
    strHarga = getValue(rawData, '|', 4); strHarga.trim();
    printDisplayState();
  } else {
    Serial.println("[FIREBASE] Gagal baca status_hp awal");
  }

  if (Firebase.RTDB.beginMultiPathStream(&streamDo, firebasePath)) {
    Serial.println("[STREAM] Stream Firebase aktif");
  } else {
    Serial.println("[STREAM] GAGAL memulai stream Firebase!");
  }

  display.clearDisplay();
  display.setCursor(10, 35);
  display.print("Sistem Siap!");
  display.display();
  beepBuzzer(2);
  delay(1000);

  lastStreamCheck = millis();
  Serial.println("[BOOT] Setup selesai, masuk loop");
  Serial.println("=============================\n");
}

void loop() {

  // CEK STREAM
  if (millis() - lastStreamCheck > STREAM_CHECK_INTERVAL) {
    lastStreamCheck = millis();

    if (WiFi.status() != WL_CONNECTED) {
      Serial.println("[WIFI] Koneksi terputus! Mencoba reconnect...");
      WiFi.reconnect();
      delay(2000);
      if (WiFi.status() == WL_CONNECTED) {
        Serial.print("[WIFI] Reconnect berhasil! IP: ");
        Serial.println(WiFi.localIP());
      } else {
        Serial.println("[WIFI] Reconnect GAGAL");
      }
    } else {
      Serial.println("[HEALTH] WiFi OK | Stream check...");
    }

    if (!streamDo.httpConnected()) {
      Serial.println("[STREAM] Stream terputus, reconnect...");
      reconnectStream();
    } else {
      Serial.println("[HEALTH] Stream OK");
    }
  }

  // BACA STREAM FIREBASE
  if (Firebase.ready() && Firebase.RTDB.readStream(&streamDo)) {
    if (streamDo.streamAvailable()) {
      String dPath = streamDo.dataPath();
      String dType = streamDo.dataType();

      Serial.print("[STREAM] Event masuk — path: ");
      Serial.print(dPath);
      Serial.print(" | type: ");
      Serial.println(dType);

      if (dPath == "/status_hp" && dType == "string") {
        String rawData = streamDo.stringData();
        Serial.print("[STREAM] status_hp baru: ");
        Serial.println(rawData);

        String newJasa = getValue(rawData, '|', 0); newJasa.trim();
        if (newJasa != strJasa) {
          Serial.print("[DISPLAY] Jasa berubah: "); Serial.print(strJasa);
          Serial.print(" → "); Serial.println(newJasa);
          strJasa = newJasa;
          scrollPosX_Jasa = 42;
        }

        strEst = getValue(rawData, '|', 1); strEst.trim();
        strTipeLayanan = getValue(rawData, '|', 2); strTipeLayanan.trim();

        String newBerat = getValue(rawData, '|', 3); newBerat.trim();
        if (newBerat != strBerat) {
          Serial.print("[DISPLAY] Berat berubah: "); Serial.print(strBerat);
          Serial.print(" → "); Serial.println(newBerat);
          strBerat = newBerat;
          scrollPosX_Berat = 42;
        }

        strHarga = getValue(rawData, '|', 4); strHarga.trim();
        printDisplayState();

      } else if (dPath == "/screen_status" && dType == "string") {
        String screenCmd = streamDo.stringData();
        Serial.print("[STREAM] screen_status: ");
        Serial.println(screenCmd);

        if (screenCmd == "RESTART") {
          Serial.println("[CMD] RESTART diterima — reset nilai & reconnect stream");
          
          strJasa = "-"; 
          strEst = "-"; 
          strTipeLayanan = "-";
          strBerat = "0.000"; 
          strHarga = "Rp0";
          scrollPosX_Jasa = 42;
          scrollPosX_Berat = 42;

          Firebase.RTDB.setStringAsync(&fbdo, firebasePath + "/screen_status", "ON");
          Serial.println("[CMD] screen_status di-set ke ON");

          reconnectStream();
        } else {
          Serial.print("[CMD] screen_status '");
          Serial.print(screenCmd);
          Serial.println("' diterima tapi tidak ada aksi");
        }

      } else if (dPath == "/current_weight" && dType == "float") {
        Serial.print("[STREAM] current_weight update dari Firebase: ");
        Serial.println(streamDo.floatData(), 3);

      } else {
        Serial.print("[STREAM] Path tidak dihandle: ");
        Serial.println(dPath);
      }
    }
  }

  // LOAD CELL & LOGIKA AUTO-LOCK
  if (scale.is_ready()) {
    float raw_weight = scale.get_units(2);
    
    // Cegah nilai minus
    if (raw_weight < 0.005f) {
      raw_weight = 0.00f;
    }

    if (isLocked) {
      // Jika beban diangkat atau ditambah drastis, LEPASKAN KUNCI
      if (abs(raw_weight - lockedWeight) > TOLERANSI_UNLOCK) {
        isLocked = false;
        Serial.println("[LOCK] Kunci terlepas!");
      } else {
        currentWeight = lockedWeight; 
      }
    } 
    
    if (!isLocked) {
      currentWeight = raw_weight; 
      
      // Cek apakah berat mulai stabil (perubahan di bawah toleransi 20 gram)
      if (abs(raw_weight - lastWeight) <= TOLERANSI_STABIL) {
        
        // Jika sudah stabil selama 2 detik, dan berat di atas 0.1 kg (jangan kunci timbangan kosong)
        if (millis() - stableStartTime > WAKTU_STABIL && raw_weight > 0.1f) {
          isLocked = true;
          lockedWeight = raw_weight;
          
          Serial.print("[LOCK] Berat dikunci di: ");
          Serial.println(lockedWeight, 3);
        }
      } else {
        // Jika berat masih naik-turun, reset timer kestabilan
        stableStartTime = millis();
        lastWeight = raw_weight;
      }
    }
  }
  // PANTAUAN SERIAL MONITOR
  if (millis() - lastSerialTime > SERIAL_PRINT_INTERVAL) {
    lastSerialTime = millis();
    Serial.print("Berat : ");
    Serial.print(currentWeight, 3);
    Serial.println(" kg");
  }

  // TICK SCROLL
  bool tickScroll = false;
  if (millis() - lastScrollTime > 30) {
    tickScroll = true;
    lastScrollTime = millis();
  }

  display.clearDisplay();
  display.setTextSize(1);

  //TEXT JASA 
  int textWidthJasa = strJasa.length() * 6;
  if (textWidthJasa > (128 - 42)) {
    if (tickScroll) {
      scrollPosX_Jasa--;
      if (scrollPosX_Jasa < -textWidthJasa) scrollPosX_Jasa = 128;
    }
    display.setCursor(scrollPosX_Jasa, 10);
    display.print(strJasa);
    display.fillRect(0, 10, 42, 9, SH110X_BLACK);
  } else {
    scrollPosX_Jasa = 42;
    display.setCursor(42, 10);
    display.print(strJasa);
  }
  display.setCursor(0, 10);
  display.print("Jasa : ");

  // ESTIMASI
  display.setCursor(0, 21);
  display.print("Est  : ");
  display.print(strEst);

  // TOTAL RINCIAN
  String fullBerat = "-";
  if (strJasa != "-" && strJasa != "") {
    if (strBerat != "" && strBerat != "0" && strBerat != "0.0" && strBerat != "0.000") {
      fullBerat = strBerat;
    }
  }

  int textWidthBerat = fullBerat.length() * 6;
  if (textWidthBerat > (128 - 42)) {
    if (tickScroll) {
      scrollPosX_Berat--;
      if (scrollPosX_Berat < -textWidthBerat) scrollPosX_Berat = 128;
    }
    display.setCursor(scrollPosX_Berat, 32);
    display.print(fullBerat);
    display.fillRect(0, 32, 42, 9, SH110X_BLACK);
  } else {
    scrollPosX_Berat = 42;
    display.setCursor(42, 32);
    display.print(fullBerat);
  }
  display.setCursor(0, 32);
  display.print("Total: ");

  // HARGA
  display.setCursor(0, 43);
  display.print("Harga: ");
  display.print(strHarga);

  // GARIS & STATUS BAR
  display.drawLine(0, 52, 128, 52, SH110X_WHITE);
  display.setCursor(0, 55);
  
  display.print("Berat: ");
  display.print(currentWeight, 3);
  display.print(" Kg");

  if (currentWeight >= 20.0f) {
    display.print(" OVL!");
  }

  // TOMBOL KIRIM BERAT
  if (digitalRead(BUTTON_PIN) == LOW) {
    delay(50);
    if (digitalRead(BUTTON_PIN) == LOW) {
      Serial.print("[BUTTON] Tombol ditekan — kirim berat: ");
      Serial.print(currentWeight, 3);
      Serial.println(" kg");

      display.fillRect(0, 54, 128, 10, SH110X_BLACK);
      display.setCursor(0, 55);
      display.print("Mengirim...");
      display.display();

      bool berhasil = false;
      if (Firebase.ready()) {
        unsigned long startSend = millis();
        if (Firebase.RTDB.setFloat(&fbdo, firebasePath + "/current_weight", currentWeight)) {
          if (millis() - startSend < 5000) {
            berhasil = true;
          }
        }
      }

      if (berhasil) {
        Serial.println("[BUTTON] Kirim berat BERHASIL");
        beepBuzzer(2);
      } else {
        Serial.println("[BUTTON] Kirim berat GAGAL — reconnect stream");
        display.fillRect(0, 54, 128, 10, SH110X_BLACK);
        display.setCursor(0, 55);
        display.print("GAGAL! Coba lagi");
        display.display();
        beepBuzzer(1);
        delay(1500);
        reconnectStream();
      }

      unsigned long startWait = millis();
      while (digitalRead(BUTTON_PIN) == LOW && (millis() - startWait < 2000)) {
        delay(10);
      }
    }
  }

  display.display();
  delay(10);
}
