/*******************************************************************************
 * 
 * ttn-esp32 - The Things Network device library for ESP-IDF / SX127x
 * 
 * Copyright (c) 2018 Manuel Bleichenbacher
 * 
 * Licensed under MIT License
 * https://opensource.org/licenses/MIT
 *
 * Hardware abstraction layer to run LMIC on a ESP32 using ESP-iDF.
 *******************************************************************************/

#include "../lmic/lmic.h"
#include "../hal/hal_esp32.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "driver/timer.h"
#include "esp_log.h"

#define LMIC_UNUSED_PIN 0xff

static const char * const TAG = "ttn_hal";

lmic_pinmap lmic_pins;
HAL_ESP32 ttn_hal;


struct HALQueueItem {
    ostime_t time;
    HAL_Event ev;

    HALQueueItem() : time(0), ev(DIO0) { }
    HALQueueItem(HAL_Event e, ostime_t t = 0)
        : time(t), ev(e) { }
};

// -----------------------------------------------------------------------------
// Constructor

HAL_ESP32::HAL_ESP32()
    : nextTimerEvent(0x200000000)
{    
}

// -----------------------------------------------------------------------------
// I/O

void IRAM_ATTR HAL_ESP32::dioIrqHandler(void *arg)
{
    uint64_t now;
    timer_get_counter_value(TTN_TIMER_GROUP, TTN_TIMER, &now);
    BaseType_t higherPrioTaskWoken = pdFALSE;
    HALQueueItem item { (HAL_Event)(long)arg, (ostime_t)now };
    xQueueSendFromISR(ttn_hal.dioQueue, &item, &higherPrioTaskWoken);
    if (higherPrioTaskWoken)
        portYIELD_FROM_ISR();
}

void HAL_ESP32::ioInit()
{
    // NSS and DIO0 and DIO1 are required
    ASSERT(lmic_pins.nss != LMIC_UNUSED_PIN);
    ASSERT(lmic_pins.dio0 != LMIC_UNUSED_PIN);
    ASSERT(lmic_pins.dio1 != LMIC_UNUSED_PIN);

    gpio_pad_select_gpio(lmic_pins.nss);
    gpio_set_level((gpio_num_t)lmic_pins.nss, 0);
    gpio_set_direction((gpio_num_t)lmic_pins.nss, GPIO_MODE_OUTPUT);

    if (lmic_pins.rxtx != LMIC_UNUSED_PIN)
    {
        gpio_pad_select_gpio(lmic_pins.rxtx);
        gpio_set_level((gpio_num_t)lmic_pins.rxtx, 0);
        gpio_set_direction((gpio_num_t)lmic_pins.rxtx, GPIO_MODE_OUTPUT);
    }

    if (lmic_pins.rst != LMIC_UNUSED_PIN)
    {
        gpio_pad_select_gpio((gpio_num_t)lmic_pins.rst);
        gpio_set_level((gpio_num_t)lmic_pins.rst, 0);
        gpio_set_direction((gpio_num_t)lmic_pins.rst, GPIO_MODE_OUTPUT);
    }

    dioQueue = xQueueCreate(12, sizeof(HALQueueItem));
    ASSERT(dioQueue != NULL);

    gpio_pad_select_gpio(lmic_pins.dio0);
    gpio_set_direction((gpio_num_t)lmic_pins.dio0, GPIO_MODE_INPUT);
    gpio_set_intr_type((gpio_num_t)lmic_pins.dio0, GPIO_INTR_POSEDGE);
    gpio_isr_handler_add((gpio_num_t)lmic_pins.dio0, dioIrqHandler, (void *)0);

    gpio_pad_select_gpio((gpio_num_t)lmic_pins.dio1);
    gpio_set_direction((gpio_num_t)lmic_pins.dio1, GPIO_MODE_INPUT);
    gpio_set_intr_type((gpio_num_t)lmic_pins.dio1, GPIO_INTR_POSEDGE);
    gpio_isr_handler_add((gpio_num_t)lmic_pins.dio1, dioIrqHandler, (void *)1);

    ESP_LOGI(TAG, "IO initialized");
}

void hal_pin_rxtx(u1_t val)
{
    if (lmic_pins.rxtx == LMIC_UNUSED_PIN)
        return;
    
    gpio_set_level((gpio_num_t)lmic_pins.rxtx, val);
}

void hal_pin_rst(u1_t val)
{
    if (lmic_pins.rst == LMIC_UNUSED_PIN)
        return;

    if (val == 0 || val == 1)
    { // drive pin
        gpio_set_level((gpio_num_t)lmic_pins.rst, val);
        gpio_set_direction((gpio_num_t)lmic_pins.rst, GPIO_MODE_OUTPUT);
    }
    else
    { // keep pin floating
        gpio_set_level((gpio_num_t)lmic_pins.rst, val);
        gpio_set_direction((gpio_num_t)lmic_pins.rst, GPIO_MODE_INPUT);
    }
}

s1_t hal_getRssiCal (void)
{
    return lmic_pins.rssi_cal;
}

