/*
 * This program source code file is part of the KTag project, a DIY laser tag
 * game with customizable features and wide interoperability.
 *
 * 🛡 <https://ktag.clubk.club> 🃞
 *
 * Copyright © 2024-2026 Joseph P. Kearney and the KTag developers.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
 * details.
 *
 * There should be a copy of the GNU Affero General Public License in the LICENSE
 * file in the root of this repository.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <SystemK.h>
#include <System_Events.h>
#include "esp_log.h"
/* BLE */
#include "esp_nimble_hci.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "console/console.h"
#include "services/gap/ble_svc_gap.h"

static const char *TAG = "BLE";
static bool Host_And_Controller_Synced = false;
static bool Is_Scanning = false;
static bool Is_Advertising = false;
static bool Is_Quiet = false;
static bool Was_Advertising = false;

static TimerHandle_t BLEUnquietTimer = NULL;
static StaticTimer_t xBLEUnquietTimerBuffer;

static uint8_t Advertising_Data[BLE_KTAG_PACKET_TOTAL_SIZE] = {0x1E, 0xFF, 0xFF, 0xFF, 'K', 'T', 'a', 'g'};

// Forward declarations of NimBLE functions.
void ble_store_config_init(void);
static int ble_gap_event(struct ble_gap_event *event, void *arg);

/**
 * Initiates the GAP general discovery procedure.
 */
static void ble_scan(void)
{
    uint8_t own_addr_type;
    struct ble_gap_disc_params disc_params;
    int rc;

    /* Figure out address to use while advertising (no privacy for now) */
    rc = ble_hs_id_infer_auto(0, &own_addr_type);
    if (rc != 0)
    {
        KLOG_ERROR(TAG, "Error determining address type; rc=%d", rc);
        return;
    }

    /* Tell the controller not to filter duplicates; we want to process
     * repeated advertisements from the same device.
     */
    disc_params.filter_duplicates = 0;

    /**
     * Perform a passive scan.  I.e., don't send follow-up scan requests to
     * each advertiser.
     */
    disc_params.passive = 1;

    /* Use defaults for the rest of the parameters. */
    disc_params.itvl = BLE_GAP_SCAN_ITVL_MS(40);
    disc_params.window = BLE_GAP_SCAN_WIN_MS(30);
    disc_params.filter_policy = 0;
    disc_params.limited = 0;

    rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params, ble_gap_event, NULL);
    if (rc != 0)
    {
        KLOG_ERROR(TAG, "Error initiating GAP discovery procedure; rc=%d", rc);
    }
}

static void ble_advertise(void)
{
    struct ble_gap_adv_params adv_params;

    int rc = ble_gap_adv_set_data((const uint8_t *)&Advertising_Data, sizeof(Advertising_Data));
    if (rc != 0)
    {
        KLOG_ERROR(TAG, "Error setting advertisement data; rc=%d", rc);
        return;
    }

    /* Begin advertising. */
    memset(&adv_params, 0, sizeof adv_params);
    adv_params.conn_mode = BLE_GAP_CONN_MODE_NON;
    adv_params.disc_mode = BLE_GAP_DISC_MODE_NON;

    // N.B. High duty-cycle mode requires modification to NimBLE, or you will get an "Error enabling advertisement; rc=530"
    // adv_params.high_duty_cycle = 1;
    // adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(20);
    // adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(25);

    adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(32);
    adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(37);

    rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
                           &adv_params, ble_gap_event, NULL);
    if (rc != 0)
    {
        KLOG_ERROR(TAG, "Error enabling advertisement; rc=%d", rc);
        return;
    }
}

// https://mynewt.apache.org/latest/network/ble_setup/ble_sync_cb.html#reset
static void ble_on_reset(int reason)
{
    KLOG_ERROR(TAG, "Resetting state for reason %d.", reason);
    Host_And_Controller_Synced = false;
}

// https://mynewt.apache.org/latest/network/ble_setup/ble_sync_cb.html#sync
static void ble_on_sync(void)
{
    KLOG_INFO(TAG, "Host and controller synchronized.");

    /* Make sure we have proper identity address set (public preferred) */
    int rc = ble_hs_util_ensure_addr(0);
    assert(rc == 0);

    Host_And_Controller_Synced = true;
}

/**
 * The NimBLE host executes this callback when a GAP event occurs.  The
 * application associates a GAP event callback with each connection that is
 * established.  KTag uses the same callback for all connections.
 *
 * @param event                 The event being signalled.
 * @param arg                   Application-specified argument; unused by KTag.
 *
 * @return                      0 if the application successfully handled the
 *                              event; nonzero on failure.  The semantics
 *                              of the return code is specific to the
 *                              particular GAP event being signalled.
 */
