#include <erl_driver.h>
#include <ei.h>
#include <ctype.h>
#include <string.h>

#include "sigar.h"
#include "sigar_fileinfo.h"
#include "sigar_format.h"
#include "sigar_ptql.h"

typedef struct {
    ErlDrvPort port;
    sigar_t *sigar;
} sigar_drv_t;

static ErlDrvData start(ErlDrvPort port, char *cmd) {
    sigar_drv_t *sd = (sigar_drv_t *)driver_alloc(sizeof(*sd));
    int status;

    status = sigar_open(&sd->sigar);
    if (status != SIGAR_OK) {
        sd->sigar = NULL;
        driver_failure_posix(port, status);
    }

    sd->port = port;

    return (ErlDrvData)sd;
}

static void stop(ErlDrvData handle) {
    sigar_drv_t *driver_data = (sigar_drv_t *)handle;
    if (driver_data->sigar) {
        sigar_close(driver_data->sigar);
    }
}

#ifdef SIGAR_64BIT
#define str2pid(value) strtoull(value, NULL, 10)
#else
#define str2pid(value) strtoul(value, NULL, 10)
#endif

static sigar_pid_t esigar_pid_get(sigar_t *sigar, char *pid)
{
    if (isdigit(*pid)) {
        return str2pid(pid);
    }
    else if ((*pid == '$') && (*(pid + 1) == '$')) {
        return sigar_pid_get(sigar);
    }
    else {
        /* XXX cache queries */
        sigar_ptql_query_t *query;
        sigar_ptql_error_t error;
        int status =
            sigar_ptql_query_create(&query, (char *)pid, &error);

        if (status == SIGAR_OK) {
            sigar_pid_t qpid;

            status = sigar_ptql_query_find_process(sigar, query, &qpid);
            sigar_ptql_query_destroy(query);
            if (status == SIGAR_OK) {
                return qpid;
            }
        }
    }
    return 0;
}

static void esigar_to_strlist(ei_x_buff *x,
                              char **data, unsigned long number)
{
    unsigned long i;

    ei_x_encode_list_header(x, number);
    for (i=0; i<number; i++) {
        ei_x_encode_string(x, data[i]);
    }
    ei_x_encode_empty_list(x);
}

typedef void (*esigar_encoder_func_t)(ei_x_buff *x, void *data);

static void esigar_to_list(ei_x_buff *x,
                           char *data, unsigned long number, int size,
                           esigar_encoder_func_t encoder)
{
    unsigned long i;

    ei_x_encode_list_header(x, number);
    for (i=0; i<number; i++, data += size) {
        encoder(x, data);
    }
    ei_x_encode_empty_list(x);
}

#define ESIGAR_NEW(x) \
    ei_x_new_with_version(x)

#define ESIGAR_OK(x) \
    ei_x_encode_tuple_header(x, 2); \
    ei_x_encode_atom(x, "ok")

#define ESIGAR_ERROR(x, sigar, status) \
    ei_x_encode_tuple_header(x, 2); \
    ei_x_encode_atom(x, "error"); \
    ei_x_encode_string(x, sigar_strerror(sigar, status))

#define ESIGAR_SEND(p, x) \
    driver_output(port, (x)->buff, (x)->index); \
    ei_x_free(x)

#define esigar_encode_long(x, k, v) \
    ei_x_encode_tuple_header(x, 2); \
    ei_x_encode_atom(x, k); \
    ei_x_encode_long(x, v)

#define esigar_encode_ulonglong(x, k, v) \
    ei_x_encode_tuple_header(x, 2); \
    ei_x_encode_atom(x, k); \
    ei_x_encode_ulonglong(x, v)

#define esigar_encode_char(x, k, v) \
    ei_x_encode_tuple_header(x, 2); \
    ei_x_encode_atom(x, k); \
    ei_x_encode_char(x, v)

#define esigar_encode_string(x, k, v) \
    ei_x_encode_tuple_header(x, 2); \
    ei_x_encode_atom(x, k); \
    ei_x_encode_string(x, v)

#define esigar_encode_double(x, k, v) \
    ei_x_encode_tuple_header(x, 2); \
    ei_x_encode_atom(x, k); \
    ei_x_encode_double(x, v)