ostime_t hal_setModuleActive (bit_t val)
{
    return 0;
}

bit_t hal_queryUsingTcxo(void)
{
    return false;
}


// -----------------------------------------------------------------------------
// SPI

void HAL_ESP32::spiInit()
{
    // init device
    spi_device_interface_config_t spiConfig;
    memset(&spiConfig, 0, sizeof(spiConfig));
    spiConfig.mode = 1;
    spiConfig.clock_speed_hz = CONFIG_TTN_SPI_FREQ;
    spiConfig.command_bits = 0;
    spiConfig.address_bits = 8;
    spiConfig.spics_io_num = lmic_pins.nss;
    spiConfig.queue_size = 1;
    spiConfig.cs_ena_posttrans = 2;

    esp_err_t ret = spi_bus_add_device(lmic_pins.spi_host, &spiConfig, &spiHandle);
    ESP_ERROR_CHECK(ret);

    ESP_LOGI(TAG, "SPI initialized");
}

void hal_spi_write(u1_t cmd, const u1_t *buf, size_t len)
{
    ttn_hal.spiWrite(cmd, buf, len);
}

void HAL_ESP32::spiWrite(uint8_t cmd, const uint8_t *buf, size_t len)
{
    memset(&spiTransaction, 0, sizeof(spiTransaction));
    spiTransaction.addr = cmd;
    spiTransaction.length = 8 * len;
    spiTransaction.tx_buffer = buf;
    esp_err_t err = spi_device_transmit(spiHandle, &spiTransaction);
    ESP_ERROR_CHECK(err);
}

void hal_spi_read(u1_t cmd, u1_t *buf, size_t len)
{
    ttn_hal.spiRead(cmd, buf, len);
}

void HAL_ESP32::spiRead(uint8_t cmd, uint8_t *buf, size_t len)
{
    memset(buf, 0, len);
    memset(&spiTransaction, 0, sizeof(spiTransaction));
    spiTransaction.addr = cmd;
    spiTransaction.length = 8 * len;
    spiTransaction.rxlength = 8 * len;
    spiTransaction.tx_buffer = buf;
    spiTransaction.rx_buffer = buf;
    esp_err_t err = spi_device_transmit(spiHandle, &spiTransaction);
    ESP_ERROR_CHECK(err);
}

// -----------------------------------------------------------------------------
// TIME

/*
 * LIMIC uses a 32 bit time (ostime_t) counting ticks. In this implementation
 * each tick is 16µs. So the timer will wrap around once every 19 hour.
 * The timer alarm should trigger when a specific value has been reached.
 * Due to the wrap around, an alarm time in the future can have a lower value
 * than the current timer value.
 * 
 * ESP32 has 64 bits counters with a pecularity: the alarm does not only
 * trigger when the exact value has been reached but also when the clock is
 * higer than the alarm value. Therefore, the wrap around is more difficult to
 * handle.
 * 
 * The approach here is to always use a higher value than the current timer
 * value. If it would be lower than the timer value, 0x100000000 is added.
 * The lower 32 bits still represent the desired value. After the timer has
 * triggered an alarm and is higher than 0x100000000, it's value is reduced
 * by 0x100000000.
 */

static const ostime_t OVERRUN_TRESHOLD = 0x10000; // approx 10 seconds

void HAL_ESP32::timerInit()
{
    timer_config_t config = {
        .alarm_en = false,
        .counter_en = false,
        .intr_type = TIMER_INTR_LEVEL,
        .counter_dir = TIMER_COUNT_UP,
        .auto_reload = false,
        .divider = 1280  /* 80 MHz APB_CLK * 16µs */
    };
    timer_init(TTN_TIMER_GROUP, TTN_TIMER, &config);
    timer_set_counter_value(TTN_TIMER_GROUP, TTN_TIMER, 0x0);
    timer_isr_register(TTN_TIMER_GROUP, TTN_TIMER, timerIrqHandler, NULL, ESP_INTR_FLAG_IRAM, NULL);
    timer_start(TTN_TIMER_GROUP, TTN_TIMER);

    ESP_LOGI(TAG, "Timer initialized");
}

void HAL_ESP32::prepareNextAlarm(u4_t time)
{
    uint64_t now;
    timer_get_counter_value(TTN_TIMER_GROUP, TTN_TIMER, &now);
    u4_t now32 = (u4_t)now;

    if (now != now32)
    {
        // decrease timer to 32 bit value
        now = now32;
        timer_pause(TTN_TIMER_GROUP, TTN_TIMER);
        timer_set_counter_value(TTN_TIMER_GROUP, TTN_TIMER, now);
        timer_start(TTN_TIMER_GROUP, TTN_TIMER);
    }

    nextTimerEvent = time;
    if (now32 > time && now32 - time > OVERRUN_TRESHOLD)
        nextTimerEvent += 0x100000000;
}

void HAL_ESP32::armTimer()
{
    timer_set_alarm(TTN_TIMER_GROUP, TTN_TIMER, TIMER_ALARM_DIS);
    timer_set_alarm_value(TTN_TIMER_GROUP, TTN_TIMER, nextTimerEvent);
    timer_set_alarm(TTN_TIMER_GROUP, TTN_TIMER, TIMER_ALARM_EN);
}

