#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wordexp.h>

#include <wayland-client.h>

#include "config.h"
#include "parser.h"

static const char *token_type_str(enum kanshi_token_type t) {
	switch (t) {
	case KANSHI_TOKEN_LBRACKET:
		return "'{'";
	case KANSHI_TOKEN_RBRACKET:
		return "'}'";
	case KANSHI_TOKEN_STR:
		return "string";
	case KANSHI_TOKEN_NEWLINE:
		return "newline";
	}
	abort();
}

static int parser_read_char(struct kanshi_parser *parser) {
	if (parser->next >= 0) {
		int ch = parser->next;
		parser->next = -1;
		return ch;
	}

	int ch = fgetc(parser->f);
	if (ch == EOF) {
		if (errno != 0) {
			fprintf(stderr, "fgetc failed: %s\n", strerror(errno));
		} else {
			return '\0';
		}
		return -1;
	}

	if (ch == '\n') {
		parser->line++;
		parser->col = 0;
	} else {
		parser->col++;
	}

	return ch;
}

static int parser_peek_char(struct kanshi_parser *parser) {
	int ch = parser_read_char(parser);
	parser->next = ch;
	return ch;
}

static bool parser_append_tok_ch(struct kanshi_parser *parser, char ch) {
	// Always keep enough room for a terminating NULL char
	if (parser->tok_str_len + 1 >= sizeof(parser->tok_str)) {
		fprintf(stderr, "string too long\n");
		return false;
	}
	parser->tok_str[parser->tok_str_len] = ch;
	parser->tok_str_len++;
	return true;
}

static bool parser_read_quoted(struct kanshi_parser *parser) {
	while (1) {
		int ch = parser_read_char(parser);
		if (ch < 0) {
			return false;
		} else if (ch == '\0') {
			fprintf(stderr, "unterminated quoted string\n");
			return false;
		}

		if (ch == '"') {
			parser->tok_str[parser->tok_str_len] = '\0';
			return true;
		}

		if (!parser_append_tok_ch(parser, ch)) {
			return false;
		}
	}
}

static void parser_ignore_line(struct kanshi_parser *parser) {
	while (1) {
		int ch = parser_read_char(parser);
		if (ch < 0) {
			return;
		}

		if (ch == '\n' || ch == '\0') {
			return;
		}
	}
}

static bool parser_read_line(struct kanshi_parser *parser) {
	while (1) {
		int ch = parser_peek_char(parser);
		if (ch < 0) {
			return false;
		}

		if (ch == '\n' || ch == '\0') {
			parser->tok_str[parser->tok_str_len] = '\0';
			return true;
		}

		if (!parser_append_tok_ch(parser, parser_read_char(parser))) {
			return false;
		}
	}
}

static bool parser_read_str(struct kanshi_parser *parser) {
	while (1) {
		int ch = parser_peek_char(parser);
		if (ch < 0) {
			return false;
		}

		if (isspace(ch) || ch == '{' || ch == '}' || ch == '\0') {
			parser->tok_str[parser->tok_str_len] = '\0';
			return true;
		}

		if (!parser_append_tok_ch(parser, parser_read_char(parser))) {
			return false;
		}
	}
}

static bool parser_next_token(struct kanshi_parser *parser) {
	while (1) {
		int ch = parser_read_char(parser);
		if (ch < 0) {
			return false;
		}

		if (ch == '{') {
			parser->tok_type = KANSHI_TOKEN_LBRACKET;
			return true;
		} else if (ch == '}') {
			parser->tok_type = KANSHI_TOKEN_RBRACKET;
			return true;
		} else if (ch == '\n') {
			parser->tok_type = KANSHI_TOKEN_NEWLINE;
			return true;
		} else if (isspace(ch)) {
			continue;
		} else if (ch == '"') {
			parser->tok_type = KANSHI_TOKEN_STR;
			parser->tok_str_len = 0;
			return parser_read_quoted(parser);
		} else if (ch == '#') {
			parser_ignore_line(parser);
			parser->tok_type = KANSHI_TOKEN_NEWLINE;
			return true;
		} else {
			parser->tok_type = KANSHI_TOKEN_STR;
			parser->tok_str[0] = ch;
			parser->tok_str_len = 1;
			return parser_read_str(parser);
		}
	}
}

