#include <assert.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

#include "def.h"
#include "util.h"

struct str_builder {
	struct array arr;
};

static void str_builder_write_ch(struct str_builder *sb, char ch) {
	char *ptr = array_add(&sb->arr, sizeof(*ptr));
	*ptr = ch;
}

static void str_builder_write_str(struct str_builder *sb, const char *str) {
	size_t len = strlen(str);
	char *ptr = array_add(&sb->arr, len);
	memcpy(ptr, str, len);
}

static char *str_builder_commit(struct str_builder *sb) {
	str_builder_write_ch(sb, '\0');
	char *str = sb->arr.data;
	sb->arr = (struct array){0};
	return str;
}

struct reader {
	FILE *f;
	int last_ch;
	int line;
};

static void log_error(struct reader *r, const char *msg, ...) {
	fprintf(stderr, "line %d: ", r->line);

	va_list args;
	va_start(args, msg);
	vfprintf(stderr, msg, args);
	va_end(args);

	fprintf(stderr, "\n");
}

static int read_ch(struct reader *r) {
	int ch = fgetc(r->f);
	if (ch == EOF && ferror(r->f)) {
		perror("Failed to read file");
	}
	if (ch == '\n') {
		r->line++;
	}
	r->last_ch = ch;
	return ch;
}

static void unread_ch(struct reader *r) {
	assert(r->last_ch != EOF);
	if (r->last_ch == '\n') {
		r->line--;
	}
	ungetc(r->last_ch, r->f);
	r->last_ch = EOF;
}

static bool skip_whitespace(struct reader *r) {
	while (1) {
		int ch = read_ch(r);
		if (ch == EOF) {
			return feof(r->f);
		}

		switch (ch) {
		case ' ':
		case '\t':
		case '\r':
		case '\n':
			break; // skip
		default:
			unread_ch(r);
			return true;
		}
	}
}

static char *read_comment(struct reader *r) {
	if (!skip_whitespace(r)) {
		return false;
	}

	int ch = read_ch(r);
	if (ch == EOF) {
		return NULL;
	} else if (ch != '#') {
		unread_ch(r);
		return NULL;
	}

	struct str_builder sb = {0};
	str_builder_write_ch(&sb, (char)ch);

	while (1) {
		int ch = read_ch(r);
		if (ch == EOF) {
			if (feof(r->f)) {
				break;
			} else {
				free(str_builder_commit(&sb));
				return NULL;
			}
		}

		str_builder_write_ch(&sb, (char)ch);

		if (ch == '\n') {
			break;
		}
	}

	return str_builder_commit(&sb);
}

static bool skip_whitespace_and_comments(struct reader *r) {
	while (1) {
		if (!skip_whitespace(r)) {
			return false;
		}

		char *comment = read_comment(r);
		if (comment == NULL) {
			return true;
		}
		free(comment);
	}
}

static char *read_token(struct reader *r) {
	if (!skip_whitespace_and_comments(r)) {
		return NULL;
	}

	struct str_builder sb = {0};
	while (1) {
		int ch = read_ch(r);
		if (ch == EOF) {
			if (feof(r->f) && sb.arr.size > 0) {
				return str_builder_commit(&sb);
			} else {
				free(str_builder_commit(&sb));
				return NULL;
			}
		}

		switch (ch) {
		case '?':
		case '(':
		case ')':
		case ',':
		case ':':
			if (sb.arr.size > 0) {
				unread_ch(r);
			} else {
				str_builder_write_ch(&sb, (char)ch);
			}
			return str_builder_commit(&sb);
		case ']':
		case '>':
			str_builder_write_ch(&sb, (char)ch);
			return str_builder_commit(&sb);
		case ' ':
		case '\t':
		case '\r':
		case '\n':
		case '#':
			unread_ch(r);
			return str_builder_commit(&sb);
		default:
			str_builder_write_ch(&sb, (char)ch);
		}
	}
}

static bool expect_token(struct reader *r, const char *want) {
	char *got = read_token(r);
	if (got == NULL) {
		return false;
	}
	bool ok = strcmp(got, want) == 0;
	if (!ok) {
		log_error(r, "expected \"%s\", got \"%s\"", want, got);
	}
	free(got);
	return ok;
}

static bool is_alpha(char ch) {
	return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
}

static bool is_alpha_num(char ch) {
	return is_alpha(ch) || (ch >= '0' && ch <= '9');
}

static bool is_interface_name(const char *str) {
	// TODO: be more strict
	if (!is_alpha(str[0])) {
		return false;
	}

	for (size_t i = 0; str[i] != '\0'; i++) {
		char ch = str[i];
		if (!is_alpha_num(ch) && ch != '-' && ch != '.') {
			return false;
		}
	}

	return true;
}