static void esigar_encode_net_address(ei_x_buff *x, const char *key,
                                      sigar_net_address_t *address)
{
    char buf[SIGAR_INET6_ADDRSTRLEN];
    sigar_net_address_to_string(NULL, address, buf);
    esigar_encode_string(x, key, buf);
}

#define esigar_encode_netaddr(x, k, v) \
    esigar_encode_net_address(x, k, &v)

static void esigar_notimpl(ErlDrvPort port, sigar_t *sigar, int cmd)
{
    ei_x_buff x;

    ESIGAR_NEW(&x);
    ESIGAR_ERROR(&x, sigar, SIGAR_ENOTIMPL);
    ESIGAR_SEND(port, &x);
}

#include "../priv/gen/sigar_drv_gen.c"

static void esigar_loadavg_get(ErlDrvPort port, sigar_t *sigar)
{
    int status;
    ei_x_buff x;
    sigar_loadavg_t loadavg;

    ESIGAR_NEW(&x);

    if ((status = sigar_loadavg_get(sigar, &loadavg)) == SIGAR_OK) {
        ESIGAR_OK(&x);

        ei_x_encode_list_header(&x, 3);
        ei_x_encode_double(&x, loadavg.loadavg[0]);
        ei_x_encode_double(&x, loadavg.loadavg[1]);
        ei_x_encode_double(&x, loadavg.loadavg[2]);
        ei_x_encode_empty_list(&x);
    }
    else {
        ESIGAR_ERROR(&x, sigar, status);
    }

    ESIGAR_SEND(port, &x);
}

static void esigar_net_connection_list_get(ErlDrvPort port, sigar_t *sigar,
                                           unsigned int flags)
{
    int status;
    ei_x_buff x;
    sigar_net_connection_list_t list;

    ei_x_new_with_version(&x);

    if ((status = sigar_net_connection_list_get(sigar, &list, flags) == SIGAR_OK)) {
        ESIGAR_OK(&x);

        esigar_to_list(&x,
                       (char *)&list.data[0], list.number,
                       sizeof(*list.data),
                       (esigar_encoder_func_t)esigar_encode_net_connection);

        sigar_net_connection_list_destroy(sigar, &list);
    }
    else {
        ESIGAR_ERROR(&x, sigar, status);
    }

    ESIGAR_SEND(port, &x);
}

static void esigar_net_interface_list_get(ErlDrvPort port, sigar_t *sigar)
{
    int status;
    ei_x_buff x;
    sigar_net_interface_list_t list;

    ei_x_new_with_version(&x);

    if ((status = sigar_net_interface_list_get(sigar, &list) == SIGAR_OK)) {
        ESIGAR_OK(&x);

        esigar_to_strlist(&x, list.data, list.number);
        sigar_net_interface_list_destroy(sigar, &list);
    }
    else {
        ESIGAR_ERROR(&x, sigar, status);
    }

    ESIGAR_SEND(port, &x);
}

static void esigar_file_system_list_get(ErlDrvPort port, sigar_t *sigar)
{
    int status;
    ei_x_buff x;
    sigar_file_system_list_t list;

    ei_x_new_with_version(&x);

    if ((status = sigar_file_system_list_get(sigar, &list) == SIGAR_OK)) {
        ESIGAR_OK(&x);

        esigar_to_list(&x,
                       (char *)&list.data[0], list.number,
                       sizeof(*list.data),
                       (esigar_encoder_func_t)esigar_encode_file_system);

        sigar_file_system_list_destroy(sigar, &list);
    }
    else {
        ESIGAR_ERROR(&x, sigar, status);
    }

    ESIGAR_SEND(port, &x);
}

static void esigar_net_route_list_get(ErlDrvPort port, sigar_t *sigar)
{
    int status;
    ei_x_buff x;
    sigar_net_route_list_t list;

    ei_x_new_with_version(&x);

    if ((status = sigar_net_route_list_get(sigar, &list) == SIGAR_OK)) {
        ESIGAR_OK(&x);

        esigar_to_list(&x,
                       (char *)&list.data[0], list.number,
                       sizeof(*list.data),
                       (esigar_encoder_func_t)esigar_encode_net_route);

        sigar_net_route_list_destroy(sigar, &list);
    }
    else {
        ESIGAR_ERROR(&x, sigar, status);
    }

    ESIGAR_SEND(port, &x);
}

