#include <WiFi.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Servo.h>
#include <RTClib.h>
#include <Firebase_ESP_Client.h>
#include <ArduinoJson.h>
#include "addons/TokenHelper.h"
#include "addons/RTDBHelper.h"
#include <PubSubClient.h>

#define LCD_ADDR 0x27
#define LCD_COLS 16
#define LCD_ROWS 2

#define API_KEY "AIzaSyBvMyCNKKCBrSjDaArHc3VF7TpjqJOYNS4"
#define DATABASE_URL "https://jam-makan-default-rtdb.asia-southeast1.firebasedatabase.app"

// Pin konfigurasi
#define SERVO_PIN 13

// Inisialisasi komponen
Servo servo;
RTC_DS3231 rtc;
LiquidCrystal_I2C lcd(LCD_ADDR, LCD_COLS, LCD_ROWS);

FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
#define JSON_DOCUMENT_SIZE 1024
StaticJsonDocument<JSON_DOCUMENT_SIZE> doc;

// Deklarasi Fungsi
String hitungWaktuPakanTerdekat(int jam, int menit, int pagi_hour, int pagi_minute, int siang_hour, int siang_minute, int malam_hour, int malam_minute);
DateTime kalibrasiWaktu(const DateTime& dt); // Tambahkan deklarasi fungsi kalibrasiWaktu

unsigned long sendDataPrevMillis = 0;
bool signupOK = false;
unsigned long servoFinishTime = 0;
const unsigned long fetchInterval = 15000; // 15 detik
String waktuPakanTerdekat;
bool servoFinished = false;

// Variabel untuk menyimpan jumlah putaran servo untuk setiap waktu makan
int pagiPutaran = 0;
int siangPutaran = 0;
int malamPutaran = 0;

// Variabel status untuk melacak aktivasi servo
bool pagiPakanDiberikan = false;
bool siangPakanDiberikan = false;
bool malamPakanDiberikan = false;
const char* mqtt_server = "broker.hivemq.com";
const char* topic = "piCo/manualFeed";
WiFiClient espClient;
PubSubClient client(espClient);

// Fungsi untuk kalibrasi waktu RTC
DateTime kalibrasiWaktu(const DateTime& dt) {
  int seconds = dt.second() + 15;
  int minutes = dt.minute();
  int hours = dt.hour();
  int days = dt.day();
  int months = dt.month();
  int years = dt.year();
  
  // Atur detik
  if (seconds >= 60) {
    seconds -= 60;
    minutes++;
    if (minutes >= 60) {
      minutes -= 60;
      hours++;
      if (hours >= 24) {
        hours -= 24;
        days++;
        // Anda bisa menambahkan kode untuk menangani bulan dan tahun jika diperlukan
      }
    }
  }

 return DateTime(years, months, days, hours, minutes, seconds);
}