static bool is_name(const char *str) {
	if (str[0] < 'A' || str[0] > 'Z') {
		return false;
	}

	for (size_t i = 0; str[i] != '\0'; i++) {
		if (!is_alpha_num(str[i])) {
			return false;
		}
	}

	return true;
}

static bool is_field_name(const char *str) {
	if (!is_alpha(str[0])) {
		return false;
	}

	for (size_t i = 0; str[i] != '\0'; i++) {
		if (!is_alpha_num(str[i]) && str[i] != '_') {
			return false;
		}
	}

	return true;
}

static char *read_interface_name(struct reader *r) {
	char *tok = read_token(r);
	if (tok == NULL) {
		return NULL;
	} else if (!is_interface_name(tok)) {
		log_error(r, "\"%s\" is not a valid interface name", tok);
		free(tok);
		return NULL;
	}
	return tok;
}

static char *read_name(struct reader *r) {
	char *tok = read_token(r);
	if (tok == NULL) {
		return NULL;
	} else if (!is_name(tok)) {
		log_error(r, "\"%s\" is not a valid name", tok);
		free(tok);
		return NULL;
	}
	return tok;
}

static bool parse_basic_type(enum varlink_kind *out, const char *str) {
	if (strcmp(str, "bool") == 0) {
		*out = VARLINK_BOOL;
	} else if (strcmp(str, "int") == 0) {
		*out = VARLINK_INT;
	} else if (strcmp(str, "float") == 0) {
		*out = VARLINK_FLOAT;
	} else if (strcmp(str, "string") == 0) {
		*out = VARLINK_STRING;
	} else if (strcmp(str, "object") == 0) {
		*out = VARLINK_OBJECT;
	} else {
		return false;
	}
	return true;
}

static bool read_struct_or_enum(struct reader *r, struct varlink_type *out);

static bool read_element_type(struct reader *r, const char *tok, struct varlink_type *out) {
	enum varlink_kind kind;
	if (parse_basic_type(&kind, tok)) {
		*out = (struct varlink_type){ .kind = kind };
		return true;
	}

	if (strcmp(tok, "(") == 0) {
		unread_ch(r);
		return read_struct_or_enum(r, out);
	}

	if (is_name(tok)) {
		*out = (struct varlink_type){ .kind = VARLINK_NAME, .name = strdup(tok) };
		return true;
	}

	log_error(r, "expected element type, got \"%s\"", tok);
	return false;
}

static bool read_type(struct reader *r, struct varlink_type *out) {
	char *tok = read_token(r);
	if (tok == NULL) {
		return false;
	}

	bool nullable = strcmp(tok, "?") == 0;
	if (nullable) {
		free(tok);
		tok = read_token(r);
		if (tok == NULL) {
			return false;
		}
	}

	enum varlink_kind kind;
	if (strcmp(tok, "[]") == 0) {
		kind = VARLINK_ARRAY;
	} else if (strcmp(tok, "[string]") == 0) {
		kind = VARLINK_MAP;
	} else {
		bool ok = read_element_type(r, tok, out);
		free(tok);
		if (!ok) {
			return false;
		}
		out->nullable = nullable;
		return true;
	}
	free(tok);

	struct varlink_type *inner = calloc(1, sizeof(*inner));
	if (inner == NULL) {
		return false;
	}

	if (!read_type(r, inner)) {
		free(inner);
		return false;
	}

	*out = (struct varlink_type){ .kind = kind, .inner = inner, .nullable = nullable };
	return true;
}

