/*
 * 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) 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 "shared-bindings/_bleio/Connection.h"

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

#include "py/gc.h"
#include "py/objlist.h"
#include "py/objstr.h"
#include "py/qstr.h"
#include "py/runtime.h"

#include "shared/runtime/interrupt_char.h"

#include "shared-bindings/_bleio/__init__.h"
#include "shared-bindings/_bleio/Adapter.h"
#include "shared-bindings/_bleio/Attribute.h"
#include "shared-bindings/_bleio/Characteristic.h"
#include "shared-bindings/_bleio/Service.h"
#include "shared-bindings/_bleio/UUID.h"

#include "supervisor/shared/tick.h"

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

#include "host/ble_att.h"

// Give 20 seconds for discovery
#define DISCOVERY_TIMEOUT_MS 20000

int bleio_connection_event_cb(struct ble_gap_event *event, void *connection_in) {
    bleio_connection_internal_t *connection = (bleio_connection_internal_t *)connection_in;

    switch (event->type) {
        case BLE_GAP_EVENT_DISCONNECT: {
            connection->conn_handle = BLEIO_HANDLE_INVALID;
            connection->pair_status = PAIR_NOT_PAIRED;

            #if CIRCUITPY_VERBOSE_BLE
            mp_printf(&mp_plat_print, "disconnected %02x\n", event->disconnect.reason);
            #endif
            if (connection->connection_obj != mp_const_none) {
                bleio_connection_obj_t *obj = connection->connection_obj;
                obj->connection = NULL;
                obj->disconnect_reason = event->disconnect.reason;
            }

            break;
        }

        case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE: {
            #if CIRCUITPY_VERBOSE_BLE
            mp_printf(&mp_plat_print, "TODO connection event: PHY update complete\n");
            #endif
            break;
        }

        case BLE_GAP_EVENT_CONN_UPDATE: {
            #if CIRCUITPY_VERBOSE_BLE
            mp_printf(&mp_plat_print, "TODO connection event: connection update\n");
            #endif
            break;
        }
        case BLE_GAP_EVENT_L2CAP_UPDATE_REQ: {
            #if CIRCUITPY_VERBOSE_BLE
            mp_printf(&mp_plat_print, "TODO connection event: l2cap update request\n");
            #endif
            break;
        }

        // These events are actually att specific so forward to all registered
        // handlers for them. The handlers themselves decide whether an event
        // is interesting to them.
        case BLE_GAP_EVENT_NOTIFY_RX:
            MP_FALLTHROUGH;
        case BLE_GAP_EVENT_NOTIFY_TX:
            MP_FALLTHROUGH;
        case BLE_GAP_EVENT_SUBSCRIBE:
            return ble_event_run_handlers(event);

        default:
            #if CIRCUITPY_VERBOSE_BLE
            mp_printf(&mp_plat_print, "Unhandled connection event: %d\n", event->type);
            #endif
            return 0;
    }
    return 0;
}

bool common_hal_bleio_connection_get_paired(bleio_connection_obj_t *self) {
    if (self->connection == NULL) {
        return false;
    }
    return self->connection->pair_status == PAIR_PAIRED;
}

bool common_hal_bleio_connection_get_connected(bleio_connection_obj_t *self) {
    if (self->connection == NULL) {
        return false;
    }
    return self->connection->conn_handle != BLEIO_HANDLE_INVALID;
}

void common_hal_bleio_connection_disconnect(bleio_connection_internal_t *self) {
    ble_gap_terminate(self->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}

void common_hal_bleio_connection_pair(bleio_connection_internal_t *self, bool bond) {
    // TODO: Implement this.
}

mp_float_t common_hal_bleio_connection_get_connection_interval(bleio_connection_internal_t *self) {
    // TODO: Implement this.
    while (self->conn_params_updating && !mp_hal_is_interrupted()) {
        RUN_BACKGROUND_TASKS;
    }
    return 0;
}

// Return the current negotiated MTU length, minus overhead.
mp_int_t common_hal_bleio_connection_get_max_packet_length(bleio_connection_internal_t *self) {
    return (self->mtu == 0 ? BLE_ATT_MTU_DFLT : self->mtu) - 3;
}

void common_hal_bleio_connection_set_connection_interval(bleio_connection_internal_t *self, mp_float_t new_interval) {
    self->conn_params_updating = true;
    // TODO: Implement this.
}

STATIC volatile int _last_discovery_status;
static TaskHandle_t discovery_task = NULL;

STATIC int _discovered_service_cb(uint16_t conn_handle,
    const struct ble_gatt_error *error,
    const struct ble_gatt_svc *svc,
    void *arg) {
    bleio_connection_internal_t *self = (bleio_connection_internal_t *)arg;

    if (error->status != BLE_ERR_SUCCESS) {
        // Keep the first error in case it's due to memory.
        if (_last_discovery_status == BLE_ERR_SUCCESS) {
            _last_discovery_status = error->status;
            xTaskNotifyGive(discovery_task);
        }
        return 0;
    }

    // If any of these memory allocations fail, we set _last_discovery_status
    // and let the process continue.
    if (_last_discovery_status != BLE_ERR_SUCCESS) {
        return 0;
    }
    bleio_service_obj_t *service = mp_obj_malloc(bleio_service_obj_t, &bleio_service_type);

    // Initialize several fields at once.
    bleio_service_from_connection(service, bleio_connection_new_from_internal(self));

    service->is_remote = true;
    service->start_handle = svc->start_handle;
    service->end_handle = svc->end_handle;
    service->handle = svc->start_handle;

    bleio_uuid_obj_t *uuid = mp_obj_malloc(bleio_uuid_obj_t, &bleio_uuid_type);

    uuid->nimble_ble_uuid = svc->uuid;
    service->uuid = uuid;

    mp_obj_list_append(MP_OBJ_FROM_PTR(self->remote_service_list),
        MP_OBJ_FROM_PTR(service));
    return 0;
}

STATIC int _discovered_characteristic_cb(uint16_t conn_handle,
    const struct ble_gatt_error *error,
    const struct ble_gatt_chr *chr,
    void *arg) {
    bleio_service_obj_t *service = (bleio_service_obj_t *)arg;

    if (error->status != BLE_ERR_SUCCESS) {
        // Keep the first error in case it's due to memory.
        if (_last_discovery_status == BLE_ERR_SUCCESS) {
            _last_discovery_status = error->status;
            xTaskNotifyGive(discovery_task);
        }
    }
    // If any of these memory allocations fail, we set _last_discovery_status
    // and let the process continue.
    if (_last_discovery_status != BLE_ERR_SUCCESS) {
        return 0;
    }

    bleio_characteristic_obj_t *characteristic =
        mp_obj_malloc(bleio_characteristic_obj_t, &bleio_characteristic_type);

    // Known characteristic UUID.
    bleio_uuid_obj_t *uuid = mp_obj_malloc(bleio_uuid_obj_t, &bleio_uuid_type);

    uuid->nimble_ble_uuid = chr->uuid;

    bleio_characteristic_properties_t props =
        ((chr->properties & BLE_GATT_CHR_PROP_BROADCAST) != 0 ? CHAR_PROP_BROADCAST : 0) |
        ((chr->properties & BLE_GATT_CHR_PROP_INDICATE) != 0 ? CHAR_PROP_INDICATE : 0) |
        ((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) != 0 ? CHAR_PROP_NOTIFY : 0) |
        ((chr->properties & BLE_GATT_CHR_PROP_READ) != 0 ? CHAR_PROP_READ : 0) |
        ((chr->properties & BLE_GATT_CHR_PROP_WRITE) != 0 ? CHAR_PROP_WRITE : 0) |
        ((chr->properties & BLE_GATT_CHR_PROP_WRITE_NO_RSP) != 0 ? CHAR_PROP_WRITE_NO_RESPONSE : 0);

    // Call common_hal_bleio_characteristic_construct() to initialize some fields and set up evt handler.
    common_hal_bleio_characteristic_construct(
        characteristic, service, chr->val_handle, uuid,
        props, SECURITY_MODE_OPEN, SECURITY_MODE_OPEN,
        0, false,   // max_length, fixed_length: values don't matter for gattc
        mp_const_empty_bytes,
        NULL);
    // Set def_handle directly since it is only used in discovery.
    characteristic->def_handle = chr->def_handle;

    mp_obj_list_append(MP_OBJ_FROM_PTR(service->characteristic_list),
        MP_OBJ_FROM_PTR(characteristic));
    return 0;
}

STATIC int _discovered_descriptor_cb(uint16_t conn_handle,
    const struct ble_gatt_error *error,
    uint16_t chr_val_handle,
    const struct ble_gatt_dsc *dsc,
    void *arg) {
    bleio_characteristic_obj_t *characteristic = (bleio_characteristic_obj_t *)arg;

    if (error->status != BLE_ERR_SUCCESS) {
        // Keep the first error in case it's due to memory.
        if (_last_discovery_status == BLE_ERR_SUCCESS) {
            _last_discovery_status = error->status;
        }
        xTaskNotifyGive(discovery_task);
    }
    // If any of these memory allocations fail, we set _last_discovery_status
    // and let the process continue.
    if (_last_discovery_status != BLE_ERR_SUCCESS) {
        return 0;
    }

    // Remember handles for certain well-known descriptors.
    switch (dsc->uuid.u16.value) {
        case 0x2902:
            characteristic->cccd_handle = dsc->handle;
            break;

        case 0x2903:
            characteristic->sccd_handle = dsc->handle;
            break;

        case 0x2901:
            characteristic->user_desc_handle = dsc->handle;
            break;

        default:
            break;
    }

    bleio_descriptor_obj_t *descriptor = mp_obj_malloc(bleio_descriptor_obj_t, &bleio_descriptor_type);

    bleio_uuid_obj_t *uuid = mp_obj_malloc(bleio_uuid_obj_t, &bleio_uuid_type);
    uuid->nimble_ble_uuid = dsc->uuid;

    common_hal_bleio_descriptor_construct(
        descriptor, characteristic, uuid,
        SECURITY_MODE_OPEN, SECURITY_MODE_OPEN,
        0, false, mp_const_empty_bytes);
    descriptor->handle = dsc->handle;

    mp_obj_list_append(MP_OBJ_FROM_PTR(characteristic->descriptor_list),
        MP_OBJ_FROM_PTR(descriptor));
    return 0;
}

STATIC void discover_remote_services(bleio_connection_internal_t *self, mp_obj_t service_uuids_whitelist) {
    // Start over with an empty list.
    self->remote_service_list = mp_obj_new_list(0, NULL);

    discovery_task = xTaskGetCurrentTaskHandle();
    if (service_uuids_whitelist == mp_const_none) {
        _last_discovery_status = BLE_ERR_SUCCESS;
        CHECK_NIMBLE_ERROR(ble_gattc_disc_all_svcs(self->conn_handle, _discovered_service_cb, self));

        // Wait for sync.
        ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS));
        if (_last_discovery_status != BLE_HS_EDONE) {
            CHECK_BLE_ERROR(_last_discovery_status);
        }
    } else {
        mp_obj_iter_buf_t iter_buf;
        mp_obj_t iterable = mp_getiter(service_uuids_whitelist, &iter_buf);
        mp_obj_t uuid_obj;
        while ((uuid_obj = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) {
            if (!mp_obj_is_type(uuid_obj, &bleio_uuid_type)) {
                mp_raise_TypeError(translate("non-UUID found in service_uuids_whitelist"));
            }
            bleio_uuid_obj_t *uuid = MP_OBJ_TO_PTR(uuid_obj);

            _last_discovery_status = BLE_ERR_SUCCESS;
            // Make sure we start with a clean notification state
            ulTaskNotifyValueClear(discovery_task, 0xffffffff);
            CHECK_NIMBLE_ERROR(ble_gattc_disc_svc_by_uuid(self->conn_handle, &uuid->nimble_ble_uuid.u,
                _discovered_service_cb, self));
            // Wait for sync.
            CHECK_NOTIFY(ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS)));
            if (_last_discovery_status != BLE_HS_EDONE) {
                CHECK_BLE_ERROR(_last_discovery_status);
            }
        }
    }

    for (size_t i = 0; i < self->remote_service_list->len; i++) {
        bleio_service_obj_t *service = MP_OBJ_TO_PTR(self->remote_service_list->items[i]);

        _last_discovery_status = BLE_ERR_SUCCESS;
        CHECK_NIMBLE_ERROR(ble_gattc_disc_all_chrs(self->conn_handle,
            service->start_handle,
            service->end_handle,
            _discovered_characteristic_cb,
            service));

        // Wait for sync.
        CHECK_NOTIFY(ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS)));
        if (_last_discovery_status != BLE_HS_EDONE) {
            CHECK_BLE_ERROR(_last_discovery_status);
        }

        // Got characteristics for this service. Now discover descriptors for each characteristic.
        size_t char_list_len = service->characteristic_list->len;
        for (size_t char_idx = 0; char_idx < char_list_len; ++char_idx) {
            bleio_characteristic_obj_t *characteristic =
                MP_OBJ_TO_PTR(service->characteristic_list->items[char_idx]);
            // Determine the handle range for the given characteristic's descriptors.
            // The end of the range is dictated by the next characteristic or the end
            // handle of the service.
            const bool last_characteristic = char_idx == char_list_len - 1;
            bleio_characteristic_obj_t *next_characteristic = last_characteristic
                ? NULL
                : MP_OBJ_TO_PTR(service->characteristic_list->items[char_idx + 1]);

            uint16_t end_handle = next_characteristic == NULL
                ? service->end_handle
                : next_characteristic->def_handle - 1;

            // Pre-check if characteristic is empty so descriptor discovery doesn't fail
            if (end_handle <= characteristic->handle) {
                continue;
            }

            _last_discovery_status = BLE_ERR_SUCCESS;
            CHECK_NIMBLE_ERROR(ble_gattc_disc_all_dscs(self->conn_handle, characteristic->handle,
                end_handle,
                _discovered_descriptor_cb, characteristic));
            // Wait for sync.
            ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(DISCOVERY_TIMEOUT_MS));
            if (_last_discovery_status != BLE_HS_EDONE) {
                CHECK_BLE_ERROR(_last_discovery_status);
            }
        }
    }
}

mp_obj_tuple_t *common_hal_bleio_connection_discover_remote_services(bleio_connection_obj_t *self, mp_obj_t service_uuids_whitelist) {
    discover_remote_services(self->connection, service_uuids_whitelist);
    bleio_connection_ensure_connected(self);
    // Convert to a tuple and then clear the list so the callee will take ownership.
    mp_obj_tuple_t *services_tuple =
        mp_obj_new_tuple(self->connection->remote_service_list->len,
            self->connection->remote_service_list->items);
    mp_obj_list_clear(MP_OBJ_FROM_PTR(self->connection->remote_service_list));

    return services_tuple;
}

uint16_t bleio_connection_get_conn_handle(bleio_connection_obj_t *self) {
    if (self == NULL || self->connection == NULL) {
        return BLEIO_HANDLE_INVALID;
    }
    return self->connection->conn_handle;
}

mp_obj_t bleio_connection_new_from_internal(bleio_connection_internal_t *internal) {
    if (internal->connection_obj != mp_const_none) {
        return internal->connection_obj;
    }
    bleio_connection_obj_t *connection = mp_obj_malloc(bleio_connection_obj_t, &bleio_connection_type);

    connection->connection = internal;
    internal->connection_obj = connection;

    return MP_OBJ_FROM_PTR(connection);
}

// Find the connection that uses the given conn_handle. Return NULL if not found.
bleio_connection_internal_t *bleio_conn_handle_to_connection(uint16_t conn_handle) {
    bleio_connection_internal_t *connection;
    for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) {
        connection = &bleio_connections[i];
        if (connection->conn_handle == conn_handle) {
            return connection;
        }
    }

    return NULL;
}
