// Copyright 2018-2019 Espressif Systems (Shanghai) PTE LTD
//
// Licensed 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 "esp_timer.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "driver/soc.h"

#define ESP_TIMER_HZ CONFIG_FREERTOS_HZ

typedef enum {
    ESP_TIMER_INIT = 0,
    ESP_TIMER_ONCE,
    ESP_TIMER_CYCLE,
    ESP_TIMER_STOP,
    ESP_TIMER_DELETE,
} esp_timer_state_t;

struct esp_timer {
    TimerHandle_t       os_timer;

    esp_timer_cb_t      cb;

    void                *arg;

    esp_timer_state_t   state;
};

static const char *TAG = "esp_timer";

static esp_err_t delete_timer(esp_timer_handle_t timer)
{
    BaseType_t ret = xTimerDelete(timer->os_timer, portMAX_DELAY);
    if (ret == pdPASS)
        heap_caps_free(timer);

    return ret == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
}

/**
 * @brief FreeRTOS callback function
 */
static void esp_timer_callback(TimerHandle_t xTimer)
{
    struct esp_timer *timer = (struct esp_timer *)pvTimerGetTimerID(xTimer);

    timer->cb(timer->arg);

    switch (timer->state) {
        case ESP_TIMER_INIT:
        case ESP_TIMER_STOP:
            break;
        case ESP_TIMER_CYCLE: {
            BaseType_t ret = xTimerReset(timer->os_timer, portMAX_DELAY);
            if (ret != pdPASS) {
                ESP_LOGE(TAG, "start timer at callback error");
            } else {
                ESP_LOGD(TAG, "start timer at callback OK");
            }
            break;
        }
        case ESP_TIMER_ONCE: {
            BaseType_t ret = xTimerStop(timer->os_timer, portMAX_DELAY);
            if (ret != pdPASS) {
                ESP_LOGE(TAG, "stop timer at callback error");
            } else {
                timer->state = ESP_TIMER_STOP;
                ESP_LOGD(TAG, "stop timer at callback OK");
            }
            break;
        }
        case ESP_TIMER_DELETE: {
            esp_err_t ret = delete_timer(timer);
            if (ret != ESP_OK) {
                ESP_LOGE(TAG, "delete timer at callback error");
            } else {
                ESP_LOGD(TAG, "delete timer at callback OK");
            }
            break;
        }
        default:
            ESP_LOGE(TAG, "timer state error is %d", timer->state);
            break;
    }
}

/**
 * @brief Initialize esp_timer library
 */
esp_err_t esp_timer_init(void)
{
    return ESP_OK;
}

/**
 * @brief De-initialize esp_timer library
 */
esp_err_t esp_timer_deinit(void)
{
    return ESP_OK;
}

/**
 * @brief Create an esp_timer instance
 */
esp_err_t esp_timer_create(const esp_timer_create_args_t* create_args,
                           esp_timer_handle_t* out_handle)
{
    assert(create_args);
    assert(out_handle);

    TimerHandle_t os_timer;
    esp_timer_handle_t esp_timer;

    esp_timer = heap_caps_malloc(sizeof(struct esp_timer), MALLOC_CAP_32BIT);
    if (!esp_timer)
        return ESP_ERR_NO_MEM;

    os_timer = xTimerCreate(create_args->name,
                            portMAX_DELAY,
                            pdFALSE,
                            esp_timer,
                            esp_timer_callback);
    if (os_timer) {
        esp_timer->os_timer = os_timer;
        esp_timer->cb = create_args->callback;
        esp_timer->arg = create_args->arg;
        esp_timer->state = ESP_TIMER_INIT;
        *out_handle = (esp_timer_handle_t)esp_timer;
    } else {
        heap_caps_free(esp_timer);
        return ESP_ERR_NO_MEM;
    }

    return ESP_OK;
}

/**
 * @brief Start one-shot timer
 */
esp_err_t esp_timer_start_once(esp_timer_handle_t timer, uint64_t timeout_us)
{
    assert(timer);

    TimerHandle_t os_timer = timer->os_timer;
    BaseType_t os_ret;
    uint64_t last_us = timeout_us % (1000 * (1000 / ESP_TIMER_HZ));
    uint32_t ticks = timeout_us / (1000 * (1000 / ESP_TIMER_HZ));

    if (last_us || !ticks)
        return ESP_ERR_INVALID_ARG;

    os_ret = xTimerChangePeriod(os_timer, ticks, portMAX_DELAY);
    if (os_ret == pdPASS) {
        timer->state = ESP_TIMER_ONCE;
    } else {
        return ESP_ERR_INVALID_STATE;
    }

    return ESP_OK;
}

/**
 * @brief Start a periodic timer
 */
esp_err_t esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period)
{
    assert(timer);

    TimerHandle_t os_timer = timer->os_timer;
    BaseType_t os_ret;
    uint64_t last_us = period % (1000 * (1000 / ESP_TIMER_HZ));
    uint32_t ticks = period / (1000 * (1000 / ESP_TIMER_HZ));

    if (last_us || !ticks)
        return ESP_ERR_INVALID_ARG;

    os_ret = xTimerChangePeriod(os_timer, ticks, portMAX_DELAY);
    if (os_ret == pdPASS) {
        timer->state = ESP_TIMER_CYCLE;
    } else {
        return ESP_ERR_INVALID_STATE;
    }

    return ESP_OK;
}

/**
 * @brief Stop the timer
 */
esp_err_t esp_timer_stop(esp_timer_handle_t timer)
{
    assert(timer);

    TimerHandle_t os_timer = timer->os_timer;
    BaseType_t os_ret;

    os_ret = xTimerStop(os_timer, portMAX_DELAY);
    if (os_ret == pdPASS)
        timer->state = ESP_TIMER_STOP;

    return os_ret == pdPASS ? ESP_OK : ESP_ERR_INVALID_STATE;   
}

/**
 * @brief Delete an esp_timer instance
 */
esp_err_t esp_timer_delete(esp_timer_handle_t timer)
{
    esp_err_t ret;

    assert(timer);

    if (xTimerGetTimerDaemonTaskHandle() == xTaskGetCurrentTaskHandle()) {
        timer->state = ESP_TIMER_DELETE;
        ret = ESP_OK;
    } else {
        UBaseType_t prio = uxTaskPriorityGet(NULL);
        if (prio >= configTIMER_TASK_PRIORITY)
            vTaskPrioritySet(NULL, configTIMER_TASK_PRIORITY - 1);
        else
            prio = 0;

        ret = delete_timer(timer);

        if (prio)
            vTaskPrioritySet(NULL, prio);
    }

    return ret;
}

int64_t esp_timer_get_time(void)
{
    extern uint64_t g_esp_os_us;

    return (int64_t)(g_esp_os_us + soc_get_ccount() / g_esp_ticks_per_us);
}