static bool read_struct_or_enum(struct reader *r, struct varlink_type *out) {
	if (!expect_token(r, "(")) {
		return false;
	}

	struct array enum_entries = {0};
	struct array field_names = {0};
	struct array field_types = {0};
	struct varlink_type typ = {0};
	while (1) {
		char *tok = read_token(r);
		if (tok == NULL) {
			return false;
		} else if (strcmp(tok, ")") == 0 && typ.kind == 0) {
			// empty parentheses
			free(tok);
			typ.kind = VARLINK_STRUCT;
			break;
		} else if (!is_field_name(tok)) {
			log_error(r, "expected field name, got \"%s\"", tok);
			return false;
		}
		char *name = tok;

		char *sep = read_token(r);
		if (sep == NULL) {
			return false;
		}

		if (typ.kind == 0) {
			if (strcmp(sep, ",") == 0 || strcmp(sep, ")") == 0) {
				typ.kind = VARLINK_ENUM;
			} else if (strcmp(sep, ":") == 0) {
				typ.kind = VARLINK_STRUCT;
			} else {
				log_error(r, "expected one of \",\", \")\" or \":\", got \"%s\"", sep);
				return false;
			}
		} else {
			switch (typ.kind) {
			case VARLINK_ENUM:
				if (strcmp(sep, ",") != 0 && strcmp(sep, ")") != 0) {
					log_error(r, "expected one of \",\" or \")\", got \"%s\"", sep);
					return false;
				}
				break;
			case VARLINK_STRUCT:
				if (strcmp(sep, ":") != 0) {
					log_error(r, "expected \":\", got \"%s\"", sep);
					return false;
				}
				break;
			default:
				abort(); // unreachable
			}
		}

		switch (typ.kind) {
		case VARLINK_ENUM:;
			char **entry = array_add(&enum_entries, sizeof(*entry));
			*entry = name;

			if (strcmp(sep, ")") == 0) {
				free(sep);
				goto out;
			}
			break;
		case VARLINK_STRUCT:;
			struct varlink_type t;
			if (!read_type(r, &t)) {
				return false;
			}

			char **name_ptr = array_add(&field_names, sizeof(*name_ptr));
			struct varlink_type *t_ptr = array_add(&field_types, sizeof(*t_ptr));
			*name_ptr = name;
			*t_ptr = t;

			free(sep);
			sep = read_token(r);
			if (sep == NULL) {
				return false;
			}

			if (strcmp(sep, ")") == 0) {
				free(sep);
				goto out;
			} else if (strcmp(sep, ",") != 0) {
				log_error(r, "expected \",\" or \")\", got \"%s\"", sep);
				return false;
			}

			break;
		default:
			abort(); // unreachable
		}

		free(sep);
	}

out:
	typ.enum_ = (struct varlink_enum){
		.entries = enum_entries.data,
		.entries_len = enum_entries.size / sizeof(char *),
	};
	typ.struct_ = (struct varlink_struct){
		.field_names = field_names.data,
		.field_types = field_types.data,
		.fields_len = field_names.size / sizeof(char *),
	};
	*out = typ;
	return true;
}

static bool read_struct(struct reader *r, struct varlink_struct *out) {
	struct varlink_type t;
	if (!read_struct_or_enum(r, &t)) {
		return false;
	} else if (t.kind != VARLINK_STRUCT) {
		log_error(r, "expected struct, got enum");
		return false;
	}
	*out = t.struct_;
	return true;
}

static bool check_type(const struct varlink_interface *iface, const struct varlink_type *t);

static bool check_struct(const struct varlink_interface *iface, const struct varlink_struct *s) {
	for (size_t i = 0; i < s->fields_len; i++) {
		if (!check_type(iface, &s->field_types[i])) {
			return false;
		}
	}
	return true;
}

static bool check_type(const struct varlink_interface *iface, const struct varlink_type *t) {
	switch (t->kind) {
	case VARLINK_STRUCT:
		return check_struct(iface, &t->struct_);
	case VARLINK_NAME:
		for (size_t i = 0; i < iface->type_aliases_len; i++) {
			if (strcmp(t->name, iface->type_aliases[i].name) == 0) {
				return true;
			}
		}

		fprintf(stderr, "unknown type '%s'\n", t->name);
		return false;
	case VARLINK_ARRAY:
	case VARLINK_MAP:
		return check_type(iface, t->inner);
	default:
		return true;
	}
}

static bool check_interface(const struct varlink_interface *iface) {
	for (size_t i = 0; i < iface->type_aliases_len; i++) {
		if (!check_type(iface, &iface->type_aliases[i].type)) {
			return false;
		}
	}

	for (size_t i = 0; i < iface->methods_len; i++) {
		const struct varlink_method *method = &iface->methods[i];
		if (!check_struct(iface, &method->in) || !check_struct(iface, &method->out)) {
			return false;
		}
	}

	for (size_t i = 0; i < iface->errors_len; i++) {
		if (!check_struct(iface, &iface->errors[i].struct_)) {
			return false;
		}
	}

	return true;
}

static char *read_description(struct reader *r) {
	struct str_builder sb = {0};
	while (1) {
		int prev_line = r->line;
		if (!skip_whitespace(r)) {
			free(str_builder_commit(&sb));
			return NULL;
		}
		if (r->line != prev_line) {
			sb.arr.size = 0;
		}

		char *comment = read_comment(r);
		if (comment == NULL) {
			break;
		}

		assert(comment[0] == '#');
		const char *text = &comment[1];
		if (text[0] == ' ') {
			text = &text[1];
		}
		str_builder_write_str(&sb, text);
		free(comment);
	}

	if (sb.arr.size == 0) {
		free(str_builder_commit(&sb));
		return NULL;
	}
	return str_builder_commit(&sb);
}

