/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

#include <assert.h>
#include "nimble/porting/nimble/include/sysinit/sysinit.h"
#include "syscfg/syscfg.h"
#include "ble_hs_priv.h"
#include "nimble/nimble/host/include/host/ble_hs_stop.h"
#include "nimble/nimble/include/nimble/nimble_npl.h"
#ifndef MYNEWT
#include "nimble/porting/nimble/include/nimble/nimble_port.h"
#endif

#define BLE_HOST_STOP_TIMEOUT_MS MYNEWT_VAL(BLE_HS_STOP_ON_SHUTDOWN_TIMEOUT)

static struct ble_gap_event_listener ble_hs_stop_gap_listener;

/**
 * List of stop listeners.  These are notified when a stop procedure completes.
 */
SLIST_HEAD(ble_hs_stop_listener_slist, ble_hs_stop_listener);
static struct ble_hs_stop_listener_slist ble_hs_stop_listeners;

/* Track number of connections */
static uint8_t ble_hs_stop_conn_cnt;

static struct ble_npl_callout ble_hs_stop_terminate_tmo;

static int
ble_hs_stop_hci_reset(void)
{
    return ble_hs_hci_cmd_tx(BLE_HCI_OP(BLE_HCI_OGF_CTLR_BASEBAND, BLE_HCI_OCF_CB_RESET),
                             NULL, 0, NULL, 0);
}

/**
 * Called when a stop procedure has completed.
 */
static void
ble_hs_stop_done(int status)
{
    struct ble_hs_stop_listener_slist slist;
    struct ble_hs_stop_listener *listener;

    ble_npl_callout_stop(&ble_hs_stop_terminate_tmo);

    ble_hs_lock();

    ble_gap_event_listener_unregister(&ble_hs_stop_gap_listener);

    slist = ble_hs_stop_listeners;
    SLIST_INIT(&ble_hs_stop_listeners);

    ble_hs_stop_hci_reset();

    ble_hs_enabled_state = BLE_HS_ENABLED_STATE_OFF;

    /* Clear advertising, scanning and connection states. */
    ble_gap_reset_state(0);

    /* After LL reset the controller loses its random address */
    ble_hs_id_reset();

    ble_hs_pvcy_reset();

    ble_hs_unlock();

    SLIST_FOREACH(listener, &slist, link) {
        listener->fn(status, listener->arg);
    }
}

#if MYNEWT_VAL(BLE_PERIODIC_ADV)
/**
 * Terminates all active periodic sync handles
 *
 * If there are no active periodic sync handles, signals completion of the
 * close procedure.
 */
static int
ble_hs_stop_terminate_all_periodic_sync(void)
{
    int rc = 0;
    struct ble_hs_periodic_sync *psync;
    uint16_t sync_handle;

    while((psync = ble_hs_periodic_sync_first())){
        /* Terminate sync command waits a command complete event, so there
         * is no need to wait for GAP event, as the calling thread will be
         * blocked on the hci semaphore until the command complete is received.
         *
         * Also, once the sync is terminated, the psync will be freed and
         * removed from the list such that the next call to
         * ble_hs_periodic_sync_first yields the next psync handle
         */
        sync_handle = psync->sync_handle;
        rc = ble_gap_periodic_adv_sync_terminate(sync_handle);
        if (rc != 0 && rc != BLE_HS_ENOTCONN) {
            BLE_HS_LOG(ERROR, "failed to terminate periodic sync=0x%04x, rc=%d\n",
                       sync_handle, rc);
            return rc;
        }
    }

    return 0;
}
#endif

/**
 * Terminates connection.
 */
static int
ble_hs_stop_terminate_conn(struct ble_hs_conn *conn, void *arg)
{
    int rc;

    rc = ble_gap_terminate_with_conn(conn, BLE_ERR_REM_USER_CONN_TERM);
    if (rc == 0 || rc == BLE_HS_EALREADY) {
        /* Terminate procedure successfully initiated.  Let the GAP event
         * handler deal with the result.
         */
        ble_hs_stop_conn_cnt++;
    } else {
        /* If failed, just make sure we are not going to wait for connection complete event,
         * just count it as already disconnected
         */
        BLE_HS_LOG(ERROR, "ble_hs_stop: failed to terminate connection; rc=%d\n", rc);
    }

    return 0;
}

/**
 * This is called when host graceful disconnect timeout fires. That means some devices
 * are out of range and disconnection completed did no happen yet.
 */
