#include <ctype.h>
#include <errno.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "tofi.h"
#include "color.h"
#include "config.h"
#include "log.h"
#include "nelem.h"
#include "unicode.h"
#include "xmalloc.h"

/* Maximum number of config file errors before we give up */
#define MAX_ERRORS 5

/* Maximum inclusion recursion depth before we give up */
#define MAX_RECURSION 32

/* Anyone with a 10M config file is doing something very wrong */
#define MAX_CONFIG_SIZE (10*1024*1024)

/* Convenience macros for anchor combinations */
#define ANCHOR_TOP_LEFT (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \
		)
#define ANCHOR_TOP (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \
		)
#define ANCHOR_TOP_RIGHT (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \
		)
#define ANCHOR_RIGHT (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \
		)
#define ANCHOR_BOTTOM_RIGHT (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \
		)
#define ANCHOR_BOTTOM (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \
		)
#define ANCHOR_BOTTOM_LEFT (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \
		)
#define ANCHOR_LEFT (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \
		)
#define ANCHOR_CENTER (\
		ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT \
		| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT \
		)

struct uint32_percent {
	uint32_t value;
	bool percent;
};

static char *strip(const char *str);
static bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const char *option, const char *value);
static char *get_config_path(void);
static uint32_t fixup_percentage(uint32_t value, uint32_t base, bool is_percent, uint32_t scale, bool use_scale);
static void fixup_text_theme(struct text_theme *theme, uint32_t scale);

static uint32_t parse_anchor(const char *filename, size_t lineno, const char *str, bool *err);
static bool parse_bool(const char *filename, size_t lineno, const char *str, bool *err);
static uint32_t parse_char(const char *filename, size_t lineno, const char *str, bool *err);
static struct color parse_color(const char *filename, size_t lineno, const char *str, bool *err);
static uint32_t parse_uint32(const char *filename, size_t lineno, const char *str, bool *err);
static int32_t parse_int32(const char *filename, size_t lineno, const char *str, bool *err);
static struct uint32_percent parse_uint32_percent(const char *filename, size_t lineno, const char *str, bool *err);
static struct directional parse_directional(const char *filename, size_t lineno, const char *str, bool *err);

/*
 * Function-like macro. Yuck.
 */
#define PARSE_ERROR_NO_ARGS(filename, lineno, fmt) \
	if ((lineno) > 0) {\
		log_error("%s: line %zu: ", (filename), (lineno));\
		log_append_error((fmt)); \
	} else {\
		log_error((fmt)); \
	}

#define PARSE_ERROR(filename, lineno, fmt, ...) \
	if ((lineno) > 0) {\
		log_error("%s: line %zu: ", (filename), (lineno));\
		log_append_error((fmt), __VA_ARGS__); \
	} else {\
		log_error((fmt), __VA_ARGS__); \
	}

