#include "Config.h"

#include "common.h"
#include "lua_helpers.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include <lauxlib.h>
#include <lualib.h>

#include <wlr/types/wlr_keyboard.h>
#include <xkbcommon/xkbcommon.h>

char const *get_config_path(void)
{
	static char const *paths[] = {
		"lunarwm/init.lua",
	};
	for (size_t i = 0; i < ARRAY_SZ(paths); ++i) {
		char const *p = paths[i];
		struct stat s;
		if (stat(p, &s) == 0)
			return p;
	}
	return NULL;
}

static int dupstr(char const *s, char **out)
{
	if (!s) {
		*out = NULL;
		return 0;
	}
	size_t n = strlen(s) + 1;
	char *m = (char *)malloc(n);
	if (!m)
		return -1;
	memcpy(m, s, n);
	*out = m;
	return 0;
}

static uint32_t mod_from_token(char const *t)
{
	if (!t)
		return 0;
	if (strcasecmp(t, "Super") == 0)
		return WLR_MODIFIER_LOGO;
	if (strcasecmp(t, "Shift") == 0)
		return WLR_MODIFIER_SHIFT;
	if (strcasecmp(t, "Ctrl") == 0 || strcasecmp(t, "Control") == 0)
		return WLR_MODIFIER_CTRL;
	if (strcasecmp(t, "Alt") == 0)
		return WLR_MODIFIER_ALT;
	return 0;
}

static int parse_bind(
    char const *bind, uint32_t *mods_out, xkb_keysym_t *sym_out)
{
	if (!bind || !mods_out || !sym_out)
		return -1;

	char buf[256];
	strncpy(buf, bind, sizeof buf - 1);
	buf[sizeof buf - 1] = 0;

	char *save = NULL;
	char *tok = strtok_r(buf, "-", &save);
	char *last = tok;
	while (tok) {
		last = tok;
		tok = strtok_r(NULL, "-", &save);
	}

	strncpy(buf, bind, sizeof buf - 1);
	buf[sizeof buf - 1] = 0;
	save = NULL;

	uint32_t mods = 0;
	for (char *t = strtok_r(buf, "-", &save); t;
	    t = strtok_r(NULL, "-", &save)) {
		if (t == last)
			break;
		mods |= mod_from_token(t);
	}

	int flags = XKB_KEYSYM_CASE_INSENSITIVE;
	xkb_keysym_t sym = xkb_keysym_from_name(last, flags);
	if (sym == XKB_KEY_NoSymbol)
		return -1;

	*mods_out = mods;
	*sym_out = sym;
	return 0;
}

static int push_config_table_from_idx(lua_State *L, int idx_abs)
{
	if (!lua_istable(L, idx_abs))
		return luaL_error(L, "config: expected table at index %d", idx_abs);
	lua_pushvalue(L, idx_abs);
	return 0;
}

static int join_string_array(lua_State *L, int idx_abs, char **out)
{
	*out = NULL;
	if (!lua_istable(L, idx_abs))
		return 0;

	size_t n = (size_t)lua_rawlen(L, idx_abs);
	if (n == 0)
		return 0;

	size_t total = 1;
	for (size_t i = 1; i <= n; ++i) {
		lua_rawgeti(L, idx_abs, (lua_Integer)i);
		size_t len = 0;
		(void)lua_tolstring(L, -1, &len);
		total += len + (i < n ? 1 : 0);
		lua_pop(L, 1);
	}

	char *buf = (char *)malloc(total);
	if (!buf)
		return -1;

	size_t off = 0;
	for (size_t i = 1; i <= n; ++i) {
		lua_rawgeti(L, idx_abs, (lua_Integer)i);
		size_t len = 0;
		char const *s = lua_tolstring(L, -1, &len);
		if (!s) {
			lua_pop(L, 1);
			free(buf);
			return -1;
		}
		memcpy(buf + off, s, len);
		off += len;
		lua_pop(L, 1);
		if (i != n)
			buf[off++] = ',';
	}
	buf[off] = 0;
	*out = buf;
	return 0;
}

