#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <Tomoto_HM330X.h>
#include <Fonts/FreeSansBold9pt7b.h>

// --- BLE Libraries ---
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// --- Pin Definitions for ST7789 ---
#define TFT_CS         5
#define TFT_RST        2
#define TFT_DC         4
#define TFT_MOSI      23
#define TFT_SCLK      18
#define TFT_MISO      19  

// --- BLE UUIDs ---
#define SERVICE_UUID        "4FAFC201-1FB5-459E-8FCC-C5C9C331914B"
#define CHARACTERISTIC_UUID "64348145-71D6-4293-A6F7-5C3D6559C39B"

// --- Global Objects ---
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
Tomoto_HM330X sensor;      

// --- BLE Globals ---
BLEServer* pServer = nullptr;
BLECharacteristic* pCharacteristic = nullptr;
bool deviceConnected = false;

// --- Timing Control ---
unsigned long lastSensorReadTime = 0;
unsigned long lastDisplayUpdateTime = 0;
const unsigned long DISPLAY_REFRESH_INTERVAL = 2; 

// --- Data Structure ---
struct PMData {
    uint16_t std_pm1_0 = 0;
    uint16_t std_pm2_5 = 0;
    uint16_t std_pm10  = 0;
    uint16_t atm_pm1_0 = 0;
    uint16_t atm_pm2_5 = 0;
    uint16_t atm_pm10  = 0;
    bool is_valid = false;
};

PMData currentData;

// --- Server Callbacks to track connection ---
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
    };
    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      // Restart advertising so phone can reconnect immediately
      pServer->startAdvertising();
    }
};

// --- Function Prototypes ---
void drawPerfectSymmetricUI();
void updateCellData(String valStr, uint16_t xStart, uint16_t yStart, uint16_t width, uint16_t height, uint16_t text_color, uint16_t bg_color);
void refreshDisplayValues(PMData data);
bool readSensorData(PMData &data);
void transmitDataViaBLEAndSerial(PMData data);

void setup() {
    Serial.begin(115200);
    
    // --- Initialize BLE Server ---
    BLEDevice::init("PM System");
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyServerCallbacks());
    
    // Create Service & Characteristic
    BLEService *pService = pServer->createService(SERVICE_UUID);
    pCharacteristic = pService->createCharacteristic(
                        CHARACTERISTIC_UUID,
                        BLECharacteristic::PROPERTY_READ   |
                        BLECharacteristic::PROPERTY_NOTIFY 
                      );
                      
    pCharacteristic->addDescriptor(new BLE2902());
    pService->start();
    
    // Start Advertising so Flutter App can scan and find it
    BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(true);
    pAdvertising->setMinPreferred(0x06);  
    pAdvertising->setMinPreferred(0x12);
    BLEDevice::startAdvertising();
    Serial.println("BLE System Initialized as 'PM System'");

    Wire.begin(21, 22);
    
    tft.init(240, 320);
    tft.invertDisplay(false);
    tft.setRotation(1);
    tft.fillScreen(tft.color565(81, 98, 114)); 
    
    if (!sensor.begin()) {
        Serial.println("HM330X initialization failed!");
        currentData.is_valid = false;
    } else {
        currentData.is_valid = true;
    }
    
    drawPerfectSymmetricUI();
}

void loop() {
    unsigned long currentMillis = millis();
    
    if (currentMillis - lastSensorReadTime >= 1000) {
        lastSensorReadTime = currentMillis;
        
        PMData newData;
        if (readSensorData(newData)) {
            currentData = newData;
            currentData.is_valid = true;
        } else {
            currentData.is_valid = false;
        }
        
        transmitDataViaBLEAndSerial(currentData);
    }
    
    if (currentMillis - lastDisplayUpdateTime >= (DISPLAY_REFRESH_INTERVAL * 1000)) {
        lastDisplayUpdateTime = currentMillis;
        refreshDisplayValues(currentData);
    }

    delay(100);
}

bool readSensorData(PMData &data) {
    if (!sensor.readSensor()) {
        return false;
    }
    
    data.std_pm1_0 = sensor.std.getPM1();
    data.std_pm2_5 = sensor.std.getPM2_5();
    data.std_pm10  = sensor.std.getPM10();
    
    data.atm_pm1_0 = sensor.atm.getPM1();
    data.atm_pm2_5 = sensor.atm.getPM2_5();
    data.atm_pm10  = sensor.atm.getPM10();
    
    return true;
}

