/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2018 Dan Halbert for Adafruit Industries
 * Copyright (c) 2016 Glenn Ruben Bakke
 * Copyright (c) 2018 Artur Pacholec
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "py/gc.h"
#include "py/objstr.h"
#include "py/runtime.h"
#include "supervisor/shared/bluetooth/bluetooth.h"
#include "supervisor/shared/safe_mode.h"
#include "supervisor/shared/tick.h"
#include "supervisor/usb.h"
#include "shared-bindings/_bleio/__init__.h"
#include "shared-bindings/_bleio/Adapter.h"
#include "shared-bindings/_bleio/Address.h"
#include "shared-bindings/nvm/ByteArray.h"
#include "shared-bindings/_bleio/Connection.h"
#include "shared-bindings/_bleio/ScanEntry.h"
#include "shared-bindings/time/__init__.h"

#include "controller/ble_ll_adv.h"
#include "nimble/hci_common.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_gap.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"

#include "common-hal/_bleio/Connection.h"

#include "esp_bt.h"
#include "esp_nimble_hci.h"

bleio_connection_internal_t bleio_connections[BLEIO_TOTAL_CONNECTION_COUNT];

// static void bluetooth_adapter_background(void *data) {
//     supervisor_bluetooth_background();
//     bleio_background();
// }

bool ble_active = false;

static void nimble_host_task(void *param) {
    nimble_port_run();
    nimble_port_freertos_deinit();
}

static TaskHandle_t cp_task = NULL;

static void _on_sync(void) {
    int rc = ble_hs_util_ensure_addr(0);
    assert(rc == 0);

    xTaskNotifyGive(cp_task);
}

void common_hal_bleio_adapter_set_enabled(bleio_adapter_obj_t *self, bool enabled) {
    const bool is_enabled = common_hal_bleio_adapter_get_enabled(self);

    // Don't enable or disable twice
    if (is_enabled == enabled) {
        return;
    }

    if (enabled) {
        esp_nimble_hci_and_controller_init();
        nimble_port_init();
        // ble_hs_cfg.reset_cb = blecent_on_reset;
        ble_hs_cfg.sync_cb = _on_sync;
        // ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
        ble_svc_gap_device_name_set("CIRCUITPY");

        // Clear all of the internal connection objects.
        for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
            bleio_connection_internal_t *connection = &bleio_connections[i];
            // Reset connection.
            connection->conn_handle = BLEIO_HANDLE_INVALID;
        }

        cp_task = xTaskGetCurrentTaskHandle();

        nimble_port_freertos_init(nimble_host_task);
        // Wait for sync.
        ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(200));
    } else {
        nimble_port_stop();
    }
}

bool common_hal_bleio_adapter_get_enabled(bleio_adapter_obj_t *self) {
    return xTaskGetHandle("ble") != NULL;
}

bleio_address_obj_t *common_hal_bleio_adapter_get_address(bleio_adapter_obj_t *self) {
    uint8_t address_bytes[6];
    uint8_t address_type = BLE_ADDR_RANDOM;
    ble_hs_id_infer_auto(0, &address_type);
    int result = ble_hs_id_copy_addr(address_type, address_bytes, NULL);
    if (result != 0) {
        return NULL;
    }

    bleio_address_obj_t *address = m_new_obj(bleio_address_obj_t);
    address->base.type = &bleio_address_type;
    common_hal_bleio_address_construct(address, address_bytes, BLEIO_ADDRESS_TYPE_RANDOM_STATIC);
    return address;
}

bool common_hal_bleio_adapter_set_address(bleio_adapter_obj_t *self, bleio_address_obj_t *address) {
    if (address->type != BLEIO_ADDRESS_TYPE_RANDOM_STATIC) {
        return false;
    }
    mp_buffer_info_t bufinfo;
    if (!mp_get_buffer(address->bytes, &bufinfo, MP_BUFFER_READ)) {
        return false;
    }
    int result = ble_hs_id_set_rnd(bufinfo.buf);
    return result == 0;
}