void setup() {
  Serial.begin(57600);
  Wire.begin();
  servo.attach(SERVO_PIN);
  if (!rtc.begin()) {
    Serial.println("RTC tidak ditemukan!");
    while (1);
  }
  if (rtc.lostPower()) {
    Serial.println("RTC kehilangan daya, set waktu ke waktu kompilasi!");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
  lcd.init();
  lcd.backlight();

  // Tampilkan pesan selamat datang
  lcd.setCursor(0, 0);
  lcd.print("  PiCo");
  lcd.setCursor(0, 1);
  lcd.print("   Initializing");

  // Tunggu beberapa detik
  delay(2000);

  // Inisialisasi WiFi
  Serial.println();
  Serial.println("Menghubungkan ke WiFi...");
  WiFi.begin("POCO F4", "123456789"); // Ganti SSID dan password dengan yang sesuai
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi terhubung");
  Serial.println("Alamat IP: ");
  Serial.println(WiFi.localIP());
  Serial.println("Menghubungkan ke Firebase...");
  setupFirebase();

  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);

  while (!client.connected()) {
    String client_id = "esp32-client-";
    client_id += String(WiFi.macAddress());
    Serial.printf("The client %s connects to the public mqtt broker\n", client_id.c_str());
    if (client.connect(client_id.c_str())) {
      Serial.println("Public emqx mqtt broker connected");
    } else {
      Serial.print("failed with state ");
      Serial.print(client.state());
      delay(2000);
    }
  }
  // publish and subscribe
  client.publish(topic, "Hi EMQX I'm ESP32 ^^");
  client.subscribe(topic);
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP32Client")) {
      Serial.println("connected");
      // Subscribe
      client.subscribe("piCo/manualFeed");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void callback(char* topic, byte* message, unsigned int length) {
  Serial.print("Message arrived on topic: ");
  Serial.print(topic);
  Serial.print(". Message: ");
  String messageTemp;
  
  for (int i = 0; i < length; i++) {
    Serial.print((char)message[i]);
    messageTemp += (char)message[i];
  }
  Serial.println();

  if (String(topic) == "piCo/manualFeed") {
    if (messageTemp == "Makan") {
      Serial.println("Pemberian pakan manual");
      activateServo(1); // Ganti angka dengan jumlah putaran servo yang diinginkan
    }
  }
}

void loop() {
  DateTime now = rtc.now();
  // Kalibrasi waktu
  DateTime kalibrasiNow = kalibrasiWaktu(now);
  
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  getDataFromFirebase(kalibrasiNow);
  tampilkanWaktuDanJadwal(kalibrasiNow);
  delay(1000);
}

// Fungsi untuk mengaktifkan servo
void activateServo(int putaran) {
  Serial.print("Makan Done");
  servo.write(90); // Putar servo ke posisi tengah (contoh)
  delay(1000);     // Tunggu satu detik
  for (int i = 0; i < putaran; i++) {
    servo.write(90); // Putar servo ke posisi maksimal
    delay(1000);      // Tunggu satu detik
    servo.write(0);   // Kembalikan servo ke posisi awal (contoh)
    delay(1000);      // Tunggu satu detik
  }
  servo.write(90); // Kembalikan servo ke posisi tengah (contoh)
  Serial.println("Makan selesai");
  servoFinished = true; // Set flag untuk menampilkan notifikasi "selesai"
  servoFinishTime = millis(); // Catat waktu selesai servo
}

// Fungsi untuk mengonfigurasi Firebase
void setupFirebase() {
  config.api_key = API_KEY;
  config.database_url = DATABASE_URL;

  if (Firebase.signUp(&config, &auth, "", "")) {
    Serial.println("Signup berhasil");
    signupOK = true;
  } else {
    Serial.printf("Signup gagal, %s\n", config.signer.signupError.message.c_str());
  }

  config.token_status_callback = tokenStatusCallback; // Mengatur callback status token

  Firebase.begin(&config, &auth);
  Firebase.reconnectWiFi(true);
}

void getDataFromFirebase(const DateTime& dt) {
  Serial.println("Masuk ke fungsi getDataFromFirebase");

  if (Firebase.ready() && signupOK) {
    Serial.println("Firebase siap dan signup berhasil");
    
    if (Firebase.RTDB.getJSON(&fbdo, "/jam_makan")) {
      Serial.println("Data diambil dari Firebase");

      // Deserialisasi JSON
      DeserializationError error = deserializeJson(doc, fbdo.to<String>());
      if (error) {
        Serial.print("deserializeJson() gagal: ");
        Serial.println(error.c_str());
      } else {
        Serial.println("deserializeJson() berhasil");

        const char* malam = doc["malam"];
        const char* pagi = doc["pagi"];
        const char* siang = doc["siang"];
        pagiPutaran = doc["pagiPutaran"];
        siangPutaran = doc["siangPutaran"];
        malamPutaran = doc["malamPutaran"];

        Serial.print("Malam: ");
        Serial.println(malam);
        Serial.print("Pagi: ");
        Serial.println(pagi);
        Serial.print("Siang: ");
        Serial.println(siang);
        Serial.print("Putaran Pagi: ");
        Serial.println(pagiPutaran);
        Serial.print("Putaran Siang: ");
        Serial.println(siangPutaran);
        Serial.print("Putaran Malam: ");
        Serial.println(malamPutaran);

        int jam = dt.hour();
        int menit = dt.minute();

        String malamStr = String(malam);
        String pagiStr = String(pagi);
        String siangStr = String(siang);

        int malam_hour = malamStr.substring(0, malamStr.indexOf(":")).toInt();
        int malam_minute = malamStr.substring(malamStr.indexOf(":") + 1).toInt();
        int pagi_hour = pagiStr.substring(0, pagiStr.indexOf(":")).toInt();
        int pagi_minute = pagiStr.substring(pagiStr.indexOf(":") + 1).toInt();
        int siang_hour = siangStr.substring(0, siangStr.indexOf(":")).toInt();
        int siang_minute = siangStr.substring(siangStr.indexOf(":") + 1).toInt();

        // Menghitung waktu pakan terdekat
        waktuPakanTerdekat = hitungWaktuPakanTerdekat(jam, menit, pagi_hour, pagi_minute, siang_hour, siang_minute, malam_hour, malam_minute);

        // Memastikan servo hanya berputar sekali pada waktu yang ditentukan
        if (jam == malam_hour && menit == malam_minute && !malamPakanDiberikan) {
          activateServo(malamPutaran);
          malamPakanDiberikan = true;
        } else if (jam != malam_hour || menit != malam_minute) {
          malamPakanDiberikan = false;
        }

        if (jam == pagi_hour && menit == pagi_minute && !pagiPakanDiberikan) {
          activateServo(pagiPutaran);
          pagiPakanDiberikan = true;
        } else if (jam != pagi_hour || menit != pagi_minute) {
          pagiPakanDiberikan = false;
        }

        if (jam == siang_hour && menit == siang_minute && !siangPakanDiberikan) {
          activateServo(siangPutaran);
          siangPakanDiberikan = true;
        } else if (jam != siang_hour || menit != siang_minute) {
          siangPakanDiberikan = false;
        }

        Serial.println("Waktu pakan terdekat: " + waktuPakanTerdekat);
        Serial.print("Jam RTC: ");
        Serial.print(jam);
        Serial.print(":");
        Serial.println(menit);
      }
    } else {
      Serial.println("Gagal mengambil data dari Firebase: " + fbdo.errorReason());
    }
  } else {
    Serial.println("Firebase tidak siap atau signup gagal");
  }
}

String hitungWaktuPakanTerdekat(int jam, int menit, int pagi_hour, int pagi_minute, int siang_hour, int siang_minute, int malam_hour, int malam_minute) {
  int currentMinutes = jam * 60 + menit;
  int pagiMinutes = pagi_hour * 60 + pagi_minute;
  int siangMinutes = siang_hour * 60 + siang_minute;
  int malamMinutes = malam_hour * 60 + malam_minute;

  int minDiff = 1440; // Jumlah menit dalam satu hari
  String nearestTime;

  if (pagiMinutes > currentMinutes && pagiMinutes - currentMinutes < minDiff) {
    minDiff = pagiMinutes - currentMinutes;
    nearestTime = String(pagi_hour) + ":" + (pagi_minute < 10 ? "0" : "") + String(pagi_minute);
  }
  if (siangMinutes > currentMinutes && siangMinutes - currentMinutes < minDiff) {
    minDiff = siangMinutes - currentMinutes;
    nearestTime = String(siang_hour) + ":" + (siang_minute < 10 ? "0" : "") + String(siang_minute);
  }
  if (malamMinutes > currentMinutes && malamMinutes - currentMinutes < minDiff) {
    minDiff = malamMinutes - currentMinutes;
    nearestTime = String(malam_hour) + ":" + (malam_minute < 10 ? "0" : "") + String(malam_minute);
  }

  // Jika tidak ada waktu pakan tersisa untuk hari ini, set ke jadwal pakan pertama besok
  if (nearestTime == "") {
    nearestTime = String(pagi_hour) + ":" + (pagi_minute < 10 ? "0" : "") + String(pagi_minute);
  }

  return nearestTime;
}

void tampilkanWaktuDanJadwal(const DateTime& dt) {
  char currentTime[16];
  snprintf(currentTime, sizeof(currentTime), "%02d:%02d:%02d", dt.hour(), dt.minute(), dt.second());

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Waktu: ");
  lcd.print(currentTime);
  
  lcd.setCursor(0, 1);
  if (servoFinished) {
    if (millis() - servoFinishTime < 5000) {
      lcd.print("Pakan selesai");
    } else {
      servoFinished = false;
      waktuPakanTerdekat = hitungWaktuPakanTerdekat(dt.hour(), dt.minute(), 
      doc["pagi"].as<int>(), doc["pagi"].as<int>(), 
      doc["siang"].as<int>(), doc["siang"].as<int>(), 
      doc["malam"].as<int>(), doc["malam"].as<int>()); // Perbarui jadwal berikutnya
    }
  } else {
    // Tambahkan 0 di depan jam jika jam kurang dari 10
    if (waktuPakanTerdekat[1] == ':') {
      lcd.print("Pakan: 0");
      lcd.print(waktuPakanTerdekat);
    } else {
      lcd.print("Pakan: ");
      lcd.print(waktuPakanTerdekat);
    }
  }
}
