/* config.c - defaults, config file parsing, and shared key/value sets. */
#include "config.h"

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void config_defaults(struct config *c)
{
	/* Home row first, then the rest of the top/bottom rows: common cells
	 * get the easiest two-key labels. */
	strcpy(c->alphabet, "fjdkslaghrueiwoqptyvbcnxmz");
	c->cell_size = 100;
	c->cols = 0;
	c->rows = 0;
	c->font_scale = 0;

	c->opacity_bg = 33;	/* how dark the screen goes (100 = solid) */
	c->opacity_fg = 65;	/* labels, grid and crosshair */

	/* Full-alpha colours (grid deliberately fainter); opacity_bg/fg
	 * scale them once at startup. */
	c->color_bg = 0xff101014;	/* near-black backdrop */
	c->color_grid = 0x40ffffff;	/* faint white grid lines */
	c->color_label = 0xffffffff;	/* white labels */
	c->color_typed = 0xff52a8ff;	/* blue for the typed prefix */
	c->color_active = 0xffff5252;	/* red refine box + crosshair */

	c->k_tl = 'u';
	c->k_tr = 'i';
	c->k_bl = 'j';
	c->k_br = 'k';
	c->k_left = 'm';
	c->k_middle = ',';
	c->k_right = '.';
	c->k_double = ';';
	c->k_drag = 'v';
}

static int parse_color(const char *s, uint32_t *out)
{
	if (*s == '#')
		s++;
	size_t n = strlen(s);
	if (n != 6 && n != 8)
		return -1;
	for (size_t i = 0; i < n; i++)
		if (!isxdigit((unsigned char)s[i]))
			return -1;

	unsigned long v = strtoul(s, NULL, 16);
	uint32_t r, g, b, a;
	if (n == 6) {
		r = (v >> 16) & 0xff;
		g = (v >> 8) & 0xff;
		b = v & 0xff;
		a = 0xff;
	} else {
		r = (v >> 24) & 0xff;
		g = (v >> 16) & 0xff;
		b = (v >> 8) & 0xff;
		a = v & 0xff;
	}
	*out = (a << 24) | (r << 16) | (g << 8) | b;
	return 0;
}

static int parse_int(const char *s, int *out)
{
	char *end;
	errno = 0;
	long v = strtol(s, &end, 10);
	if (errno || end == s || *end || v < 0 || v > 100000)
		return -1;
	*out = (int)v;
	return 0;
}

/* A character the user can actually type at the grid: printable ASCII, no
 * space. Uppercase is rejected because incoming keys are lowercased (so Caps
 * Lock cannot break labels) - an uppercase binding would never fire. */
static int typeable(unsigned char ch)
{
	return ch > 0x20 && ch < 0x7f && !(ch >= 'A' && ch <= 'Z');
}

int config_set(struct config *c, const char *key, const char *val)
{
	if (!strcmp(key, "alphabet")) {
		if (strlen(val) < 2 || strlen(val) >= sizeof c->alphabet)
			return -2;
		for (size_t i = 0; val[i]; i++) {
			if (!typeable((unsigned char)val[i]))
				return -2;
			if (strchr(val + i + 1, val[i]))
				return -2; /* duplicate char breaks labels */
		}
		strcpy(c->alphabet, val);
		return 0;
	}

	if (!strcmp(key, "opacity_bg")) {
		int v;
		if (parse_int(val, &v) || v > 100)
			return -2;
		c->opacity_bg = v;
		return 0;
	}
	if (!strcmp(key, "opacity_fg")) {
		int v;
		if (parse_int(val, &v) || v > 100)
			return -2;
		c->opacity_fg = v;
		return 0;
	}

	if (!strcmp(key, "cell_size"))
		return parse_int(val, &c->cell_size) ? -2 : 0;
	if (!strcmp(key, "cols"))
		return parse_int(val, &c->cols) ? -2 : 0;
	if (!strcmp(key, "rows"))
		return parse_int(val, &c->rows) ? -2 : 0;
	if (!strcmp(key, "font_scale"))
		return parse_int(val, &c->font_scale) ? -2 : 0;

	if (!strcmp(key, "color_bg"))
		return parse_color(val, &c->color_bg) ? -2 : 0;
	if (!strcmp(key, "color_grid"))
		return parse_color(val, &c->color_grid) ? -2 : 0;
	if (!strcmp(key, "color_label"))
		return parse_color(val, &c->color_label) ? -2 : 0;
	if (!strcmp(key, "color_typed"))
		return parse_color(val, &c->color_typed) ? -2 : 0;
	if (!strcmp(key, "color_active"))
		return parse_color(val, &c->color_active) ? -2 : 0;

	/* Single-character key bindings. */
	struct { const char *name; char *slot; } keys[] = {
		{ "refine_tl", &c->k_tl }, { "refine_tr", &c->k_tr },
		{ "refine_bl", &c->k_bl }, { "refine_br", &c->k_br },
		{ "click_left", &c->k_left }, { "click_middle", &c->k_middle },
		{ "click_right", &c->k_right }, { "click_double", &c->k_double },
		{ "drag", &c->k_drag },
	};
	for (size_t i = 0; i < sizeof keys / sizeof keys[0]; i++) {
		if (strcmp(key, keys[i].name))
			continue;
		if (strlen(val) != 1 || !typeable((unsigned char)val[0]))
			return -2;
		*keys[i].slot = val[0];
		return 0;
	}

	return -1;
}

