/*
 * Copyright (C) 2019 Intel Corporation.  All rights reserved.
 * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 */

#include "runtime_timer.h"

#if 1
#define PRINT(...) (void)0
#else
#define PRINT printf
#endif

typedef struct _app_timer {
    struct _app_timer *next;
    uint32 id;
    uint32 interval;
    uint64 expiry;
    bool is_periodic;
} app_timer_t;

struct _timer_ctx {
    app_timer_t *app_timers;
    app_timer_t *idle_timers;
    app_timer_t *free_timers;
    uint32 max_timer_id;
    int pre_allocated;
    uint32 owner;

    /* mutex and condition */
    korp_cond cond;
    korp_mutex mutex;

    timer_callback_f timer_callback;
    check_timer_expiry_f refresh_checker;
};

uint64
bh_get_tick_ms()
{
    return os_time_get_boot_us() / 1000;
}

uint32
bh_get_elpased_ms(uint32 *last_system_clock)
{
    uint32 elpased_ms;
    /* attention: the bh_get_tick_ms() return 64 bits integer, but
       the bh_get_elpased_ms() is designed to use 32 bits clock count */
    uint32 now = (uint32)bh_get_tick_ms();

    /* system clock overrun */
    if (now < *last_system_clock) {
        PRINT("system clock overrun!\n");
        elpased_ms = now + (UINT32_MAX - *last_system_clock) + 1;
    }
    else {
        elpased_ms = now - *last_system_clock;
    }

    *last_system_clock = now;
    return elpased_ms;
}

static app_timer_t *
remove_timer_from(timer_ctx_t ctx, uint32 timer_id, bool active_list)
{
    app_timer_t **head, *prev, *t;

    os_mutex_lock(&ctx->mutex);

    if (active_list)
        head = &ctx->app_timers;
    else
        head = &ctx->idle_timers;

    t = *head;
    prev = NULL;

    while (t) {
        if (t->id == timer_id) {
            if (prev == NULL) {
                *head = t->next;
                PRINT("removed timer [%d] at head from list %d\n", t->id,
                      active_list);
            }
            else {
                prev->next = t->next;
                PRINT("removed timer [%d] after [%d] from list %d\n", t->id,
                      prev->id, active_list);
            }
            os_mutex_unlock(&ctx->mutex);

            if (active_list && prev == NULL && ctx->refresh_checker)
                ctx->refresh_checker(ctx);
            return t;
        }
        else {
            prev = t;
            t = t->next;
        }
    }

    os_mutex_unlock(&ctx->mutex);
    return NULL;
}

static app_timer_t *
remove_timer(timer_ctx_t ctx, uint32 timer_id, bool *active)
{
    app_timer_t *t = remove_timer_from(ctx, timer_id, true);

    if (t) {
        if (active)
            *active = true;
        return t;
    }

    if (active)
        *active = false;
    return remove_timer_from(ctx, timer_id, false);
}

static void
reschedule_timer(timer_ctx_t ctx, app_timer_t *timer)
{
    app_timer_t *t;
    app_timer_t *prev = NULL;

    os_mutex_lock(&ctx->mutex);

    t = ctx->app_timers;
    timer->next = NULL;
    timer->expiry = bh_get_tick_ms() + timer->interval;

    while (t) {
        if (timer->expiry < t->expiry) {
            if (prev == NULL) {
                timer->next = ctx->app_timers;
                ctx->app_timers = timer;
                PRINT("rescheduled timer [%d] at head\n", timer->id);
            }
            else {
                timer->next = t;
                prev->next = timer;
                PRINT("rescheduled timer [%d] after [%d]\n", timer->id,
                      prev->id);
            }

            goto out;
        }
        else {
            prev = t;
            t = t->next;
        }
    }

    if (prev) {
        /* insert to the list end */
        prev->next = timer;
        PRINT("rescheduled timer [%d] at end, after [%d]\n", timer->id,
              prev->id);
    }
    else {
        /* insert at the begin */
        bh_assert(ctx->app_timers == NULL);
        ctx->app_timers = timer;
        PRINT("rescheduled timer [%d] as first\n", timer->id);
    }

out:
    os_mutex_unlock(&ctx->mutex);

    /* ensure the refresh_checker() is called out of the lock */
    if (prev == NULL && ctx->refresh_checker)
        ctx->refresh_checker(ctx);
}