mp_obj_str_t *common_hal_bleio_adapter_get_name(bleio_adapter_obj_t *self) {
    const char *name = ble_svc_gap_device_name();

    return mp_obj_new_str(name, strlen(name));
}

void common_hal_bleio_adapter_set_name(bleio_adapter_obj_t *self, const char *name) {
    ble_svc_gap_device_name_set(name);
}

static int _scan_event(struct ble_gap_event *event, void *scan_results_in) {
    bleio_scanresults_obj_t *scan_results = (bleio_scanresults_obj_t *)scan_results_in;

    if (event->type == BLE_GAP_EVENT_DISC_COMPLETE) {
        shared_module_bleio_scanresults_set_done(scan_results, true);
        return 0;
    }

    if (event->type != BLE_GAP_EVENT_DISC && event->type != BLE_GAP_EVENT_EXT_DISC) {
        #if CIRCUITPY_VERBOSE_BLE
        mp_printf(&mp_plat_print, "Unsupported scan event %d\n", event->type);
        #endif
        return 0;
    }

    if (event->type == BLE_GAP_EVENT_DISC) {
        // Legacy advertisement
        struct ble_gap_disc_desc *disc = &event->disc;
        bool connectable = disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_ADV_IND ||
            disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_DIR_IND;
        shared_module_bleio_scanresults_append(scan_results,
            supervisor_ticks_ms64(),
            connectable,
            disc->event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP,
            disc->rssi,
            disc->addr.val,
            disc->addr.type,
            disc->data,
            disc->length_data);
    } else {
        // Extended advertisement
        struct ble_gap_ext_disc_desc *disc = &event->ext_disc;
        shared_module_bleio_scanresults_append(scan_results,
            supervisor_ticks_ms64(),
            (disc->props & BLE_HCI_ADV_CONN_MASK) != 0,
            (disc->props & BLE_HCI_ADV_SCAN_MASK) != 0,
            disc->rssi,
            disc->addr.val,
            disc->addr.type,
            disc->data,
            disc->length_data);
    }

    return 0;
}

mp_obj_t common_hal_bleio_adapter_start_scan(bleio_adapter_obj_t *self, uint8_t *prefixes,
    size_t prefix_length, bool extended, mp_int_t buffer_size, mp_float_t timeout,
    mp_float_t interval, mp_float_t window, mp_int_t minimum_rssi, bool active) {
    if (self->scan_results != NULL) {
        if (!shared_module_bleio_scanresults_get_done(self->scan_results)) {
            mp_raise_bleio_BluetoothError(translate("Scan already in progess. Stop with stop_scan."));
        }
        self->scan_results = NULL;
    }

    self->scan_results = shared_module_bleio_new_scanresults(buffer_size, prefixes, prefix_length, minimum_rssi);
    // size_t max_packet_size = extended ? BLE_HCI_MAX_EXT_ADV_DATA_LEN : BLE_HCI_MAX_ADV_DATA_LEN;

    uint8_t own_addr_type;
    struct ble_gap_disc_params disc_params;

    /* Figure out address to use while advertising (no privacy for now) */
    int privacy = 0;
    CHECK_NIMBLE_ERROR(ble_hs_id_infer_auto(privacy, &own_addr_type));

    disc_params.filter_duplicates = 0;
    disc_params.passive = !active;
    disc_params.itvl = interval / 0.625;
    disc_params.window = window / 0.625;
    disc_params.filter_policy = 0;
    disc_params.limited = 0;

    size_t duration_ms = timeout * 1000;
    if (duration_ms == 0) {
        duration_ms = BLE_HS_FOREVER;
    }

    CHECK_NIMBLE_ERROR(ble_gap_disc(own_addr_type, duration_ms, &disc_params,
        _scan_event, self->scan_results));

    return MP_OBJ_FROM_PTR(self->scan_results);
}

void common_hal_bleio_adapter_stop_scan(bleio_adapter_obj_t *self) {
    if (self->scan_results == NULL) {
        return;
    }
    ble_gap_disc_cancel();
    shared_module_bleio_scanresults_set_done(self->scan_results, true);
    self->scan_results = NULL;
}

