/*
 * 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 "sysinit/sysinit.h"
#include "syscfg/syscfg.h"
#include "ble_hs_priv.h"

static ble_npl_event_fn ble_hs_stop_term_event_cb;
static struct ble_npl_event ble_hs_stop_term_ev;

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;

/**
 * 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_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_enabled_state = BLE_HS_ENABLED_STATE_OFF;

    ble_hs_unlock();

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

/**
 * Terminates the first open connection.
 *
 * If there are no open connections, signals completion of the close procedure.
 */
static void
ble_hs_stop_terminate_next_conn(void)
{
    uint16_t handle;
    int rc;

    handle = ble_hs_atomic_first_conn_handle();
    if (handle == BLE_HS_CONN_HANDLE_NONE) {
        /* No open connections.  Signal completion of the stop procedure. */
        ble_hs_stop_done(0);
        return;
    }

    rc = ble_gap_terminate(handle, BLE_ERR_REM_USER_CONN_TERM);
    if (rc == 0) {
        /* Terminate procedure successfully initiated.  Let the GAP event
         * handler deal with the result.
         */
    } else {
        BLE_HS_LOG(ERROR,
            "ble_hs_stop: failed to terminate connection; rc=%d\n", rc);
        ble_hs_stop_done(rc);
    }
}

/**
 * Event handler.  Attempts to terminate the first open connection if there is
 * one.  All additional connections are terminated elsewhere in the GAP event
 * handler.
 *
 * If there are no connections, signals completion of the stop procedure.
 */
static void
ble_hs_stop_term_event_cb(struct ble_npl_event *ev)
{
    ble_hs_stop_terminate_next_conn();
}

/**
 * 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_terminate_next_conn();
    }

    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();

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

    /* Schedule termination of all open connections in the host task.  This is
     * done even if there are no open connections so that the result of the
     * stop procedure is signaled in a consistent manner (asynchronously).
     */
    ble_npl_eventq_put(ble_hs_evq_get(), &ble_hs_stop_term_ev);

    return 0;
}

void
ble_hs_stop_init(void)
{
    ble_npl_event_init(&ble_hs_stop_term_ev, ble_hs_stop_term_event_cb, NULL);
}
