BLEScan* pBLEScan = nullptr;
BLEClient* pClient = nullptr;
BLEAdvertisedDevice* pRemoteDevice = nullptr;
BLERemoteService* pRemoteService = nullptr;
BLERemoteCharacteristic* pRemoteCharacteristic_rx = nullptr;
BLERemoteCharacteristic* pRemoteCharacteristic_tx = nullptr;

// 0000ff01-0000-1000-8000-00805f9b34fb
// Notifications from this characteristic is received data from BMS
// NOTIFY, READ

// 0000ff02-0000-1000-8000-00805f9b34fb
// Write this characteristic to send data to BMS
// READ, WRITE, WRITE NO RESPONSE

boolean doScan = false; // becomes true when BLE is initialized and scanning is allowed
boolean doConnect = false; // becomes true when correct ID is found during scanning

boolean ble_client_connected = false; // true when fully connected

unsigned int ble_packets_requested = 0b00; // keeps track of requested packets
unsigned int ble_packets_received = 0b00; // keeps track of received packets


// ======= CALLBACKS =========

void MyEndOfScanCallback(BLEScanResults pBLEScanResult){
    bms_status=false; // BMS not found

    if(BLE_CALLBACK_DEBUG){
      debug("BLE: scan finished");
      Serial.println("Scan finished.");
    }
}

class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks{
  // called for each advertising BLE server

  void onResult(BLEAdvertisedDevice advertisedDevice){
    // found a device


    if(BLE_CALLBACK_DEBUG){
      debug(
          String("BLE: found ") +
          String(advertisedDevice.getName().c_str()) +
          String(" with address ") +
          String(advertisedDevice.getAddress().toString().c_str()) +
          String(" and RSSI ") +
          String(advertisedDevice.getRSSI())
        );

      Serial.print("BLE: found ");
      Serial.println(advertisedDevice.toString().c_str());
    }

    // Check if device is advertising the specific service UUID
    if (!advertisedDevice.isAdvertisingService(serviceUUID)) {
      debug("Device does not advertise the specified service UUID.");
      return;
    }

      if(BLE_CALLBACK_DEBUG){
        debug("BLE: target device found");
      }

      pBLEScan->stop();

        // delete old remote device, create new one
        if(pRemoteDevice != nullptr){
          delete pRemoteDevice;
        }
        pRemoteDevice = new BLEAdvertisedDevice(advertisedDevice);

        doConnect = true;
  }
};

class MyClientCallback : public BLEClientCallbacks{
  // called on connect/disconnect
  void onConnect(BLEClient* pclient){

    if(BLE_CALLBACK_DEBUG){
      debug(String("BLE: connecting to ") + String(pclient->getPeerAddress().toString().c_str()));
    }
  }

  void onDisconnect(BLEClient* pclient){
    ble_client_connected = false;
    doConnect = false;

    if(BLE_CALLBACK_DEBUG){
      debug(String("BLE: disconnected from ") + String(pclient->getPeerAddress().toString().c_str()));
    }

  }
};

static void MyNotifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify){
  //this is called when BLE server sents data via notification
  //hexDump((char*)pData, length);
  if(!bleCollectPacket((char *)pData, length)){
    debug("ERROR: packet could not be collected.");
  }
}





// ======= OTHERS =========

void handleBLE(){
  static unsigned long prev_millis_standby = 0;

  prev_millis_standby = millis();

  while(true){ // loop until we hit a timeout or gathered all packets

    if((ble_packets_received == BLE_PACKETSRECEIVED_BEFORE_STANDBY) || (millis()>prev_millis_standby+BLE_TIMEOUT)){
      if(ble_packets_received == BLE_PACKETSRECEIVED_BEFORE_STANDBY){
        debug("BLE: all packets received");
        bms_status=true; // BMS was connected, data up-to-date

      }else{
        debug("BLE: connection timeout");
        bms_status=false; // BMS not (fully) connected
      }

      break; // we're done with BLE, exit while loop
    }
    else if (doConnect){

      // found the desired BLE server, now connect to it
      if (connectToServer()){
        ble_client_connected = true;
        ble_packets_received=0;
        ble_packets_requested=0;

      }else{
        ble_client_connected = false;
        debug("BLE: failed to connect");
      }

      doConnect = false;
    }

    if (ble_client_connected){
      // if connected to BLE server, request all data
      if((ble_packets_requested & 0b01)!=0b01){
        // request packet 0b01
        debug("BLE: requesting packet 0b01");
        delay(50);
        if(bmsRequestBasicInfo()){
          ble_packets_requested |= 0b01;
        }

      }else if(((ble_packets_received & 0b01)==0b01) && ((ble_packets_requested & 0b10)!=0b10)){
        // request packet 0b10 after 0b01 has been received
        debug("BLE: requesting packet 0b10");
        delay(50);
        if(bmsRequestCellInfo()){
          ble_packets_requested |= 0b10;
        }
      }

    }else if ((!doConnect)&&(doScan)){
      // we are not connected, so we can scan for devices
      debug("BLE: not connected, starting scan");
      Serial.print("BLE is not connected, starting scan");

      // Disconnect client
      if((pClient != nullptr)&&(pClient->isConnected())){
        pClient->disconnect();
      }

      // stop scan (if running) and start a new one
      pBLEScan->setActiveScan(true);
      pBLEScan->setInterval(1 << 8); // 160 ms
      pBLEScan->setWindow(1 << 7); // 80 ms
      pBLEScan->start(BLE_SCAN_DURATION, MyEndOfScanCallback, false); // non-blocking, use a callback

      doScan=false;

      debug("BLE: scan started");
    }
  }
}