static int ble_gap_event(struct ble_gap_event *event, void *arg)
{
    struct ble_hs_adv_fields fields;
    int rc;

    switch (event->type)
    {
    case BLE_GAP_EVENT_DISC:
        rc = ble_hs_adv_parse_fields(&fields, event->disc.data,
                                     event->disc.length_data);
        if (rc != 0)
        {
            return 0;
        }

        BLE_Packet_T *packet = BLE_DecodeKTagPacket(event->disc.data,
                                                    event->disc.length_data,
                                                    &(event->disc.addr.val[0]),
                                                    event->disc.rssi);

        if (packet != NULL)
        {
            if (packet->Generic.type == BLE_PACKET_TYPE_CONSOLE)
            {
                packet->Console.console_data[BLE_KTAG_PACKET_DATA_SIZE - 1] = '\0';
                HW_Execute_Console_Command(packet->Console.console_data);
            }
            else
            {
                KEvent_T packet_received_event = {.ID = KEVENT_BLE_PACKET_RECEIVED, .Data = packet};
                Post_KEvent(&packet_received_event);
            }
        }
        return 0;

    case BLE_GAP_EVENT_ADV_COMPLETE:
        KLOG_INFO(TAG, "Advertise complete; reason=%d", event->adv_complete.reason);
        ble_advertise();
        return 0;

    default:
        return 0;
    }
}

void ble_host_task(void *param)
{
    KLOG_INFO(TAG, "BLE Host Task Started");

    // This function will return only after nimble_port_stop() is called:
    nimble_port_run();

    nimble_port_freertos_deinit();
}

void Initialize_BLE(void)
{
    KLOG_INFO(TAG, "Initializing BLE...");

    esp_err_t ret = nimble_port_init();
    if (ret != ESP_OK)
    {
        KLOG_ERROR(TAG, "Failed to initialize NimBLE for reason %d.", ret);
        return;
    }
    /* Configure the host. */
    ble_hs_cfg.reset_cb = ble_on_reset;
    ble_hs_cfg.sync_cb = ble_on_sync;
    ble_hs_cfg.store_status_cb = ble_store_util_status_rr;

    ble_store_config_init();

    nimble_port_freertos_init(ble_host_task);

    xEventGroupSetBits(Get_System_Events(), SYS_BLE_READY);
    KLOG_INFO(TAG, "Initialization complete.");
}

void Disable_BLE(void)
{
    KLOG_INFO(TAG, "Disabling BLE...");

    esp_err_t ret = nimble_port_deinit();

    if (ret != ESP_OK)
    {
        KLOG_ERROR(TAG, "Failed to deinitialize NimBLE for reason %d.", ret);
    }
    else
    {
        KLOG_INFO(TAG, "NimBLE deinitialized--BLE disabled.");
    }
}

SystemKResult_T BLE_GetMyAddress(uint8_t *BD_ADDR)
{
    /* Try to load a public address. */
    int result = ble_hs_id_copy_addr(BLE_ADDR_PUBLIC, BD_ADDR, NULL);
    if (result == BLE_HS_ENOADDR)
    {
        return SYSTEMK_RESULT_NOT_IMPLEMENTED;
    }
    else
    {
        return SYSTEMK_RESULT_SUCCESS;
    }
}

SystemKResult_T BLE_SetAdvertisingData(BLE_AdvertisingData_T *data)
{
    SystemKResult_T result = SYSTEMK_RESULT_SUCCESS;

    if (data->length > BLE_MAX_ADVERTISING_BYTES)
    {
        result = SYSTEMK_RESULT_TOO_MANY_DATA;
    }
    else if (data->length < BLE_KTAG_PACKET_TOTAL_SIZE)
    {
        result = SYSTEMK_RESULT_TOO_FEW_DATA;
    }
    else
    {
        memcpy(Advertising_Data, data, BLE_KTAG_PACKET_TOTAL_SIZE);
    }

    if (Is_Advertising == true)
    {
        // Restart advertising to put the new data into the advertisement.
        ble_gap_adv_stop();
        ble_advertise();
    }

    return result;
}

SystemKResult_T BLE_ScanAndAdvertise(void)
{
    SystemKResult_T result = SYSTEMK_RESULT_NOT_READY;

    if (Host_And_Controller_Synced == true)
    {
        if (Is_Scanning == false)
        {
            ble_scan();
            Is_Scanning = true;
        }
        if (Is_Advertising == false)
        {
            if (Is_Quiet == false)
            {
                ble_advertise();
                Is_Advertising = true;
            }
            else
            {
                // If we are quiet, we don't want to advertise yet,
                // but we will remember that we were asked to advertise.
                Was_Advertising = true;
            }
        }
        result = SYSTEMK_RESULT_SUCCESS;
    }

    return result;
}

