/*
 * VMC Indoor Unit Firmware (FireBeetle 2 ESP32-E)
 * Features: ESP-NOW RX/TX (ACK), SHT45 Sensor, ST7789 TFT Display,
 *           Physical Wake-Up Button, NTP Time Sync, Blynk IoT Integration.
 */

// --- BLYNK CONFIGURATION ---
#define BLYNK_PRINT Serial // Enable Blynk debug logs on Serial monitor
#define BLYNK_TEMPLATE_ID "YOUR_TEMPLATE_ID"
#define BLYNK_TEMPLATE_NAME "YOUR_TEMPLATE_NAME"
#define BLYNK_AUTH_TOKEN "YOUR_AUTH_TOKEN"

// --- WI-FI CREDENTIALS ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

#include <Wire.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <SensirionI2cSht4x.h>
#include <WiFi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <BlynkSimpleEsp32.h>
#include <time.h>
#include <Adafruit_NeoPixel.h>

// --- PIN DEFINITIONS FOR FIREBEETLE 2 ESP32-E ---
#define BUTTON_PIN 26        // Pin D3 (GPIO 26) with internal pull-up resistor
#define BACKLIGHT_PIN 13     // Pin D7 (GPIO 13) for TFT backlight control
#define LED_ALERT_PIN 19     // Pin MI (GPIO 19) for physical alert LED
#define RGB_LED_PIN 5        // Pin D8 (GPIO 5) for onboard WS2812 RGB LED
#define SHT4X_I2C_ADDRESS 0x44

// --- NEOPIXEL CONFIGURATION ---
Adafruit_NeoPixel rgbLed(1, RGB_LED_PIN, NEO_GRB + NEO_KHZ800);

// Display colors
#define TFT_DARKGRAY 0x4208

// --- TIMING & STATE VARIABLES ---
unsigned long lastSensorRead = 0;
const unsigned long sensorInterval = 2000;    // Read indoor sensor every 2 seconds

unsigned long lastScreenUpdate = 0;
const unsigned long screenInterval = 1000;    // Update local screen every 1 second

// Initialize Blynk Timer
BlynkTimer timer;
const unsigned long blynkInterval = 600000;   // Update Blynk IoT cloud every 10 minutes (600,000 ms)

const unsigned long ADD_TIME_MS = 300000;     // Add 5 minutes (300,000 ms) screen time per button press
unsigned long backlightTurnOffTime = 0;
bool isScreenOn = true;

bool lastButtonState = HIGH;
unsigned long lastPressTime = 0;

// Outdoor unit data timeout (18 minutes to account for RTC clock drift and RF scanning)
const unsigned long DATA_TIMEOUT = 1080000; 
unsigned long lastRxTime = 0;
bool externalDataReceived = false;

// --- ESP-NOW STRUCTURES ---
typedef struct struct_message {
    uint32_t msgId;
    float tempExt;
    float humExt;
    float vbatExt;
} struct_message;

typedef struct struct_ack {
    uint32_t msgId;
    bool received;
} struct_ack;

struct_message externalData;
SensirionI2cSht4x sht4x;
TFT_eSPI tft = TFT_eSPI();

// Internal measurement variables
float tempInt = 0.0;
float humInt = 0.0;
float absHumInt = 0.0;
float absHumExt = 0.0;

bool ventilationActive = false;
bool previousVentilationState = false;

// --- VMC CONTROL SETTINGS ---
// Difference in g/m3 needed to turn on ventilation (Hysteresis). 
const float HUMIDITY_DIFF_THRESHOLD = 2.0; 

// --- NTP & TIME CONFIGURATION ---
const char* ntpServer = "pool.ntp.org";
const char* timeZone = "CET-1CEST,M3.5.0,M10.5.0/3"; // Italian Timezone

// Helper function to get current timestamp string
String getTimestamp() {
    struct tm timeinfo;
    char buffer[25];
    if (getLocalTime(&timeinfo, 10)) { 
        strftime(buffer, sizeof(buffer), "[%H:%M:%S]", &timeinfo);
        return String(buffer);
    } else {
        unsigned long sec = millis() / 1000;
        unsigned long min = sec / 60;
        unsigned long hr = min / 60;
        snprintf(buffer, sizeof(buffer), "[+%02lu:%02lu:%02lu]", hr % 24, min % 60, sec % 60);
        return String(buffer);
    }
}

