#include "common.h"
#include "config.h"

#include <esp_spiffs.h>
#include <ctype.h>

#define CONFIG_FILE	"/spiffs/config.txt"

struct env_var {
    const char *var, *val;
};
static const struct env_var default_config[] = {
    {"LANG","sv"},
    {"TZ", "CET-1CEST,M3.5.0,M10.5.0/3"}, /* Sweden */
    {"tzname", "Europe/Stockholm"},
    {"hostname", "max80"},
    {"mdns.enabled", "1"},
    {"sntp.enabled", "1"},
    {"sntp.server","time.max80.abc80.org"},
    {"abc.hosttype","auto"}
};

static int save_config(void);

static bool config_changed;
int setenv_config(const char *name, const char *value)
{
    config_changed = true;
    if (name[0] == '-') {
	name++;
	value = NULL;
    }
    return setenv_cond(name, value);
}

int setenv_cond(const char *name, const char *value)
{
    const char *pfx;
    size_t skip;

    if (!strncmp("status.", name, 7)) {
	pfx = "STATUS";
	skip = 7;
    } else {
	pfx = "CONFIG";
	skip = 0;
    }
    logmsg(pfx, "%s <- %s\n", name+skip, value ? value : "(deleted)");

    if (value)
      return setenv(name, value, 1);
    else
      return unsetenv(name);
}

static void reset_config(void)
{
    while (1) {
	char **envp;

	for (envp = environ; *envp; envp++) {
	    if (!strncmp("status.", *envp, 7))
		continue;
	    else
		break;
	}

	if (!*envp)
	    break;

	const char *eq = strchr(*envp, '=');
	if (!eq)
	    continue;		/* This should never happen... */

	char ename[eq - *envp + 1];
	memcpy(ename, *envp, eq - *envp);
	ename[eq - *envp] = '\0';

	setenv_cond(ename, NULL);
    }

    size_t i;
    for (i = 0; i < ARRAY_SIZE(default_config); i++)
	setenv_cond(default_config[i].var, default_config[i].val);

    config_changed = true;
}

static bool is_end_of_string(int c)
{
    return c <= 0 || c == '\n' || c == '\r';
}

/*
 * Note: the string must be mutable; use strdup() if necessary.
 * The "separator" allows the string to be separated into multiple
 * arguments; no other decoding is done.
 */
static int set_config_string(char *str, unsigned int separator)
{
    char *p, *q;
    unsigned char c;

    p = str;
    do {
	const char *var = p;

	do {
	    c = *p++;
	} while (isalnum(c) || c == '.' || c == '-');
	if (c != '=')
	    return -EINVAL; /* Invalid config line (blank, comment...) */

	p[-1] = '\0';

	q = p;

	do {
	    c = *q++;
	} while (!is_end_of_string(c) && c != separator);

	/* Overlong line */
	if (q >= str + MAX_CONFIG_LINE)
	    return -EOVERFLOW;

	q[-1] = '\0';
	setenv_config(var, p);

	p = q;
    } while (c == separator);

    return 0;
}

static void finish_config_update(bool save)
{
    if (config_changed) {
	if (save)
	    save_config();
	tzset();
	config_changed = false;
    }
}

/*
 * At the moment "url" just allows values to be separated by semicolons;
 * no other decoding is done, and '&' is not supported.
 */
int set_config_url_string(const char *str)
{
    char *wstr = strdup(str);

    if (!wstr)
	return -ENOMEM;

    int err = set_config_string(wstr, ';');
    free(wstr);

    finish_config_update(true);
    return err;
}

static void skip_rest_of_line(FILE *f)
{
    int c;
    do {
	c = getc(f);
    } while (!is_end_of_string(c));
}

/* Calling with with f == NULL just finalizes the update */
int read_config(FILE *f, bool save)
{
    char *linebuf = NULL;
    int err = -1;

    if (f) {
	linebuf = malloc(MAX_CONFIG_LINE);
	if (!linebuf) {
	    err = -ENOMEM;
	    goto exit;
	}

	while (fgets(linebuf, MAX_CONFIG_LINE, f)) {
	    if (set_config_string(linebuf, -1) == -EOVERFLOW)
		skip_rest_of_line(f);
	}
    }

    err = 0;

    if (linebuf)
	free(linebuf);

exit:
    finish_config_update(save);
    return err;
};

int write_env(FILE *f, bool status)
{
    size_t skip = status ? 7 : 0;
    for (char **var = environ; *var; var++) {
	if (!strncmp(*var, "status.", 7) == status) {
		fputs(*var + skip, f);
		putc('\n', f);
	}
    }

    return ferror(f) ? -1 : 0;
}

static int save_config(void)
{
    int err = -ENOENT;

    FILE *f = fopen(CONFIG_FILE, "w");
    if (f) {
	err = write_env(f, false);
	fclose(f);
    }

    if (err)
	printf("[CONF] Failed to save configuration (error %d)\n", err);

    return err;
}

static const esp_vfs_spiffs_conf_t spiffs_conf = {
    .base_path              = "/spiffs",
    .partition_label        = NULL,
    .max_files              = 4,
    .format_if_mount_failed = true
};

void init_config(void)
{
    if (!esp_spiffs_mounted(spiffs_conf.partition_label)) {
	esp_err_t err;
	err = esp_vfs_spiffs_register(&spiffs_conf);
	if (err)
	    printf("[CONF] Failed to mount %s (error 0x%x)\n",
		   spiffs_conf.base_path, err);
    }

    reset_config();

    FILE *f = fopen(CONFIG_FILE, "r");
    if (!f)
	printf("[CONF] No configuration file found, using defaults\n");

    read_config(f, false);
    if (f)
	fclose(f);
}

const char *getenv_def(const char *var, const char *def)
{
    const char *val = getenv(var);
    return val ? val : def;
}

long getenv_l(const char *var, long def)
{
    const char *ep;

    var = getenv(var);
    if (!var || !*var)
	return def;

    long val = strtol(var, (char **)&ep, 0);
    return *ep ? def : val;
}

void setenv_l(const char *var, long val)
{
    char vbuf[2+3*sizeof val];
    snprintf(vbuf, sizeof vbuf, "%ld", val);
    setenv_cond(var, vbuf);
}

unsigned long getenv_ul(const char *var, unsigned long def)
{
    const char *ep;

    var = getenv(var);
    if (!var || !*var)
	return def;

    unsigned long val = strtol(var, (char **)&ep, 0);
    return *ep ? def : val;
}

void setenv_ul(const char *var, unsigned long val)
{
    char vbuf[2+3*sizeof val];
    snprintf(vbuf, sizeof vbuf, "%lu", val);
    setenv_cond(var, vbuf);
}

bool getenv_bool(const char *var)
{
    var = getenv(var);

    if (!var)
	return false;

    unsigned char c = *var;
    unsigned char cl = c | 0x20;

    return !(!c || c == '0' || cl == 'f' || cl == 'n' || cl == 'd' ||
	     (cl == 'o' && (var[1] | 0x20) == 'f'));
}

void setenv_bool(const char *var, bool val)
{
    return setenv_ul(var, val);
}

const char *getenv_notempty(const char *env)
{
    const char *str = getenv(env);
    if (str && !*str)
	str = NULL;
    return str;
}

void log_config_status(void)
{
    const char *pfx;
    size_t skip;

    for (char **var = environ; *var; var++) {
	if (!strncmp(*var, "status.", 7)) {
	    pfx = "STATUS";
	    skip = 7;
	} else {
	    pfx = "CONFIG";
	    skip = 0;
	}
	logmsg(pfx, "%s\n", *var+skip);
    }
}