#include "common.h"
#include "sysvars.h"

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>

bool sysvar_print_updates;

#ifndef DEBUG
# ifdef ON_FPGA
#  define DEBUG 0
# else
#  define DEBUG sysvar_print_updates
# endif
#endif

static const char *sysvar_bool_tostr(sysvar_t from, char *buf)
{
    buf[0] = '0' + from.v_bool;
    buf[1] = '\0';
    return buf;
}

static bool sysvar_bool_fromstr(sysvar_t *to, const char *from)
{
    char c = from[0] | 0x20;
    switch (c) {
    case '1':
    case 't':
    case 'y':
    case 'j':
    case 's':
	to->v_bool = true;
	return true;
    case ' ':			/* Blank or null */
    case '0':
    case 'f':
    case 'n':
	to->v_bool = false;
	return true;
    default:
	return false;
    }
}

const struct sysvar_ops sysvar_bool_ops = {
    .tostr = sysvar_bool_tostr,
    .fromstr = sysvar_bool_fromstr,
    .buflen = BOOL_BUFLEN
};

static const char *sysvar_int_tostr(sysvar_t from, char *buf)
{
    snprintf(buf, INT_BUFLEN, "%ld", from.v_int);
    return buf;
}

static bool sysvar_int_fromstr(sysvar_t *to, const char *from)
{
    char *ep;
    long v = strtol(from, &ep, 0);
    if (ep == from || *ep)
	return false;
    to->v_int = v;
    return true;
}

const struct sysvar_ops sysvar_int_ops = {
    .tostr = sysvar_int_tostr,
    .fromstr = sysvar_int_fromstr,
    .buflen = INT_BUFLEN
};

static const char *sysvar_uint_tostr(sysvar_t from, char *buf)
{
    snprintf(buf, UINT_BUFLEN, "%lu", from.v_uint);
    return buf;
}

static bool sysvar_uint_fromstr(sysvar_t *to, const char *from)
{
    char *ep;
    unsigned long v = strtoul(from, &ep, 0);
    if (ep == from || *ep)
	return false;
    to->v_uint = v;
    return true;
}

const struct sysvar_ops sysvar_uint_ops = {
    .tostr = sysvar_uint_tostr,
    .fromstr = sysvar_uint_fromstr,
    .buflen = UINT_BUFLEN
};

static const char *sysvar_str_tostr(sysvar_t from, char *buf)
{
    (void)buf;
    return from.v_str;
}

static bool sysvar_str_fromstr(sysvar_t *to, const char *from)
{
    char *ns;

    if (!from) {
	ns = NULL;
    } else {
	ns = strdup(from);
	if (!ns)
	    return false;
    }
    to->v_str = ns;
    return true;
}

static bool sysvar_str_set(sysvar_t *to, sysvar_t from)
{
    return sysvar_str_fromstr(to, from.v_str);
}

static size_t sysvar_str_datasize(sysvar_t val)
{
    return (val.v_str ? strlen(val.v_str) : 0) + 1;
}

const struct sysvar_ops sysvar_str_ops = {
    .set = sysvar_str_set,
    .tostr = sysvar_str_tostr,
    .fromstr = sysvar_str_fromstr,
    .datasize = sysvar_str_datasize
};

static void sysvar_tz_update(sysvar_t val, bool isset)
{
    if (isset)
	setenv("TZ", val.v_tz, 1);
    else
	unsetenv("TZ");
    tzset();
}

const struct sysvar_ops sysvar_tz_ops = {
    .set = sysvar_str_set,
    .tostr = sysvar_str_tostr,
    .fromstr = sysvar_str_fromstr,
    .update = sysvar_tz_update,
    .datasize = sysvar_str_datasize
};

static const char *sysvar_ip_tostr(sysvar_t from, char *buf)
{
    union ip_bytes {
	uint8_t b[4];
	uint32_t l;
    } ip;

    ip.l = from.v_ip;
    snprintf(buf, IP_BUFLEN,
	     "%u.%u.%u.%u", ip.b[0], ip.b[1], ip.b[2], ip.b[3]);
    return buf;
}

static bool sysvar_ip_fromstr(sysvar_t *to, const char *str)
{
    union ip_bytes {
	uint8_t b[4];
	uint32_t l;
    } ip;

    ip.l = 0;
    for (int i = 0; i < 4; i++) {
	char *ep;
	unsigned long v = strtoul(str, &ep, 10);
	if (ep == str || *ep != (i == 3) ? '\0' : '.' || v > 255)
	    return false;
	str = ep + 1;
	ip.b[i] = v;
    }

    to->v_ip = ip.l;
    return true;
}

const struct sysvar_ops sysvar_ip_ops = {
    .tostr = sysvar_ip_tostr,
    .fromstr = sysvar_ip_fromstr,
    .buflen = IP_BUFLEN
};

static bool sysvar_mac_set(sysvar_t *to, sysvar_t from)
{
    uint8_t *buf;

    if (!from.v_mac) {
	buf = NULL;
    } else {
	buf = malloc(6);
	if (!buf)
	    return false;

	memcpy(buf, from.v_mac, 6);
    }
    to->v_mac = buf;
    return true;
}

static const char *sysvar_mac_tostr(sysvar_t from, char *buf)
{
    const uint8_t *m = from.v_mac;
    if (!m)
	return NULL;

    snprintf(buf, MAC_BUFLEN, "%02x:%02x:%02x:%02x:%02x:%02x",
	     m[0], m[1], m[2], m[3], m[4], m[5]);
    return buf;
}