static bool parser_expect_token(struct kanshi_parser *parser,
		enum kanshi_token_type want) {
	if (!parser_next_token(parser)) {
		return false;
	}
	if (parser->tok_type != want) {
		fprintf(stderr, "expected %s, got %s\n",
			token_type_str(want), token_type_str(parser->tok_type));
		return false;
	}
	return true;
}

static bool parse_int(int *dst, const char *str) {
	char *end;
	errno = 0;
	int v = strtol(str, &end, 10);
	if (errno != 0 || end[0] != '\0' || str[0] == '\0') {
		return false;
	}
	*dst = v;
	return true;
}

static bool parse_mode(struct kanshi_profile_output *output, char *str) {
	const char *width = strtok(str, "x");
	const char *height = strtok(NULL, "@");
	const char *refresh = strtok(NULL, "");

	if (width == NULL || height == NULL) {
		fprintf(stderr, "invalid output mode: missing width/height\n");
		return false;
	}

	if (!parse_int(&output->mode.width, width)) {
		fprintf(stderr, "invalid output mode: invalid width\n");
		return false;
	}
	if (!parse_int(&output->mode.height, height)) {
		fprintf(stderr, "invalid output mode: invalid height\n");
		return false;
	}

	if (refresh != NULL) {
		char *end;
		errno = 0;
		float v = strtof(refresh, &end);
		if (errno != 0 || (end[0] != '\0' && strcmp(end, "Hz") != 0) ||
				str[0] == '\0') {
			fprintf(stderr, "invalid output mode: invalid refresh rate\n");
			return false;
		}
		output->mode.refresh = v * 1000;
	}

	return true;
}

static bool parse_position(struct kanshi_profile_output *output, char *str) {
	const char *x = strtok(str, ",");
	const char *y = strtok(NULL, "");

	if (x == NULL || y == NULL) {
		fprintf(stderr, "invalid output position: missing x/y\n");
		return false;
	}

	if (!parse_int(&output->position.x, x)) {
		fprintf(stderr, "invalid output position: invalid x\n");
		return false;
	}
	if (!parse_int(&output->position.y, y)) {
		fprintf(stderr, "invalid output position: invalid y\n");
		return false;
	}

	return true;
}

static bool parse_float(float *dst, const char *str) {
	char *end;
	errno = 0;
	float v = strtof(str, &end);
	if (errno != 0 || end[0] != '\0' || str[0] == '\0') {
		return false;
	}
	*dst = v;
	return true;
}

static bool parse_transform(enum wl_output_transform *dst, const char *str) {
	if (strcmp(str, "normal") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_NORMAL;
	} else if (strcmp(str, "90") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_90;
	} else if (strcmp(str, "180") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_180;
	} else if (strcmp(str, "270") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_270;
	} else if (strcmp(str, "flipped") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_FLIPPED;
	} else if (strcmp(str, "flipped-90") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_FLIPPED_90;
	} else if (strcmp(str, "flipped-180") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_FLIPPED_180;
	} else if (strcmp(str, "flipped-270") == 0) {
		*dst = WL_OUTPUT_TRANSFORM_FLIPPED_270;
	} else {
		return false;
	}
	return true;
}