STATIC void _convert_address(const bleio_address_obj_t *address, ble_addr_t *nimble_address) {
    nimble_address->type = address->type;
    mp_buffer_info_t address_buf_info;
    mp_get_buffer_raise(address->bytes, &address_buf_info, MP_BUFFER_READ);
    memcpy(nimble_address->val, (uint8_t *)address_buf_info.buf, NUM_BLEIO_ADDRESS_BYTES);
}

STATIC int _mtu_reply(uint16_t conn_handle,
    const struct ble_gatt_error *error,
    uint16_t mtu, void *arg) {
    bleio_connection_internal_t *connection = (bleio_connection_internal_t *)arg;
    if (conn_handle != connection->conn_handle || error->status != 0) {
        return 0;
    }
    connection->mtu = mtu;
    return 0;
}

STATIC void _new_connection(uint16_t conn_handle) {
    // Set the tx_power for the connection higher than the advertisement.
    esp_ble_tx_power_set(conn_handle, ESP_PWR_LVL_N0);


    // Find an empty connection. One must always be available because the SD has the same
    // total connection limit.
    bleio_connection_internal_t *connection;
    for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
        connection = &bleio_connections[i];
        if (connection->conn_handle == BLEIO_HANDLE_INVALID) {
            break;
        }
    }
    connection->conn_handle = conn_handle;
    connection->connection_obj = mp_const_none;
    connection->pair_status = PAIR_NOT_PAIRED;
    connection->mtu = 0;

    ble_gattc_exchange_mtu(conn_handle, _mtu_reply, connection);

    // Change the callback for the connection.
    ble_gap_set_event_cb(conn_handle, bleio_connection_event_cb, connection);
}

static int _connect_event(struct ble_gap_event *event, void *self_in) {
    bleio_adapter_obj_t *self = (bleio_adapter_obj_t *)self_in;

    #if CIRCUITPY_VERBOSE_BLE
    mp_printf(&mp_plat_print, "Connect event: %d\n", event->type);
    #endif
    switch (event->type) {
        case BLE_GAP_EVENT_CONNECT:
            if (event->connect.status == 0) {
                _new_connection(event->connect.conn_handle);
                // Set connections objs back to NULL since we have a new
                // connection and need a new tuple.
                self->connection_objs = NULL;
                xTaskNotify(cp_task, event->connect.conn_handle, eSetValueWithOverwrite);
            } else {
                xTaskNotify(cp_task, -event->connect.status, eSetValueWithOverwrite);
            }
            break;

        default:
            #if CIRCUITPY_VERBOSE_BLE
            // For debugging.
            mp_printf(&mp_plat_print, "Unhandled connect event: %d\n", event->type);
            #endif
            break;
    }
    return 0;
}

mp_obj_t common_hal_bleio_adapter_connect(bleio_adapter_obj_t *self, bleio_address_obj_t *address, mp_float_t timeout) {
    // Stop any active scan.
    if (self->scan_results != NULL) {
        common_hal_bleio_adapter_stop_scan(self);
    }

    struct ble_gap_conn_params conn_params = {
        .scan_itvl = MSEC_TO_UNITS(100, UNIT_0_625_MS),
        .scan_window = MSEC_TO_UNITS(100, UNIT_0_625_MS),
        .itvl_min = MSEC_TO_UNITS(15, UNIT_1_25_MS),
        .itvl_max = MSEC_TO_UNITS(300, UNIT_1_25_MS),
        .latency = 0,
        .supervision_timeout = MSEC_TO_UNITS(4000, UNIT_10_MS),
        .min_ce_len = BLE_GAP_INITIAL_CONN_MIN_CE_LEN,
        .max_ce_len = BLE_GAP_INITIAL_CONN_MAX_CE_LEN
    };

    uint8_t own_addr_type;
    // TODO: Use a resolvable address if the peer has our key.
    CHECK_NIMBLE_ERROR(ble_hs_id_infer_auto(false, &own_addr_type));

    ble_addr_t addr;
    _convert_address(address, &addr);

    cp_task = xTaskGetCurrentTaskHandle();
    // Make sure we don't have a pending notification from a previous time. This
    // can happen if a previous wait timed out before the notification was given.
    xTaskNotifyStateClear(cp_task);
    CHECK_NIMBLE_ERROR(
        ble_gap_connect(own_addr_type, &addr,
            SEC_TO_UNITS(timeout, UNIT_1_MS) + 0.5f,
            &conn_params,
            _connect_event, self));

    int error_code;
    CHECK_NOTIFY(xTaskNotifyWait(0, 0, (uint32_t *)&error_code, 200));
    // Negative values are error codes, connection handle otherwise.
    if (error_code < 0) {
        CHECK_BLE_ERROR(-error_code);
    }
    uint16_t conn_handle = error_code;

    // TODO: If we have keys, then try and encrypt the connection.

    // TODO: Negotiate for better PHY and data lengths since we are the central. These are
    // nice-to-haves so ignore any errors.

    // Make the connection object and return it.
    for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
        bleio_connection_internal_t *connection = &bleio_connections[i];
        if (connection->conn_handle == conn_handle) {
            connection->is_central = true;
            return bleio_connection_new_from_internal(connection);
        }
    }

    mp_raise_bleio_BluetoothError(translate("Failed to connect: internal error"));

    return mp_const_none;
}