static void
release_timer(timer_ctx_t ctx, app_timer_t *t)
{
    if (ctx->pre_allocated) {
        os_mutex_lock(&ctx->mutex);
        t->next = ctx->free_timers;
        ctx->free_timers = t;
        PRINT("recycle timer :%d\n", t->id);
        os_mutex_unlock(&ctx->mutex);
    }
    else {
        PRINT("destroy timer :%d\n", t->id);
        BH_FREE(t);
    }
}

void
release_timer_list(app_timer_t **p_list)
{
    app_timer_t *t = *p_list;

    while (t) {
        app_timer_t *next = t->next;
        PRINT("destroy timer list:%d\n", t->id);
        BH_FREE(t);
        t = next;
    }

    *p_list = NULL;
}

/*
 * API exposed
 */

timer_ctx_t
create_timer_ctx(timer_callback_f timer_handler,
                 check_timer_expiry_f expiery_checker, int prealloc_num,
                 unsigned int owner)
{
    timer_ctx_t ctx = (timer_ctx_t)BH_MALLOC(sizeof(struct _timer_ctx));

    if (ctx == NULL)
        return NULL;

    memset(ctx, 0, sizeof(struct _timer_ctx));

    ctx->timer_callback = timer_handler;
    ctx->pre_allocated = prealloc_num;
    ctx->refresh_checker = expiery_checker;
    ctx->owner = owner;

    while (prealloc_num > 0) {
        app_timer_t *timer = (app_timer_t *)BH_MALLOC(sizeof(app_timer_t));

        if (timer == NULL)
            goto cleanup;

        memset(timer, 0, sizeof(*timer));
        timer->next = ctx->free_timers;
        ctx->free_timers = timer;
        prealloc_num--;
    }

    if (os_cond_init(&ctx->cond) != 0)
        goto cleanup;

    if (os_mutex_init(&ctx->mutex) != 0) {
        os_cond_destroy(&ctx->cond);
        goto cleanup;
    }

    PRINT("timer ctx created. pre-alloc: %d\n", ctx->pre_allocated);
    return ctx;

cleanup:
    if (ctx) {
        release_timer_list(&ctx->free_timers);
        BH_FREE(ctx);
    }
    PRINT("timer ctx create failed\n");
    return NULL;
}

void
destroy_timer_ctx(timer_ctx_t ctx)
{
    while (ctx->free_timers) {
        void *tmp = ctx->free_timers;
        ctx->free_timers = ctx->free_timers->next;
        BH_FREE(tmp);
    }

    cleanup_app_timers(ctx);

    os_cond_destroy(&ctx->cond);
    os_mutex_destroy(&ctx->mutex);
    BH_FREE(ctx);
}

unsigned int
timer_ctx_get_owner(timer_ctx_t ctx)
{
    return ctx->owner;
}

void
add_idle_timer(timer_ctx_t ctx, app_timer_t *timer)
{
    os_mutex_lock(&ctx->mutex);
    timer->next = ctx->idle_timers;
    ctx->idle_timers = timer;
    os_mutex_unlock(&ctx->mutex);
}

