/*
 * 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 <stdint.h>
#include <stddef.h>
#include <assert.h>
#include "syscfg/syscfg.h"
#include "controller/ble_phy.h"
#include "controller/ble_ll.h"
#include "controller/ble_ll_sched.h"
#include "controller/ble_ll_rfmgmt.h"
#include "controller/ble_ll_tmr.h"
#include "ble_ll_priv.h"

#if MYNEWT_VAL(BLE_LL_RFMGMT_ENABLE_TIME) > 0

enum ble_ll_rfmgmt_state {
    RFMGMT_STATE_OFF = 0,
    RFMGMT_STATE_ENABLING = 1,
    RFMGMT_STATE_ENABLED = 2,
};

struct ble_ll_rfmgmt_data {
    enum ble_ll_rfmgmt_state state;
    uint16_t ticks_to_enabled;

    struct ble_ll_tmr timer;
    bool timer_scheduled;
    uint32_t timer_scheduled_at;

    bool enable_scan;
    bool enable_sched;
    uint32_t enable_scan_at;
    uint32_t enable_sched_at;

    uint32_t enabled_at;

    struct ble_npl_event release_ev;
};

static struct ble_ll_rfmgmt_data g_ble_ll_rfmgmt_data;

static void
ble_ll_rfmgmt_enable(void)
{
    OS_ASSERT_CRITICAL();

    if (g_ble_ll_rfmgmt_data.state == RFMGMT_STATE_OFF) {
        g_ble_ll_rfmgmt_data.state = RFMGMT_STATE_ENABLING;
        g_ble_ll_rfmgmt_data.enabled_at = ble_ll_tmr_get();
        ble_phy_rfclk_enable();
        BLE_LL_DEBUG_GPIO(RFMGMT, 1);
    }
}

static void
ble_ll_rfmgmt_disable(void)
{
    OS_ASSERT_CRITICAL();

    if (g_ble_ll_rfmgmt_data.state != RFMGMT_STATE_OFF) {
        BLE_LL_DEBUG_GPIO(RFMGMT, 0);
        ble_phy_rfclk_disable();
        g_ble_ll_rfmgmt_data.state = RFMGMT_STATE_OFF;
    }
}

static void
ble_ll_rfmgmt_timer_reschedule(void)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;
    uint32_t enable_at;

    /* Figure out when we need to enable RF */
    if (rfmgmt->enable_scan && rfmgmt->enable_sched) {
        if (CPUTIME_LT(rfmgmt->enable_scan_at, rfmgmt->enable_sched_at)) {
            enable_at = rfmgmt->enable_scan_at;
        } else {
            enable_at = rfmgmt->enable_sched_at;
        }
    } else if (rfmgmt->enable_scan) {
        enable_at = rfmgmt->enable_scan_at;
    } else if (rfmgmt->enable_sched) {
        enable_at = rfmgmt->enable_sched_at;
    } else {
        rfmgmt->timer_scheduled = false;
        ble_ll_tmr_stop(&rfmgmt->timer);
        return;
    }

    if (rfmgmt->timer_scheduled) {
        /*
         * If there is timer already scheduled at the same time we do not need
         * to do anything. Otherwise we need to stop timer and schedule it again
         * regardless if it's earlier or later to make sure it fires at the time
         * something expects it.
         */

        if (rfmgmt->timer_scheduled_at == enable_at) {
            return;
        }

        rfmgmt->timer_scheduled = false;
        ble_ll_tmr_stop(&rfmgmt->timer);
    }

    /*
     * In case timer was requested to be enabled before current time, just make
     * sure it's enabled and assume caller can deal with this. This will happen
     * if something is scheduled "now" since "enable_at" is in the past, but in
     * such case it's absolutely harmless since we already have clock enabled
     * and this will do nothing.
     */
    if (CPUTIME_LEQ(enable_at, ble_ll_tmr_get())) {
        ble_ll_rfmgmt_enable();
        return;
    }

    rfmgmt->timer_scheduled = true;
    rfmgmt->timer_scheduled_at = enable_at;
    ble_ll_tmr_start(&rfmgmt->timer, enable_at);
}

static void
ble_ll_rfmgmt_timer_exp(void *arg)
{
    g_ble_ll_rfmgmt_data.timer_scheduled = false;
    ble_ll_rfmgmt_enable();
}

static void
ble_ll_rfmgmt_release_ev(struct ble_npl_event *ev)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;
    uint32_t now;
    bool can_disable;
    uint8_t lls;
    os_sr_t sr;

    OS_ENTER_CRITICAL(sr);

    now = ble_ll_tmr_get();

    can_disable = true;
    lls = ble_ll_state_get();

    if (rfmgmt->enable_scan && CPUTIME_GEQ(now, rfmgmt->enable_scan_at)) {
        /* Blocked by scan */
        can_disable = false;
    } else if (rfmgmt->enable_sched && CPUTIME_GEQ(now, rfmgmt->enable_sched_at)) {
        /* Blocked by scheduler item */
        can_disable = false;
    } else if (lls != BLE_LL_STATE_STANDBY) {
        /* Blocked by LL state */
        can_disable = false;
    }

    if (can_disable) {
        ble_ll_rfmgmt_disable();
    }

    OS_EXIT_CRITICAL(sr);
}

static uint32_t
ble_ll_rfmgmt_ticks_to_enabled(void)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;
    uint32_t rem_ticks;
    uint32_t now;

    switch (rfmgmt->state) {
    case RFMGMT_STATE_OFF:
        rem_ticks = rfmgmt->ticks_to_enabled;
        break;
    case RFMGMT_STATE_ENABLING:
        now = ble_ll_tmr_get();
        if (CPUTIME_LT(now, rfmgmt->enabled_at + rfmgmt->ticks_to_enabled)) {
            rem_ticks = rfmgmt->enabled_at + rfmgmt->ticks_to_enabled - now;
            break;
        }
        rfmgmt->state = RFMGMT_STATE_ENABLED;
        /* no break */
    case RFMGMT_STATE_ENABLED:
        rem_ticks = 0;
        break;
    default:
        BLE_LL_ASSERT(0);
        rem_ticks = 0;
        break;
    }

    return rem_ticks;
}

void
ble_ll_rfmgmt_init(void)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;

    BLE_LL_DEBUG_GPIO_INIT(RFMGMT);

    rfmgmt->state = RFMGMT_STATE_OFF;

    rfmgmt->ticks_to_enabled =
            ble_ll_tmr_u2t_up(MYNEWT_VAL(BLE_LL_RFMGMT_ENABLE_TIME));

    rfmgmt->timer_scheduled = false;
    ble_ll_tmr_init(&rfmgmt->timer, ble_ll_rfmgmt_timer_exp, NULL);

    ble_npl_event_init(&rfmgmt->release_ev, ble_ll_rfmgmt_release_ev, NULL);
}

void
ble_ll_rfmgmt_reset(void)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;

    rfmgmt->timer_scheduled = false;
    rfmgmt->timer_scheduled_at = 0;
    ble_ll_tmr_stop(&rfmgmt->timer);

    ble_ll_event_remove(&rfmgmt->release_ev);

    ble_ll_rfmgmt_disable();

    rfmgmt->enable_scan = false;
    rfmgmt->enable_scan_at = 0;
    rfmgmt->enable_sched = false;
    rfmgmt->enable_sched_at = 0;

    rfmgmt->enabled_at = 0;
}

void
ble_ll_rfmgmt_scan_changed(bool enabled, uint32_t next_window)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;
    os_sr_t sr;

    OS_ENTER_CRITICAL(sr);

    rfmgmt->enable_scan = enabled;
    rfmgmt->enable_scan_at = next_window - rfmgmt->ticks_to_enabled;

    ble_ll_rfmgmt_timer_reschedule();

    OS_EXIT_CRITICAL(sr);
}

void
ble_ll_rfmgmt_sched_changed(struct ble_ll_sched_item *first)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;
    os_sr_t sr;

    OS_ENTER_CRITICAL(sr);

    rfmgmt->enable_sched = (first != NULL);
    if (first) {
        rfmgmt->enable_sched_at = first->start_time - rfmgmt->ticks_to_enabled;
    }

    ble_ll_rfmgmt_timer_reschedule();

    OS_EXIT_CRITICAL(sr);
}

void
ble_ll_rfmgmt_release(void)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;
    os_sr_t sr;

    OS_ENTER_CRITICAL(sr);

    ble_ll_event_remove(&rfmgmt->release_ev);

    if (g_ble_ll_rfmgmt_data.state != RFMGMT_STATE_OFF) {
        ble_ll_event_add(&rfmgmt->release_ev);
    }

    OS_EXIT_CRITICAL(sr);
}

uint32_t
ble_ll_rfmgmt_enable_now(void)
{
    struct ble_ll_rfmgmt_data *rfmgmt = &g_ble_ll_rfmgmt_data;
    uint32_t enabled_at;
    os_sr_t sr;

    OS_ENTER_CRITICAL(sr);

    ble_ll_rfmgmt_enable();

    if (rfmgmt->state == RFMGMT_STATE_ENABLED) {
        enabled_at = ble_ll_tmr_get();
    } else {
        enabled_at = rfmgmt->enabled_at + rfmgmt->ticks_to_enabled + 1;
    }

    OS_EXIT_CRITICAL(sr);

    return enabled_at;
}

bool
ble_ll_rfmgmt_is_enabled(void)
{
    bool ret;

    OS_ASSERT_CRITICAL();

    ret = ble_ll_rfmgmt_ticks_to_enabled() == 0;

    return ret;
}

#else

void
ble_ll_rfmgmt_init(void)
{
    static bool enabled = false;

    if (!enabled) {
        ble_phy_rfclk_enable();
    }

    enabled = true;
}

#endif