static bool sysvar_mac_fromstr(sysvar_t *to, const char *str)
{
    sysvar_t from;
    uint8_t m[6];

    if (!str) {
	from.v_mac = NULL;
    } else {
	for (int i = 0; i < 6; i++) {
	    char *ep;
	    unsigned long v = strtoul(str, &ep, 16);
	    if (ep == str || *ep != (i == 5) ? '\0' : ':' || v > 255)
		return false;
	    str = ep + 1;
	    m[i] = v;
	}

	from.v_mac = m;
    }

    return sysvar_mac_set(to, from);
}

static size_t sysvar_mac_datasize(sysvar_t val)
{
    (void)val;
    return 6;
}

const struct sysvar_ops sysvar_mac_ops = {
    .set = sysvar_mac_set,
    .tostr = sysvar_mac_tostr,
    .fromstr = sysvar_mac_fromstr,
    .buflen = MAC_BUFLEN,
    .datasize = sysvar_mac_datasize
};

/* --- Generic getters/setters --- */

/* Contains the lowest numbered sysvar changed; or sysvar_count if nothing */
enum sysvar_enum sysvar_changed;

sysvar_t getvar(size_t var)
{
    if (var >= (size_t)sysvar_count)
	return sysvar_defval[sysvar_null];

    return sysvar_val[var];
}

static bool do_setvar(size_t var, sysvar_t val, bool is_set)
{
    const struct sysvar_ops *type = sysvar_types[var];
    sysvar_t *to = &sysvar_val[var];
    void *free_ptr = NULL;

    if (type->datasize)
	free_ptr = to->v_ptr;

    if (DEBUG) {
	char tostr_buf[SYSVAR_BUFLEN];
	printf("%ssetvar %zu %s <- %s\n", is_set ? "" : "un",
	       var, sysvar_name[var],
	       notempty(type->tostr(val, tostr_buf)));
    }

    if (!type->set || (type->datasize && !val.v_ptr)) {
	sysvar_val[var] = val;
    } else {
	if (!type->set(to, val))
	    return false;
    }

    if (free_ptr)
	free(free_ptr);

    if (var < (size_t)sysvar_changed)
	sysvar_changed = (enum sysvar_enum)var;

    sysvar_isset[var] = is_set;
    if (type->update)
	type->update(*to, is_set);

    return true;
}

bool setvar(size_t var, sysvar_t val)
{
    if (var >= (size_t)sysvar_count)
	return false;

    return do_setvar(var, val, true);
}

bool unsetvar(size_t var)
{
    if (var >= (size_t)sysvar_count)
	return false;

    return do_setvar(var, sysvar_defval[var], false);
}

/* --- Getters/setters converting to/from strings --- */

const char *getvar_tostr(size_t var)
{
    static char buf[SYSVAR_BUFLEN];
    return getvar_tostr_r(var, buf);
}

const char *getvar_tostr_r(size_t var, char *buf)
{
    if (var >= (size_t)sysvar_count)
	return NULL;

    const struct sysvar_ops *type = sysvar_types[var];

    /* A tostr method is required */
    return type->tostr(sysvar_val[var], buf);
}

bool setvar_fromstr(size_t var, const char *str)
{
    if (var >= (size_t)sysvar_count)
	return NULL;

    if (!str)
	return unsetvar(var);

    const struct sysvar_ops *type = sysvar_types[var];
    sysvar_t *to = &sysvar_val[var];
    void *free_ptr = NULL;

    if (type->datasize)
	free_ptr = to->v_ptr;

    if (DEBUG) {
	printf("setvar_fromstr %zu %s <- %s\n", var, sysvar_name[var], str);
    }

    /* A fromstr method is required */
    if (!type->fromstr(to, str))
	return false;

    if (free_ptr)
	free(free_ptr);

    if (var < (size_t)sysvar_changed)
	sysvar_changed = (enum sysvar_enum)var;

    sysvar_isset[var] = true;
    if (type->update)
	type->update(*to, true);

    return true;
}

/* --- Find the index of a specific variable --- */

static int string_ptr_compare(const void *a, const void *b)
{
    const char * const *aa = a;
    const char * const *bb = b;
    return strcmp(*aa, *bb);
}

size_t sysvar_find(size_t ns, const char *name)
{
    if (ns >= (size_t)sysvar_nscount)
	return 0;

    const sysvar_ns_t *nsi = &sysvar_ns[ns];
    const char * const *varname;
    const size_t count = nsi[1].first - nsi->first;

    varname = bsearch(&name, sysvar_name + nsi->first,
		      count, sizeof *sysvar_name,
		      string_ptr_compare);
    if (!varname)
	return 0;

    return varname - sysvar_name;
}

/* --- Initialization/reset to defaults --- */

void sysvar_reset(size_t ns)
{
    if (ns >= (size_t)sysvar_nscount)
	return;

    bool print_updates = sysvar_print_updates;

    enum sysvar_enum i;
    for (i = sysvar_ns[ns].first; i < sysvar_ns[ns+1].first; i++)
	unsetvar(i);

    sysvar_print_updates = print_updates;

    if (sysvar_changed < i)
	sysvar_changed = i;
}

void sysvar_init(void)
{
    sysvar_print_updates = false;

    for (enum sysvar_enum i = sysvar_null+1; i < sysvar_count; i++)
	unsetvar(i);

    sysvar_changed = sysvar_count;
}