void config_load(struct tofi *tofi, const char *filename)
{
	char *default_filename = NULL;
	if (!filename) {
		default_filename = get_config_path();
		if (!default_filename) {
			return;
		}
		filename = default_filename;
	}
	/*
	 * Track and limit recursion depth, so we don't overflow the stack if
	 * a config file loop is created.
	 */
	static uint8_t recursion_depth = 0;
	recursion_depth++;
	if (recursion_depth > MAX_RECURSION) {
		log_error("Refusing to load %s, recursion too deep (>%u layers).\n", filename, MAX_RECURSION);
		recursion_depth--;
		return;
	}
	char *config;
	FILE *fp = fopen(filename, "rb");
	if (!fp) {
		if (!default_filename || errno != ENOENT) {
			log_error("Failed to open config file %s: %s\n", filename, strerror(errno));
		}
		goto CLEANUP_FILENAME;
	}
	if (fseek(fp, 0, SEEK_END)) {
		log_error("Failed to seek in config file: %s\n", strerror(errno));
		fclose(fp);
		goto CLEANUP_FILENAME;
	}
	size_t size;
	{
		long ssize = ftell(fp);
		if (ssize < 0) {
			log_error("Failed to determine config file size: %s\n", strerror(errno));
			fclose(fp);
			goto CLEANUP_FILENAME;
		}
		if (ssize > MAX_CONFIG_SIZE) {
			log_error("Config file too big (> %d MiB)! Are you sure it's a file?\n", MAX_CONFIG_SIZE / 1024 / 1024);
			fclose(fp);
			goto CLEANUP_FILENAME;
		}
		size = (size_t)ssize;
	}
	config = xmalloc(size + 1);
	if (!config) {
		log_error("Failed to malloc buffer for config file.\n");
		fclose(fp);
		goto CLEANUP_FILENAME;
	}
	rewind(fp);
	if (fread(config, 1, size, fp) != size) {
		log_error("Failed to read config file: %s\n", strerror(errno));
		fclose(fp);
		goto CLEANUP_CONFIG;
	}
	fclose(fp);
	config[size] = '\0';

	char *config_copy = xstrdup(config);
	if (!config_copy) {
		log_error("Failed to malloc second buffer for config file.\n");
		goto CLEANUP_ALL;
	}

	log_debug("Loading config file %s.\n", filename);

	char *saveptr1 = NULL;
	char *saveptr2 = NULL;

	char *copy_pos = config_copy;
	size_t lineno = 1;
	size_t num_errs = 0;
	for (char *str1 = config; ; str1 = NULL, saveptr2 = NULL) {
		if (num_errs > MAX_ERRORS) {
			log_error("Too many config file errors (>%u), giving up.\n", MAX_ERRORS);
			break;
		}
		char *line = strtok_r(str1, "\r\n", &saveptr1);
		if (!line) {
			/* We're done here */
			break;
		}
		while ((copy_pos - config_copy) < (line - config)) {
			if (*copy_pos == '\n') {
				lineno++;
			}
			copy_pos++;
		}
		{
			/* Grab first non-space character on the line. */
			char c = '\0';
			for (char *tmp = line; *tmp != '\0'; tmp++) {
				c = *tmp;
				if (!isspace(c)) {
					break;
				}
			}
			/*
			 * Comment characters.
			 * N.B. treating section headers as comments for now.
			 */
			switch (c) {
				case '#':
				case ';':
				case '[':
					continue;
			}
		}
		if (line[0] == '=') {
			PARSE_ERROR_NO_ARGS(filename, lineno, "Missing option.\n");
			num_errs++;
			continue;
		}
		char *option = strtok_r(line, "=", &saveptr2);
		if (!option) {
			char *tmp = strip(line);
			PARSE_ERROR(filename, lineno, "Config option \"%s\" missing value.\n", tmp);
			num_errs++;
			free(tmp);
			continue;
		}
		char *option_stripped = strip(option);
		if (!option_stripped) {
			PARSE_ERROR_NO_ARGS(filename, lineno, "Missing option.\n");
			num_errs++;
			continue;
		}
		char *value = strtok_r(NULL, "\r\n", &saveptr2);
		if (!value) {
			PARSE_ERROR(filename, lineno, "Config option \"%s\" missing value.\n", option_stripped);
			num_errs++;
			free(option_stripped);
			continue;
		}
		char *value_stripped = strip(value);
		if (!value_stripped) {
			PARSE_ERROR(filename, lineno, "Config option \"%s\" missing value.\n", option_stripped);
			num_errs++;
			free(option_stripped);
			continue;
		}
		if (!parse_option(tofi, filename, lineno, option_stripped, value_stripped)) {
			num_errs++;
		}

		/* Cleanup */
		free(value_stripped);
		free(option_stripped);
	}

CLEANUP_ALL:
	free(config_copy);
CLEANUP_CONFIG:
	free(config);
CLEANUP_FILENAME:
	if (default_filename) {
		free(default_filename);
	}
	recursion_depth--;
}