static void
ble_hs_stop_terminate_timeout_cb(struct ble_npl_event *ev)
{
    BLE_HS_LOG(ERROR, "ble_hs_stop_terminate_timeout_cb,"
                      "%d connection(s) still up \n", ble_hs_stop_conn_cnt);

    /* TODO: Shall we send error here? */
    ble_hs_stop_done(0);
}

/**
 * GAP event callback.  Listens for connection termination and then terminates
 * the next one.
 *
 * If there are no connections, signals completion of the stop procedure.
 */
static int
ble_hs_stop_gap_event(struct ble_gap_event *event, void *arg)
{
    /* Only process connection termination events. */
    if (event->type == BLE_GAP_EVENT_DISCONNECT ||
        event->type == BLE_GAP_EVENT_TERM_FAILURE) {

        ble_hs_stop_conn_cnt--;

        if (ble_hs_stop_conn_cnt == 0) {
            ble_hs_stop_done(0);
        }
    }

    return 0;
}

/**
 * Registers a listener to listen for completion of the current stop procedure.
 */
static void
ble_hs_stop_register_listener(struct ble_hs_stop_listener *listener,
                              ble_hs_stop_fn *fn, void *arg)
{
    BLE_HS_DBG_ASSERT(fn != NULL);

    listener->fn = fn;
    listener->arg = arg;
    SLIST_INSERT_HEAD(&ble_hs_stop_listeners, listener, link);
}

static int
ble_hs_stop_begin(struct ble_hs_stop_listener *listener,
                   ble_hs_stop_fn *fn, void *arg)
{
    switch (ble_hs_enabled_state) {
    case BLE_HS_ENABLED_STATE_ON:
        /* Host is enabled; proceed with the stop procedure. */
        ble_hs_enabled_state = BLE_HS_ENABLED_STATE_STOPPING;
        if (listener != NULL) {
            ble_hs_stop_register_listener(listener, fn, arg);
        }

        /* Put the host in the "stopping" state and ensure the host timer is
         * not running.
         */
        ble_hs_timer_resched();
        return 0;

    case BLE_HS_ENABLED_STATE_STOPPING:
        /* A stop procedure is already in progress.  Just listen for the
         * procedure's completion.
         */
        if (listener != NULL) {
            ble_hs_stop_register_listener(listener, fn, arg);
        }
        return BLE_HS_EBUSY;

    case BLE_HS_ENABLED_STATE_OFF:
        /* Host already stopped. */
        return BLE_HS_EALREADY;

    default:
        assert(0);
        return BLE_HS_EUNKNOWN;
    }
}

int
ble_hs_stop(struct ble_hs_stop_listener *listener,
            ble_hs_stop_fn *fn, void *arg)
{
    int rc;

    ble_hs_lock();
    rc = ble_hs_stop_begin(listener, fn, arg);
    ble_hs_unlock();

    switch (rc) {
    case 0:
        break;

    case BLE_HS_EBUSY:
        return 0;

    default:
        return rc;
    }

    /* Abort all active GAP procedures. */
    ble_gap_preempt();
    ble_gap_preempt_done();

#if MYNEWT_VAL(BLE_PERIODIC_ADV)
    /* Check for active periodic sync first and terminate it all */
    rc = ble_hs_stop_terminate_all_periodic_sync();
    if (rc != 0) {
        return rc;
    }
#endif

    rc = ble_gap_event_listener_register(&ble_hs_stop_gap_listener,
                                         ble_hs_stop_gap_event, NULL);
    if (rc != 0) {
        return rc;
    }

    ble_hs_stop_conn_cnt = 0;

    ble_hs_lock();
    ble_hs_conn_foreach(ble_hs_stop_terminate_conn, NULL);
    ble_hs_unlock();

    if (ble_hs_stop_conn_cnt > 0) {
        ble_npl_callout_reset(&ble_hs_stop_terminate_tmo,
                              ble_npl_time_ms_to_ticks32(BLE_HOST_STOP_TIMEOUT_MS));
    } else {
        /* No connections, stop is completed */
        ble_hs_stop_done(0);
    }

    return 0;
}

void
ble_hs_stop_init(void)
{
#ifdef MYNEWT
    ble_npl_callout_init(&ble_hs_stop_terminate_tmo, ble_npl_eventq_dflt_get(),
                         ble_hs_stop_terminate_timeout_cb, NULL);
#else
    ble_npl_callout_init(&ble_hs_stop_terminate_tmo, nimble_port_get_dflt_eventq(),
                         ble_hs_stop_terminate_timeout_cb, NULL);
#endif
}

void
ble_hs_stop_deinit(void)
{
    ble_npl_callout_deinit(&ble_hs_stop_terminate_tmo);
}
