/*
 * Copyright 2017-2026 Espressif Systems (Shanghai) PTE LTD
 * Copyright 2020-2025 Ryan Powell <ryan@nable-embedded.io> and
 * esp-nimble-cpp, NimBLE-Arduino contributors.
 * Copyright 2017 Neil Kolban
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * BLEAdvertising.cpp
 *
 * This class encapsulates advertising a BLE Server.
 *  Created on: Jun 21, 2017
 *      Author: kolban
 *
 *  Modified on: Feb 18, 2025
 *      Author: lucasssvaz (based on kolban's and h2zero's work)
 *      Description: Added support for NimBLE
 *
 * The ESP-IDF provides a framework for BLE advertising.  It has determined that there are a common set
 * of properties that are advertised and has built a data structure that can be populated by the programmer.
 * This means that the programmer doesn't have to "mess with" the low level construction of a low level
 * BLE advertising frame.  Many of the fields are determined for us while others we can set before starting
 * to advertise.
 *
 * Should we wish to construct our own payload, we can use the BLEAdvertisementData class and call the setters
 * upon it.  Once it is populated, we can then associate it with the advertising and what ever the programmer
 * set in the data will be advertised.
 *
 */

#include "soc/soc_caps.h"
#include "sdkconfig.h"
#if defined(SOC_BLE_SUPPORTED) || defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE)
#if defined(CONFIG_BLUEDROID_ENABLED) || defined(CONFIG_NIMBLE_ENABLED)

/***************************************************************************
 *                           Common includes                               *
 ***************************************************************************/

#include "BLEAdvertising.h"
#include <esp_err.h>
#include "BLEUtils.h"
#include "BLEDevice.h"
#include "GeneralUtils.h"
#include "esp32-hal-log.h"

/***************************************************************************
 *                           Common functions                              *
 ***************************************************************************/

/**
 * @brief Construct a default advertising object.
 */
BLEAdvertising::BLEAdvertising() {
  reset();
}  // BLEAdvertising

/**
 * @brief Add a service uuid to exposed list of services.
 * @param [in] serviceUUID The UUID of the service to expose.
 */
void BLEAdvertising::addServiceUUID(BLEUUID serviceUUID) {
  m_serviceUUIDs.push_back(serviceUUID);
  m_advDataSet = false;
}  // addServiceUUID

/**
 * @brief Add a service uuid to exposed list of services.
 * @param [in] serviceUUID The string representation of the service to expose.
 */
void BLEAdvertising::addServiceUUID(const char *serviceUUID) {
  addServiceUUID(BLEUUID(serviceUUID));
}  // addServiceUUID

/**
 * @brief Remove a service uuid to exposed list of services.
 * @param [in] index The index of the service to stop exposing.
 */
bool BLEAdvertising::removeServiceUUID(int index) {

  // If index is larger than the size of the
  // advertised services, return false
  if (index > m_serviceUUIDs.size()) {
    return false;
  }

  m_serviceUUIDs.erase(m_serviceUUIDs.begin() + index);
  m_advDataSet = false;
  return true;
}

/**
 * @brief Remove a service uuid to exposed list of services.
 * @param [in] serviceUUID The BLEUUID of the service to stop exposing.
 */
bool BLEAdvertising::removeServiceUUID(BLEUUID serviceUUID) {
  for (int i = 0; i < m_serviceUUIDs.size(); i++) {
    if (m_serviceUUIDs.at(i).equals(serviceUUID)) {
      return removeServiceUUID(i);
    }
  }
  return false;
}

/**
 * @brief Remove a service uuid to exposed list of services.
 * @param [in] serviceUUID The string of the service to stop exposing.
 */
bool BLEAdvertising::removeServiceUUID(const char *serviceUUID) {
  return removeServiceUUID(BLEUUID(serviceUUID));
}

/**
 * @brief Set the device appearance in the advertising data.
 * The appearance attribute is of type 0x19.  The codes for distinct appearances can be found here:
 * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.gap.appearance.xml.
 * @param [in] appearance The appearance of the device in the advertising data.
 * @return N/A.
 */
void BLEAdvertising::setAppearance(uint16_t appearance) {
  m_advData.appearance = appearance;
#ifdef CONFIG_NIMBLE_ENABLED
  m_advData.appearance_is_present = 1;
#endif
  m_advDataSet = false;
}  // setAppearance

void BLEAdvertising::setAdvertisementType(uint8_t adv_type) {
#ifdef CONFIG_BLUEDROID_ENABLED
  m_advParams.adv_type = (esp_ble_adv_type_t)adv_type;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
  m_advParams.conn_mode = adv_type;
#endif
}  // setAdvertisementType

void BLEAdvertising::setMinInterval(uint16_t mininterval) {
#ifdef CONFIG_BLUEDROID_ENABLED
  m_advParams.adv_int_min = mininterval;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
  m_advParams.itvl_min = mininterval;
#endif
}  // setMinInterval

void BLEAdvertising::setMaxInterval(uint16_t maxinterval) {
#ifdef CONFIG_BLUEDROID_ENABLED
  m_advParams.adv_int_max = maxinterval;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
  m_advParams.itvl_max = maxinterval;
#endif
}  // setMaxInterval

void BLEAdvertising::setMinPreferred(uint16_t mininterval) {
#ifdef CONFIG_BLUEDROID_ENABLED
  m_advData.min_interval = mininterval;
  m_advDataSet = false;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
  // invalid parameters, set the slave interval to null
  if (mininterval < 0x0006 || mininterval > 0x0C80) {
    m_advData.slave_itvl_range = nullptr;
    return;
  }

  if (m_advData.slave_itvl_range == nullptr) {
    m_advData.slave_itvl_range = m_slaveItvl;
  }

  m_slaveItvl[0] = mininterval;
  m_slaveItvl[1] = mininterval >> 8;

  uint16_t maxinterval = *(uint16_t *)(m_advData.slave_itvl_range + 2);

  // If mininterval is higher than the maxinterval make them the same
  if (mininterval > maxinterval) {
    m_slaveItvl[2] = m_slaveItvl[0];
    m_slaveItvl[3] = m_slaveItvl[1];
  }

  m_advDataSet = false;
#endif
}  //

void BLEAdvertising::setMaxPreferred(uint16_t maxinterval) {
#ifdef CONFIG_BLUEDROID_ENABLED
  m_advData.max_interval = maxinterval;
  m_advDataSet = false;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
  // invalid parameters, set the slave interval to null
  if (maxinterval < 0x0006 || maxinterval > 0x0C80) {
    m_advData.slave_itvl_range = nullptr;
    return;
  }
  if (m_advData.slave_itvl_range == nullptr) {
    m_advData.slave_itvl_range = m_slaveItvl;
  }
  m_slaveItvl[2] = maxinterval;
  m_slaveItvl[3] = maxinterval >> 8;

  uint16_t mininterval = *(uint16_t *)(m_advData.slave_itvl_range);

  // If mininterval is higher than the maxinterval make them the same
  if (mininterval > maxinterval) {
    m_slaveItvl[0] = m_slaveItvl[2];
    m_slaveItvl[1] = m_slaveItvl[3];
  }

  m_advDataSet = false;
#endif
}  //

void BLEAdvertising::setScanResponse(bool set) {
  m_scanResp = set;
  m_advDataSet = false;
}

/**
 * @brief Set the filtering for the scan filter.
 * @param [in] scanRequestWhitelistOnly If true, only allow scan requests from those on the white list.
 * @param [in] connectWhitelistOnly If true, only allow connections from those on the white list.
 */
