#include "moar.h"

/* Info we convey about a timer. */
typedef struct {
    int timeout;
    int repeat;
    uv_timer_t *handle;
    MVMThreadContext *tc;
    int work_idx;
} TimerInfo;

/* Frees the timer's handle memory. */
static void free_timer(uv_handle_t *handle) {
    MVM_free(handle);
}

/* Timer callback; dispatches schedulee to the queue. */
static void timer_cb(uv_timer_t *handle) {
    TimerInfo        *ti = (TimerInfo *)handle->data;
    MVMThreadContext *tc = ti->tc;
    MVMAsyncTask     *t  = MVM_io_eventloop_get_active_work(tc, ti->work_idx);
    MVM_repr_push_o(tc, t->body.queue, t->body.schedulee);
    if (!ti->repeat && ti->work_idx >= 0) {
        /* The timer will only fire once. Having now fired, stop the callback,
         * clean up the handle, and remove the active work so that we will not
         * hold on to the callback and its associated memory. */
        uv_timer_stop(ti->handle);
        uv_close((uv_handle_t *)ti->handle, free_timer);
        MVM_io_eventloop_remove_active_work(tc, &(ti->work_idx));
    }
}

/* Sets the timer up on the event loop. */
static void setup(MVMThreadContext *tc, uv_loop_t *loop, MVMObject *async_task, void *data) {
    TimerInfo *ti = (TimerInfo *)data;
    ti->handle = MVM_malloc(sizeof(uv_timer_t));
    uv_timer_init(loop, ti->handle);
    ti->work_idx     = MVM_io_eventloop_add_active_work(tc, async_task);
    ti->tc           = tc;
    ti->handle->data = ti;
    uv_timer_start(ti->handle, timer_cb, ti->timeout, ti->repeat);
}

/* Stops the timer. */
static void cancel(MVMThreadContext *tc, uv_loop_t *loop, MVMObject *async_task, void *data) {
    TimerInfo *ti = (TimerInfo *)data;
    if (ti->work_idx >= 0) {
        uv_timer_stop(ti->handle);
        uv_close((uv_handle_t *)ti->handle, free_timer);
        MVM_io_eventloop_send_cancellation_notification(ti->tc,
            MVM_io_eventloop_get_active_work(tc, ti->work_idx));
        MVM_io_eventloop_remove_active_work(tc, &(ti->work_idx));
    }
}

/* Frees data associated with a timer async task. */
static void gc_free(MVMThreadContext *tc, MVMObject *t, void *data) {
    if (data)
        MVM_free(data);
}

/* Operations table for async timer task. */
static const MVMAsyncTaskOps op_table = {
    setup,
    NULL,
    cancel,
    NULL,
    gc_free
};

/* Creates a new timer. */
MVMObject * MVM_io_timer_create(MVMThreadContext *tc, MVMObject *queue,
                                MVMObject *schedulee, MVMint64 timeout,
                                MVMint64 repeat, MVMObject *async_type) {
    MVMAsyncTask *task;
    TimerInfo *timer_info;

    /* Validate REPRs. */
    if (REPR(queue)->ID != MVM_REPR_ID_ConcBlockingQueue)
        MVM_exception_throw_adhoc(tc,
            "timer target queue must have ConcBlockingQueue REPR");
    if (REPR(async_type)->ID != MVM_REPR_ID_MVMAsyncTask)
        MVM_exception_throw_adhoc(tc,
            "timer result type must have REPR AsyncTask");

    /* Create async task handle. */
    MVMROOT2(tc, queue, schedulee, {
        task = (MVMAsyncTask *)MVM_repr_alloc_init(tc, async_type);
    });
    MVM_ASSIGN_REF(tc, &(task->common.header), task->body.queue, queue);
    MVM_ASSIGN_REF(tc, &(task->common.header), task->body.schedulee, schedulee);
    task->body.ops      = &op_table;
    timer_info          = MVM_malloc(sizeof(TimerInfo));
    timer_info->timeout = timeout;
    timer_info->repeat  = repeat;
    task->body.data     = timer_info;

    /* Hand the task off to the event loop, which will set up the timer on the
     * event loop. */
    MVMROOT(tc, task, {
        MVM_io_eventloop_queue_work(tc, (MVMObject *)task);
    });

    return (MVMObject *)task;
}
