#include "epqueue.h"
#include "epqueue_item.h"
#include "epqueue_nif.h"
#include "priority_queue.h"
#include "critical_section.h"
#include "nif_utils.h"
#include "macros.h"

#include <string.h>

struct epqueue
{
    CriticalSection* crit;
    PriorityQueue* queue;
    ERL_NIF_TERM owner_pid;
};

void nif_epqueue_free(ErlNifEnv* env, void* obj)
{
    UNUSED(env);
    epqueue* qinst = static_cast<epqueue*>(obj);

    if(qinst->queue)
        delete qinst->queue;

    if(qinst->crit)
        delete qinst->crit;
}

bool internal_insert(epqueue* q, queue_item* item)
{
    CritScope ss(q->crit);
    return q->queue->insert(item);
}

bool internal_remove(epqueue* q, queue_item* item)
{
    CritScope ss(q->crit);

    if(item->heap_index == -1)
        return false;

    return q->queue->remove(item, item->heap_index);
}

queue_item* internal_pop(epqueue* q)
{
    CritScope ss(q->crit);
    return static_cast<queue_item*>(q->queue->pop());
}

bool is_owner(ErlNifEnv* env, epqueue* q)
{
    if(q->owner_pid == 0)
        return true;

    ErlNifPid self;

    if(enif_self(env, &self) && !enif_is_identical(q->owner_pid, enif_make_pid(env, &self)))
        return false;

    return true;
}

ERL_NIF_TERM nif_epqueue_new(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    UNUSED(argc);

    epqueue_data* data = static_cast<epqueue_data*>(enif_priv_data(env));

    if(!enif_is_list(env, argv[0]))
        return enif_make_badarg(env);

    bool use_lock = false;

    ERL_NIF_TERM settings_list = argv[0];
    ERL_NIF_TERM head;

    while(enif_get_list_cell(env, settings_list, &head, &settings_list))
    {
        const ERL_NIF_TERM *items;
        int arity;

        if(!enif_get_tuple(env, head, &arity, &items) || arity != 2)
            return enif_make_badarg(env);

        if(enif_is_identical(items[0], ATOMS.atomGlobalLock))
            use_lock = enif_is_identical(items[1], ATOMS.atomTrue);
        else
            return enif_make_badarg(env);
    }

    epqueue* qinst = static_cast<epqueue*>(enif_alloc_resource(data->resPQueueInstance, sizeof(epqueue)));

    if(qinst == NULL)
        return make_error(env, "enif_alloc_resource failed");

    memset(qinst, 0, sizeof(epqueue));

    if(use_lock)
    {
        qinst->crit = new EnifCriticalSection();
        qinst->owner_pid = 0;
    }
    else
    {
        qinst->crit = new NullCriticalSection();
        ErlNifPid self;
        enif_self(env, &self);
        qinst->owner_pid = enif_make_pid(env, &self);
    }

    qinst->queue = new PriorityQueue(epqueue_item_less, epqueue_item_update_pos, enif_release_resource);

    ERL_NIF_TERM term = enif_make_resource(env, qinst);
    enif_release_resource(qinst);
    return enif_make_tuple2(env, ATOMS.atomOk, term);
}

ERL_NIF_TERM nif_epqueue_size(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    UNUSED(argc);

    epqueue_data* data = static_cast<epqueue_data*>(enif_priv_data(env));

    epqueue* inst = NULL;

    if(!enif_get_resource(env, argv[0], data->resPQueueInstance, reinterpret_cast<void**>(&inst)))
        return enif_make_badarg(env);

    if(!is_owner(env, inst))
        return enif_make_badarg(env);

    CritScope ss(inst->crit);
    return enif_make_int(env, inst->queue->size());
}

ERL_NIF_TERM nif_epqueue_insert(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    UNUSED(argc);

    epqueue_data* data = static_cast<epqueue_data*>(enif_priv_data(env));

    epqueue* inst;
    ErlNifBinary data_bin;
    long priority;

    if(!enif_get_resource(env, argv[0], data->resPQueueInstance, reinterpret_cast<void**>(&inst)))
        return enif_make_badarg(env);

    if(!is_owner(env, inst))
        return enif_make_badarg(env);

    if(!enif_get_long(env, argv[2], &priority))
        return enif_make_badarg(env);

    if(!enif_term_to_binary(env, argv[1], &data_bin))
        return enif_make_badarg(env);

    queue_item* item = epqueue_item_new(data, data_bin, priority);

    if(item == NULL)
    {
        enif_release_binary(&data_bin);
        return make_error(env, "failed to allocate a new item");
    }

    if(!internal_insert(inst, item))
    {
        enif_release_resource(&item);
        return make_error(env, "failed to insert new item in the queue");
    }

    ERL_NIF_TERM ref = enif_make_resource(env, item);
    return enif_make_tuple2(env, ATOMS.atomOk, ref);
}

ERL_NIF_TERM nif_epqueue_remove(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    UNUSED(argc);

    epqueue_data* data = static_cast<epqueue_data*>(enif_priv_data(env));
    epqueue* inst = NULL;
    queue_item* item = NULL;

    if(!enif_get_resource(env, argv[0], data->resPQueueInstance, reinterpret_cast<void**>(&inst)))
        return enif_make_badarg(env);

    if(!is_owner(env, inst))
        return enif_make_badarg(env);

    if(!enif_get_resource(env, argv[1], data->resPQueueItem, reinterpret_cast<void**>(&item)))
        return enif_make_badarg(env);

    if(!internal_remove(inst, item))
        return ATOMS.atomFalse;

    enif_release_resource(&item);
    return ATOMS.atomTrue;
}

ERL_NIF_TERM nif_epqueue_pop(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    UNUSED(argc);

    epqueue_data* data = static_cast<epqueue_data*>(enif_priv_data(env));
    epqueue* inst = NULL;

    if(!enif_get_resource(env, argv[0], data->resPQueueInstance, reinterpret_cast<void**>(&inst)))
        return enif_make_badarg(env);

    if(!is_owner(env, inst))
        return enif_make_badarg(env);

    queue_item* item = internal_pop(inst);

    if(item == NULL)
        return ATOMS.atomUndefined;

    ERL_NIF_TERM bin_term;

    if(enif_binary_to_term(env, item->data.data, item->data.size, &bin_term, 0) == 0)
    {
        enif_release_resource(item);
        return make_error(env, "failed to decode data");
    }

    enif_release_resource(item);
    return enif_make_tuple3(env, ATOMS.atomOk, bin_term, enif_make_long(env, item->priority));
}

ERL_NIF_TERM nif_epqueue_peek(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
    UNUSED(argc);

    epqueue_data* data = static_cast<epqueue_data*>(enif_priv_data(env));
    epqueue* inst = NULL;

    if(!enif_get_resource(env, argv[0], data->resPQueueInstance, reinterpret_cast<void**>(&inst)))
        return enif_make_badarg(env);

    if(!is_owner(env, inst))
        return enif_make_badarg(env);

    CritScope ss(inst->crit);
    queue_item* item = static_cast<queue_item*>(inst->queue->peek());

    if(item == NULL)
        return ATOMS.atomUndefined;

    ERL_NIF_TERM bin_term;

    if(enif_binary_to_term(env, item->data.data, item->data.size, &bin_term, 0) == 0)
        return make_error(env, "failed to decode data");

    return enif_make_tuple3(env, ATOMS.atomOk, bin_term, enif_make_int64(env, item->priority));
}