int config_load_ref(lua_State *L, int idx, Config *out)
{
	if (!L || !out)
		return -1;

	memset(out, 0, sizeof(*out));

	// ======== DEFAULTS ========
	out->space.offset = (Vector3) { 0, 0, 0 };
	out->space.radius = 1.0f;
	out->space.window_scale = 0.001f;
	out->displays.hud.size = 720;
	out->displays.hud.font_size = 24;
	out->displays.virtual.resolution = (Vector2) { 2560, 1440 };
	out->xwayland.enabled = true;
	out->xwayland.lazy = false;
	out->input.mouse.invert_x = false;
	out->input.mouse.invert_y = false;
	// ====== END DEFAULTS ======

	int cfg_abs = lua_absindex(L, idx);
	if (push_config_table_from_idx(L, cfg_abs) != 0)
		return -1;

	lua_getfield(L, -1, "keybindings");
	if (!lua_istable(L, -1)) {
		lua_pop(L, 2);
		return luaL_error(L, "config: 'keybindings' must be a table (array)");
	}

	size_t n = (size_t)lua_rawlen(L, -1);
	if (n) {
		BindingRef *arr = (BindingRef *)calloc(n, sizeof(*arr));
		if (!arr) {
			lua_pop(L, 2);
			return luaL_error(L, "config: OOM");
		}

		size_t ok = 0;
		for (size_t i = 0; i < n; ++i) {
			lua_rawgeti(L, -1, (lua_Integer)(i + 1));
			if (!lua_istable(L, -1)) {
				lua_pop(L, 1);
				continue;
			}

			lua_getfield(L, -1, "bind");
			char const *bind = lua_tostring(L, -1);
			if (!bind) {
				lua_pop(L, 2);
				continue;
			}

			uint32_t mods = 0;
			xkb_keysym_t sym = XKB_KEY_NoSymbol;
			if (parse_bind(bind, &mods, &sym) != 0) {
				lua_pop(L, 2);
				continue;
			}

			lua_getfield(L, -2, "action");
			if (!lua_isfunction(L, -1)) {
				lua_pop(L, 3);
				continue;
			}

			int ref = luaL_ref(L, LUA_REGISTRYINDEX);
			lua_pop(L, 1);

			arr[ok].mods_mask = mods;
			arr[ok].sym = sym;
			arr[ok].action_ref = ref;
			++ok;

			lua_pop(L, 1);
		}

		if (ok == 0) {
			free(arr);
		} else if (ok < n) {
			BindingRef *shr = (BindingRef *)realloc(arr, ok * sizeof(*shr));
			if (shr)
				arr = shr;
		}

		out->keybindings.items = ok ? arr : NULL;
		out->keybindings.count = ok;
	}
	lua_pop(L, 1);

	lua_getfield(L, -1, "input");
	if (lua_istable(L, -1)) {
		lua_getfield(L, -1, "keyboard");
		if (lua_istable(L, -1)) {
			lua_getfield(L, -1, "xkb_options");
			if (lua_istable(L, -1)) {
				(void)join_string_array(
				    L, lua_absindex(L, -1), &out->input.keyboard.xkb_options);
			}
			lua_pop(L, 1);
		}
		lua_pop(L, 1);

		lua_getfield(L, -1, "mouse");
		if (lua_istable(L, -1)) {
			lua_getfield(L, -1, "invert_x");
			if (lua_isboolean(L, -1))
				out->input.mouse.invert_x = lua_toboolean(L, -1);
			lua_pop(L, 1);

			lua_getfield(L, -1, "invert_y");
			if (lua_isboolean(L, -1))
				out->input.mouse.invert_y = lua_toboolean(L, -1);
			lua_pop(L, 1);
		}
		lua_pop(L, 1);
	}
	lua_pop(L, 1);

	lua_getfield(L, -1, "space");
	if (lua_istable(L, -1)) {
		lua_getfield(L, -1, "offset");
		if (lua_istable(L, -1) || lua_isuserdata(L, -1))
			out->space.offset = lua_readVector3(L, lua_absindex(L, -1));
		lua_pop(L, 1);

		lua_getfield(L, -1, "radius");
		if (lua_isnumber(L, -1))
			out->space.radius = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);

		lua_getfield(L, -1, "window_scale");
		if (lua_isnumber(L, -1))
			out->space.window_scale = (float)lua_tonumber(L, -1);
		lua_pop(L, 1);
	}
	lua_pop(L, 1);

	lua_getfield(L, -1, "xwayland");
	if (lua_istable(L, -1)) {
		lua_getfield(L, -1, "enabled");
		if (lua_isboolean(L, -1))
			out->xwayland.enabled = lua_toboolean(L, -1);
		lua_pop(L, 1);

		lua_getfield(L, -1, "lazy");
		if (lua_isboolean(L, -1))
			out->xwayland.lazy = lua_toboolean(L, -1);
		lua_pop(L, 1);
	}
	lua_pop(L, 1);

	lua_getfield(L, -1, "displays");
	if (lua_istable(L, -1)) {
		lua_getfield(L, -1, "hud");
		if (lua_istable(L, -1)) {
			lua_getfield(L, -1, "size");
			if (lua_isnumber(L, -1))
				out->displays.hud.size = (float)lua_tonumber(L, -1);
			lua_pop(L, 1);

			lua_getfield(L, -1, "font_size");
			if (lua_isnumber(L, -1))
				out->displays.hud.font_size = (float)lua_tonumber(L, -1);
			lua_pop(L, 1);
		}
		lua_pop(L, 1);

		lua_getfield(L, -1, "virtual");
		if (lua_istable(L, -1)) {
			lua_getfield(L, -1, "resolution");
			if (lua_istable(L, -1) || lua_isuserdata(L, -1))
				out->displays.virtual.resolution
				    = lua_readVector2(L, lua_absindex(L, -1));
			lua_pop(L, 1);
		}
		lua_pop(L, 1);
	}
	lua_pop(L, 1);

	lua_getfield(L, -1, "cubemap");
	if (lua_isstring(L, -1)) {
		char const *s = lua_tostring(L, -1);
		if (s && s[0]) {
			(void)dupstr(s, (char **)&out->cubemap);
		}
	}
	lua_pop(L, 1);

	lua_pop(L, 1);
	return 0;
}