void BLEAdvertising::setScanFilter(bool scanRequestWhitelistOnly, bool connectWhitelistOnly) {
  log_v(">> setScanFilter: scanRequestWhitelistOnly: %d, connectWhitelistOnly: %d", scanRequestWhitelistOnly, connectWhitelistOnly);
  if (!scanRequestWhitelistOnly && !connectWhitelistOnly) {

#ifdef CONFIG_BLUEDROID_ENABLED
    m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
    m_advParams.filter_policy = BLE_HCI_ADV_FILT_NONE;
#endif

    log_v("<< setScanFilter");
    return;
  }
  if (scanRequestWhitelistOnly && !connectWhitelistOnly) {

#ifdef CONFIG_BLUEDROID_ENABLED
    m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_WLST_CON_ANY;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
    m_advParams.filter_policy = BLE_HCI_ADV_FILT_SCAN;
#endif

    log_v("<< setScanFilter");
    return;
  }
  if (!scanRequestWhitelistOnly && connectWhitelistOnly) {

#ifdef CONFIG_BLUEDROID_ENABLED
    m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_WLST;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
    m_advParams.filter_policy = BLE_HCI_ADV_FILT_CONN;
#endif

    log_v("<< setScanFilter");
    return;
  }
  if (scanRequestWhitelistOnly && connectWhitelistOnly) {

#ifdef CONFIG_BLUEDROID_ENABLED
    m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_WLST_CON_WLST;
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
    m_advParams.filter_policy = BLE_HCI_ADV_FILT_BOTH;
#endif

    log_v("<< setScanFilter");
    return;
  }
}  // setScanFilter

/**
 * @brief Set the advertisement data that is to be published in a regular advertisement.
 * @param [in] advertisementData The data to be advertised.
 */
bool BLEAdvertising::setAdvertisementData(BLEAdvertisementData &advertisementData) {
  log_v(">> setAdvertisementData");

#ifdef CONFIG_BLUEDROID_ENABLED
  esp_err_t errRc = ::esp_ble_gap_config_adv_data_raw((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length());
  if (errRc != ESP_OK) {
    log_e("esp_ble_gap_config_adv_data_raw: %d %s", errRc, GeneralUtils::errorToString(errRc));
  }
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
  esp_err_t errRc = ble_gap_adv_set_data((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length());
  if (errRc != ESP_OK) {
    log_e("ble_gap_adv_set_data: %d %s", errRc, BLEUtils::returnCodeToString(errRc));
  }
#endif

  m_customAdvData = true;  // Set the flag that indicates we are using custom advertising data.
  log_v("<< setAdvertisementData");
  return ESP_OK == errRc;
}  // setAdvertisementData

/**
 * @brief Set the advertisement data that is to be published in a scan response.
 * @param [in] advertisementData The data to be advertised.
 */
bool BLEAdvertising::setScanResponseData(BLEAdvertisementData &advertisementData) {
  log_v(">> setScanResponseData");

#ifdef CONFIG_BLUEDROID_ENABLED
  esp_err_t errRc = ::esp_ble_gap_config_scan_rsp_data_raw((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length());
  if (errRc != ESP_OK) {
    log_e("esp_ble_gap_config_scan_rsp_data_raw: %d %s", errRc, GeneralUtils::errorToString(errRc));
  }
#endif

#if defined(CONFIG_NIMBLE_ENABLED)
  esp_err_t errRc = ble_gap_adv_rsp_set_data((uint8_t *)advertisementData.getPayload().c_str(), advertisementData.getPayload().length());
  if (errRc != ESP_OK) {
    log_e("ble_gap_adv_rsp_set_data: %d %s", errRc, BLEUtils::returnCodeToString(errRc));
  }
#endif

  m_customScanResponseData = true;  // Set the flag that indicates we are using custom scan response data.
  log_v("<< setScanResponseData");
  return ESP_OK == errRc;
}  // setScanResponseData

/**
 * @brief Add data to the payload to be advertised.
 * @param [in] data The data to be added to the payload.
 */
void BLEAdvertisementData::addData(String data) {
  if ((m_payload.length() + data.length()) > ESP_BLE_ADV_DATA_LEN_MAX) {
    return;
  }
  m_payload.concat(data);
}  // addData

void BLEAdvertisementData::addData(char *data, size_t length) {
  if ((m_payload.length() + length) > ESP_BLE_ADV_DATA_LEN_MAX) {
    return;
  }
  m_payload.concat(String(data, length));
}  // addData

/**
 * @brief Set the appearance.
 * @param [in] appearance The appearance code value.
 *
 * See also:
 * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.gap.appearance.xml
 */
void BLEAdvertisementData::setAppearance(uint16_t appearance) {
  char cdata[2];
  cdata[0] = 3;
  cdata[1] = ESP_BLE_AD_TYPE_APPEARANCE;  // 0x19
  addData(String(cdata, 2) + String((char *)&appearance, 2));
}  // setAppearance

/**
 * @brief Set the complete services.
 * @param [in] uuid The single service to advertise.
 */
void BLEAdvertisementData::setCompleteServices(BLEUUID uuid) {
  char cdata[2];
  switch (uuid.bitSize()) {
    case 16:
    {
      // [Len] [0x03] [LL] [HH]
      cdata[0] = 3;
      cdata[1] = ESP_BLE_AD_TYPE_16SRV_CMPL;  // 0x03
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid16, 2));
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u16.value, 2));
#endif
      break;
    }

    case 32:
    {
      // [Len] [0x05] [LL] [LL] [HH] [HH]
      cdata[0] = 5;
      cdata[1] = ESP_BLE_AD_TYPE_32SRV_CMPL;  // 0x05
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid32, 4));
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u32.value, 4));
#endif
      break;
    }

    case 128:
    {
      // [Len] [0x07] [0] [1] ... [15]
      cdata[0] = 17;
      cdata[1] = ESP_BLE_AD_TYPE_128SRV_CMPL;  // 0x07
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)uuid.getNative()->uuid.uuid128, 16));
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)uuid.getNative()->u128.value, 16));
#endif
      break;
    }

    default: return;
  }
}  // setCompleteServices

/**
 * @brief Set the advertisement flags.
 * @param [in] The flags to be set in the advertisement.
 *
 * * ESP_BLE_ADV_FLAG_LIMIT_DISC
 * * ESP_BLE_ADV_FLAG_GEN_DISC
 * * ESP_BLE_ADV_FLAG_BREDR_NOT_SPT
 * * ESP_BLE_ADV_FLAG_DMT_CONTROLLER_SPT
 * * ESP_BLE_ADV_FLAG_DMT_HOST_SPT
 * * ESP_BLE_ADV_FLAG_NON_LIMIT_DISC
 */
void BLEAdvertisementData::setFlags(uint8_t flag) {
  char cdata[3];
  cdata[0] = 2;
  cdata[1] = ESP_BLE_AD_TYPE_FLAG;  // 0x01
  cdata[2] = flag;
  addData(String(cdata, 3));
}  // setFlag

/**
 * @brief Set manufacturer specific data.
 * @param [in] data Manufacturer data.
 */
void BLEAdvertisementData::setManufacturerData(String data) {
  log_d("BLEAdvertisementData", ">> setManufacturerData");
  char cdata[2];
  cdata[0] = data.length() + 1;
  cdata[1] = ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE;  // 0xff
  addData(String(cdata, 2) + data);
  log_d("BLEAdvertisementData", "<< setManufacturerData");
}  // setManufacturerData

/**
 * @brief Set the name.
 * @param [in] The complete name of the device.
 */
void BLEAdvertisementData::setName(String name) {
  log_d("BLEAdvertisementData", ">> setName: %s", name.c_str());
  char cdata[2];
  cdata[0] = name.length() + 1;
  cdata[1] = ESP_BLE_AD_TYPE_NAME_CMPL;  // 0x09
  addData(String(cdata, 2) + name);
  log_d("BLEAdvertisementData", "<< setName");
}  // setName

