/**
 * Παράδειγμα Πελάτη (Client) BLE πλούσιο σε δυνατότητες.
 * 
 * Αυτός ο κώδικας θα αναζητήσει τον διακομιστή (Server) BLE,
 * θα συνδεθεί σε αυτόν και θα ανταλλάξει δεδομένα μεταβάλλοντας καταστάσεις 
 * όπως το LED και το κουμπί.
 */

#include "BLEDevice.h"

// Ορισμός των Pins για το LED και το Κουμπί
#define ledPin 23
#define buttonPin 22

// Το UUID της απομακρυσμένης υπηρεσίας (Service) που θέλουμε να συνδεθούμε.
static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
// Το UUID του χαρακτηριστικού (Characteristic) TX της απομακρυσμένης υπηρεσίας.
static BLEUUID txcharUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8");
// Το UUID του χαρακτηριστικού (Characteristic) RX της απομακρυσμένης υπηρεσίας.
static BLEUUID rxcharUUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");

// Μεταβλητές Κατάστασης (Flags)
static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;

// Δείκτες (Pointers) για τα Χαρακτηριστικά (Characteristics) και τη Συσκευή
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLERemoteCharacteristic* pRemoteCharacteristic2;
static BLEAdvertisedDevice* myDevice;

/**
 * Συνάρτηση Callback όταν λαμβάνεται Notification από τον Server
 */
static void notifyCallback(
    BLERemoteCharacteristic* pBLERemoteCharacteristic,
    uint8_t* pData,
    size_t length,
    bool isNotify
) {
    Serial.print("RX --> ");
    for (int i = 0; i < length; i++) {
        Serial.print((char)pData[i]);
    }
    Serial.println();
    
    // Ενεργοποίηση/Απενεργοποίηση του LED με βάση τα ληφθέντα δεδομένα
    if (pData[0] != '0') {
        digitalWrite(ledPin, HIGH);
    } else {
        digitalWrite(ledPin, LOW);
    }
}

/**
 * Κλάση διαχείρισης συμβάντων (callbacks) του Client (Σύνδεση/Αποσύνδεση)
 */
class MyClientCallback : public BLEClientCallbacks {
    void onConnect(BLEClient* pclient) {
        Serial.println("Επιτυχής σύνδεση");
    }

    void onDisconnect(BLEClient* pclient) {
        connected = false;
        Serial.println("Αποσυνδέθηκε (onDisconnect).");
    }
};

/**
 * Συνάρτηση σύνδεσης στον Διακομιστή (Server)
 */
bool connectToServer() {
    Serial.print("Δημιουργία σύνδεσης με ");
    Serial.println(myDevice->getAddress().toString().c_str());
    
    BLEClient* pClient = BLEDevice::createClient();
    Serial.println(" - Ο client δημιουργήθηκε");

    pClient->setClientCallbacks(new MyClientCallback());

    // Σύνδεση στον απομακρυσμένο διακομιστή BLE
    pClient->connect(myDevice);
    Serial.println(" - Συνδέθηκε στον server");

    // Λήψη αναφοράς στην υπηρεσία (Service) του απομακρυσμένου διακομιστή BLE.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
        Serial.print("Αποτυχία εύρεσης του UUID του service: ");
        Serial.println(serviceUUID.toString().c_str());
        pClient->disconnect();
        return false;
    }
    Serial.println(" - Βρέθηκε το service");

    // Λήψη αναφοράς στα χαρακτηριστικά (Characteristics) της υπηρεσίας
    pRemoteCharacteristic = pRemoteService->getCharacteristic(txcharUUID);
    if (pRemoteCharacteristic == nullptr) {
        Serial.print("Αποτυχία εύρεσης του UUID του χαρακτηριστικού TX: ");
        Serial.println(txcharUUID.toString().c_str());
        pClient->disconnect();
        return false;
    }

    pRemoteCharacteristic2 = pRemoteService->getCharacteristic(rxcharUUID);
    if (pRemoteCharacteristic2 == nullptr) {
        Serial.print("Αποτυχία εύρεσης του UUID του χαρακτηριστικού RX: ");
        Serial.println(rxcharUUID.toString().c_str());
        pClient->disconnect();
        return false;
    }

    Serial.println(" - Βρέθηκαν τα παρακάτω characteristics");

    // Ανάγνωση της τρέχουσας τιμής του χαρακτηριστικού (αν υποστηρίζεται)
    if (pRemoteCharacteristic->canRead()) {
        String value = pRemoteCharacteristic->readValue();
        Serial.print("Η τιμή του χαρακτηριστικού (characteristic) ήταν: ");
        Serial.println(value.c_str());
    }

    // Εγγραφή για ειδοποιήσεις (Notifications)
    if (pRemoteCharacteristic->canNotify()) {
        pRemoteCharacteristic->registerForNotify(notifyCallback);
    }

    connected = true;
    return connected;
}