void config_unref(lua_State *L, Config *cfg)
{
	if (!cfg)
		return;

	for (size_t i = 0; i < cfg->keybindings.count; ++i) {
		int r = cfg->keybindings.items ? cfg->keybindings.items[i].action_ref
		                               : LUA_NOREF;
		if (r != LUA_NOREF && r != LUA_REFNIL)
			luaL_unref(L, LUA_REGISTRYINDEX, r);
	}
	free(cfg->keybindings.items);
	cfg->keybindings.items = NULL;
	cfg->keybindings.count = 0;

	free(cfg->input.keyboard.xkb_options);
	cfg->input.keyboard.xkb_options = NULL;

	if (cfg->cubemap) {
		free((void *)cfg->cubemap);
		cfg->cubemap = NULL;
	}
}

void config_trigger_ref(lua_State *L, Config *cfg, int ref)
{
	lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
	if (!lua_isfunction(L, -1)) {
		lua_pop(L, 1);
		return;
	}

	if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
		fprintf(stderr, "config: action error: %s\n", lua_tostring(L, -1));
		lua_pop(L, 1);
		return;
	}
}

static int load_config_file(lua_State *L, char const *path)
{
	if (!path || !path[0])
		return -1;

	if (luaL_loadfile(L, path) != LUA_OK) {
		char const *err = lua_tostring(L, -1);
		fprintf(
		    stderr, "config: loadfile failed: %s\n", err ? err : "(unknown)");
		lua_pop(L, 1);
		return -1;
	}
	if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
		char const *err = lua_tostring(L, -1);
		fprintf(stderr, "config: executing '%s' failed: %s\n", path,
		    err ? err : "(unknown)");
		lua_pop(L, 1);
		return -1;
	}
	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		fprintf(stderr, "config: '%s' did not return a table\n", path);
		return -1;
	}
	return 0;
}

ConfigManager *config_manager_create(char const *path)
{
	ConfigManager *cm = (ConfigManager *)calloc(1, sizeof(*cm));
	if (!cm)
		return NULL;

	cm->L = luaL_newstate();
	if (!cm->L) {
		free(cm);
		return NULL;
	}
	luaL_openlibs(cm->L);

	if (!path)
		path = get_config_path();
	if (path && dupstr(path, &cm->path) != 0) {
		lua_close(cm->L);
		free(cm);
		return NULL;
	}

	return cm;
}

void config_manager_destroy(ConfigManager *cm)
{
	if (!cm)
		return;
	config_unref(cm->L, &cm->cfg);
	if (cm->L)
		lua_close(cm->L);
	free(cm->path);
	free(cm);
}

int config_manager_reload(ConfigManager *cm)
{
	if (!cm || !cm->path)
		return -1;

	config_unref(cm->L, &cm->cfg);

	if (load_config_file(cm->L, cm->path) != 0)
		return -1;

	int rc = config_load_ref(cm->L, -1, &cm->cfg);
	lua_pop(cm->L, 1);
	if (rc != 0)
		return rc;

	if (cm->cfg.cubemap && cm->cfg.cubemap[0] != '/') {
		char const *slash = strrchr(cm->path, '/');
		char const *dir = ".";
		size_t dirlen = 1;
		if (slash) {
			dir = cm->path;
			dirlen = (size_t)(slash - cm->path);
		}
		size_t n = dirlen + 1 + strlen(cm->cfg.cubemap)
		    + 1; // dir + '/' + hdri + '\0'
		char *full = (char *)malloc(n);
		if (full) {
			memcpy(full, dir, dirlen);
			full[dirlen] = '/';
			strcpy(full + dirlen + 1, cm->cfg.cubemap);
			free((void *)cm->cfg.cubemap);
			cm->cfg.cubemap = full;
		}
	}

	return 0;
}

lua_State *config_manager_lua(ConfigManager *cm) { return cm ? cm->L : NULL; }
Config const *config_manager_get(ConfigManager *cm)
{
	return cm ? &cm->cfg : NULL;
}
char const *config_manager_path(ConfigManager *cm)
{
	return cm ? cm->path : NULL;
}