static void esigar_cpu_info_list_get(ErlDrvPort port, sigar_t *sigar)
{
    int status;
    ei_x_buff x;
    sigar_cpu_info_list_t list;

    ei_x_new_with_version(&x);

    if ((status = sigar_cpu_info_list_get(sigar, &list) == SIGAR_OK)) {
        ESIGAR_OK(&x);

        esigar_to_list(&x,
                       (char *)&list.data[0], list.number,
                       sizeof(*list.data),
                       (esigar_encoder_func_t)esigar_encode_cpu_info);

        sigar_cpu_info_list_destroy(sigar, &list);
    }
    else {
        ESIGAR_ERROR(&x, sigar, status);
    }

    ESIGAR_SEND(port, &x);
}

static void esigar_who_list_get(ErlDrvPort port, sigar_t *sigar)
{
    int status;
    ei_x_buff x;
    sigar_who_list_t list;

    ei_x_new_with_version(&x);

    if ((status = sigar_who_list_get(sigar, &list) == SIGAR_OK)) {
        ESIGAR_OK(&x);

        esigar_to_list(&x,
                       (char *)&list.data[0], list.number,
                       sizeof(*list.data),
                       (esigar_encoder_func_t)esigar_encode_who);

        sigar_who_list_destroy(sigar, &list);
    }
    else {
        ESIGAR_ERROR(&x, sigar, status);
    }

    ESIGAR_SEND(port, &x);
}

static void outputv(ErlDrvData handle, ErlIOVec *ev) {
    sigar_drv_t *sd = (sigar_drv_t *)handle;
    sigar_t *sigar = sd->sigar;
    ErlDrvPort port = sd->port;
    ErlDrvBinary *data = ev->binv[1];
    int cmd = data->orig_bytes[0];

    switch(cmd) {
    case ESIGAR_NET_CONNECTION_LIST:
        esigar_net_connection_list_get(port, sigar,
                                       data->orig_bytes[1]);
        break;
    case ESIGAR_NET_INTERFACE_LIST:
        esigar_net_interface_list_get(port, sigar);
        break;
    case ESIGAR_NET_ROUTE_LIST:
        esigar_net_route_list_get(port, sigar);
        break;
    case ESIGAR_FILE_SYSTEM_LIST:
        esigar_file_system_list_get(port, sigar);
        break;
    case ESIGAR_CPU_INFO_LIST:
        esigar_cpu_info_list_get(port, sigar);
        break;
    case ESIGAR_WHO_LIST:
        esigar_who_list_get(port, sigar);
        break;
    case ESIGAR_LOADAVG:
        esigar_loadavg_get(port, sigar);
        break;
    default:
        esigar_dispatch(port, sigar, cmd, &data->orig_bytes[1]);
        break;
    }
}

static ErlDrvEntry sigar_driver_entry = {
    NULL,                             /* init */
    start,                            /* startup */
    stop,                             /* shutdown */
    NULL,                             /* output */
    NULL,                             /* ready_input */
    NULL,                             /* ready_output */
    "sigar_drv",                      /* name of the driver */
    NULL,                             /* finish */
    NULL,                             /* handle */
    NULL,                             /* control */
    NULL,                             /* timeout */
    outputv,                          /* outputv */
    NULL,                             /* ready_async */
    NULL,                             /* flush */
    NULL,                             /* call */
    NULL,                             /* event */
    ERL_DRV_EXTENDED_MARKER,          /* ERL_DRV_EXTENDED_MARKER */
    ERL_DRV_EXTENDED_MAJOR_VERSION,   /* ERL_DRV_EXTENDED_MAJOR_VERSION */
    ERL_DRV_EXTENDED_MAJOR_VERSION,   /* ERL_DRV_EXTENDED_MINOR_VERSION */
    ERL_DRV_FLAG_USE_PORT_LOCKING     /* ERL_DRV_FLAGs */
};

DRIVER_INIT(sigar_driver) {
    return &sigar_driver_entry;
}