SystemKResult_T BLE_StopScanning(void)
{
    SystemKResult_T result = SYSTEMK_RESULT_NOT_READY;

    if (Host_And_Controller_Synced == true)
    {
        if (Is_Scanning == true)
        {
            ble_gap_disc_cancel();
            Is_Scanning = false;
        }
        result = SYSTEMK_RESULT_SUCCESS;
    }

    return result;
}

SystemKResult_T BLE_StopAdvertising(void)
{
    SystemKResult_T result = SYSTEMK_RESULT_NOT_READY;

    if (Host_And_Controller_Synced == true)
    {
        if (Is_Advertising == true)
        {
            ble_gap_adv_stop();
            Is_Advertising = false;
        }
        result = SYSTEMK_RESULT_SUCCESS;
    }

    return result;
}

static void Unquiet_Timer_Callback(TimerHandle_t xTimer)
{
    KLOG_INFO(TAG, "Quiet time expired after %lu ms.", (xTimerGetPeriod(xTimer) * portTICK_PERIOD_MS));
    BLE_Unquiet();
}

SystemKResult_T BLE_Quiet(uint32_t duration_ms)
{
    SystemKResult_T result = SYSTEMK_RESULT_NOT_READY;

    if (Is_Quiet == true)
    {
        // Already quiet; reset timer with new duration if duration_ms > 0.
        if (duration_ms > 0)
        {
            // Stop the timer and change the period if it is already running.
            if (BLEUnquietTimer != NULL)
            {
                xTimerStop(BLEUnquietTimer, 0);
                xTimerChangePeriod(BLEUnquietTimer, pdMS_TO_TICKS(duration_ms), 0);
                xTimerStart(BLEUnquietTimer, 0);
            }
            else
            {
                // There should already be a timer, but just in case...
                BLEUnquietTimer = xTimerCreateStatic(
                    "BLEUnquietTimer",
                    pdMS_TO_TICKS(duration_ms),
                    pdFALSE,
                    (void *)0,
                    Unquiet_Timer_Callback,
                    &xBLEUnquietTimerBuffer);

                if (BLEUnquietTimer == NULL)
                {
                    KLOG_ERROR(TAG, "Couldn't create the BLEUnquietTimer!");
                    result = SYSTEMK_RESULT_FAILED_TO_CREATE_RTOS_TIMER;
                }
                else
                {
                    if (xTimerStart(BLEUnquietTimer, 0) != pdPASS)
                    {
                        KLOG_ERROR(TAG, "Couldn't start the BLEUnquietTimer!");
                        result = SYSTEMK_RESULT_FAILED_TO_START_RTOS_TIMER;
                    }
                }
            }
        }
        else
        {
            // If duration_ms is 0, stop the timer to stay quiet indefinitely.
            if (BLEUnquietTimer != NULL)
            {
                xTimerStop(BLEUnquietTimer, 0);
            }
        }
        result = SYSTEMK_RESULT_SUCCESS;
    }
    else if (Host_And_Controller_Synced == true)
    {
        Is_Quiet = true;

        if (Is_Advertising == true)
        {
            ble_gap_adv_stop();
            Is_Advertising = false;
            Was_Advertising = true;
        }

        if (duration_ms > 0)
        {
            // Set a timer to unquiet after the specified duration.
            BLEUnquietTimer = xTimerCreateStatic(
                "BLEUnquietTimer",
                pdMS_TO_TICKS(duration_ms),
                pdFALSE,
                (void *)0,
                Unquiet_Timer_Callback,
                &xBLEUnquietTimerBuffer);

            if (BLEUnquietTimer == NULL)
            {
                KLOG_ERROR(TAG, "Couldn't create the BLEUnquietTimer!");
                result = SYSTEMK_RESULT_FAILED_TO_CREATE_RTOS_TIMER;
            }
            else
            {
                if (xTimerStart(BLEUnquietTimer, 0) != pdPASS)
                {
                    KLOG_ERROR(TAG, "Couldn't start the BLEUnquietTimer!");
					result = SYSTEMK_RESULT_FAILED_TO_START_RTOS_TIMER;
                }
            }
        }
        result = SYSTEMK_RESULT_SUCCESS;
    }

    return result;
}

SystemKResult_T BLE_Unquiet(void)
{
    SystemKResult_T result = SYSTEMK_RESULT_NOT_READY;

    KLOG_INFO(TAG, "BLE_Unquiet()");

    if (Is_Quiet == false)
    {
        // Already unquiet; no action needed.
        return SYSTEMK_RESULT_SUCCESS;
    }

    if (Host_And_Controller_Synced == true)
    {
        Is_Quiet = false;
        if (Was_Advertising == true)
        {
            ble_advertise();
            Is_Advertising = true;
            Was_Advertising = false;
        }
        result = SYSTEMK_RESULT_SUCCESS;
    }

    return result;
}