// Function to calculate Absolute Humidity (g/m3)
float calculateAbsoluteHumidity(float temp, float hum) {
    if (temp == 0.0 && hum == 0.0) return 0.0;
    float es = 6.112 * exp((17.67 * temp) / (temp + 243.5));
    float e = es * (hum / 100.0);
    float absoluteHumidity = (216.7 * e) / (273.15 + temp);
    return absoluteHumidity;
}

// --- BLYNK DATA TRANSMISSION FUNCTION ---
// This function is automatically called by BlynkTimer
void sendBlynkData() {
    Serial.printf("\n%s [BLYNK UPDATE] Transmitting telemetry batch to Blynk IoT...\n", getTimestamp().c_str());
    
    Blynk.virtualWrite(V0, tempInt);
    Blynk.virtualWrite(V1, humInt);
    Blynk.virtualWrite(V2, absHumInt);
    
    if (externalDataReceived) {
        Blynk.virtualWrite(V3, externalData.tempExt);
        Blynk.virtualWrite(V4, externalData.humExt);
        Blynk.virtualWrite(V5, absHumExt);
        Blynk.virtualWrite(V6, externalData.vbatExt);
    } else {
        // If outdoor unit is offline, send 0.0 (or omit to avoid messing up the chart)
        Blynk.virtualWrite(V3, 0.0);
        Blynk.virtualWrite(V4, 0.0);
        Blynk.virtualWrite(V5, 0.0);
        Blynk.virtualWrite(V6, 0.0);
    }
    
    Blynk.virtualWrite(V7, ventilationActive ? 1 : 0);
}

// ESP-NOW Callback for receiving data from Outdoor Unit
void OnDataRecv(const esp_now_recv_info *info, const uint8_t *incomingData, int len) {
    if (len == sizeof(struct_message)) {
        memcpy(&externalData, incomingData, sizeof(struct_message));
        externalDataReceived = true;
        
        unsigned long timeSinceLastSec = (lastRxTime > 0) ? (millis() - lastRxTime) / 1000 : 0;
        lastRxTime = millis();

        absHumExt = calculateAbsoluteHumidity(externalData.tempExt, externalData.humExt);

        Serial.println("\n------------------------------------------------");
        Serial.printf("%s [INDOOR ESP-NOW RX] Packet received from Outdoor Unit!\n", getTimestamp().c_str());
        if (timeSinceLastSec > 0) {
            Serial.printf("  Time since last packet: %lu sec (%lu min %lu sec)\n", 
                          timeSinceLastSec, timeSinceLastSec / 60, timeSinceLastSec % 60);
        }
        Serial.printf("  Sender MAC : %02X:%02X:%02X:%02X:%02X:%02X\n",
                      info->src_addr[0], info->src_addr[1], info->src_addr[2],
                      info->src_addr[3], info->src_addr[4], info->src_addr[5]);
        Serial.printf("  Packet ID  : %lu\n", externalData.msgId);
        Serial.printf("  Temp Ext   : %.2f C\n", externalData.tempExt);
        Serial.printf("  Hum Ext    : %.2f %%\n", externalData.humExt);
        Serial.printf("  VBat Ext   : %.2f V\n", externalData.vbatExt);
        Serial.printf("  AbsHum Ext : %.2f g/m3\n", absHumExt);

        esp_now_peer_info_t peerInfo = {};
        memcpy(peerInfo.peer_addr, info->src_addr, 6);
        peerInfo.channel = 0; 
        peerInfo.encrypt = false;
        
        if (!esp_now_is_peer_exist(info->src_addr)) {
            esp_now_add_peer(&peerInfo);
        }

        struct_ack ackData;
        ackData.msgId = externalData.msgId;
        ackData.received = true;

        esp_err_t ackStatus = esp_now_send(info->src_addr, (uint8_t *) &ackData, sizeof(ackData));
        Serial.printf("  [ACK TX] Sent ACK response (ID %lu) -> Status: %s\n", 
                      ackData.msgId, (ackStatus == ESP_OK) ? "OK" : "ERROR");
        Serial.println("------------------------------------------------\n");
    }
}