static bool read_interface(struct reader *r, struct varlink_interface *out) {
	char *description = read_description(r);

	if (!expect_token(r, "interface")) {
		return false;
	}

	char *name = read_interface_name(r);
	if (name == NULL) {
		return false;
	}

	struct array type_aliases = {0};
	struct array methods = {0};
	struct array errors = {0};

	char *keyword = NULL;
	while (1) {
		char *description = read_description(r);

		keyword = read_token(r);
		if (keyword == NULL) {
			break;
		}

		if (strcmp(keyword, "type") == 0) {
			char *name = read_name(r);
			if (name == NULL) {
				return false;
			}
			struct varlink_type t;
			if (!read_struct_or_enum(r, &t)) {
				free(name);
				return false;
			}

			struct varlink_type_alias *type_alias_entry = array_add(&type_aliases, sizeof(*type_alias_entry));
			*type_alias_entry = (struct varlink_type_alias){
				.name = name,
				.description = description,
				.type = t,
			};
		} else if (strcmp(keyword, "method") == 0) {
			char *name = read_name(r);
			if (name == NULL) {
				return false;
			}
			struct varlink_struct in;
			if (!read_struct(r, &in)) {
				return false;
			}
			if (!expect_token(r, "->")) {
				return false;
			}
			struct varlink_struct out;
			if (!read_struct(r, &out)) {
				return false;
			}

			struct varlink_method *method_entry = array_add(&methods, sizeof(*method_entry));
			*method_entry = (struct varlink_method){
				.name = name,
				.description = description,
				.in = in,
				.out = out,
			};
		} else if (strcmp(keyword, "error") == 0) {
			char *name = read_name(r);
			if (name == NULL) {
				return false;
			}
			struct varlink_struct s;
			if (!read_struct(r, &s)) {
				return false;
			}

			struct varlink_error *error_entry = array_add(&errors, sizeof(*error_entry));
			*error_entry = (struct varlink_error){
				.name = name,
				.description = description,
				.struct_ = s,
			};
		} else {
			log_error(r, "expected one of \"type\", \"method\", \"error\", got \"%s\"", keyword);
			return false;
		}

		free(keyword);
	}
	free(keyword);

	struct varlink_interface iface = {
		.name = name,
		.description = description,
		.type_aliases = type_aliases.data,
		.type_aliases_len = type_aliases.size / sizeof(struct varlink_type_alias),
		.methods = methods.data,
		.methods_len = methods.size / sizeof(struct varlink_method),
		.errors = errors.data,
		.errors_len = errors.size / sizeof(struct varlink_error),
	};

	if (!check_interface(&iface)) {
		return false;
	}

	*out = iface;
	return true;
}

struct varlink_interface *varlink_interface_read(FILE *f) {
	struct reader r = { .f = f, .last_ch = EOF, .line = 1 };

	struct varlink_interface *iface = calloc(1, sizeof(*iface));
	if (iface == NULL) {
		return NULL;
	}

	if (!read_interface(&r, iface)) {
		return NULL;
	}

	if (!feof(r.f)) {
		varlink_interface_destroy(iface);
		return NULL;
	}

	return iface;
}

static void finish_type(struct varlink_type *t);

static void finish_struct(struct varlink_struct *s) {
	for (size_t i = 0; i < s->fields_len; i++) {
		free(s->field_names[i]);
		finish_type(&s->field_types[i]);
	}
	free(s->field_names);
	free(s->field_types);
}

static void finish_enum(struct varlink_enum *e) {
	for (size_t i = 0; i < e->entries_len; i++) {
		free(e->entries[i]);
	}
	free(e->entries);
}

static void finish_type(struct varlink_type *t) {
	if (t->inner != NULL) {
		finish_type(t->inner);
		free(t->inner);
	}

	free(t->name);
	finish_struct(&t->struct_);
	finish_enum(&t->enum_);
}

static void finish_method(struct varlink_method *method) {
	free(method->name);
	free(method->description);
	finish_struct(&method->in);
	finish_struct(&method->out);
}

static void finish_type_alias(struct varlink_type_alias *type_alias) {
	free(type_alias->name);
	free(type_alias->description);
	finish_type(&type_alias->type);
}

static void finish_error(struct varlink_error *error) {
	free(error->name);
	free(error->description);
	finish_struct(&error->struct_);
}

void varlink_interface_destroy(struct varlink_interface *iface) {
	for (size_t i = 0; i < iface->type_aliases_len; i++) {
		finish_type_alias(&iface->type_aliases[i]);
	}
	free(iface->type_aliases);

	for (size_t i = 0; i < iface->methods_len; i++) {
		finish_method(&iface->methods[i]);
	}
	free(iface->methods);

	for (size_t i = 0; i < iface->errors_len; i++) {
		finish_error(&iface->errors[i]);
	}
	free(iface->errors);

	free(iface->name);
	free(iface->description);
	free(iface);
}