/**
 * Κλάση σάρωσης (Scanning) για BLE Servers.
 * Θα βρει τον πρώτο που διαφημίζει την υπηρεσία που ψάχνουμε.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
        Serial.print("Βρέθηκε Συσκευή BLE: ");
        Serial.println(advertisedDevice.toString().c_str());

        // Έλεγχος αν η συσκευή προσφέρει την υπηρεσία (Service) που θέλουμε να βρούμε
        if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {
            BLEDevice::getScan()->stop();            // Σταματάμε το γενικό scan
            myDevice = new BLEAdvertisedDevice(advertisedDevice);
            doConnect = true;                        // Ορίστε το flag για σύνδεση
            doScan = true;                           // Ορίστε το flag για επιστροφή σε σάρωση αργότερα
        }
    }
};

void setup() {
    Serial.begin(115200);
    
    // Αρχικοποίηση των Pins
    pinMode(ledPin, OUTPUT);
    pinMode(buttonPin, INPUT);
    
    Serial.println("Εκκίνηση εφαρμογής Πελάτη (Client) BLE στο Arduino...");
    BLEDevice::init("");

    // Ρύθμιση του Χρονοδιακόπτη (Scanner) και των Callbacks
    BLEScan* pBLEScan = BLEDevice::getScan();
    pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
    pBLEScan->setInterval(1349);
    pBLEScan->setWindow(449);
    pBLEScan->setActiveScan(true);
    
    // Έναρξη σάρωσης (Scanning) για 5 δευτερόλεπτα
    pBLEScan->start(5, false);
}

void loop() {
    // Αν έχει βρεθεί συσκευή, προσπαθούμε να συνδεθούμε σε αυτήν
    if (doConnect == true) {
        if (connectToServer()) {
            Serial.println("Είμαστε πλέον συνδεδεμένοι στον Διακομιστή (Server) BLE.");
        } else {
            Serial.println("Αποτυχία σύνδεσης στον διακομιστή (server). Καμία άλλη ενέργεια.");
        }
        doConnect = false;
    }
  
    // Εάν είμαστε συνδεδεμένοι, ενημερώνουμε το χαρακτηριστικό με την τιμή του κουμπιού
    if (connected) {
        // Ανάγνωση της κατάστασης του αισθητήρα/κουμπιού
        String newValue = String(digitalRead(buttonPin));
        Serial.print("TX --> ");
        Serial.println(newValue);
        
        // Αποστολή στο Server
        pRemoteCharacteristic2->writeValue(newValue.c_str(), newValue.length());
        
    } else if (doScan) {
        // Αν δεν είμαστε συνδεδεμένοι αλλά θέλουμε να σαρώσουμε, επανεκκινείται το scanning
        BLEDevice::getScan()->start(0);
    }
  
    // Καθυστέρηση ενός δευτερολέπτου σε κάθε λούπα
    delay(1000); 
}