static struct kanshi_profile_output *parse_profile_output(
		struct kanshi_parser *parser) {
	struct kanshi_profile_output *output = calloc(1, sizeof(*output));

	if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) {
		return NULL;
	}
	output->name = strdup(parser->tok_str);

	bool has_key = false;
	enum kanshi_output_field key = 0;
	while (1) {
		if (!parser_next_token(parser)) {
			return NULL;
		}

		switch (parser->tok_type) {
		case KANSHI_TOKEN_STR:
			if (has_key) {
				char *value = parser->tok_str;
				switch (key) {
				case KANSHI_OUTPUT_MODE:
					if (!parse_mode(output, value)) {
						return NULL;
					}
					break;
				case KANSHI_OUTPUT_POSITION:
					if (!parse_position(output, value)) {
						return NULL;
					}
					break;
				case KANSHI_OUTPUT_SCALE:
					if (!parse_float(&output->scale, value)) {
						fprintf(stderr, "invalid output scale\n");
						return NULL;
					}
					break;
				case KANSHI_OUTPUT_TRANSFORM:
					if (!parse_transform(&output->transform, value)) {
						fprintf(stderr, "invalid output transform\n");
						return NULL;
					}
					break;
				default:
					abort();
				}
				has_key = false;
				output->fields |= key;
			} else {
				has_key = true;
				const char *key_str = parser->tok_str;
				if (strcmp(key_str, "enable") == 0) {
					output->enabled = true;
					output->fields |= KANSHI_OUTPUT_ENABLED;
					has_key = false;
				} else if (strcmp(key_str, "disable") == 0) {
					output->enabled = false;
					output->fields |= KANSHI_OUTPUT_ENABLED;
					has_key = false;
				} else if (strcmp(key_str, "mode") == 0) {
					key = KANSHI_OUTPUT_MODE;
				} else if (strcmp(key_str, "position") == 0) {
					key = KANSHI_OUTPUT_POSITION;
				} else if (strcmp(key_str, "scale") == 0) {
					key = KANSHI_OUTPUT_SCALE;
				} else if (strcmp(key_str, "transform") == 0) {
					key = KANSHI_OUTPUT_TRANSFORM;
				} else {
					fprintf(stderr,
						"unknown directive '%s' in profile output '%s'\n",
						key_str, output->name);
					return NULL;
				}
			}
			break;
		case KANSHI_TOKEN_NEWLINE:
			return output;
		default:
			fprintf(stderr, "unexpected %s in output\n",
				token_type_str(parser->tok_type));
			return NULL;
		}
	}
}

static struct kanshi_profile_command *parse_profile_command(
		struct kanshi_parser *parser) {
	// Skip the 'exec' directive.
	if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) {
		return NULL;
	}

	if (!parser_read_line(parser)) {
		return NULL;
	}

	if (parser->tok_str_len <= 0) {
		fprintf(stderr, "Ignoring empty command in config file on line %d\n",
			parser->line);
		return NULL;
	}

	struct kanshi_profile_command *command = calloc(1, sizeof(*command));
	command->command = strdup(parser->tok_str);
	return command;
}

static struct kanshi_profile *parse_profile(struct kanshi_parser *parser) {
	struct kanshi_profile *profile = calloc(1, sizeof(*profile));
	wl_list_init(&profile->outputs);
	wl_list_init(&profile->commands);

	if (!parser_next_token(parser)) {
		return NULL;
	}

	switch (parser->tok_type) {
	case KANSHI_TOKEN_LBRACKET:
		break;
	case KANSHI_TOKEN_STR:
		// Parse an optional profile name
		profile->name = strdup(parser->tok_str);
		if (!parser_expect_token(parser, KANSHI_TOKEN_LBRACKET)) {
			return NULL;
		}
		break;
	default:
		fprintf(stderr, "unexpected %s, expected '{' or a profile name\n",
			token_type_str(parser->tok_type));
	}

	// Use the bracket position to generate a default profile name
	if (profile->name == NULL) {
		char generated_name[100];
		int ret = snprintf(generated_name, sizeof(generated_name),
				"<anonymous at line %d, col %d>", parser->line, parser->col);
		if (ret >= 0) {
			profile->name = strdup(generated_name);
		} else {
			profile->name = strdup("<anonymous>");
		}
	}