const char *config_validate(const struct config *c)
{
	if (c->cell_size < 1)
		return "cell_size must be greater than zero";
	if ((c->cols > 0) != (c->rows > 0))
		return "cols and rows must be set together";
	if (c->cols > MB_MAX_GRID_DIM || c->rows > MB_MAX_GRID_DIM)
		return "cols and rows must not exceed 200";
	if (c->font_scale > MB_MAX_FONT_SCALE)
		return "font_scale must not exceed 64";

	const char keys[] = {
		c->k_tl, c->k_tr, c->k_bl, c->k_br, c->k_left,
		c->k_middle, c->k_right, c->k_double, c->k_drag,
	};
	for (size_t i = 0; i < sizeof keys; i++)
		for (size_t j = i + 1; j < sizeof keys; j++)
			if (keys[i] == keys[j])
				return "refine and action keys must be unique";
	return NULL;
}

void config_apply_opacity(struct config *c)
{
	uint32_t *fg[] = { &c->color_grid, &c->color_label,
			   &c->color_typed, &c->color_active };
	uint32_t a = (c->color_bg >> 24) * (uint32_t)c->opacity_bg / 100;
	c->color_bg = (a << 24) | (c->color_bg & 0x00ffffff);
	for (size_t i = 0; i < sizeof fg / sizeof fg[0]; i++) {
		a = (*fg[i] >> 24) * (uint32_t)c->opacity_fg / 100;
		*fg[i] = (a << 24) | (*fg[i] & 0x00ffffff);
	}
}

static char *trim(char *s)
{
	while (*s && isspace((unsigned char)*s))
		s++;
	char *e = s + strlen(s);
	while (e > s && isspace((unsigned char)e[-1]))
		*--e = 0;
	return s;
}

int config_load_file(struct config *c, const char *path)
{
	FILE *f = fopen(path, "r");
	if (!f) {
		if (errno == ENOENT)
			return -1;
		fprintf(stderr, "%s: %s\n", path, strerror(errno));
		return -2;
	}

	char line[256];
	int lineno = 0, rc = 0;
	while (fgets(line, sizeof line, f)) {
		lineno++;
		size_t len = strlen(line);
		if (len && line[len - 1] != '\n' && !feof(f)) {
			int ch;
			while ((ch = fgetc(f)) != '\n' && ch != EOF)
				;
			fprintf(stderr, "%s:%d: line too long\n", path, lineno);
			rc = -2;
			continue;
		}
		/* Comments are whole lines starting with '#'; this keeps '#'
		 * usable inside values (colours are #RRGGBB). */
		char *body = trim(line);
		if (!*body || *body == '#')
			continue;
		char *eq = strchr(body, '=');
		if (!eq) {
			fprintf(stderr, "%s:%d: missing '='\n", path, lineno);
			rc = -2;
			continue;
		}
		*eq = 0;
		char *key = trim(body);
		char *val = trim(eq + 1);
		int r = config_set(c, key, val);
		if (r == -1)
			fprintf(stderr, "%s:%d: unknown key '%s'\n", path,
				lineno, key);
		else if (r == -2)
			fprintf(stderr, "%s:%d: bad value for '%s'\n", path,
				lineno, key);
		if (r)
			rc = -2;
	}
	fclose(f);
	return rc;
}

const char *config_default_path(void)
{
	static char path[512];
	const char *xdg = getenv("XDG_CONFIG_HOME");
	if (xdg && *xdg) {
		snprintf(path, sizeof path, "%s/mouseboard/config", xdg);
		return path;
	}
	/* Windows sets APPDATA, not HOME; fopen accepts the mixed slashes. */
	const char *appdata = getenv("APPDATA");
	if (appdata && *appdata) {
		snprintf(path, sizeof path, "%s/mouseboard/config", appdata);
		return path;
	}
	const char *home = getenv("HOME");
	if (!home)
		home = "";
	snprintf(path, sizeof path, "%s/.config/mouseboard/config", home);
	return path;
}