/**
 * @brief Set the partial services.
 * @param [in] uuid The single service to advertise.
 */
void BLEAdvertisementData::setPartialServices(BLEUUID uuid) {
  char cdata[2];
  switch (uuid.bitSize()) {
    case 16:
    {
      // [Len] [0x02] [LL] [HH]
      cdata[0] = 3;
      cdata[1] = ESP_BLE_AD_TYPE_16SRV_PART;  // 0x02
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid16, 2));
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u16.value, 2));
#endif
      break;
    }

    case 32:
    {
      // [Len] [0x04] [LL] [LL] [HH] [HH]
      cdata[0] = 5;
      cdata[1] = ESP_BLE_AD_TYPE_32SRV_PART;  // 0x04
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid32, 4));
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u32.value, 4));
#endif
      break;
    }

    case 128:
    {
      // [Len] [0x06] [0] [1] ... [15]
      cdata[0] = 17;
      cdata[1] = ESP_BLE_AD_TYPE_128SRV_PART;  // 0x06
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid128, 16));
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u128.value, 16));
#endif
      break;
    }

    default: return;
  }
}  // setPartialServices

/**
 * @brief Set the service data (UUID + data)
 * @param [in] uuid The UUID to set with the service data.  Size of UUID will be used.
 * @param [in] data The data to be associated with the service data advert.
 */
void BLEAdvertisementData::setServiceData(BLEUUID uuid, String data) {
  char cdata[2];
  switch (uuid.bitSize()) {
    case 16:
    {
      // [Len] [0x16] [UUID16] data
      cdata[0] = data.length() + 3;
      cdata[1] = ESP_BLE_AD_TYPE_SERVICE_DATA;  // 0x16
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid16, 2) + data);
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u16.value, 2) + data);
#endif
      break;
    }

    case 32:
    {
      // [Len] [0x20] [UUID32] data
      cdata[0] = data.length() + 5;
      cdata[1] = ESP_BLE_AD_TYPE_32SERVICE_DATA;  // 0x20
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid32, 4) + data);
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u32.value, 4) + data);
#endif
      break;
    }

    case 128:
    {
      // [Len] [0x21] [UUID128] data
      cdata[0] = data.length() + 17;
      cdata[1] = ESP_BLE_AD_TYPE_128SERVICE_DATA;  // 0x21
#if defined(CONFIG_BLUEDROID_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->uuid.uuid128, 16) + data);
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
      addData(String(cdata, 2) + String((char *)&uuid.getNative()->u128.value, 16) + data);
#endif
      break;
    }

    default: return;
  }
}  // setServiceData

/**
 * @brief Set the short name.
 * @param [in] The short name of the device.
 */
void BLEAdvertisementData::setShortName(String name) {
  log_d("BLEAdvertisementData", ">> setShortName: %s", name.c_str());
  char cdata[2];
  cdata[0] = name.length() + 1;
  cdata[1] = ESP_BLE_AD_TYPE_NAME_SHORT;  // 0x08
  addData(String(cdata, 2) + name);
  log_d("BLEAdvertisementData", "<< setShortName");
}  // setShortName

/**
 * @brief Adds Tx power level to the advertisement data.
 */
void BLEAdvertisementData::addTxPower() {
  char cdata[3];
  cdata[0] = 2;  // length
  cdata[1] = ESP_BLE_AD_TYPE_TX_PWR;
  cdata[2] = BLEDevice::getPower();
  addData(cdata, 3);
}  // addTxPower

/**
 * @brief Set the preferred connection interval parameters.
 * @param [in] min The minimum interval desired.
 * @param [in] max The maximum interval desired.
 */
void BLEAdvertisementData::setPreferredParams(uint16_t min, uint16_t max) {
  char cdata[6];
  cdata[0] = 5;  // length
  cdata[1] = ESP_BLE_AD_TYPE_INT_RANGE;
  cdata[2] = min;
  cdata[3] = min >> 8;
  cdata[4] = max;
  cdata[5] = max >> 8;
  addData(cdata, 6);
}  // setPreferredParams

/**
 * @brief Retrieve the payload that is to be advertised.
 * @return The payload that is to be advertised.
 */
String BLEAdvertisementData::getPayload() {
  return m_payload;
}  // getPayload

/***************************************************************************
 *                           Bluedroid functions                           *
 ***************************************************************************/

#if defined(CONFIG_BLUEDROID_ENABLED)

void BLEAdvertising::reset() {
  if (BLEDevice::getInitialized()) {
    stop();
  }

  memset(&m_scanRespData, 0, sizeof(esp_ble_adv_data_t));
  memset(&m_advData, 0, sizeof(esp_ble_adv_data_t));
  memset(&m_advParams, 0, sizeof(esp_ble_adv_params_t));

  m_advData.set_scan_rsp = false;
  m_advData.include_name = true;
  m_advData.include_txpower = true;
  m_advData.min_interval = 0x20;
  m_advData.max_interval = 0x40;
  m_advData.appearance = 0x00;
  m_advData.manufacturer_len = 0;
  m_advData.p_manufacturer_data = nullptr;
  m_advData.service_data_len = 0;
  m_advData.p_service_data = nullptr;
  m_advData.service_uuid_len = 0;
  m_advData.p_service_uuid = nullptr;
  m_advData.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);

  m_advParams.adv_int_min = 0x20;
  m_advParams.adv_int_max = 0x40;
  m_advParams.adv_type = ADV_TYPE_IND;
  m_advParams.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
  m_advParams.channel_map = ADV_CHNL_ALL;
  m_advParams.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY;
  m_advParams.peer_addr_type = BLE_ADDR_TYPE_PUBLIC;

  m_customAdvData = false;           // No custom advertising data
  m_customScanResponseData = false;  // No custom scan response data
  m_advDataSet = false;              // Force advertising data reconfiguration
  m_advConfiguring = false;          // Not currently configuring
  m_nameInScanResp = false;          // Name placement decided fresh on each start()
  m_advertisingPending = false;      // No pending advertising start
}  // BLEAdvertising

void BLEAdvertising::freeServiceUUIDs() {
  free(m_advData.p_service_uuid);
  m_advData.p_service_uuid = nullptr;
  m_advData.service_uuid_len = 0;
}

/**
 * @brief Build raw scan response data bytes containing TX power and, when the
 *        device name overflowed the advertising packet, the device name.
 *
 * TX power is encoded first (always 3 bytes). The device name is appended only
 * when @ref m_nameInScanResp is true (i.e. the name did not fit in the
 * advertising packet). This mirrors NimBLE's behavior: the name stays in the
 * advertising packet when there is room for it there, and is moved to the scan
 * response only when the advertising payload is already full.
 *
 * @param [out] buf    Destination buffer; at most ESP_BLE_ADV_DATA_LEN_MAX bytes
 *                     will be written.
 * @param [in]  bufLen Size of buf; clamped to ESP_BLE_ADV_DATA_LEN_MAX.
 * @return Number of bytes written.
 */
uint16_t BLEAdvertising::buildRawScanRespData(uint8_t *buf, uint16_t bufLen) {
  uint8_t *p = buf;
  uint16_t remaining = (bufLen < ESP_BLE_ADV_DATA_LEN_MAX) ? bufLen : ESP_BLE_ADV_DATA_LEN_MAX;

  // --- TX power level ---
  if (remaining >= 3) {
    *p++ = 2;
    *p++ = ESP_BLE_AD_TYPE_TX_PWR;
    *p++ = (uint8_t)BLEDevice::getPower();
    remaining -= 3;
  }

  // --- Device name (only when it overflowed the advertising packet) ---
  // Mirrors NimBLE: name stays in the adv packet when it fits there; only
  // moved here when the adv payload was already full.
  if (m_nameInScanResp) {
    String deviceName = BLEDevice::getDeviceName();
    uint16_t nameLen = deviceName.length();
    if (nameLen > 0 && remaining >= 3) {
      uint16_t maxChars = remaining - 2;
      bool complete = (nameLen <= maxChars);
      uint16_t actualLen = complete ? nameLen : maxChars;
      *p++ = actualLen + 1;
      *p++ = complete ? ESP_BLE_AD_TYPE_NAME_CMPL : ESP_BLE_AD_TYPE_NAME_SHORT;
      memcpy(p, deviceName.c_str(), actualLen);
      p += actualLen;
      remaining -= 2 + actualLen;
    }
  }

  return (uint16_t)(p - buf);
}