// Function to draw Battery Icon on TFT
void drawBatteryIcon(int x, int y, float voltage, bool online) {
    int w = 45;
    int h = 20;
    
    uint16_t color = TFT_GREEN;
    
    float percent = (voltage - 3.2) / (4.0 - 3.2);
    if (percent < 0.0) percent = 0.0;
    if (percent > 1.0) percent = 1.0;

    if (!online) { color = TFT_DARKGREY; } 
    else if (voltage < 3.4) { color = TFT_RED; } 
    else if (voltage < 3.6) { color = TFT_YELLOW; }

    tft.fillRect(x, y, w + 10, h + 4, TFT_BLACK);
    tft.drawRect(x, y, w, h, color);
    tft.drawRect(x+1, y+1, w-2, h-2, color); 
    tft.fillRect(x + w, y + 5, 4, 10, color); 

    int numBars = 4;
    int barWidth = (w - 8) / numBars;
    int activeBars = online ? (int)(percent * numBars + 0.5) : 0;
    if (activeBars > numBars) activeBars = numBars;

    for (int i = 0; i < numBars; i++) {
        int bx = x + 3 + i * (barWidth + 1);
        if (i < activeBars) {
            tft.fillRect(bx, y + 3, barWidth, h - 6, color);
        } else {
            tft.fillRect(bx, y + 3, barWidth, h - 6, TFT_BLACK);
        }
    }
}

// Function to draw Wi-Fi Icon on TFT
void drawWiFiIcon(int x, int y, bool connected, uint16_t activeColor) {
    uint16_t color = connected ? activeColor : TFT_DARKGREY;
    tft.fillRect(x, y, 32, 26, TFT_BLACK);
    
    int cx = x + 16;
    int cy = y + 22;

    tft.fillCircle(cx, cy, 3, color); 
    
    for (int r = 8; r <= 20; r += 6) {
        for (int angle = -45; angle <= 45; angle += 3) {
            float rad = angle * 3.14159 / 180.0;
            int px = (int)(cx + r * sin(rad));
            int py = (int)(cy - r * cos(rad));
            tft.drawPixel(px, py, color);
            tft.drawPixel(px, py+1, color); 
        }
    }
}

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

    Serial.println("\n==================================================");
    Serial.println("   VMC Indoor Unit (FireBeetle 2 ESP32-E) Start   ");
    Serial.println("==================================================");

    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(BACKLIGHT_PIN, OUTPUT);
    pinMode(LED_ALERT_PIN, OUTPUT);
    
    // Initialize Onboard RGB LED
    rgbLed.begin();
    rgbLed.setBrightness(30); // Set brightness (0-255) to avoid glare
    rgbLed.setPixelColor(0, rgbLed.Color(0, 0, 255)); // Blue color during startup
    rgbLed.show();

    digitalWrite(BACKLIGHT_PIN, HIGH); 
    digitalWrite(LED_ALERT_PIN, LOW);

    backlightTurnOffTime = millis() + ADD_TIME_MS; 

    tft.init();
    tft.setRotation(0);
    tft.fillScreen(TFT_BLACK);
    tft.setTextColor(TFT_WHITE, TFT_BLACK);
    tft.setTextSize(2);
    tft.setCursor(10, 100);
    tft.print("Wi-Fi Connecting...");

    Wire.begin();
    sht4x.begin(Wire, SHT4X_I2C_ADDRESS);

    Serial.printf("[WI-FI] Connecting to SSID: %s ...\n", ssid);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    
    unsigned long wifiStart = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 10000) {
        delay(500);
        Serial.print(".");
    }

    if (WiFi.status() == WL_CONNECTED) {
        int32_t currentChannel = WiFi.channel();
        Serial.println("\n[WI-FI SUCCESS] Connected!");
        Serial.printf("  Wi-Fi Channel: %d\n", currentChannel);

        configTzTime(timeZone, ntpServer, "time.nist.gov");
    }

    // Initialize Blynk Cloud
    Blynk.config(BLYNK_AUTH_TOKEN);
    Blynk.connect(3000); 

    // --- BLYNK TIMER SETUP ---
    // Set the sendBlynkData function to run every blynkInterval (10 minutes)
    timer.setInterval(blynkInterval, sendBlynkData);

    // Initialize ESP-NOW Protocol
    if (esp_now_init() != ESP_OK) {
        Serial.println("[ESP-NOW ERROR] Failed to initialize ESP-NOW!");
    } else {
        esp_now_register_recv_cb(OnDataRecv);
        Serial.println("[ESP-NOW SUCCESS] Protocol initialized and ready.");
    }
    
    // Clear screen before loop starts
    tft.fillScreen(TFT_BLACK);
}