void drawPerfectSymmetricUI() {
    tft.fillRect(0, 0, 320, 60, tft.color565(81, 98, 114));
    tft.setFont(&FreeSansBold9pt7b);
    tft.setTextSize(1);
    tft.setTextColor(tft.color565(239, 239, 239));
    tft.setCursor(46, 35); 
    tft.print("PM Concentration ");
    
    int16_t currentX = tft.getCursorX();
    tft.setCursor(currentX, 35);
    tft.print("(ug/m");
    
    currentX = tft.getCursorX();
    tft.setCursor(currentX + 1, 27); 
    tft.print("3");
    
    tft.setCursor(currentX + 11, 38);
    tft.print(")");
    tft.setFont(); 
    
    tft.fillRect(0, 60,   100, 45, tft.color565(129, 144, 158));
    tft.fillRect(100, 60,  110, 45, tft.color565(0, 165, 91)); 
    tft.fillRect(210, 60,  110, 45, tft.color565(234, 114, 44)); 
    
    tft.fillRect(0, 105,  100, 45, tft.color565(25, 142, 210)); 
    tft.fillRect(100, 105, 110, 45, tft.color565(203, 237, 219)); 
    tft.fillRect(210, 105, 110, 45, tft.color565(251, 225, 206)); 
    
    tft.fillRect(0, 150,  100, 45, tft.color565(25, 142, 210)); 
    tft.fillRect(100, 150, 110, 45, tft.color565(203, 237, 219)); 
    tft.fillRect(210, 150, 110, 45, tft.color565(251, 225, 206)); 
    
    tft.fillRect(0, 195,  100, 45, tft.color565(25, 142, 210)); 
    tft.fillRect(100, 195, 110, 45, tft.color565(203, 237, 219)); 
    tft.fillRect(210, 195, 110, 45, tft.color565(251, 225, 206)); 

    tft.drawFastHLine(0, 60,  320, tft.color565(239, 239, 239));
    tft.drawFastHLine(0, 105, 320, tft.color565(239, 239, 239));
    tft.drawFastHLine(0, 150, 320, tft.color565(239, 239, 239));
    tft.drawFastHLine(0, 195, 320, tft.color565(239, 239, 239));
    tft.drawFastHLine(0, 239, 320, tft.color565(239, 239, 239)); 
    
    tft.drawFastVLine(100, 60, 180, tft.color565(239, 239, 239));
    tft.drawFastVLine(210, 60, 180, tft.color565(239, 239, 239));
    tft.drawLine(0, 60, 100, 105, tft.color565(239, 239, 239));

    tft.setFont(&FreeSansBold9pt7b);
    tft.setTextSize(1);
    tft.setTextColor(tft.color565(239, 239, 239));
    tft.setCursor(136, 87); tft.print("STD");
    tft.setCursor(246, 87); tft.print("ATM");
    
    tft.setCursor(22, 134); tft.print("PM1.0");
    tft.setCursor(22, 178); tft.print("PM2.5");
    tft.setCursor(22, 222); tft.print("PM10");
    tft.setFont(); 
}

void updateCellData(String valStr, uint16_t xStart, uint16_t yStart, uint16_t width, uint16_t height, uint16_t text_color, uint16_t bg_color) {
    tft.fillRect(xStart + 1, yStart + 1, width - 2, height - 2, bg_color);
    tft.setFont(&FreeSansBold9pt7b);
    tft.setTextSize(1);
    tft.setTextColor(text_color);
    
    int16_t textWidth = valStr.length() * 11;
    int16_t targetX = xStart + (width - textWidth) / 2;
    int16_t targetY = yStart + (height + 13) / 2;
    
    tft.setCursor(targetX, targetY);
    tft.print(valStr);
    tft.setFont();
}

void refreshDisplayValues(PMData data) {
    if (!data.is_valid) {
        updateCellData("-", 100, 105, 110, 45, tft.color565(238, 65, 57), tft.color565(243, 209, 208));
        updateCellData("-", 100, 150, 110, 45, tft.color565(238, 65, 57), tft.color565(243, 209, 208));
        updateCellData("-", 100, 195, 110, 45, tft.color565(238, 65, 57), tft.color565(243, 209, 208));
        
        updateCellData("-", 210, 105, 110, 45, tft.color565(238, 65, 57), tft.color565(243, 209, 208));
        updateCellData("-", 210, 150, 110, 45, tft.color565(238, 65, 57), tft.color565(243, 209, 208));
        updateCellData("-", 210, 195, 110, 45, tft.color565(238, 65, 57), tft.color565(243, 209, 208));
        return;
    }
    
    updateCellData(String(data.std_pm1_0), 100, 105, 110, 45, tft.color565(70, 90, 103), tft.color565(203, 237, 219));
    updateCellData(String(data.std_pm2_5), 100, 150, 110, 45, tft.color565(70, 90, 103), tft.color565(203, 237, 219));
    updateCellData(String(data.std_pm10),  100, 195, 110, 45, tft.color565(70, 90, 103), tft.color565(203, 237, 219));
    
    updateCellData(String(data.atm_pm1_0), 210, 105, 110, 45, tft.color565(70, 90, 103), tft.color565(251, 225, 206));
    updateCellData(String(data.atm_pm2_5), 210, 150, 110, 45, tft.color565(70, 90, 103), tft.color565(251, 225, 206));
    updateCellData(String(data.atm_pm10),  210, 195, 110, 45, tft.color565(70, 90, 103), tft.color565(251, 225, 206));
}

void transmitDataViaBLEAndSerial(PMData data) {
    String payload = "";
    if (data.is_valid) {
        payload = String(data.std_pm1_0) + "," + String(data.std_pm2_5) + "," + String(data.std_pm10) + "," +
                  String(data.atm_pm1_0) + "," + String(data.atm_pm2_5) + "," + String(data.atm_pm10);
    } else {
        payload = "ERROR,ERROR,ERROR,ERROR,ERROR,ERROR";
    }
    
    // Add explicit newline delimiter for Flutter parser engine to trigger line detection
    String blePayload = payload + "\n";
    
    // Serial Output (For PC or Laptop)
    Serial.println(payload);
    
    // BLE Transmission Block
    if (deviceConnected && pCharacteristic != nullptr) {
        // Adjust MTU dynamically inside core stack implicitly if required, or cast standard package safely
        pCharacteristic->setValue((uint8_t*)blePayload.c_str(), blePayload.length());
        pCharacteristic->notify();
    }
}