	// Parse the profile commands until the closing bracket
	while (1) {
		if (!parser_next_token(parser)) {
			return NULL;
		}

		switch (parser->tok_type) {
		case KANSHI_TOKEN_RBRACKET:
			return profile;
		case KANSHI_TOKEN_STR:;
			const char *directive = parser->tok_str;
			if (strcmp(directive, "output") == 0) {
				struct kanshi_profile_output *output =
					parse_profile_output(parser);
				if (output == NULL) {
					return NULL;
				}
				// Store wildcard outputs at the end of the list
				if (strcmp(output->name, "*") == 0) {
					wl_list_insert(profile->outputs.prev, &output->link);
				} else {
					wl_list_insert(&profile->outputs, &output->link);
				}
			} else if (strcmp(directive, "exec") == 0) {
				struct kanshi_profile_command *command =
					parse_profile_command(parser);
				if (command == NULL) {
					return NULL;
				}
				// Insert commands at the end to preserve order
				wl_list_insert(profile->commands.prev, &command->link);
			} else {
				fprintf(stderr, "unknown directive '%s' in profile '%s'\n",
					directive, profile->name);
				return NULL;
			}
			break;
		case KANSHI_TOKEN_NEWLINE:
			break; // No-op
		default:
			fprintf(stderr, "unexpected %s in profile '%s'\n",
				token_type_str(parser->tok_type), profile->name);
			return NULL;
		}
	}
}

static bool parse_config_file(const char *path, struct kanshi_config *config);

static bool parse_include_command(struct kanshi_parser *parser, struct kanshi_config *config) {
	// Skip the 'include' directive.
	if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) {
		return false;
	}

	if (!parser_read_line(parser)) {
		return false;
	}

	if (parser->tok_str_len <= 0) {
		return true;
	}

	wordexp_t p;
	if (wordexp(parser->tok_str, &p, WRDE_SHOWERR | WRDE_UNDEF) != 0) {
		fprintf(stderr, "Could not expand include path: '%s'\n", parser->tok_str);
		return false;
	}

	char **w = p.we_wordv;
	for (size_t idx = 0; idx < p.we_wordc; idx++) {
		if (!parse_config_file(w[idx], config)) {
			fprintf(stderr, "Could not parse included config: '%s'\n", w[idx]);
			wordfree(&p);
			return false;
		}
	}
	wordfree(&p);
	return true;
}

static bool _parse_config(struct kanshi_parser *parser, struct kanshi_config *config) {
	while (1) {
		int ch = parser_peek_char(parser);
		if (ch < 0) {
			return false;
		} else if (ch == 0) {
			return true;
		} else if (ch == '#') {
			parser_ignore_line(parser);
			continue;
		} else if (isspace(ch)) {
			parser_read_char(parser);
			continue;
		}

		if (ch == '{') {
			// Legacy profile syntax without a profile directive
			struct kanshi_profile *profile = parse_profile(parser);
			if (!profile) {
				return false;
			}
			wl_list_insert(config->profiles.prev, &profile->link);
		} else {
			if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) {
				return false;
			}

			const char *directive = parser->tok_str;
			if (strcmp(parser->tok_str, "profile") == 0) {
				struct kanshi_profile *profile = parse_profile(parser);
				if (!profile) {
					return false;
				}
				wl_list_insert(config->profiles.prev, &profile->link);
			} else if (strcmp(parser->tok_str, "include") == 0) {
				if (!parse_include_command(parser, config)) {
					return false;
				}
			} else {
				fprintf(stderr, "unknown directive '%s'\n", directive);
				return false;
			}
		}
	}
}

static bool parse_config_file(const char *path, struct kanshi_config *config) {
	FILE *f = fopen(path, "r");
	if (f == NULL) {
		fprintf(stderr, "failed to open file %s: %s\n",
			path,
			strerror(errno));
		return false;
	}

	struct kanshi_parser parser = {
		.f = f,
		.next = -1,
		.line = 1,
	};

	bool res = _parse_config(&parser, config);
	fclose(f);
	if (!res) {
		fprintf(stderr, "failed to parse config file: "
			"error on line %d, column %d\n", parser.line, parser.col);
		return false;
	}

	return true;
}

struct kanshi_config *parse_config(const char *path) {
	struct kanshi_config *config = calloc(1, sizeof(*config));
	if (config == NULL) {
		return NULL;
	}
	wl_list_init(&config->profiles);

	if (!parse_config_file(path, config)) {
		free(config);
		return NULL;
	}

	return config;
}
