#include <Arduino.h>
#include "BLEDevice.h"
#include "Regexp.h"
#include "SimpleMap.h"

static int timerLog = 5;        // seconds
static int factorMsToPpm = 700; // US: 500, EU: 640, AU: 700 (= device default)

static BLEUUID serviceUUID("0000ffe0-0000-1000-8000-00805f9b34fb");
static BLEUUID charUUID("0000ffe1-0000-1000-8000-00805f9b34fb");

static boolean doConnect = false;
static boolean doScan = true;
static BLERemoteCharacteristic *pRemoteCharacteristic;
static BLEAdvertisedDevice *myDevice;

SimpleMap<String, double> *sensorData;

void bleDataCallback(const char *match,
                     const unsigned int length,
                     const MatchState &ms)
{
  char cap[20];
  String value = ms.GetCapture(cap, 0);
  String unit = ms.GetCapture(cap, 1);
  String temp = ms.GetCapture(cap, 2);

  if (unit != "ppt")
  {
    sensorData->put("temperature", temp.toFloat());
  }

  //Serial.println(unit + " = " + value + " @ " + temp + " °C");
  if (unit == "pH")
  {
    sensorData->put("ph", value.toFloat());
  }
  else if (unit == "mS")
  {
    sensorData->put("ec_ms", value.toFloat());
    sensorData->put("ec_us", value.toFloat() * 1000);
    sensorData->put("ec_ppm", value.toFloat() * factorMsToPpm);
    sensorData->put("ec_ppt", value.toFloat() * factorMsToPpm / 1000);
  }
  else if (unit == "uS")
  {
    sensorData->put("ec_ms", value.toFloat() / 1000);
    sensorData->put("ec_us", value.toFloat());
    sensorData->put("ec_ppm", value.toFloat() / 1000 * factorMsToPpm);
    sensorData->put("ec_ppt", value.toFloat() / 1000 * factorMsToPpm / 1000);
  }
  else if (unit == "ppm")
  {
    sensorData->put("ec_ms", value.toFloat() / factorMsToPpm);
    sensorData->put("ec_us", value.toFloat() / factorMsToPpm * 1000);
    sensorData->put("ec_ppm", value.toFloat());
    sensorData->put("ec_ppt", value.toFloat() / 1000);
  }
  else if (unit == "ppt")
  {
    sensorData->put("ec_ms", value.toFloat() / factorMsToPpm * 1000);
    sensorData->put("ec_us", value.toFloat() / factorMsToPpm * 1000 * 1000);
    sensorData->put("ec_ppm", value.toFloat() * 1000);
    sensorData->put("ec_ppt", value.toFloat());
  }
}

void bleParseData(uint8_t *bleData)
{
  MatchState ms((char *)bleData);
  ms.GlobalMatch("([%d%.?]+)%s+(%a+)%c+([%d%.]+)", bleDataCallback);
}

static void notifyCallback(
    BLERemoteCharacteristic *pBLERemoteCharacteristic,
    uint8_t *pData,
    size_t length,
    bool isNotify)
{
  //Serial.println((char *)pData);
  //Serial.println("---");
  bleParseData(pData);
}

class MyClientCallback : public BLEClientCallbacks
{
  void onConnect(BLEClient *pclient)
  {
    doConnect = false;
  }

  void onDisconnect(BLEClient *pclient)
  {
    Serial.println("[BLE] Disconnected");
    ESP.restart();
  }
};

bool connectToServer()
{
  Serial.println("[BLE] Connecting to: " + String(myDevice->getAddress().toString().c_str()));
  BLEClient *pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());
  if (pClient->connect(myDevice))
  {
    Serial.println("[BLE] Connected");
  }
  else
  {
    Serial.println("[BLE] Could not connect");
    return false;
  }

  BLERemoteService *pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr)
  {
    Serial.print("[BLE] Failed to find service UUID");
    pClient->disconnect();
    return false;
  }

  pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
  if (pRemoteCharacteristic == nullptr)
  {
    Serial.print("[BLE] Failed to find characteristic UUID");
    pClient->disconnect();
    return false;
  }
  else
  {
    Serial.println("[BLE] Enabling data sending");
    // magic number:  "0003000000144414";
    byte tmp[8] = {0x00, 0x03, 0x00, 0x00, 0x00, 0x14, 0x44, 0x14};
    pRemoteCharacteristic->writeValue(tmp, sizeof(tmp));
  }

  if (pRemoteCharacteristic->canNotify())
  {
    pRemoteCharacteristic->registerForNotify(notifyCallback);
  }

  return true;
}

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
  void onResult(BLEAdvertisedDevice advertisedDevice)
  {
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID))
    {
      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = false;
    }
  }
};

void bleScan()
{
  Serial.println("[BLE] Scanning...");
  BLEScan *pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
  Serial.println("[BLE] Scan finished");
}

void logSensorData()
{
  Serial.printf("Temp : %.1f °C\r\n", sensorData->get("temperature"));
  Serial.printf("pH   : %.2f\r\n", sensorData->get("ph"));
  Serial.printf("mS/cm: %.2f\r\n", sensorData->get("ec_ms"));
  Serial.printf("uS/cm: %.0f\r\n", sensorData->get("ec_us"));
  Serial.printf("ppm  : %.0f\r\n", sensorData->get("ec_ppm"));
  Serial.printf("ppt  : %.2f\r\n", sensorData->get("ec_ppt"));
  Serial.println("---");
}

void setup()
{
  Serial.begin(115200);
  Serial.println();
  BLEDevice::init("");
  bleScan();
  sensorData = new SimpleMap<String, double>([](String &a, String &b) -> int
                                             {
                                               if (a == b)
                                                 return 0;
                                               if (a > b)
                                                 return 1;
                                               /*if (a < b)*/ return -1;
                                             });
}

void loop()
{

  if (doConnect && !connectToServer())
  {
    ESP.restart();
  }

  if (doScan)
  {
    BLEDevice::getScan()->start(0);
  }

  static unsigned long logTimer = millis();
  if (millis() - logTimer >= timerLog * 1000)
  {
    logSensorData();
    logTimer = millis();
  }
}