void HAL_ESP32::disarmTimer()
{
    timer_set_alarm(TTN_TIMER_GROUP, TTN_TIMER, TIMER_ALARM_DIS);
    nextTimerEvent = 0x200000000; // wait indefinitely (almost)
}

void IRAM_ATTR HAL_ESP32::timerIrqHandler(void *arg)
{
    TTN_CLEAR_TIMER_ALARM;
    BaseType_t higherPrioTaskWoken = pdFALSE;
    HALQueueItem item { TIMER };
    xQueueSendFromISR(ttn_hal.dioQueue, &item, &higherPrioTaskWoken);
    if (higherPrioTaskWoken)
        portYIELD_FROM_ISR();
}

bool HAL_ESP32::wait(WaitKind waitKind)
{
    TickType_t ticksToWait = waitKind == CHECK_IO ? 0 : portMAX_DELAY;
    while (true)
    {
        HALQueueItem item;
        if (xQueueReceive(dioQueue, &item, ticksToWait) == pdFALSE)
            return false;

        if (item.ev == WAKEUP)
        {
            if (waitKind != WAIT_FOR_TIMER)
            {
                disarmTimer();
                return true;
            }
        }
        else if (item.ev == TIMER)
        {
            disarmTimer();
            if (waitKind != CHECK_IO)
                return true;
        }
        else // IO interrupt
        {
            if (waitKind != WAIT_FOR_TIMER)
                disarmTimer();
            enterCriticalSection();
            radio_irq_handler_v2(item.ev, item.time);
            leaveCriticalSection();
            if (waitKind != WAIT_FOR_TIMER)
                return true;
        }
    }
}

u4_t hal_ticks()
{
    uint64_t val;
    timer_get_counter_value(TTN_TIMER_GROUP, TTN_TIMER, &val);
    return (u4_t)val;
}

void hal_waitUntil(u4_t time)
{
    ttn_hal.waitUntil(time);
}

void HAL_ESP32::waitUntil(uint32_t time)
{
    prepareNextAlarm(time);
    armTimer();
    wait(WAIT_FOR_TIMER);
}

void HAL_ESP32::wakeUp()
{
    HALQueueItem item { WAKEUP };
    xQueueSend(dioQueue, &item, 0);
}

// check and rewind for target time
u1_t hal_checkTimer(u4_t time)
{
    return ttn_hal.checkTimer(time);
}

uint8_t HAL_ESP32::checkTimer(uint32_t time)
{
    uint64_t now;
    timer_get_counter_value(TTN_TIMER_GROUP, TTN_TIMER, &now);
    u4_t now32 = (u4_t)now;

    if (time >= now32)
    {
        if (time - now32 < 5)
            return 1; // timer will expire very soon
    }
    else
    {
        if (now32 - time < OVERRUN_TRESHOLD)
            return 1; // timer has expired recently
    }

    prepareNextAlarm(time);
    return 0;
}

void hal_sleep()
{
    ttn_hal.sleep();
}

void HAL_ESP32::sleep()
{
    if (wait(CHECK_IO))
        return;

    armTimer();
    wait(WAIT_FOR_ANY_EVENT);
}


// -----------------------------------------------------------------------------
// IRQ

void hal_disableIRQs()
{
    // nothing to do as interrupt handlers post message to queue
    // and don't access any shared data structures
}

void hal_enableIRQs()
{
    // nothing to do as interrupt handlers post message to queue
    // and don't access any shared data structures
}


// -----------------------------------------------------------------------------
// Synchronization between application code and background task

void HAL_ESP32::initCriticalSection()
{
    mutex = xSemaphoreCreateRecursiveMutex();
}

void HAL_ESP32::enterCriticalSection()
{
    xSemaphoreTakeRecursive(mutex, portMAX_DELAY);
}

void HAL_ESP32::leaveCriticalSection()
{
    xSemaphoreGiveRecursive(mutex);
}

// -----------------------------------------------------------------------------

void HAL_ESP32::backgroundTask(void* pvParameter) {
    os_runloop();
}

void hal_init_ex(const void *pContext)
{
    ttn_hal.init();
}

void HAL_ESP32::init()
{
    // configure radio I/O and interrupt handler
    ioInit();
    // configure radio SPI
    spiInit();
    // configure timer and interrupt handler
    timerInit();
}

void HAL_ESP32::startBackgroundTask() {
    xTaskCreate(backgroundTask, "ttn_lora_task", 1024 * 4, NULL, CONFIG_TTN_BG_TASK_PRIO, NULL);
}

void hal_failed(const char *file, u2_t line)
{
    ESP_LOGE(TAG, "%s:%d", file, line);
    ASSERT(0);
}

uint8_t hal_getTxPowerPolicy(u1_t inputPolicy, s1_t requestedPower, u4_t frequency) {
    return LMICHAL_radio_tx_power_policy_paboost;
}