typedef struct {
    struct os_mbuf mbuf;
    struct os_mbuf_pkthdr hdr;
} os_mbuf_t;

STATIC void _wrap_in_mbuf(const uint8_t *data, uint16_t len, os_mbuf_t *buf) {
    struct os_mbuf *mbuf = &buf->mbuf;
    mbuf->om_data = (uint8_t *)data,
    mbuf->om_flags = 0;
    mbuf->om_pkthdr_len = 0;
    mbuf->om_len = len;
    mbuf->om_next.sle_next = NULL;

    buf->hdr.omp_len = len;

    // Setting the pool to NULL will cause frees to fail. Hopefully that failure
    // is ignored.
    mbuf->om_omp = NULL;
}

static int _advertising_event(struct ble_gap_event *event, void *self_in) {
    bleio_adapter_obj_t *self = (bleio_adapter_obj_t *)self_in;

    #if CIRCUITPY_VERBOSE_BLE
    mp_printf(&mp_plat_print, "Advertising event: %d\n", event->type);
    #endif
    switch (event->type) {
        case BLE_GAP_EVENT_CONNECT:
            // Spurious connect events can happen.
            break;

        case BLE_GAP_EVENT_ADV_COMPLETE:
            if (event->adv_complete.reason == NIMBLE_OK) {
                _new_connection(event->adv_complete.conn_handle);
                // Set connections objs back to NULL since we have a new
                // connection and need a new tuple.
                self->connection_objs = NULL;
            }
            // Other statuses indicate timeout or preemption.
            common_hal_bleio_adapter_stop_advertising(self);
            break;

        default:
            #if CIRCUITPY_VERBOSE_BLE
            // For debugging.
            mp_printf(&mp_plat_print, "Unhandled advertising event: %d\n", event->type);
            #endif
            break;
    }
    return 0;
}

uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self,
    bool connectable, bool anonymous, uint32_t timeout, float interval,
    const uint8_t *advertising_data, uint16_t advertising_data_len,
    const uint8_t *scan_response_data, uint16_t scan_response_data_len,
    mp_int_t tx_power, const bleio_address_obj_t *directed_to) {

    if (ble_gap_adv_active() && !self->user_advertising) {
        return BLE_HS_EBUSY;
    }

    uint32_t rc;
    bool extended = advertising_data_len > BLE_ADV_LEGACY_DATA_MAX_LEN ||
        scan_response_data_len > BLE_ADV_LEGACY_DATA_MAX_LEN;

    ble_addr_t peer;
    if (directed_to != NULL) {
        _convert_address(directed_to, &peer);
    }

    uint8_t own_addr_type;
    // Anonymous addresses are still resolvable. (Following what the NRF
    // implementation does.)
    rc = ble_hs_id_infer_auto(anonymous, &own_addr_type);
    if (rc != NIMBLE_OK) {
        return rc;
    }

    bool high_duty_directed = directed_to != NULL && interval <= 3.5 && timeout <= 1.3;

    struct ble_gap_ext_adv_params adv_params = {
        .connectable = connectable,
        .scannable = scan_response_data_len > 0,
        .directed = directed_to != NULL,
        .high_duty_directed = high_duty_directed,
        .legacy_pdu = !extended,
        .anonymous = anonymous,
        .include_tx_power = extended,
        .scan_req_notif = false,
        .itvl_min = SEC_TO_UNITS(interval, UNIT_0_625_MS) + 0.5f,
        .itvl_max = SEC_TO_UNITS(interval, UNIT_0_625_MS) + 0.5f,
        .channel_map = 0,
        .own_addr_type = own_addr_type,
        .peer = peer,
        .filter_policy = BLE_HCI_CONN_FILT_NO_WL,
        .primary_phy = BLE_HCI_LE_PHY_1M,
        .secondary_phy = BLE_HCI_LE_PHY_1M,
        .tx_power = tx_power,
        .sid = 0,
    };

    // Configure must come before setting payloads.
    rc = ble_gap_ext_adv_configure(0,
        &adv_params,
        NULL,
        _advertising_event, self);
    if (rc != NIMBLE_OK) {
        return rc;
    }

    os_mbuf_t buf;
    _wrap_in_mbuf(advertising_data, advertising_data_len, &buf);
    // This copies the advertising data into buffers to send to the controller.
    // Therefore, we don't need to worry about the lifetime of our copy.
    rc = ble_gap_ext_adv_set_data(0, &buf.mbuf);
    if (rc != NIMBLE_OK) {
        return rc;
    }

    if (scan_response_data_len > 0) {
        _wrap_in_mbuf(scan_response_data, scan_response_data_len, &buf);
        // This copies the advertising data into buffers to send to the controller.
        // Therefore, we don't need to worry about the lifetime of our copy.
        rc = ble_gap_ext_adv_rsp_set_data(0, &buf.mbuf);
        if (rc != NIMBLE_OK) {
            return rc;
        }
    }

    rc = ble_gap_ext_adv_start(0, timeout / 10, 0);
    if (rc != NIMBLE_OK) {
        return rc;
    }

    return NIMBLE_OK;
}

STATIC void check_data_fit(size_t data_len, bool connectable) {
    if (data_len > MYNEWT_VAL(BLE_EXT_ADV_MAX_SIZE) ||
        (connectable && data_len > MYNEWT_VAL(BLE_EXT_ADV_MAX_SIZE))) {
        mp_raise_ValueError(translate("Data too large for advertisement packet"));
    }
}

void common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, bool connectable,
    bool anonymous, uint32_t timeout, mp_float_t interval,
    mp_buffer_info_t *advertising_data_bufinfo, mp_buffer_info_t *scan_response_data_bufinfo,
    mp_int_t tx_power, const bleio_address_obj_t *directed_to) {
    if (self->user_advertising) {
        mp_raise_bleio_BluetoothError(translate("Already advertising."));
    } else {
        // If the user isn't advertising, then the background is. So, stop the
        // background advertising so the user can.
        common_hal_bleio_adapter_stop_advertising(self);
    }
    // interval value has already been validated.

    check_data_fit(advertising_data_bufinfo->len, connectable);
    check_data_fit(scan_response_data_bufinfo->len, connectable);

    if (advertising_data_bufinfo->len > 31 && scan_response_data_bufinfo->len > 0) {
        mp_raise_bleio_BluetoothError(translate("Extended advertisements with scan response not supported."));
    }


    if (advertising_data_bufinfo->len > 0 && directed_to != NULL) {
        mp_raise_bleio_BluetoothError(translate("Data not supported with directed advertising"));
    }

    if (anonymous) {
        mp_raise_NotImplementedError(NULL);
    }

    if (!timeout) {
        timeout = BLE_HS_FOREVER;
    } else if (timeout > INT32_MAX) {
        mp_raise_bleio_BluetoothError(translate("Timeout is too long: Maximum timeout length is %d seconds"),
            INT32_MAX / 1000);
    }

    CHECK_NIMBLE_ERROR(_common_hal_bleio_adapter_start_advertising(self, connectable, anonymous, timeout, interval,
        advertising_data_bufinfo->buf,
        advertising_data_bufinfo->len,
        scan_response_data_bufinfo->buf,
        scan_response_data_bufinfo->len,
        tx_power,
        directed_to));
    self->user_advertising = true;
}