uint32
sys_create_timer(timer_ctx_t ctx, int interval, bool is_period, bool auto_start)
{
    app_timer_t *timer;

    if (ctx->pre_allocated) {
        if (ctx->free_timers == NULL) {
            return (uint32)-1;
        }
        else {
            timer = ctx->free_timers;
            ctx->free_timers = timer->next;
        }
    }
    else {
        timer = (app_timer_t *)BH_MALLOC(sizeof(app_timer_t));
        if (timer == NULL)
            return (uint32)-1;
    }

    memset(timer, 0, sizeof(*timer));

    ctx->max_timer_id++;
    if (ctx->max_timer_id == (uint32)-1)
        ctx->max_timer_id++;
    timer->id = ctx->max_timer_id;
    timer->interval = (uint32)interval;
    timer->is_periodic = is_period;

    if (auto_start)
        reschedule_timer(ctx, timer);
    else
        add_idle_timer(ctx, timer);

    return timer->id;
}

bool
sys_timer_cancel(timer_ctx_t ctx, uint32 timer_id)
{
    bool from_active;
    app_timer_t *t = remove_timer(ctx, timer_id, &from_active);

    if (t == NULL)
        return false;

    add_idle_timer(ctx, t);

    PRINT("sys_timer_stop called\n");
    return from_active;
}

bool
sys_timer_destroy(timer_ctx_t ctx, uint32 timer_id)
{
    bool from_active;
    app_timer_t *t = remove_timer(ctx, timer_id, &from_active);

    if (t == NULL)
        return false;

    release_timer(ctx, t);

    PRINT("sys_timer_destroy called\n");
    return true;
}

bool
sys_timer_restart(timer_ctx_t ctx, uint32 timer_id, int interval)
{
    app_timer_t *t = remove_timer(ctx, timer_id, NULL);

    if (t == NULL)
        return false;

    t->interval = (uint32)interval;

    reschedule_timer(ctx, t);

    PRINT("sys_timer_restart called\n");
    return true;
}

/*
 * API called by the timer manager from another thread or the kernel timer
 * handler
 */

/**
 * lookup the app queue by the module name
 * post a timeout message to the app queue
 */
static void
handle_expired_timers(timer_ctx_t ctx, app_timer_t *expired)
{
    while (expired) {
        app_timer_t *t = expired;
        ctx->timer_callback(t->id, ctx->owner);

        /* get next expired timer first, since the following
           operation may change expired->next */
        expired = expired->next;
        if (t->is_periodic) {
            /* if it is repeating, then reschedule it */
            reschedule_timer(ctx, t);
        }
        else {
            /* else move it to idle list */
            add_idle_timer(ctx, t);
        }
    }
}

uint32
get_expiry_ms(timer_ctx_t ctx)
{
    uint32 ms_to_next_expiry;
    uint64 now = bh_get_tick_ms();

    os_mutex_lock(&ctx->mutex);
    if (ctx->app_timers == NULL)
        ms_to_next_expiry = (uint32)-1;
    else if (ctx->app_timers->expiry >= now)
        ms_to_next_expiry = (uint32)(ctx->app_timers->expiry - now);
    else
        ms_to_next_expiry = 0;
    os_mutex_unlock(&ctx->mutex);

    return ms_to_next_expiry;
}

uint32
check_app_timers(timer_ctx_t ctx)
{
    app_timer_t *t, *expired = NULL, *expired_end = NULL;
    uint64 now = bh_get_tick_ms();

    os_mutex_lock(&ctx->mutex);

    t = ctx->app_timers;
    while (t) {
        if (now >= t->expiry) {
            ctx->app_timers = t->next;

            /* append t to the end of expired list */
            t->next = NULL;
            if (!expired_end) {
                expired = expired_end = t;
            }
            else {
                expired_end->next = t;
                expired_end = t;
            }

            t = ctx->app_timers;
        }
        else {
            break;
        }
    }
    os_mutex_unlock(&ctx->mutex);

    handle_expired_timers(ctx, expired);
    return get_expiry_ms(ctx);
}

void
cleanup_app_timers(timer_ctx_t ctx)
{
    os_mutex_lock(&ctx->mutex);

    release_timer_list(&ctx->app_timers);
    release_timer_list(&ctx->idle_timers);

    os_mutex_unlock(&ctx->mutex);
}