char *strip(const char *str)
{
	size_t start = 0;
	size_t end = strlen(str);
	while (start <= end && isspace(str[start])) {
		start++;
	}
	if (start == end) {
		return NULL;
	}
	while (end > start && (isspace(str[end]) || str[end] == '\0')) {
		end--;
	}
	if (end < start) {
		return NULL;
	}
	if (str[start] == '"' && str[end] == '"' && end > start) {
		start++;
		end--;
	}
	size_t len = end - start + 1;
	char *buf = xcalloc(len + 1, 1);
	strncpy(buf, str + start, len);
	buf[len] = '\0';
	return buf;
}

bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const char *option, const char *value)
{
	bool err = false;
	struct uint32_percent percent;
	if (strcasecmp(option, "include") == 0) {
		if (value[0] == '/') {
			config_load(tofi, value);
		} else {
			char *tmp = xstrdup(filename);
			char *dir = dirname(tmp);
			size_t len = strlen(dir) + 1 + strlen(value) + 1;
			char *config = xcalloc(len, 1);
			snprintf(config, len, "%s/%s", dir, value);
			config_load(tofi, config);
			free(config);
			free(tmp);
		}
	} else if (strcasecmp(option, "anchor") == 0) {
		uint32_t val = parse_anchor(filename, lineno, value, &err);
		if (!err) {
			tofi->anchor = val;
		}
	} else if (strcasecmp(option, "background-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.background_color = val;
		}
	} else if (strcasecmp(option, "corner-radius") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.corner_radius = val;
		}
	} else if (strcasecmp(option, "output") == 0) {
		snprintf(tofi->target_output_name, N_ELEM(tofi->target_output_name), "%s", value);
	} else if (strcasecmp(option, "font") == 0) {
		snprintf(tofi->window.entry.font_name, N_ELEM(tofi->window.entry.font_name), "%s", value);
	} else if (strcasecmp(option, "font-size") == 0) {
		uint32_t val =  parse_uint32(filename, lineno, value, &err);
		if (val == 0) {
			err = true;
			PARSE_ERROR(filename, lineno, "Option \"%s\" must be greater than 0.\n", option);
		} else {
			tofi->window.entry.font_size = val;
		}
	} else if (strcasecmp(option, "font-features") == 0) {
		snprintf(tofi->window.entry.font_features, N_ELEM(tofi->window.entry.font_features), "%s", value);
	} else if (strcasecmp(option, "font-variations") == 0) {
		snprintf(tofi->window.entry.font_variations, N_ELEM(tofi->window.entry.font_variations), "%s", value);
	} else if (strcasecmp(option, "num-results") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.num_results = val;
		}
	} else if (strcasecmp(option, "outline-width") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.outline_width = val;
		}
	} else if (strcasecmp(option, "outline-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.outline_color = val;
		}
	} else if (strcasecmp(option, "prompt-text") == 0) {
		snprintf(tofi->window.entry.prompt_text, N_ELEM(tofi->window.entry.prompt_text), "%s", value);
	} else if (strcasecmp(option, "prompt-padding") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.prompt_padding = val;
		}
	} else if (strcasecmp(option, "prompt-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.prompt_theme.foreground_color = val;
			tofi->window.entry.prompt_theme.foreground_specified = true;
		}
	} else if (strcasecmp(option, "prompt-background") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.prompt_theme.background_color = val;
			tofi->window.entry.prompt_theme.background_specified = true;
		}
	} else if (strcasecmp(option, "prompt-background-padding") == 0) {
		struct directional val = parse_directional(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.prompt_theme.padding = val;
			tofi->window.entry.prompt_theme.padding_specified = true;
		}
	} else if (strcasecmp(option, "prompt-background-corner-radius") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.prompt_theme.background_corner_radius = val;
			tofi->window.entry.prompt_theme.radius_specified = true;
		}
	} else if (strcasecmp(option, "placeholder-text") == 0) {
		snprintf(tofi->window.entry.placeholder_text, N_ELEM(tofi->window.entry.placeholder_text), "%s", value);
	} else if (strcasecmp(option, "placeholder-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.placeholder_theme.foreground_color = val;
			tofi->window.entry.placeholder_theme.foreground_specified = true;
		}
	} else if (strcasecmp(option, "placeholder-background") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.placeholder_theme.background_color = val;
			tofi->window.entry.placeholder_theme.background_specified = true;
		}
	} else if (strcasecmp(option, "placeholder-background-padding") == 0) {
		struct directional val = parse_directional(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.placeholder_theme.padding = val;
			tofi->window.entry.placeholder_theme.padding_specified = true;
		}
	} else if (strcasecmp(option, "placeholder-background-corner-radius") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.placeholder_theme.background_corner_radius = val;
			tofi->window.entry.placeholder_theme.radius_specified = true;
		}
	} else if (strcasecmp(option, "input-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.input_theme.foreground_color = val;
			tofi->window.entry.input_theme.foreground_specified = true;
		}
	} else if (strcasecmp(option, "input-background") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.input_theme.background_color = val;
			tofi->window.entry.input_theme.background_specified = true;
		}
	} else if (strcasecmp(option, "input-background-padding") == 0) {
		struct directional val = parse_directional(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.input_theme.padding = val;
			tofi->window.entry.input_theme.padding_specified = true;
		}
	} else if (strcasecmp(option, "input-background-corner-radius") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.input_theme.background_corner_radius = val;
			tofi->window.entry.input_theme.radius_specified = true;
		}
	} else if (strcasecmp(option, "default-result-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.default_result_theme.foreground_color = val;
			tofi->window.entry.default_result_theme.foreground_specified = true;
		}
	} else if (strcasecmp(option, "default-result-background") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.default_result_theme.background_color = val;
			tofi->window.entry.default_result_theme.background_specified = true;
		}
	} else if (strcasecmp(option, "default-result-background-padding") == 0) {
		struct directional val = parse_directional(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.default_result_theme.padding = val;
			tofi->window.entry.default_result_theme.padding_specified = true;
		}
	} else if (strcasecmp(option, "default-result-background-corner-radius") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.default_result_theme.background_corner_radius = val;
			tofi->window.entry.default_result_theme.radius_specified = true;
		}
	} else if (strcasecmp(option, "alternate-result-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.alternate_result_theme.foreground_color = val;
			tofi->window.entry.alternate_result_theme.foreground_specified = true;
		}
	} else if (strcasecmp(option, "alternate-result-background") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.alternate_result_theme.background_color = val;
			tofi->window.entry.alternate_result_theme.background_specified = true;
		}
	} else if (strcasecmp(option, "alternate-result-background-padding") == 0) {
		struct directional val = parse_directional(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.alternate_result_theme.padding = val;
			tofi->window.entry.alternate_result_theme.padding_specified = true;
		}
	} else if (strcasecmp(option, "alternate-result-background-corner-radius") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.alternate_result_theme.background_corner_radius = val;
			tofi->window.entry.alternate_result_theme.radius_specified = true;
		}
	} else if (strcasecmp(option, "min-input-width") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.input_width = val;
		}
	} else if (strcasecmp(option, "result-spacing") == 0) {
		int32_t val = parse_int32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.result_spacing = val;
		}
	} else if (strcasecmp(option, "border-width") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.border_width = val;
		}
	} else if (strcasecmp(option, "border-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.border_color = val;
		}
	} else if (strcasecmp(option, "text-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.foreground_color = val;
		}
	} else if (strcasecmp(option, "selection-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.selection_theme.foreground_color = val;
			tofi->window.entry.selection_theme.foreground_specified = true;
		}
	} else if (strcasecmp(option, "selection-match-color") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.selection_highlight_color = val;
		}
	} else if (strcasecmp(option, "selection-padding") == 0) {
		log_warning("The \"selection-padding\" option is deprecated, and will be removed in future. Please switch to \"selection-background-padding\".\n");
		int32_t val = parse_int32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.selection_theme.padding.left = val;
			tofi->window.entry.selection_theme.padding.right = val;
			tofi->window.entry.selection_theme.padding_specified = true;
		}
	} else if (strcasecmp(option, "selection-background") == 0) {
		struct color val = parse_color(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.selection_theme.background_color = val;
			tofi->window.entry.selection_theme.background_specified = true;
		}
	} else if (strcasecmp(option, "selection-background-padding") == 0) {
		struct directional val = parse_directional(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.selection_theme.padding = val;
			tofi->window.entry.selection_theme.padding_specified = true;
		}
	} else if (strcasecmp(option, "selection-background-corner-radius") == 0) {
		uint32_t val = parse_uint32(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.selection_theme.background_corner_radius = val;
			tofi->window.entry.selection_theme.radius_specified = true;
		}
	} else if (strcasecmp(option, "exclusive-zone") == 0) {
		if (strcmp(value, "-1") == 0) {
			tofi->window.exclusive_zone = -1;
		} else {
			percent = parse_uint32_percent(filename, lineno, value, &err);
			if (!err) {
				tofi->window.exclusive_zone = percent.value;
				tofi->window.exclusive_zone_is_percent = percent.percent;
			}
		}
	} else if (strcasecmp(option, "width") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.width = percent.value;
			tofi->window.width_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "height") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.height = percent.value;
			tofi->window.height_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "margin-top") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.margin_top = percent.value;
			tofi->window.margin_top_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "margin-bottom") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.margin_bottom = percent.value;
			tofi->window.margin_bottom_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "margin-left") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.margin_left = percent.value;
			tofi->window.margin_left_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "margin-right") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.margin_right = percent.value;
			tofi->window.margin_right_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "padding-top") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.padding_top = percent.value;
			tofi->window.entry.padding_top_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "padding-bottom") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.padding_bottom = percent.value;
			tofi->window.entry.padding_bottom_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "padding-left") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.padding_left = percent.value;
			tofi->window.entry.padding_left_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "padding-right") == 0) {
		percent = parse_uint32_percent(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.padding_right = percent.value;
			tofi->window.entry.padding_right_is_percent = percent.percent;
		}
	} else if (strcasecmp(option, "clip-to-padding") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.clip_to_padding = val;
		}
	} else if (strcasecmp(option, "horizontal") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.horizontal = val;
		}
	} else if (strcasecmp(option, "hide-cursor") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->hide_cursor = val;
		}
	} else if (strcasecmp(option, "history") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->use_history = val;
		}
	} else if (strcasecmp(option, "history-file") == 0) {
		snprintf(tofi->history_file, N_ELEM(tofi->history_file), "%s", value);
	} else if (strcasecmp(option, "fuzzy-match") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->fuzzy_match = val;
		}
	} else if (strcasecmp(option, "require-match") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->require_match = val;
		}
	} else if (strcasecmp(option, "hide-input") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.hide_input = val;
		}
	} else if (strcasecmp(option, "hidden-character") == 0) {
		uint32_t ch = parse_char(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.hidden_character_utf8_length = 
				utf32_to_utf8(ch, tofi->window.entry.hidden_character_utf8);
		}
	} else if (strcasecmp(option, "drun-launch") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->drun_launch = val;
		}
	} else if (strcasecmp(option, "drun-print-exec") == 0) {
		log_warning("drun-print-exec is deprecated, as it is now always true.\n"
				"           This option may be removed in a future version of tofi.\n");
	} else if (strcasecmp(option, "terminal") == 0) {
		snprintf(tofi->default_terminal, N_ELEM(tofi->default_terminal), "%s", value);
	} else if (strcasecmp(option, "hint-font") == 0) {
		bool val = !parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->window.entry.harfbuzz.disable_hinting = val;
		}
	} else if (strcasecmp(option, "multi-instance") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->multiple_instance = val;
		}
	} else if (strcasecmp(option, "ascii-input") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->ascii_input = val;
		}
	} else if (strcasecmp(option, "late-keyboard-init") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->late_keyboard_init = val;
		}
	} else if (strcasecmp(option, "output") == 0) {
		snprintf(tofi->target_output_name, N_ELEM(tofi->target_output_name), "%s", value);
	} else if (strcasecmp(option, "scale") == 0) {
		bool val = parse_bool(filename, lineno, value, &err);
		if (!err) {
			tofi->use_scale = val;
		}
	} else {
		PARSE_ERROR(filename, lineno, "Unknown option \"%s\"\n", option);
		err = true;
	}
	return !err;
}

