#include "moar.h"

/* Info we convey about a file watcher. */
typedef struct {
    char             *path;
    uv_fs_event_t     handle;
    MVMThreadContext *tc;
    int               work_idx;
} WatchInfo;

static void on_changed(uv_fs_event_t *handle, const char *filename, int events, int status) {
    WatchInfo        *wi  = (WatchInfo *)handle->data;
    MVMThreadContext *tc  = wi->tc;
    MVMObject        *arr = MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTArray);
    MVMAsyncTask     *t   = MVM_io_eventloop_get_active_work(tc, wi->work_idx);
    MVM_repr_push_o(tc, arr, t->body.schedulee);
    MVMROOT2(tc, t, arr) {
        MVMObject *filename_boxed;
        MVMObject *rename_boxed;
        if (filename) {
            MVMString *filename_str = MVM_string_utf8_c8_decode(tc,
                tc->instance->VMString, filename, strlen(filename));
            filename_boxed = MVM_repr_box_str(tc,
                tc->instance->boot_types.BOOTStr,
                filename_str);
        }
        else {
            filename_boxed = tc->instance->boot_types.BOOTStr;
        }
        MVM_repr_push_o(tc, arr, filename_boxed);
        rename_boxed = MVM_repr_box_int(tc,
            tc->instance->boot_types.BOOTInt,
            events == UV_RENAME ? 1 : 0);
        MVM_repr_push_o(tc, arr, rename_boxed);
        MVM_repr_push_o(tc, arr, tc->instance->boot_types.BOOTStr);
    }
    MVM_repr_push_o(tc, t->body.queue, arr);
}

/* Sets the signal handler up on the event loop. */
static void setup(MVMThreadContext *tc, uv_loop_t *loop, MVMObject *async_task, void *data) {
    WatchInfo *wi = (WatchInfo *)data;
    int        r;

    /* Add task to active list. */
    wi->work_idx    = MVM_io_eventloop_add_active_work(tc, async_task);
    wi->tc          = tc;
    wi->handle.data = wi;

    /* Start watching. */
    uv_fs_event_init(loop, &wi->handle);
    if ((r = uv_fs_event_start(&wi->handle, on_changed, wi->path, 0)) != 0) {
        /* Error; need to notify. */
        MVMROOT(tc, async_task) {
            MVMObject    *arr = MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTArray);
            MVM_repr_push_o(tc, arr, ((MVMAsyncTask *)async_task)->body.schedulee);
            MVM_repr_push_o(tc, arr, tc->instance->boot_types.BOOTStr);
            MVM_repr_push_o(tc, arr, tc->instance->boot_types.BOOTInt);
            MVMROOT(tc, arr) {
                MVMString *msg_str = MVM_string_ascii_decode_nt(tc,
                    tc->instance->VMString, uv_strerror(r));
                MVMObject *msg_box = MVM_repr_box_str(tc,
                    tc->instance->boot_types.BOOTStr, msg_str);
                MVM_repr_push_o(tc, arr, msg_box);
            }
            MVM_repr_push_o(tc, ((MVMAsyncTask *)async_task)->body.queue, arr);
        }
    }
}

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

static void cancel(MVMThreadContext *tc, uv_loop_t *loop, MVMObject *t, void *data) {
    WatchInfo *wi = (WatchInfo *)data;
    uv_fs_event_stop(&wi->handle);
}

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

MVMObject * MVM_io_file_watch(MVMThreadContext *tc, MVMObject *queue,
                              MVMObject *schedulee, MVMString *path,
                              MVMObject *async_type) {
    MVMAsyncTask *task;
    WatchInfo    *watch_info;

    /* Encode path. */
    char *c_path = MVM_string_utf8_c8_encode_C_string(tc, path);

    /* Validate REPRs. */
    if (REPR(queue)->ID != MVM_REPR_ID_ConcBlockingQueue) {
        MVM_free(c_path);
        MVM_exception_throw_adhoc(tc,
            "file watch target queue must have ConcBlockingQueue REPR");
    }
    if (REPR(async_type)->ID != MVM_REPR_ID_MVMAsyncTask) {
        MVM_free(c_path);
        MVM_exception_throw_adhoc(tc,
            "file watch 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;
    watch_info       = MVM_malloc(sizeof(WatchInfo));
    watch_info->path = c_path;
    task->body.data  = watch_info;

    /* Hand the task off to the event loop. */
    MVMROOT(tc, task) {
        MVM_io_eventloop_queue_work(tc, (MVMObject *)task);
    }

    return (MVMObject *)task;
}