bool BLEAdvertising::configureScanResponseData() {
  // Build a compact raw scan response carrying TX power and, when the device
  // name overflowed the advertising packet (m_nameInScanResp == true), the
  // device name. Service UUIDs are never duplicated here — they are already
  // encoded in the advertising packet.
  // This mirrors NimBLE's behavior: the name only moves to the scan response
  // when the advertising payload was too full to accommodate it.
  uint8_t rawBuf[ESP_BLE_ADV_DATA_LEN_MAX];
  uint16_t rawLen = buildRawScanRespData(rawBuf, sizeof(rawBuf));
  esp_err_t errRc = ::esp_ble_gap_config_scan_rsp_data_raw(rawBuf, rawLen);
  if (errRc != ESP_OK) {
    log_e("esp_ble_gap_config_scan_rsp_data_raw: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
    return false;
  }
  return true;
}

/**
 * @brief Build raw advertising data bytes with fields encoded in their native sizes.
 *
 * Used for all advertising (both with and without scan response). Fields are
 * written in priority order: flags, service UUIDs (16/32/128-bit in native
 * sizes), appearance, manufacturer data, service data, and optionally the
 * device name. TX power and connection interval are intentionally omitted from
 * the advertising packet to maximize space; TX power is placed in the scan
 * response when scan response is enabled. If the full name does not fit, it is
 * truncated and the AD type is set to Shortened Local Name (0x08).
 *
 * Encoding UUIDs in their native sizes (rather than expanding all to 128-bit as
 * the Bluedroid structured API does) maximizes the available advertising payload.
 *
 * @param [out] buf         Destination buffer; at most ESP_BLE_ADV_DATA_LEN_MAX
 *                          bytes will be written.
 * @param [in]  bufLen      Size of buf; clamped to ESP_BLE_ADV_DATA_LEN_MAX.
 * @param [in]  includeName Whether to append the device name. Pass false when
 *                          the name will be carried in the scan response.
 * @return Number of bytes written.
 */
uint16_t BLEAdvertising::buildRawAdvData(uint8_t *buf, uint16_t bufLen, bool includeName) {
  uint8_t *p = buf;
  uint16_t remaining = (bufLen < ESP_BLE_ADV_DATA_LEN_MAX) ? bufLen : ESP_BLE_ADV_DATA_LEN_MAX;

  // --- Flags ---
  if (m_advData.flag != 0 && remaining >= 3) {
    *p++ = 2;
    *p++ = ESP_BLE_AD_TYPE_FLAG;
    *p++ = m_advData.flag;
    remaining -= 3;
  }

  // --- Service UUIDs (grouped by size) ---
  // 16-bit
  {
    uint8_t *hdr = nullptr;
    bool hasMore16 = false;
    for (auto &uuid : m_serviceUUIDs) {
      if (uuid.getNative()->len == ESP_UUID_LEN_16) {
        if (remaining >= (hdr ? 2u : 4u)) {
          if (!hdr) {
            hdr = p;
            *p++ = 1;
            *p++ = ESP_BLE_AD_TYPE_16SRV_CMPL;
            remaining -= 2;
          }
          uint16_t val = uuid.getNative()->uuid.uuid16;
          *p++ = val & 0xFF;
          *p++ = (val >> 8) & 0xFF;
          remaining -= 2;
          *hdr += 2;
        } else {
          hasMore16 = true;
        }
      }
    }
    if (hdr != nullptr && hasMore16) {
      *(hdr + 1) = ESP_BLE_AD_TYPE_16SRV_PART;
    }
  }
  // 32-bit
  {
    uint16_t num32 = 0;
    for (auto &uuid : m_serviceUUIDs) {
      if (uuid.getNative()->len == ESP_UUID_LEN_32) {
        ++num32;
      }
    }
    bool allFit32 = (num32 == 0) || (remaining >= 2u + 4u * num32);
    uint8_t *hdr = nullptr;
    for (auto &uuid : m_serviceUUIDs) {
      if (uuid.getNative()->len == ESP_UUID_LEN_32 && remaining >= (hdr ? 4u : 6u)) {
        if (!hdr) {
          hdr = p;
          *p++ = 1;
          *p++ = allFit32 ? ESP_BLE_AD_TYPE_32SRV_CMPL : ESP_BLE_AD_TYPE_32SRV_PART;
          remaining -= 2;
        }
        uint32_t val = uuid.getNative()->uuid.uuid32;
        *p++ = val & 0xFF;
        *p++ = (val >> 8) & 0xFF;
        *p++ = (val >> 16) & 0xFF;
        *p++ = (val >> 24) & 0xFF;
        remaining -= 4;
        *hdr += 4;
      }
    }
  }
  // 128-bit (at most one in a legacy advertising PDU)
  {
    size_t num128 = 0;
    for (auto &uuid : m_serviceUUIDs) {
      if (uuid.getNative()->len == ESP_UUID_LEN_128) {
        ++num128;
      }
    }
    for (auto &uuid : m_serviceUUIDs) {
      if (uuid.getNative()->len == ESP_UUID_LEN_128 && remaining >= 18) {
        *p++ = 17;
        *p++ = (num128 == 1) ? ESP_BLE_AD_TYPE_128SRV_CMPL : ESP_BLE_AD_TYPE_128SRV_PART;
        memcpy(p, uuid.getNative()->uuid.uuid128, 16);
        p += 16;
        remaining -= 18;
        break;
      }
    }
  }

  // --- Appearance ---
  if (m_advData.appearance != 0 && remaining >= 4) {
    *p++ = 3;
    *p++ = ESP_BLE_AD_TYPE_APPEARANCE;
    *p++ = m_advData.appearance & 0xFF;
    *p++ = (m_advData.appearance >> 8) & 0xFF;
    remaining -= 4;
  }

  // --- Manufacturer data ---
  if (m_advData.manufacturer_len > 0 && m_advData.p_manufacturer_data && remaining >= (uint16_t)(2 + m_advData.manufacturer_len)) {
    *p++ = m_advData.manufacturer_len + 1;
    *p++ = ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE;
    memcpy(p, m_advData.p_manufacturer_data, m_advData.manufacturer_len);
    p += m_advData.manufacturer_len;
    remaining -= 2 + m_advData.manufacturer_len;
  }

  // --- Service data ---
  if (m_advData.service_data_len > 0 && m_advData.p_service_data && remaining >= (uint16_t)(2 + m_advData.service_data_len)) {
    *p++ = m_advData.service_data_len + 1;
    *p++ = ESP_BLE_AD_TYPE_SERVICE_DATA;
    memcpy(p, m_advData.p_service_data, m_advData.service_data_len);
    p += m_advData.service_data_len;
    remaining -= 2 + m_advData.service_data_len;
  }

  // --- Device name (truncated to fit, omitted when name goes in the scan response) ---
  if (includeName) {
    String deviceName = BLEDevice::getDeviceName();
    uint16_t nameLen = deviceName.length();
    if (nameLen > 0 && remaining >= 3) {
      uint16_t maxChars = remaining - 2;
      bool complete = (nameLen <= maxChars);
      uint16_t actualLen = complete ? nameLen : maxChars;
      *p++ = actualLen + 1;
      *p++ = complete ? ESP_BLE_AD_TYPE_NAME_CMPL : ESP_BLE_AD_TYPE_NAME_SHORT;
      memcpy(p, deviceName.c_str(), actualLen);
      p += actualLen;
      remaining -= 2 + actualLen;
    }
  }

  return (uint16_t)(p - buf);
}

void BLEAdvertising::setAdvertisementChannelMap(esp_ble_adv_channel_t channel_map) {
  m_advParams.channel_map = channel_map;
}  // setAdvertisementChannelMap

/**
 * @brief Start advertising.
 * Start advertising.
 * @return N/A.
 */
bool BLEAdvertising::start() {
  log_v(">> start: customAdvData: %d, customScanResponseData: %d, advDataSet: %d", m_customAdvData, m_customScanResponseData, m_advDataSet);

  // If async configuration is already in progress, don't re-enter.
  // The GAP event handler will start advertising when the config completes.
  if (m_advConfiguring) {
    log_w("Advertising configuration already in progress");
    return true;
  }

  // If advertising data needs to be (re)configured, kick off the async configuration.
  // Bluedroid's esp_ble_gap_config_adv_data() is asynchronous and fires a GAP completion
  // event when done. We must NOT block here with semaphores because start() can be called
  // from BT callbacks (e.g., the disconnect handler), and both GATT and GAP callbacks are
  // dispatched on the same BTC task - blocking would deadlock.
  // Instead, we use the same non-blocking approach as NimBLE: kick off the config and let
  // the GAP event handler (handleGAPEvent) chain the remaining steps.
  if (!m_advDataSet) {
    freeServiceUUIDs();

    if (!m_customAdvData) {
      // Always use the raw advertising API to encode service UUIDs in their
      // native compact sizes (16/32/128-bit).
      //
      // Match NimBLE behavior for name placement: try to fit the device name
      // in the advertising packet first; only move it to the scan response when
      // the advertising payload is already full (consistent with NimBLE's
      // name-overflow check at lines 1609-1633 of the NimBLE start() path).
      uint8_t rawBuf[ESP_BLE_ADV_DATA_LEN_MAX];

      // First pass: build the adv packet without the name to measure remaining space.
      uint16_t baseLen = buildRawAdvData(rawBuf, sizeof(rawBuf), false);
      String deviceName = BLEDevice::getDeviceName();
      // +2 accounts for the 2-byte AD field header (length byte + type byte).
      uint16_t nameBytes = (deviceName.length() > 0) ? (uint16_t)(deviceName.length() + 2) : 0;
      bool nameFitsInAdv = (nameBytes == 0) || (nameBytes <= (ESP_BLE_ADV_DATA_LEN_MAX - baseLen));

      uint16_t rawLen;
      if (nameFitsInAdv || !m_scanResp) {
        // Name fits in the adv packet, or there is no scan response fallback.
        // buildRawAdvData will truncate the name if it does not fully fit and
        // scan response is disabled (matching NimBLE's truncation path).
        rawLen = buildRawAdvData(rawBuf, sizeof(rawBuf), true);
        m_nameInScanResp = false;
      } else {
        // Name overflows the adv packet and scan response is available:
        // keep the adv packet without the name, let configureScanResponseData()
        // include it in the scan response (matching NimBLE's overflow path).
        rawLen = baseLen;
        m_nameInScanResp = true;
      }

      esp_err_t errRc = ::esp_ble_gap_config_adv_data_raw(rawBuf, rawLen);
      if (errRc != ESP_OK) {
        log_e("esp_ble_gap_config_adv_data_raw: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
        return false;
      }
      m_advConfiguring = true;
      return true;
    }

    if (!m_customScanResponseData && m_scanResp) {
      // Custom adv data but auto scan response - configure scan response asynchronously.
      // Since the adv data is user-supplied we do not know whether it contains
      // the name; always include the name in the scan response to be safe.
      m_nameInScanResp = true;
      // The GAP event handler will start advertising when config completes.
      if (!configureScanResponseData()) {
        freeServiceUUIDs();
        return false;
      }
      m_advConfiguring = true;
      log_d("Scan response data configuration started (async)");
      return true;
    }

    // Both adv data and scan response are custom (or no scan response needed).
    // No async operations, mark as configured and proceed to start.
    m_advDataSet = true;
    freeServiceUUIDs();
  }

  // Advertising data is already configured, just start advertising.
  m_advertisingPending = true;
  esp_err_t errRc = ::esp_ble_gap_start_advertising(&m_advParams);
  if (errRc != ESP_OK) {
    m_advertisingPending = false;
    log_e("<< esp_ble_gap_start_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
  } else {
    log_v("<< start");
  }
  return ESP_OK == errRc;
}  // start

/**
 * @brief Stop advertising.
 * Stop advertising.
 * @return N/A.
 */
bool BLEAdvertising::stop() {
  log_v(">> stop");
  m_advertisingPending = false;  // Cancel any pending advertising restart
  esp_err_t errRc = ::esp_ble_gap_stop_advertising();
  if (errRc != ESP_OK) {
    log_e("esp_ble_gap_stop_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
  } else {
    log_v("<< stop");
  }
  return ESP_OK == errRc;
}  // stop

void BLEAdvertising::handleGAPEvent(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
  log_d("handleGAPEvent [event no: %d]", (int)event);

  switch (event) {
    // Advertising data has been configured. Next step: configure scan response if needed,
    // otherwise start advertising directly. This chains the async operations started by start().
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
    {
      log_d("Advertising data set complete, status=%d", param->adv_data_cmpl.status);
      if (param->adv_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
        log_e("Advertising data set failed, status=%d", param->adv_data_cmpl.status);
        freeServiceUUIDs();
        m_advConfiguring = false;
        break;
      }

      if (!m_customScanResponseData && m_scanResp) {
        if (!configureScanResponseData()) {
          freeServiceUUIDs();
          m_advConfiguring = false;
        }
      } else {
        m_advDataSet = true;
        m_advConfiguring = false;
        freeServiceUUIDs();
        m_advertisingPending = true;
        esp_err_t errRc = ::esp_ble_gap_start_advertising(&m_advParams);
        if (errRc != ESP_OK) {
          m_advertisingPending = false;
          log_e("esp_ble_gap_start_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
        }
      }
      break;
    }

    // Scan response data has been configured. Final step: start advertising.
    case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
    {
      log_d("Scan response data set complete, status=%d", param->scan_rsp_data_cmpl.status);
      if (param->scan_rsp_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
        log_e("Scan response data set failed, status=%d", param->scan_rsp_data_cmpl.status);
        freeServiceUUIDs();
        m_advConfiguring = false;
        break;
      }

      m_advDataSet = true;
      m_advConfiguring = false;
      freeServiceUUIDs();
      m_advertisingPending = true;
      esp_err_t errRc = ::esp_ble_gap_start_advertising(&m_advParams);
      if (errRc != ESP_OK) {
        m_advertisingPending = false;
        log_e("esp_ble_gap_start_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
      }
      break;
    }

    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
    {
      log_d("Raw advertising data set complete, status=%d", param->adv_data_raw_cmpl.status);
      if (param->adv_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
        log_e("Raw advertising data set failed, status=%d", param->adv_data_raw_cmpl.status);
        m_advConfiguring = false;
        break;
      }

      if (!m_customScanResponseData && m_scanResp) {
        // Chain scan response configuration (TX power + device name).
        // Advertising will start in ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT.
        if (!configureScanResponseData()) {
          m_advConfiguring = false;
        }
      } else {
        m_advDataSet = true;
        m_advConfiguring = false;
        m_advertisingPending = true;
        esp_err_t errRc = ::esp_ble_gap_start_advertising(&m_advParams);
        if (errRc != ESP_OK) {
          m_advertisingPending = false;
          log_e("esp_ble_gap_start_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
        }
      }
      break;
    }

    case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
    {
      log_d("Raw scan response data set complete, status=%d", param->scan_rsp_data_raw_cmpl.status);
      if (param->scan_rsp_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
        log_e("Raw scan response data set failed, status=%d", param->scan_rsp_data_raw_cmpl.status);
        m_advConfiguring = false;
        break;
      }

      m_advDataSet = true;
      m_advConfiguring = false;
      m_advertisingPending = true;
      esp_err_t errRc = ::esp_ble_gap_start_advertising(&m_advParams);
      if (errRc != ESP_OK) {
        m_advertisingPending = false;
        log_e("esp_ble_gap_start_advertising: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
      }
      break;
    }

    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
    {
      log_d("Advertising start complete, status=%d", param->adv_start_cmpl.status);
      if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
        m_advertisingPending = false;
      } else if (m_advertisingPending) {
        // Start failed but advertising was still desired — retry once.
        // Clear the flag first to prevent infinite retries if the retry also fails.
        m_advertisingPending = false;
        log_w("Advertising start failed (status=%d), retrying", param->adv_start_cmpl.status);
        esp_err_t errRc = ::esp_ble_gap_start_advertising(&m_advParams);
        if (errRc != ESP_OK) {
          log_e("esp_ble_gap_start_advertising retry: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
        }
      }
      break;
    }
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
    {
      log_i("STOP advertising");
      m_advConfiguring = false;  // Clear configuring flag so start() can be called again
      // If a start was requested but advertising was stopped (e.g., due to a connection) before
      // the start complete event fired, re-issue start_advertising to honor the request.
      if (m_advertisingPending) {
        esp_err_t errRc = ::esp_ble_gap_start_advertising(&m_advParams);
        if (errRc != ESP_OK) {
          m_advertisingPending = false;
          log_e("esp_ble_gap_start_advertising (restart): rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
        }
      }
      break;
    }
    default: break;
  }
}

/**
 * @brief Set BLE address.
 * @param [in] Bluetooth address.
 * @param [in] Bluetooth address type.
 * Set BLE address.
 */
bool BLEAdvertising::setDeviceAddress(esp_bd_addr_t addr, esp_ble_addr_type_t type) {
  log_v(">> setPrivateAddress");

  m_advParams.own_addr_type = type;
  esp_err_t errRc = esp_ble_gap_set_rand_addr((uint8_t *)addr);
  if (errRc != ESP_OK) {
    log_e("esp_ble_gap_set_rand_addr: rc=%d %s", errRc, GeneralUtils::errorToString(errRc));
  } else {
    log_v("<< setPrivateAddress");
  }
  return ESP_OK == errRc;
}  // setPrivateAddress

#if defined(SOC_BLE_50_SUPPORTED)

/**
* @brief           Creator
*
* @param[in]       instance : number of multi advertising instances
*
*
*/
BLEMultiAdvertising::BLEMultiAdvertising(uint8_t num) {
  params_arrays = (esp_ble_gap_ext_adv_params_t *)calloc(num, sizeof(esp_ble_gap_ext_adv_params_t));
  ext_adv = (esp_ble_gap_ext_adv_t *)calloc(num, sizeof(esp_ble_gap_ext_adv_t));
  count = num;
}

/**
* @brief           This function is used by the Host to set the advertising parameters.
*
* @param[in]       instance : identifies the advertising set whose parameters are being configured.
* @param[in]       params   : advertising parameters
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::setAdvertisingParams(uint8_t instance, const esp_ble_gap_ext_adv_params_t *params) {
  if (params->type == ESP_BLE_GAP_SET_EXT_ADV_PROP_LEGACY_IND && params->primary_phy == ESP_BLE_GAP_PHY_2M) {
    return false;
  }
  esp_err_t rc;
  rc = esp_ble_gap_ext_adv_set_params(instance, params);

  return ESP_OK == rc;
}

/**
* @brief           This function is used to set the data used in advertising PDUs that have a data field
*
* @param[in]       instance : identifies the advertising set whose data are being configured
* @param[in]       length   : data length
* @param[in]       data     : data information
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::setAdvertisingData(uint8_t instance, uint16_t length, const uint8_t *data) {
  esp_err_t rc;
  rc = esp_ble_gap_config_ext_adv_data_raw(instance, length, data);
  if (rc) {
    log_e("set advertising data err: %d", rc);
  }

  return ESP_OK == rc;
}

bool BLEMultiAdvertising::setScanRspData(uint8_t instance, uint16_t length, const uint8_t *data) {
  esp_err_t rc;
  rc = esp_ble_gap_config_ext_scan_rsp_data_raw(instance, length, data);
  if (rc) {
    log_e("set scan resp data err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used to request the Controller to enable one or more
*                  advertising sets using the advertising sets identified by the instance parameter.
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::start() {
  return start(count, 0);
}

/**
* @brief           This function is used to request the Controller to enable one or more
*                  advertising sets using the advertising sets identified by the instance parameter.
*
* @param[in]       num : Number of advertising sets to enable or disable
* @param[in]       from : first sxt adv set to use
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::start(uint8_t num, uint8_t from) {
  if (num > count || from >= count) {
    return false;
  }

  esp_err_t rc;
  rc = esp_ble_gap_ext_adv_start(num, &ext_adv[from]);
  if (rc) {
    log_e("start extended advertising err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used to request the Controller to disable one or more
*                  advertising sets using the advertising sets identified by the instance parameter.
*
* @param[in]       num_adv : Number of advertising sets to enable or disable
* @param[in]       ext_adv_inst : ext adv instance
*
* @return            - ESP_OK : success
*                    - other  : failed
*
*/
bool BLEMultiAdvertising::stop(uint8_t num_adv, const uint8_t *ext_adv_inst) {
  esp_err_t rc;
  rc = esp_ble_gap_ext_adv_stop(num_adv, ext_adv_inst);
  if (rc) {
    log_e("stop extended advertising err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used to remove an advertising set from the Controller.
*
* @param[in]       instance : Used to identify an advertising set
*
* @return            - ESP_OK : success
*                    - other  : failed
*
*/
bool BLEMultiAdvertising::remove(uint8_t instance) {
  esp_err_t rc;
  rc = esp_ble_gap_ext_adv_set_remove(instance);
  if (rc) {
    log_e("remove extended advertising err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used to remove all existing advertising sets from the Controller.
*
*
* @return            - ESP_OK : success
*                    - other  : failed
*
*/
bool BLEMultiAdvertising::clear() {
  esp_err_t rc;
  rc = esp_ble_gap_ext_adv_set_clear();
  if (rc) {
    log_e("clear extended advertising err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used by the Host to set the random device address specified by the Random_Address parameter.
*
* @param[in]       instance  : Used to identify an advertising set
* @param[in]       addr_legacy : Random Device Address
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::setInstanceAddress(uint8_t instance, uint8_t *addr_legacy) {
  esp_err_t rc;
  rc = esp_ble_gap_ext_adv_set_rand_addr(instance, addr_legacy);
  if (rc) {
    log_e("set random address err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used by the Host to set the parameters for periodic advertising.
*
* @param[in]       instance : identifies the advertising set whose periodic advertising parameters are being configured.
* @param[in]       params : periodic adv parameters
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::setPeriodicAdvertisingParams(uint8_t instance, const esp_ble_gap_periodic_adv_params_t *params) {
  esp_err_t rc;
  rc = esp_ble_gap_periodic_adv_set_params(instance, params);
  if (rc) {
    log_e("set periodic advertising params err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used to set the data used in periodic advertising PDUs.
*
* @param[in]       instance : identifies the advertising set whose periodic advertising parameters are being configured.
* @param[in]       length : the length of periodic data
* @param[in]       data : periodic data information
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::setPeriodicAdvertisingData(uint8_t instance, uint16_t length, const uint8_t *data) {
  esp_err_t rc;
  rc = esp_ble_gap_config_periodic_adv_data_raw(instance, length, data);
  if (rc) {
    log_e("set periodic advertising raw data err: %d", rc);
  }

  return ESP_OK == rc;
}

/**
* @brief           This function is used to request the Controller to enable the periodic advertising for the advertising set specified
*
* @param[in]       instance : Used to identify an advertising set
*
* @return            - true : success
*                    - false  : failed
*
*/
bool BLEMultiAdvertising::startPeriodicAdvertising(uint8_t instance) {
  esp_err_t rc;
  rc = esp_ble_gap_periodic_adv_start(instance);
  if (rc) {
    log_e("start periodic advertising err: %d", rc);
  }

  return ESP_OK == rc;
}

void BLEMultiAdvertising::setDuration(uint8_t instance, int duration, int max_events) {
  ext_adv[instance] = {instance, duration, max_events};
}

#endif /* SOC_BLE_50_SUPPORTED */

#endif /* CONFIG_BLUEDROID_ENABLED */

/***************************************************************************
 *                           NimBLE functions                              *
 ***************************************************************************/

#if defined(CONFIG_NIMBLE_ENABLED)

void BLEAdvertising::reset() {
  if (BLEDevice::getInitialized() && isAdvertising()) {
    stop();
  }
  memset(&m_advData, 0, sizeof m_advData);
  memset(&m_scanData, 0, sizeof m_scanData);
  memset(&m_advParams, 0, sizeof m_advParams);
  memset(&m_slaveItvl, 0, sizeof m_slaveItvl);
  const char *name = ble_svc_gap_device_name();

  m_advData.name = (uint8_t *)name;
  m_advData.name_len = strlen(name);
  m_advData.name_is_complete = 1;
  m_advData.tx_pwr_lvl = BLEDevice::getPower();
  m_advData.flags = (BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP);
  m_advParams.conn_mode = BLE_GAP_CONN_MODE_UND;
  m_advParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
  m_customAdvData = false;
  m_customScanResponseData = false;
  m_scanResp = true;
  m_advDataSet = false;
  // Set this to non-zero to prevent auto start if host reset before started by app.
  m_duration = BLE_HS_FOREVER;
  m_advCompCB = nullptr;
}  // BLEAdvertising

void BLEAdvertising::setName(String name) {
  m_name = name;
  m_advData.name = (uint8_t *)m_name.c_str();
  m_advData.name_len = m_name.length();
  m_advDataSet = false;
}

/**
 * @brief Add the transmission power level to the advertisement packet.
 */
void BLEAdvertising::addTxPower() {
  m_advData.tx_pwr_lvl_is_present = 1;
  m_advDataSet = false;
}  // addTxPower

/**
 * @brief Handles the callback when advertising stops.
 */
void BLEAdvertising::advCompleteCB() {
  if (m_advCompCB != nullptr) {
    m_advCompCB(this);
  }
}  // advCompleteCB

/**
 * @brief Check if currently advertising.
 * @return true if advertising is active.
 */
bool BLEAdvertising::isAdvertising() {
  return ble_gap_adv_active();
}  // isAdvertising

/*
 * Host reset seems to clear advertising data,
 * we need clear the flag so it reloads it.
 */
void BLEAdvertising::onHostSync() {
  log_v("Host re-synced");

  m_advDataSet = false;
  // If we were advertising forever, restart it now
  if (m_duration == 0) {
    start(m_duration, m_advCompCB);
  } else {
    // Otherwise we should tell the app that advertising stopped.
    advCompleteCB();
  }
}  // onHostSync

/**
 * @brief Handler for gap events when not using peripheral role.
 * @param [in] event the event data.
 * @param [in] arg pointer to the advertising instance.
 */
int BLEAdvertising::handleGAPEvent(struct ble_gap_event *event, void *arg) {
  BLEAdvertising *pAdv = (BLEAdvertising *)arg;

  if (event->type == BLE_GAP_EVENT_ADV_COMPLETE) {
    switch (event->adv_complete.reason) {
      // Don't call the callback if host reset, we want to
      // preserve the active flag until re-sync to restart advertising.
      case BLE_HS_ETIMEOUT_HCI:
      case BLE_HS_EOS:
      case BLE_HS_ECONTROLLER:
      case BLE_HS_ENOTSYNCED:
        log_e("host reset, rc=%d", event->adv_complete.reason);
        BLEDevice::onReset(event->adv_complete.reason);
        return 0;
      default: break;
    }
    pAdv->advCompleteCB();
  }
  return 0;
}

/**
 * @brief Start advertising.
 * @param [in] duration The duration, in seconds, to advertise, 0 == advertise forever.
 * @param [in] advCompleteCB A pointer to a callback to be invoked when advertising ends.
 * @return True if advertising started successfully.
 */
bool BLEAdvertising::start(uint32_t duration, void (*advCompleteCB)(BLEAdvertising *pAdv)) {
  log_v(">> Advertising start: customAdvData: %d, customScanResponseData: %d", m_customAdvData, m_customScanResponseData);

  // If Host is not synced we cannot start advertising.
  if (!BLEDevice::m_synced) {
    log_e("Host reset, wait for sync.");
    return false;
  }

  // If already advertising just return
  if (ble_gap_adv_active()) {
    log_w("Advertising already active");
    return true;
  }

  BLEServer *pServer = BLEDevice::getServer();
  if (pServer != nullptr) {
    if (!pServer->m_gattsStarted) {
      pServer->start();
    } else if (pServer->getConnectedCount() >= CONFIG_BT_NIMBLE_MAX_CONNECTIONS) {
      log_e("Max connections reached - not advertising");
      return false;
    }
  }

  // Save the duration in case of host reset so we can restart with the same parameters
  m_duration = duration;

  if (duration == 0) {
    duration = BLE_HS_FOREVER;
  } else {
    duration = duration * 1000;  // convert duration to milliseconds
  }

  m_advCompCB = advCompleteCB;

  m_advParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
  m_advData.flags = (BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP);
  if (m_advParams.conn_mode == BLE_GAP_CONN_MODE_NON) {
    if (!m_scanResp) {
      m_advParams.disc_mode = BLE_GAP_DISC_MODE_NON;
      m_advData.flags = BLE_HS_ADV_F_BREDR_UNSUP;
    }
  }

  int rc = 0;

  // Use the device name from BLEDevice for advertising
  // Note: ble_svc_gap_device_name() doesn't reliably return the name set via
  // ble_svc_gap_device_name_set(), so we store and use the name directly (like SimpleBLE does)
  // If setName() was called on advertising, m_name will already be set and we use that
  if (!m_customAdvData && m_name.isEmpty()) {
    m_name = BLEDevice::getDeviceName();
    if (m_name.length() > 0) {
      m_advData.name = (uint8_t *)m_name.c_str();
      m_advData.name_len = m_name.length();
      m_advData.name_is_complete = 1;
      m_advDataSet = false;  // Force rebuild of advertising data
    }
  }

  if (!m_customAdvData && !m_advDataSet) {
    //start with 3 bytes for the flags data
    uint8_t payloadLen = (2 + 1);
    if (m_advData.mfg_data_len > 0) {
      payloadLen += (2 + m_advData.mfg_data_len);
    }

    if (m_advData.svc_data_uuid16_len > 0) {
      payloadLen += (2 + m_advData.svc_data_uuid16_len);
    }

    if (m_advData.svc_data_uuid32_len > 0) {
      payloadLen += (2 + m_advData.svc_data_uuid32_len);
    }

    if (m_advData.svc_data_uuid128_len > 0) {
      payloadLen += (2 + m_advData.svc_data_uuid128_len);
    }

    if (m_advData.uri_len > 0) {
      payloadLen += (2 + m_advData.uri_len);
    }

    if (m_advData.appearance_is_present) {
      payloadLen += (2 + BLE_HS_ADV_APPEARANCE_LEN);
    }

    if (m_advData.tx_pwr_lvl_is_present) {
      payloadLen += (2 + BLE_HS_ADV_TX_PWR_LVL_LEN);
    }

    if (m_advData.slave_itvl_range != nullptr) {
      payloadLen += (2 + BLE_HS_ADV_SLAVE_ITVL_RANGE_LEN);
    }

    for (auto &it : m_serviceUUIDs) {
      if (it.getNative()->u.type == BLE_UUID_TYPE_16) {
        int add = (m_advData.num_uuids16 > 0) ? 2 : 4;
        if ((payloadLen + add) > BLE_HS_ADV_MAX_SZ) {
          m_advData.uuids16_is_complete = 0;
          continue;
        }
        payloadLen += add;

        if (nullptr == (m_advData.uuids16 = (ble_uuid16_t *)realloc((void *)m_advData.uuids16, (m_advData.num_uuids16 + 1) * sizeof(ble_uuid16_t)))) {
          log_e("Error, no mem");
          abort();
        }
        memcpy((void *)&m_advData.uuids16[m_advData.num_uuids16], &it.getNative()->u16, sizeof(ble_uuid16_t));
        m_advData.uuids16_is_complete = 1;
        m_advData.num_uuids16++;
      }
      if (it.getNative()->u.type == BLE_UUID_TYPE_32) {
        int add = (m_advData.num_uuids32 > 0) ? 4 : 6;
        if ((payloadLen + add) > BLE_HS_ADV_MAX_SZ) {
          m_advData.uuids32_is_complete = 0;
          continue;
        }
        payloadLen += add;

        if (nullptr == (m_advData.uuids32 = (ble_uuid32_t *)realloc((void *)m_advData.uuids32, (m_advData.num_uuids32 + 1) * sizeof(ble_uuid32_t)))) {
          log_e("Error, no mem");
          abort();
        }
        memcpy((void *)&m_advData.uuids32[m_advData.num_uuids32], &it.getNative()->u32, sizeof(ble_uuid32_t));
        m_advData.uuids32_is_complete = 1;
        m_advData.num_uuids32++;
      }
      if (it.getNative()->u.type == BLE_UUID_TYPE_128) {
        int add = (m_advData.num_uuids128 > 0) ? 16 : 18;
        if ((payloadLen + add) > BLE_HS_ADV_MAX_SZ) {
          m_advData.uuids128_is_complete = 0;
          continue;
        }
        payloadLen += add;

        if (nullptr == (m_advData.uuids128 = (ble_uuid128_t *)realloc((void *)m_advData.uuids128, (m_advData.num_uuids128 + 1) * sizeof(ble_uuid128_t)))) {
          log_e("Error, no mem");
          abort();
        }
        memcpy((void *)&m_advData.uuids128[m_advData.num_uuids128], &it.getNative()->u128, sizeof(ble_uuid128_t));
        m_advData.uuids128_is_complete = 1;
        m_advData.num_uuids128++;
      }
    }

    // check if there is room for the name, if not put it in scan data
    if ((payloadLen + (2 + m_advData.name_len)) > BLE_HS_ADV_MAX_SZ) {
      if (m_scanResp && !m_customScanResponseData) {
        m_scanData.name = m_advData.name;
        m_scanData.name_len = m_advData.name_len;
        if (m_scanData.name_len > BLE_HS_ADV_MAX_SZ - 2) {
          m_scanData.name_len = BLE_HS_ADV_MAX_SZ - 2;
          m_scanData.name_is_complete = 0;
        } else {
          m_scanData.name_is_complete = 1;
        }
        m_advData.name = nullptr;
        m_advData.name_len = 0;
        m_advData.name_is_complete = 0;
      } else {
        if (m_advData.tx_pwr_lvl_is_present) {
          m_advData.tx_pwr_lvl_is_present = 0;
          payloadLen -= (2 + 1);
        }
        // if not using scan response just cut the name down
        // leaving 2 bytes for the data specifier.
        if (m_advData.name_len > (BLE_HS_ADV_MAX_SZ - payloadLen - 2)) {
          m_advData.name_len = (BLE_HS_ADV_MAX_SZ - payloadLen - 2);
          m_advData.name_is_complete = 0;
        }
      }
    }

    if (m_scanResp && !m_customScanResponseData) {
      rc = ble_gap_adv_rsp_set_fields(&m_scanData);
      switch (rc) {
        case 0: break;

        case BLE_HS_EBUSY: log_e("Already advertising"); break;

        case BLE_HS_EMSGSIZE: log_e("Scan data too long"); break;

        default: log_e("Error setting scan response data; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); break;
      }
    }

    if (rc == 0) {
      rc = ble_gap_adv_set_fields(&m_advData);
      switch (rc) {
        case 0: break;

        case BLE_HS_EBUSY: log_e("Already advertising"); break;

        case BLE_HS_EMSGSIZE: log_e("Advertisement data too long"); break;

        default: log_e("Error setting advertisement data; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); break;
      }
    }

    if (m_advData.num_uuids128 > 0) {
      free((void *)m_advData.uuids128);
      m_advData.uuids128 = nullptr;
      m_advData.num_uuids128 = 0;
    }

    if (m_advData.num_uuids32 > 0) {
      free((void *)m_advData.uuids32);
      m_advData.uuids32 = nullptr;
      m_advData.num_uuids32 = 0;
    }

    if (m_advData.num_uuids16 > 0) {
      free((void *)m_advData.uuids16);
      m_advData.uuids16 = nullptr;
      m_advData.num_uuids16 = 0;
    }

    if (rc != 0) {
      return false;
    }

    m_advDataSet = true;
  }

  rc = ble_gap_adv_start(
    BLEDevice::m_ownAddrType, NULL, duration, &m_advParams, (pServer != nullptr) ? BLEServer::handleGATTServerEvent : BLEAdvertising::handleGAPEvent,
    (pServer != nullptr) ? (void *)pServer : (void *)this
  );

  switch (rc) {
    case 0: break;

    case BLE_HS_EINVAL: log_e("Unable to advertise - Duration too long"); break;

    case BLE_HS_EPREEMPTED: log_e("Unable to advertise - busy"); break;

    case BLE_HS_ETIMEOUT_HCI:
    case BLE_HS_EOS:
    case BLE_HS_ECONTROLLER:
    case BLE_HS_ENOTSYNCED:   log_e("Unable to advertise - Host Reset"); break;

    default: log_e("Error enabling advertising; rc=%d, %s", rc, BLEUtils::returnCodeToString(rc)); break;
  }

  log_d("<< Advertising start");
  return (rc == 0);
}  // start

/**
 * @brief Stop advertising.
 */
bool BLEAdvertising::stop() {
  log_d(">> stop");

  int rc = ble_gap_adv_stop();
  if (rc != 0 && rc != BLE_HS_EALREADY) {
    log_e("ble_gap_adv_stop rc=%d %s", rc, BLEUtils::returnCodeToString(rc));
    return false;
  }

  log_d("<< stop");
  return true;
}  // stop

#endif /* CONFIG_NIMBLE_ENABLED */

#endif /* CONFIG_BLUEDROID_ENABLED || CONFIG_NIMBLE_ENABLED */
#endif /* SOC_BLE_SUPPORTED || CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE */
