/*
 * Copyright (c) 2022 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdio.h>

#include "pico/cyw43_arch.h"
#include "pico/mutex.h"
#include "pico/sem.h"

#include "hardware/gpio.h"
#include "hardware/irq.h"

#include "cyw43_stats.h"

#if CYW43_LWIP
#include <lwip/init.h>
#include "lwip/timeouts.h"
#endif

// note same code
#if PICO_CYW43_ARCH_THREADSAFE_BACKGROUND

#if PICO_CYW43_ARCH_THREADSAFE_BACKGROUND && CYW43_LWIP && !NO_SYS
#error PICO_CYW43_ARCH_THREADSAFE_BACKGROUND requires lwIP NO_SYS=1
#endif
#if PICO_CYW43_ARCH_THREADSAFE_BACKGROUND && CYW43_LWIP && MEM_LIBC_MALLOC
#error MEM_LIBC_MALLOC is incompatible with PICO_CYW43_ARCH_THREADSAFE_BACKGROUND
#endif
// todo right now we are now always doing a cyw43_dispatch along with a lwip one when hopping cores in low_prio_irq_schedule_dispatch

#ifndef CYW43_SLEEP_CHECK_MS
#define CYW43_SLEEP_CHECK_MS 50 // How often to run lwip callback
#endif
static alarm_id_t periodic_alarm = -1;

static inline uint recursive_mutex_enter_count(recursive_mutex_t *mutex) {
    return mutex->enter_count;
}

static inline lock_owner_id_t recursive_mutex_owner(recursive_mutex_t *mutex) {
    return mutex->owner;
}

#define CYW43_GPIO_IRQ_HANDLER_PRIORITY 0x40

enum {
    CYW43_DISPATCH_SLOT_CYW43 = 0,
    CYW43_DISPATCH_SLOT_ADAPTER,
    CYW43_DISPATCH_SLOT_ENUM_COUNT
};
#ifndef CYW43_DISPATCH_SLOT_COUNT
#define CYW43_DISPATCH_SLOT_COUNT CYW43_DISPATCH_SLOT_ENUM_COUNT
#endif

typedef void (*low_prio_irq_dispatch_t)(void);
static void low_prio_irq_schedule_dispatch(size_t slot, low_prio_irq_dispatch_t f);

static uint8_t cyw43_core_num;
#ifndef NDEBUG
static bool in_low_priority_irq;
#endif
static uint8_t low_priority_irq_num;
static bool low_priority_irq_missed;
static low_prio_irq_dispatch_t low_priority_irq_dispatch_slots[CYW43_DISPATCH_SLOT_COUNT];
static recursive_mutex_t cyw43_mutex;
semaphore_t cyw43_irq_sem;

// Called in low priority pendsv interrupt only to do lwip processing and check cyw43 sleep
static void periodic_worker(void)
{
#if CYW43_USE_STATS
    static uint32_t counter;
    if (counter++ % (30000 / LWIP_SYS_CHECK_MS) == 0) {
        cyw43_dump_stats();
    }
#endif

    CYW43_STAT_INC(LWIP_RUN_COUNT);
#if CYW43_LWIP
    sys_check_timeouts();
#endif
    if (cyw43_poll) {
        if (cyw43_sleep > 0) {
            if (--cyw43_sleep == 0) {
                low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
            }
        }
    }
}

// Regular callback to get lwip to check for timeouts
static int64_t periodic_alarm_handler(__unused alarm_id_t id, __unused void *user_data)
{
    // Do lwip processing in low priority pendsv interrupt
    low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_ADAPTER, periodic_worker);
    return CYW43_SLEEP_CHECK_MS * 1000;
}

void cyw43_await_background_or_timeout_us(uint32_t timeout_us) {
    // if we are called from within an IRQ, then don't wait (we are only ever called in a polling loop)
    if (!__get_current_exception()) {
        sem_acquire_timeout_us(&cyw43_irq_sem, timeout_us);
    }
}

// GPIO interrupt handler to tell us there's cyw43 has work to do
static void gpio_irq_handler(void)
{
    uint32_t events = gpio_get_irq_event_mask(CYW43_PIN_WL_HOST_WAKE);
    if (events & GPIO_IRQ_LEVEL_HIGH) {
        // As we use a high level interrupt, it will go off forever until it's serviced
        // So disable the interrupt until this is done. It's re-enabled again by CYW43_POST_POLL_HOOK
        // which is called at the end of cyw43_poll_func
        gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
        // also clear the force bit which we use to progratically cause this handler to fire (on the right core)
        io_irq_ctrl_hw_t *irq_ctrl_base = get_core_num() ?
                                          &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
        hw_clear_bits(&irq_ctrl_base->intf[CYW43_PIN_WL_HOST_WAKE/8], GPIO_IRQ_LEVEL_HIGH << (4 * (CYW43_PIN_WL_HOST_WAKE & 7)));
        low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
        CYW43_STAT_INC(IRQ_COUNT);
    }
}

// Low priority interrupt handler to perform background processing
static void low_priority_irq_handler(void) {
    assert(cyw43_core_num == get_core_num());
    if (recursive_mutex_try_enter(&cyw43_mutex, NULL)) {
        if (recursive_mutex_enter_count(&cyw43_mutex) != 1) {
            low_priority_irq_missed = true;
            CYW43_STAT_INC(PENDSV_DISABLED_COUNT);
        } else {
            CYW43_STAT_INC(PENDSV_RUN_COUNT);
#ifndef NDEBUG
            in_low_priority_irq = true;
#endif
            for (size_t i = 0; i < count_of(low_priority_irq_dispatch_slots); i++) {
                if (low_priority_irq_dispatch_slots[i] != NULL) {
                    low_prio_irq_dispatch_t f = low_priority_irq_dispatch_slots[i];
                    low_priority_irq_dispatch_slots[i] = NULL;
                    f();
                }
            }
#ifndef NDEBUG
            in_low_priority_irq = false;
#endif
        }
        recursive_mutex_exit(&cyw43_mutex);
    } else {
        CYW43_STAT_INC(PENDSV_DISABLED_COUNT);
        low_priority_irq_missed = true;
    }
    sem_release(&cyw43_irq_sem);
}

static bool low_prio_irq_init(uint8_t priority) {
    assert(get_core_num() == cyw43_core_num);
    int irq = user_irq_claim_unused(false);
    if (irq < 0) return false;
    low_priority_irq_num = (uint8_t) irq;
    irq_set_exclusive_handler(low_priority_irq_num, low_priority_irq_handler);
    irq_set_enabled(low_priority_irq_num, true);
    irq_set_priority(low_priority_irq_num, priority);
    return true;
}

static void low_prio_irq_deinit(void) {
    if (low_priority_irq_num > 0) {
        irq_set_enabled(low_priority_irq_num, false);
        irq_remove_handler(low_priority_irq_num, low_priority_irq_handler);
        user_irq_unclaim(low_priority_irq_num);
        low_priority_irq_num = 0;
    }
}

int cyw43_arch_init(void) {
    cyw43_core_num = get_core_num();
    recursive_mutex_init(&cyw43_mutex);
    cyw43_init(&cyw43_state);
    sem_init(&cyw43_irq_sem, 0, 1);

    // Start regular lwip callback to handle timeouts
    periodic_alarm = add_alarm_in_us(CYW43_SLEEP_CHECK_MS * 1000, periodic_alarm_handler, NULL, true);
    if (periodic_alarm < 0) {
        return PICO_ERROR_GENERIC;
    }

    gpio_add_raw_irq_handler_with_order_priority(IO_IRQ_BANK0, gpio_irq_handler, CYW43_GPIO_IRQ_HANDLER_PRIORITY);
    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
    irq_set_enabled(IO_IRQ_BANK0, true);

#if CYW43_LWIP
    lwip_init();
#endif
    // start low priority handler (no background work is done before this)
    bool ok = low_prio_irq_init(PICO_LOWEST_IRQ_PRIORITY);
    if (!ok) {
        cyw43_arch_deinit();
        return PICO_ERROR_GENERIC;
    }
    return PICO_OK;
}

void cyw43_arch_deinit(void) {
    if (periodic_alarm >= 0) {
        cancel_alarm(periodic_alarm);
        periodic_alarm = -1;
    }
    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, false);
    gpio_remove_raw_irq_handler(IO_IRQ_BANK0, gpio_irq_handler);
    low_prio_irq_deinit();
}

void cyw43_post_poll_hook(void) {
    gpio_set_irq_enabled(CYW43_PIN_WL_HOST_WAKE, GPIO_IRQ_LEVEL_HIGH, true);
}

// This is called in the gpio and low_prio_irq interrupts and on either core
static void low_prio_irq_schedule_dispatch(size_t slot, low_prio_irq_dispatch_t f) {
    assert(slot < count_of(low_priority_irq_dispatch_slots));
    low_priority_irq_dispatch_slots[slot] = f;
    if (cyw43_core_num == get_core_num()) {
        //on same core, can dispatch directly
        irq_set_pending(low_priority_irq_num);
    } else {
        // on wrong core, so force via GPIO IRQ which itself calls this method for the CYW43 slot.
        // since the CYW43 slot always uses the same function, this is fine with the addition of an
        // extra (but harmless) CYW43 slot call when another SLOT is invoked.
        // We could do better, but would have to track why the IRQ was called.
        io_irq_ctrl_hw_t *irq_ctrl_base = cyw43_core_num ?
                                          &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
        hw_set_bits(&irq_ctrl_base->intf[CYW43_PIN_WL_HOST_WAKE/8], GPIO_IRQ_LEVEL_HIGH << (4 * (CYW43_PIN_WL_HOST_WAKE & 7)));
    }
}

void cyw43_schedule_internal_poll_dispatch(void (*func)(void)) {
    low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, func);
}

// Prevent background processing in pensv and access by the other core
// These methods are called in pensv context and on either core
// They can be called recursively
void cyw43_thread_enter(void) {
    // Lock the other core and stop low_prio_irq running
    recursive_mutex_enter_blocking(&cyw43_mutex);
}

#ifndef NDEBUG
void cyw43_thread_lock_check(void) {
    // Lock the other core and stop low_prio_irq running
    if (recursive_mutex_enter_count(&cyw43_mutex) < 1 || recursive_mutex_owner(&cyw43_mutex) != lock_get_caller_owner_id()) {
        panic("cyw43_thread_lock_check failed");
    }
}
#endif

// Re-enable background processing
void cyw43_thread_exit(void) {
    // Run low_prio_irq if needed
    if (1 == recursive_mutex_enter_count(&cyw43_mutex)) {
        // note the outer release of the mutex is not via cyw43_exit in the low_priority_irq case (it is a direct mutex exit)
        assert(!in_low_priority_irq);
//        if (low_priority_irq_missed) {
//            low_priority_irq_missed = false;
            if (low_priority_irq_dispatch_slots[CYW43_DISPATCH_SLOT_CYW43]) {
                low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
            }
//        }
    }
    recursive_mutex_exit(&cyw43_mutex);
}


static void cyw43_delay_until(absolute_time_t until) {
    // sleep can be called in IRQs, so there's not much we can do there
    if (__get_current_exception()) {
        busy_wait_until(until);
    } else {
        sleep_until(until);
    }
}

void cyw43_delay_ms(uint32_t ms) {
    cyw43_delay_until(make_timeout_time_ms(ms));
}

void cyw43_delay_us(uint32_t us) {
    cyw43_delay_until(make_timeout_time_us(us));
}

void cyw43_arch_poll() {
    // should not be necessary
//    if (cyw43_poll) {
//        low_prio_irq_schedule_dispatch(CYW43_DISPATCH_SLOT_CYW43, cyw43_poll);
//    }
}

#if !CYW43_LWIP
static void no_lwip_fail() {
    panic("You cannot use IP with pico_cyw43_arch_none");
}
void cyw43_cb_tcpip_init(cyw43_t *self, int itf) {
}
void cyw43_cb_tcpip_deinit(cyw43_t *self, int itf) {
}
void cyw43_cb_tcpip_set_link_up(cyw43_t *self, int itf) {
    no_lwip_fail();
}
void cyw43_cb_tcpip_set_link_down(cyw43_t *self, int itf) {
    no_lwip_fail();
}
void cyw43_cb_process_ethernet(void *cb_data, int itf, size_t len, const uint8_t *buf) {
    no_lwip_fail();
}
#endif

#endif