void loop() {
    // Run cloud services and timers
    Blynk.run();
    timer.run(); // This triggers data transmission when 10 minutes expire
    
    unsigned long currentMillis = millis();

    // 1. Physical Button Management 
    bool currentButtonState = digitalRead(BUTTON_PIN);
    if (lastButtonState == HIGH && currentButtonState == LOW && (currentMillis - lastPressTime > 200)) {
        lastPressTime = currentMillis;
        
        if (!isScreenOn || backlightTurnOffTime < currentMillis) {
            backlightTurnOffTime = currentMillis + ADD_TIME_MS;
        } else {
            backlightTurnOffTime += ADD_TIME_MS; 
        }
        
        isScreenOn = true;
        digitalWrite(BACKLIGHT_PIN, HIGH);
        Serial.printf("%s [BUTTON] Pressed. Backlight active until: %lu ms\n", getTimestamp().c_str(), backlightTurnOffTime);
    }
    lastButtonState = currentButtonState;

    // 2. Automatic Backlight Timeout Management
    if (isScreenOn && currentMillis >= backlightTurnOffTime) {
        isScreenOn = false;
        digitalWrite(BACKLIGHT_PIN, LOW); 
        Serial.printf("%s [SCREEN] Timeout reached -> Backlight turned OFF.\n", getTimestamp().c_str());
    }

    // 3. Read Indoor SHT45 Sensor
    if (currentMillis - lastSensorRead >= sensorInterval) {
        lastSensorRead = currentMillis;
        float t = 0.0, h = 0.0;
        if (sht4x.measureHighPrecision(t, h) == 0) {
            tempInt = t;
            humInt = h;
            absHumInt = calculateAbsoluteHumidity(tempInt, humInt);
        }
    }

    // 4. Check Outdoor Unit Signal Timeout
    if (externalDataReceived && (currentMillis - lastRxTime > DATA_TIMEOUT)) {
        externalDataReceived = false;
        unsigned long elapsedSec = (currentMillis - lastRxTime) / 1000;
        Serial.printf("%s [TIMEOUT WARNING] Outdoor unit timed out! No signal received for %lu min %lu sec.\n", 
                      getTimestamp().c_str(), elapsedSec / 60, elapsedSec % 60);
    }

    // 5. Ventilation Control Logic (with Hysteresis)
    if (externalDataReceived) {
        if (!ventilationActive) {
            // Turn ON: Outdoor must be significantly drier than indoor
            if (absHumExt <= (absHumInt - HUMIDITY_DIFF_THRESHOLD)) {
                ventilationActive = true;
            }
        } else {
            // Turn OFF: Outdoor is now equal or more humid than indoor
            if (absHumExt >= absHumInt) {
                ventilationActive = false;
            }
        }
    } else {
        ventilationActive = false;
    }

    // --- LED CONTROL LOGIC (Physical + Onboard RGB) ---
    if (ventilationActive) {
        digitalWrite(LED_ALERT_PIN, HIGH);
        // VMC ON: Green LED (Dry outdoor air)
        rgbLed.setPixelColor(0, rgbLed.Color(0, 255, 0));
    } else {
        digitalWrite(LED_ALERT_PIN, LOW);
        if (externalDataReceived && absHumExt < absHumInt) {
            // VMC OFF (but getting closer): Yellow LED
            // Outdoor humidity is lower, but hasn't surpassed the hysteresis threshold yet
            rgbLed.setPixelColor(0, rgbLed.Color(255, 200, 0)); // Mix for Yellow
        } else {
            // VMC OFF (Unfavorable condition or no data): Red LED
            rgbLed.setPixelColor(0, rgbLed.Color(255, 0, 0));
        }
    }
    rgbLed.show(); // Physically update the LED color

    if (ventilationActive != previousVentilationState) {
        backlightTurnOffTime = currentMillis + ADD_TIME_MS;
        isScreenOn = true;
        digitalWrite(BACKLIGHT_PIN, HIGH);
        
        if (ventilationActive) {
            Serial.printf("%s [VMC STATE CHANGE] Ventilation turned ON!\n", getTimestamp().c_str());
            Blynk.logEvent("vmc_status", "VENTILATION ON: Outdoor air is drier.");
        } else {
            Serial.printf("%s [VMC STATE CHANGE] Ventilation turned OFF!\n", getTimestamp().c_str());
            Blynk.logEvent("vmc_status", "VENTILATION OFF: Humidity aligned or unfavorable.");
        }
        previousVentilationState = ventilationActive;
    }

    // Blynk transmission via millis() was removed here and delegated to BlynkTimer above.

    // 6. Graphic Update 
    if (isScreenOn && (currentMillis - lastScreenUpdate >= screenInterval)) {
        lastScreenUpdate = currentMillis;
        
        tft.setCursor(5, 5);
        tft.setTextSize(2); 
        if (ventilationActive) {
            tft.setTextColor(TFT_GREEN, TFT_BLACK);
            tft.print("VENTILATION ON ");
        } else {
            tft.setTextColor(TFT_RED, TFT_BLACK);
            tft.print("VENTILATION OFF");
        }
        drawWiFiIcon(200, 2, WiFi.status() == WL_CONNECTED, TFT_WHITE);

        tft.setTextColor(TFT_GREEN, TFT_BLACK);
        
        tft.setCursor(2, 40);
        tft.setTextSize(2);
        tft.print("Int:");
        
        tft.setTextSize(3);
        tft.printf("%.1f", tempInt);
        tft.setTextSize(2);
        tft.print("C "); 
        tft.setTextSize(3);
        tft.printf("%.1f", humInt);
        tft.setTextSize(2);
        tft.print("%  "); 
        
        tft.setCursor(2, 75);
        tft.setTextSize(2);
        tft.print("AbsH:");
        tft.setTextSize(3);
        tft.printf("%5.2fg/m3 ", absHumInt);

        tft.setTextColor(TFT_YELLOW, TFT_BLACK);
        
        tft.setCursor(2, 125);
        if (externalDataReceived) {
            tft.setTextSize(2);
            tft.print("Ext:");
            
            tft.setTextSize(3);
            tft.printf("%.1f", externalData.tempExt);
            tft.setTextSize(2);
            tft.print("C ");
            tft.setTextSize(3);
            tft.printf("%.1f", externalData.humExt);
            tft.setTextSize(2);
            tft.print("%  ");
            
            tft.setCursor(2, 160);
            tft.setTextSize(2);
            tft.print("AbsH:");
            tft.setTextSize(3);
            tft.printf("%5.2fg/m3 ", absHumExt);
        } else {
            tft.setTextSize(2);
            tft.print("Ext:");
            
            tft.setTextSize(3);
            tft.print("--.-");
            tft.setTextSize(2);
            tft.print("C ");
            tft.setTextSize(3);
            tft.print("--.-");
            tft.setTextSize(2);
            tft.print("%  ");
            
            tft.setCursor(2, 160);
            tft.setTextSize(2);
            tft.print("AbsH:");
            tft.setTextSize(3);
            tft.print("--.--g/m3 ");
        }

        drawBatteryIcon(5, 210, externalDataReceived ? externalData.vbatExt : 0.0, externalDataReceived);
        
        tft.setTextColor(TFT_WHITE, TFT_BLACK);
        tft.setCursor(60, 210);
        
        if (externalDataReceived) {
            tft.setTextSize(3);
            tft.printf("%.2f", externalData.vbatExt);
        } else {
            tft.setTextSize(3);
            tft.print("-.--");
        }
        
        tft.setTextSize(2);
        tft.print("V ");
        
        tft.setTextSize(1);
        tft.setCursor(155, 220); 
        tft.print("ESPNOW ");
        
        drawWiFiIcon(195, 210, externalDataReceived, TFT_GREEN);
    }

    delay(10); 
}