void bleGatherPackets(){
  bleStart();
  handleBLE();
  blePause();
  BLEDevice::deinit(false);
}

void bleStart(){
  Serial.print("Starting BLE... ");

  BLEDevice::init("");
  //esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); // release some unused memory

  // Retrieve a BLE client
  pClient = BLEDevice::createClient();
  pClient->setClientCallbacks(new MyClientCallback());

  // Retrieve a BLE scanner
  pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());

  bleContinue();
  Serial.println("done");
}

void blePause(){
  // stop scanning and disconnect from all devices
  doScan=false;

  // Disconnect client
  if((pClient != nullptr)&&(pClient->isConnected())){
    pClient->disconnect();
  }

  delay(50);

  pBLEScan->stop();

  ble_client_connected=false;
  doConnect=false;
  ble_packets_received=0;
  ble_packets_requested=0;

}


void bleContinue(){
  // Prepare for scanning
  ble_client_connected=false;
  doConnect=false;
  ble_packets_received=0;

  doScan=true; // start scanning for new devices
}

bool connectToServer(){
  if(pRemoteDevice==nullptr){
    Serial.println("Invalid remote device, can't connect");
    return false;
  }

  // Disconnect client
  if((pClient != nullptr)&&(pClient->isConnected())){
    pClient->disconnect();
  }

  Serial.print("Forming a connection to ");
  Serial.println(pRemoteDevice->getAddress().toString().c_str());

  delay(100);

  // Connect to the remote BLE Server.
  pClient->connect(pRemoteDevice);
  if(!(pClient->isConnected())){
    debug(String("BLE: failed to connect"));
    Serial.println("Failed to connect to server");
    pClient->disconnect();
    return false;
  }

  Serial.println(" - Connected to server");


  // Get remote service
  pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr){
    debug(String("BLE: failed to find service UUID"));
    Serial.print("Failed to find our service UUID: ");
    Serial.println(serviceUUID.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found our service");


  // Get BMS receive characteristic
  pRemoteCharacteristic_rx = pRemoteService->getCharacteristic(charUUID_rx);
  if (pRemoteCharacteristic_rx == nullptr){
    debug(String("BLE: failed to find RX UUID"));
    Serial.print("Failed to find rx UUID: ");
    Serial.println(charUUID_rx.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found RX characteristic");


  // Register callback for remote characteristic (receive channel)
  if (pRemoteCharacteristic_rx->canNotify()){
      pRemoteCharacteristic_rx->registerForNotify(MyNotifyCallback);
  }else{
    debug(String("BLE: failed to register notification of remote characteristic"));
    Serial.println("Failed to register notification of remote characteristic");
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Registered remote characteristic for notification");


  // Get BMS transmit characteristic
  pRemoteCharacteristic_tx = pRemoteService->getCharacteristic(charUUID_tx);
  if (pRemoteCharacteristic_tx == nullptr){
    debug(String("BLE: failed to find TX UUID"));
    Serial.print("Failed to find tx UUID: ");
    Serial.println(charUUID_tx.toString().c_str());
    pClient->disconnect();
    return false;
  }
  Serial.println(" - Found TX characteristic");


  // Check whether tx is writeable
  if (!(pRemoteCharacteristic_tx->canWriteNoResponse())){
    debug(String("BLE: failed TX remote characteristic is not writable"));
    Serial.println("Failed TX remote characteristic is not writable");
    pClient->disconnect();
    return false;
  }
  Serial.println(" - TX is writable");


  delay(BLE_REQUEST_DELAY); // wait, otherwise writeValue doesn't work for some reason
                            // to do: fix this ugly hack

  debug(String("BLE: connected"));

  return true;
}

bool sendCommand(uint8_t *data, uint32_t dataLen){
  if((pClient!=nullptr)&&(pClient->isConnected())){
    pRemoteCharacteristic_tx->writeValue(data, dataLen, false);
    return true;
  }else{
    return false;
  }
}