void config_apply(struct tofi *tofi, const char *option, const char *value)
{
	if (!parse_option(tofi, "", 0, option, value)) {
		exit(EXIT_FAILURE);
	};
}

uint32_t fixup_percentage(uint32_t value, uint32_t base, bool is_percent, uint32_t scale, bool use_scale)
{
	if (is_percent) {
		return value * base / 100;
	} else if (use_scale) {
		return value * scale;
	}
	return value;
}

void fixup_text_theme(struct text_theme *theme, uint32_t scale)
{
	theme->background_corner_radius *= scale;
	theme->padding.top *= scale;
	theme->padding.bottom *= scale;
	theme->padding.left *= scale;
	theme->padding.right *= scale;
}

void config_fixup_values(struct tofi *tofi)
{
	uint32_t scale = tofi->window.scale;

	if (tofi->use_scale) {
		struct entry *entry = &tofi->window.entry;

		entry->font_size *= scale;
		entry->prompt_padding *= scale;
		entry->corner_radius *= scale;
		entry->result_spacing *= scale;
		entry->input_width *= scale;
		entry->outline_width *= scale;
		entry->border_width *= scale;

		fixup_text_theme(&entry->prompt_theme, scale);
		fixup_text_theme(&entry->placeholder_theme, scale);
		fixup_text_theme(&entry->input_theme, scale);
		fixup_text_theme(&entry->default_result_theme, scale);
		fixup_text_theme(&entry->alternate_result_theme, scale);
		fixup_text_theme(&entry->selection_theme, scale);
	}

	/* These values should only be scaled if they're not percentages. */
	tofi->window.width = fixup_percentage(
			tofi->window.width,
			tofi->output_width,
			tofi->window.width_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.height = fixup_percentage(
			tofi->window.height,
			tofi->output_height,
			tofi->window.height_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.margin_top = fixup_percentage(
			tofi->window.margin_top,
			tofi->output_height,
			tofi->window.margin_top_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.margin_bottom = fixup_percentage(
			tofi->window.margin_bottom,
			tofi->output_height,
			tofi->window.margin_bottom_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.margin_left = fixup_percentage(
			tofi->window.margin_left,
			tofi->output_width,
			tofi->window.margin_left_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.margin_right = fixup_percentage(
			tofi->window.margin_right,
			tofi->output_width,
			tofi->window.margin_right_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.entry.padding_top = fixup_percentage(
			tofi->window.entry.padding_top,
			tofi->output_height,
			tofi->window.entry.padding_top_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.entry.padding_bottom = fixup_percentage(
			tofi->window.entry.padding_bottom,
			tofi->output_height,
			tofi->window.entry.padding_bottom_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.entry.padding_left = fixup_percentage(
			tofi->window.entry.padding_left,
			tofi->output_width,
			tofi->window.entry.padding_left_is_percent,
			tofi->window.scale,
			tofi->use_scale);
	tofi->window.entry.padding_right = fixup_percentage(
			tofi->window.entry.padding_right,
			tofi->output_width,
			tofi->window.entry.padding_right_is_percent,
			tofi->window.scale,
			tofi->use_scale);

	/* Don't attempt percentage handling if exclusive_zone is set to -1. */
	if (tofi->window.exclusive_zone > 0) {
		/* Exclusive zone base depends on anchor. */
		switch (tofi->anchor) {
			case ANCHOR_TOP:
			case ANCHOR_BOTTOM:
				tofi->window.exclusive_zone = fixup_percentage(
						tofi->window.exclusive_zone,
						tofi->output_height,
						tofi->window.exclusive_zone_is_percent,
						tofi->window.scale,
						tofi->use_scale);
				break;
			case ANCHOR_LEFT:
			case ANCHOR_RIGHT:
				tofi->window.exclusive_zone = fixup_percentage(
						tofi->window.exclusive_zone,
						tofi->output_width,
						tofi->window.exclusive_zone_is_percent,
						tofi->window.scale,
						tofi->use_scale);
				break;
			default:
				/*
				 * Exclusive zone >0 is meaningless for other anchor
				 * positions.
				 */
				tofi->window.exclusive_zone =
					MIN(tofi->window.exclusive_zone, 0);
				break;
		}
	}
}

char *get_config_path()
{
	char *base_dir = getenv("XDG_CONFIG_HOME");
	char *ext = "";
	size_t len = strlen("/tofi/config") + 1;
	if (!base_dir) {
		base_dir = getenv("HOME");
		ext = "/.config";
		if (!base_dir) {
			log_error("Couldn't find XDG_CONFIG_HOME or HOME envvars\n");
			return NULL;
		}
	}
	len += strlen(base_dir) + strlen(ext) + 2;
	char *name = xcalloc(len, sizeof(*name));
	snprintf(name, len, "%s%s%s", base_dir, ext, "/tofi/config");
	return name;
}

bool parse_bool(const char *filename, size_t lineno, const char *str, bool *err)
{
	if (strcasecmp(str, "true") == 0) {
		return true;
	} else if (strcasecmp(str, "false") == 0) {
		return false;
	}
	PARSE_ERROR(filename, lineno, "Invalid boolean value \"%s\".\n", str);
	if (err) {
		*err = true;
	}
	return false;
}

uint32_t parse_char(const char *filename, size_t lineno, const char *str, bool *err)
{
	uint32_t ch = U'\0';
	if (*str == '\0') {
		return ch;
	}
	if (!utf8_validate(str)) {
		PARSE_ERROR(filename, lineno, "Invalid UTF-8 string \"%s\".\n", str);
		if (err) {
			*err = true;
		}
		return ch;
	}
	char *tmp = utf8_compose(str);
	ch = utf8_to_utf32_validate(tmp);
	if (ch == (uint32_t)-2 || ch == (uint32_t)-1 || *utf8_next_char(tmp) != '\0') {
		PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as a character.\n", str);
	}
	free(tmp);

	return ch;
}

uint32_t parse_anchor(const char *filename, size_t lineno, const char *str, bool *err)
{
	if(strcasecmp(str, "top-left") == 0) {
		return ANCHOR_TOP_LEFT;
	}
	if (strcasecmp(str, "top") == 0) {
		return ANCHOR_TOP;
	}
	if (strcasecmp(str, "top-right") == 0) {
		return ANCHOR_TOP_RIGHT;
	}
	if (strcasecmp(str, "right") == 0) {
		return ANCHOR_RIGHT;
	}
	if (strcasecmp(str, "bottom-right") == 0) {
		return ANCHOR_BOTTOM_RIGHT;
	}
	if (strcasecmp(str, "bottom") == 0) {
		return ANCHOR_BOTTOM;
	}
	if (strcasecmp(str, "bottom-left") == 0) {
		return ANCHOR_BOTTOM_LEFT;
	}
	if (strcasecmp(str, "left") == 0) {
		return ANCHOR_LEFT;
	}
	if (strcasecmp(str, "center") == 0) {
		return ANCHOR_CENTER;
	}
	PARSE_ERROR(filename, lineno, "Invalid anchor \"%s\".\n", str);
	*err = true;
	return 0;
}

struct color parse_color(const char *filename, size_t lineno, const char *str, bool *err)
{
	struct color color = hex_to_color(str);
	if (color.r == -1) {
		PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as a color.\n", str);
		if (err) {
			*err = true;
		}
	}
	return color;
}

uint32_t parse_uint32(const char *filename, size_t lineno, const char *str, bool *err)
{
	errno = 0;
	char *endptr;
	int32_t ret = strtoul(str, &endptr, 0);
	if (endptr == str) {
		PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as unsigned int.\n", str);
		if (err) {
			*err = true;
		}
	} else if (errno || ret < 0) {
		PARSE_ERROR(filename, lineno, "Unsigned int value \"%s\" out of range.\n", str);
		if (err) {
			*err = true;
		}
	}
	return ret;
}

int32_t parse_int32(const char *filename, size_t lineno, const char *str, bool *err)
{
	errno = 0;
	char *endptr;
	int32_t ret = strtol(str, &endptr, 0);
	if (endptr == str) {
		PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as int.\n", str);
		if (err) {
			*err = true;
		}
	} else if (errno) {
		PARSE_ERROR(filename, lineno, "Int value \"%s\" out of range.\n", str);
		if (err) {
			*err = true;
		}
	}
	return ret;
}

struct uint32_percent parse_uint32_percent(const char *filename, size_t lineno, const char *str, bool *err)
{
	errno = 0;
	char *endptr;
	int32_t val = strtoul(str, &endptr, 0);
	bool percent = false;
	if (endptr == str) {
		PARSE_ERROR(filename, lineno, "Failed to parse \"%s\" as unsigned int.\n", str);
		if (err) {
			*err = true;
		}
	} else if (errno || val < 0) {
		PARSE_ERROR(filename, lineno, "Unsigned int value \"%s\" out of range.\n", str);
		if (err) {
			*err = true;
		}
	}
	if (!err || !*err) {
		if (*endptr == '%') {
			percent = true;
		}
	}
	return (struct uint32_percent){ val, percent };
}

struct directional parse_directional(const char *filename, size_t lineno, const char *str, bool *err)
{
	int32_t values[4];
	char *saveptr = NULL;
	char *tmp = xstrdup(str);
	char *val = strtok_r(tmp, ",", &saveptr);

	size_t n;

	for (n = 0; n < N_ELEM(values) && val != NULL; n++) {
		values[n] = parse_int32(filename, lineno, val, err);
		if (err && *err) {
			break;
		}
		val = strtok_r(NULL, ",", &saveptr);
	}
	free(tmp);

	struct directional ret = {0};
	if (err && *err) {
		return ret;
	}

	switch (n) {
		case 0:
			break;
		case 1:
			ret = (struct directional) {
				.top = values[0],
				.right = values[0],
				.bottom = values[0],
				.left = values[0],
			};
			break;
		case 2:
			ret = (struct directional) {
				.top = values[0],
				.right = values[1],
				.bottom = values[0],
				.left = values[1],
			};
			break;
		case 3:
			ret = (struct directional) {
				.top = values[0],
				.right = values[1],
				.bottom = values[2],
				.left = values[1],
			};
			break;
		case 4:
			ret = (struct directional) {
				.top = values[0],
				.right = values[1],
				.bottom = values[2],
				.left = values[3],
			};
			break;
		default:
			PARSE_ERROR(filename, lineno, "Too many values in \"%s\" for directional.\n", str);
			if (err) {
				*err = true;
			}
			break;
	};

	return ret;
}