void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) {
    if (!common_hal_bleio_adapter_get_advertising(self)) {
        return;
    }
    int err_code = ble_gap_ext_adv_stop(0);
    self->user_advertising = false;

    if ((err_code != NIMBLE_OK) &&
        (err_code != BLE_HS_EALREADY) &&
        (err_code != BLE_HS_EINVAL)) {
        CHECK_NIMBLE_ERROR(err_code);
    }
}

bool common_hal_bleio_adapter_get_advertising(bleio_adapter_obj_t *self) {
    return ble_gap_adv_active();
}

bool common_hal_bleio_adapter_get_connected(bleio_adapter_obj_t *self) {
    for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
        bleio_connection_internal_t *connection = &bleio_connections[i];
        if (connection->conn_handle != BLEIO_HANDLE_INVALID) {
            return true;
        }
    }
    return false;
}

mp_obj_t common_hal_bleio_adapter_get_connections(bleio_adapter_obj_t *self) {
    if (self->connection_objs != NULL) {
        return self->connection_objs;
    }
    size_t total_connected = 0;
    mp_obj_t items[BLEIO_TOTAL_CONNECTION_COUNT];
    for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
        bleio_connection_internal_t *connection = &bleio_connections[i];
        if (connection->conn_handle != BLEIO_HANDLE_INVALID) {
            if (connection->connection_obj == mp_const_none) {
                connection->connection_obj = bleio_connection_new_from_internal(connection);
            }
            items[total_connected] = connection->connection_obj;
            total_connected++;
        }
    }
    self->connection_objs = mp_obj_new_tuple(total_connected, items);
    return self->connection_objs;
}

void common_hal_bleio_adapter_erase_bonding(bleio_adapter_obj_t *self) {
    mp_raise_NotImplementedError(NULL);
    // bonding_erase_storage();
}

bool common_hal_bleio_adapter_is_bonded_to_central(bleio_adapter_obj_t *self) {
    mp_raise_NotImplementedError(NULL);
    // return bonding_peripheral_bond_count() > 0;
    return false;
}

void bleio_adapter_gc_collect(bleio_adapter_obj_t *adapter) {
    // We divide by size_t so that we can scan each 32-bit aligned value to see
    // if it is a pointer. This allows us to change the structs without worrying
    // about collecting new pointers.
    gc_collect_root((void **)adapter, sizeof(bleio_adapter_obj_t) / (sizeof(size_t)));
    gc_collect_root((void **)bleio_connections, sizeof(bleio_connections) / (sizeof(size_t)));
}

void bleio_adapter_reset(bleio_adapter_obj_t *adapter) {
    common_hal_bleio_adapter_stop_scan(adapter);
    if (common_hal_bleio_adapter_get_advertising(adapter)) {
        common_hal_bleio_adapter_stop_advertising(adapter);
    }

    adapter->connection_objs = NULL;
    for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
        bleio_connection_internal_t *connection = &bleio_connections[i];
        // Disconnect all connections cleanly.
        if (connection->conn_handle != BLEIO_HANDLE_INVALID) {
            common_hal_bleio_connection_disconnect(connection);
        }
        connection->connection_obj = mp_const_none;
    }

    // Wait up to 125 ms (128 ticks) for disconnect to complete. This should be
    // greater than most connection intervals.
    bool any_connected = false;
    uint64_t start_ticks = supervisor_ticks_ms64();
    while (any_connected && supervisor_ticks_ms64() - start_ticks < 128) {
        any_connected = false;
        for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
            bleio_connection_internal_t *connection = &bleio_connections[i];
            any_connected |= connection->conn_handle != BLEIO_HANDLE_INVALID;
        }
    }
}
