#include <err.h>
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>

#define TODO() errx(EXIT_FAILURE, "%s:%d: todo", __FILE__,  __LINE__)

struct fwdval;
struct fwdvals;

struct fwdcnt {
	size_t pc;
	struct fwdvalues *ctx;
};

enum fwdtype {
	FWDSCRIPT_NUM,
	FWDSCRIPT_STR,
	FWDSCRIPT_CNT,
	FWDSCRIPT_TBL,
	FWDSCRIPT_OPQ
};

static const char *fwdtype_str(enum fwdtype type)
{
	switch (type) {
	case FWDSCRIPT_NUM: return "/num";
	case FWDSCRIPT_STR: return "/str";
	case FWDSCRIPT_CNT: return "/cnt";
	case FWDSCRIPT_TBL: return "/tbl";
	case FWDSCRIPT_OPQ: return "/opq";
	default: break;
	}

	return "/???";
}

struct fwdval {
	enum fwdtype kind;

	union {
		double num;
		char *str;
		struct fwdcnt cnt;
		struct fwdtbl *tbl;
		void *opq;
	};
};

#define FWDSTR(s) (struct fwdval){.kind = FWDSCRIPT_STR, .str = (s)}

static int fwdcmp(const struct fwdval *a, const struct fwdval *b)
{
	if (a->kind != b->kind)
		return a->kind - b->kind;

	switch (a->kind) {
	case FWDSCRIPT_NUM: return a->num - b->num;
	case FWDSCRIPT_STR: return strcmp(a->str, b->str);
	case FWDSCRIPT_CNT: return a->cnt.pc - b->cnt.pc;
	case FWDSCRIPT_TBL: return (intptr_t)a->tbl - (intptr_t)b->tbl;
	case FWDSCRIPT_OPQ: return (intptr_t)a->opq - (intptr_t)b->opq;
	default: abort();
	}

	return 0;
}

static int fwdhash(const struct fwdval *a)
{
	switch (a->kind) {
	/* trash hashes, lol */
	case FWDSCRIPT_NUM: return a->num * 15647839;
	case FWDSCRIPT_STR: return a->str[0];
	case FWDSCRIPT_CNT: return a->cnt.pc * 15647839;
	case FWDSCRIPT_TBL: return 10;
	case FWDSCRIPT_OPQ: return (intptr_t)a->opq;
	default: abort();
	}

	return 0;
}

#define MAP_KEY struct fwdval
#define MAP_TYPE struct fwdval
#define MAP_CMP(A, B) fwdcmp(&(A), &(B))
#define MAP_HASH(A) fwdhash(&(A))
#define MAP_NAME fwdtbl
#include <conts/map.h>

#define MAP_KEY char *
#define MAP_TYPE size_t
#define MAP_CMP(A, B) strcmp((A), (B))
#define MAP_HASH(A) (A)[0]
#define MAP_NAME fwdfuncs
#include <conts/map.h>

#define VEC_TYPE struct fwdval
#define VEC_NAME fwdvals
#include <conts/vec.h>

#define VEC_TYPE enum fwdtype
#define VEC_NAME fwdtypes
#include <conts/vec.h>

struct fwdinsn {
	/* use args[0] as destination, if /cnt then call the
	 * continuation, if /opq call builtin, otherwise abort */
	struct fwdvals args;
};

#define VEC_TYPE struct fwdinsn
#define VEC_NAME fwdinsns
#include <conts/vec.h>

struct fwdscript;

typedef int (*fwd_typecheck_t)(struct fwdscript *s, struct fwdtypes *types);
typedef int (*fwd_callback_t)(struct fwdscript *s, struct fwdvals *args);

struct fwdbuiltin {
	fwd_callback_t cb;
	fwd_typecheck_t tp;
};

#define MAP_KEY char *
#define MAP_TYPE struct fwdbuiltin
#define MAP_CMP(A, B) strcmp((A), (B))
#define MAP_HASH(A) (A)[0]
#define MAP_NAME fwdbuiltins
#include <conts/map.h>

#define VEC_TYPE struct fwdast *
#define VEC_NAME fwdrefs
#include <conts/vec.h>

struct fwdctx {
	struct fwdfuncs defs;
	struct fwdrefs refs;
};

static struct fwdctx fwdctx_create()
{
	return (struct fwdctx){
		.defs = fwdfuncs_create(0),
		.refs = fwdrefs_create(0)
	};
}

static void fwdctx_destroy(struct fwdctx *ctx)
{
	fwdfuncs_destroy(&ctx->defs);
	fwdrefs_destroy(&ctx->refs);
}

static struct fwdast *fwdctx_lookup(struct fwdctx *global, struct fwdctx *local, char *name)
{
	size_t *ref = NULL;
	if (local)
	if ((ref = fwdfuncs_find(&local->defs, name)) != NULL)
		return *fwdrefs_at(&local->refs, *ref);

	if (global)
	if ((ref = fwdfuncs_find(&global->defs, name)) != NULL)
		return *fwdrefs_at(&global->refs, *ref);

	return NULL;
}

struct fwdscript {
	struct fwdctx ctx;
	struct fwdinsns insns;
	struct fwdbuiltins builtins;
};

static struct fwdscript fwdscript_create()
{
	struct fwdscript inst = {
		.insns = fwdinsns_create(1),
		.builtins = fwdbuiltins_create(1),
		.ctx = fwdctx_create()
	};

	return inst;
}

static void *fwdscript_move(void **p)
{
	void *c = *p;
	*p = NULL;
	return c;
}

static int fwdscript_register(struct fwdscript *s, const char *name,
		fwd_callback_t cb, fwd_typecheck_t tp)
{
	struct fwdbuiltin builtin = {
		.cb = cb,
		.tp = tp
	};

	char *dname = strdup(name);
	if (!dname) {
		fprintf(stderr, "failed duping register name\n");
		return -1;
	}

	struct fwdbuiltin *b = fwdbuiltins_insert(&s->builtins,
			dname, builtin);

	if (b == NULL) {
		fprintf(stderr, "failed registering %s\n", name);
		free(dname);
		return -1;
	}

	return 0;
}

static int fwdscript_get(struct fwdscript *s, struct fwdvals *args)
{
	TODO();
}

static int fwdscript_get_typecheck(struct fwdscript *s, struct fwdtypes *types)
{
	(void)s;
	size_t l = fwdtypes_len(types);
	if (l != 4) {
		/** @todo better error messages? */
		fprintf(stderr, "expected 4 args, got %zu\n", l);
		return -1;
	}

	enum fwdtype table = *fwdtypes_at(types, 0);
	/* index can be whatever but would look something like */
	//enum fwdtype index = fwdtypes_at(types, 1);
	enum fwdtype err   = *fwdtypes_at(types, 2);
	enum fwdtype ok    = *fwdtypes_at(types, 3);

	if (table != FWDSCRIPT_TBL) {
		fprintf(stderr, "expected /tbl, got %s\n",
				fwdtype_str(table));
		return -1;
	}

	if (err != FWDSCRIPT_CNT) {
		fprintf(stderr, "expected /cnt, got %s\n",
				fwdtype_str(err));
		return -1;
	}

	if (ok != FWDSCRIPT_CNT) {
		fprintf(stderr, "expected /cnt, got %s\n",
				fwdtype_str(ok));
		return -1;
	}

	/* check cnt args at runtime */
	return 0;
}

static int fwdscript_abort(struct fwdscript *s, struct fwdvals *args)
{
	TODO();
}

static int fwdscript_abort_typecheck(struct fwdscript *s, struct fwdtypes *types)
{
	(void)s;

	/* I guess we could also accept anything that's convertible to string,
	 * so I guess number for now? */
	size_t l = fwdtypes_len(types);
	if (l != 1) {
		fprintf(stderr, "expected 1 arg, got %zu\n", l);
		return -1;
	}

	enum fwdtype msg = *fwdtypes_at(types, 0);
	if (msg != FWDSCRIPT_STR) {
		fprintf(stderr, "expected /str, got %s\n",
				fwdtype_str(msg));
		return -1;
	}

	return 0;
}

static int fwdscript_print(struct fwdscript *s, struct fwdvals *args)
{
	TODO();
}

static int fwdscript_print_typecheck(struct fwdscript *s, struct fwdtypes *args)
{
	(void)s;

	/* we take any number of args as long as they're printable */
	foreach(fwdtypes, type, args) {
		if (*type != FWDSCRIPT_STR && *type != FWDSCRIPT_NUM) {
			fprintf(stderr, "arg not printable\n");
			return -1;
		}
	}

	return 0;
}

static int fwdscript_register_builtins(struct fwdscript *s)
{
	if (fwdscript_register(s, "get", fwdscript_get, fwdscript_get_typecheck))
		return -1;

	if (fwdscript_register(s, "abort", fwdscript_abort, fwdscript_abort_typecheck))
		return -1;

	if (fwdscript_register(s, "print", fwdscript_print, fwdscript_print_typecheck))
		return -1;

	return 0;
}

static const char *skip_ignored(const char *buf)
{
	while (*buf && isspace(*buf) && *buf != '\n')
		buf++;

	if (*buf == '#')
	while (*buf && *buf != '\n')
		buf++;

	return buf;
}

static int separator(int c)
{
	return c == ';' || c == '{' || c == '}'
	    || c == ';' || c == '=' || c == '"'
	    || c == '\n';
}

static const char *next_separator(const char *buf)
{
	while (*buf && !isspace(*buf) && !separator(*buf))
		buf++;

	return buf;
}

enum fwdtokentype {
	FWDTOKEN_ID,
	FWDTOKEN_STR,
	FWDTOKEN_NUM,
	FWDTOKEN_OPEN,
	FWDTOKEN_CLOSE,
	FWDTOKEN_TO,
	FWDTOKEN_END,
	FWDTOKEN_EOL,

	/* special token used for peeking */
	FWDTOKEN_EOF
};

struct fwdtoken {
	enum fwdtokentype kind;

	size_t line;
	union {
		char *s;
		double d;
	};
};

#define VEC_TYPE struct fwdtoken
#define VEC_NAME fwdtokens
#include <conts/vec.h>

static int fwdscript_tokenize(struct fwdtokens *tokens, const char *buf)
{
	size_t line = 0;

	const char *start = buf;
	while (*start) {
		const char *tok = skip_ignored(start);
		if (!*tok)
			break;

		if (*tok == '\n') {
			struct fwdtoken eol = {
				.kind = FWDTOKEN_EOL,
				.line = line,
				.s    = NULL
			};

			if (!fwdtokens_append(tokens, eol)) {
				fprintf(stderr, "failed appending eol token\n");
				return -1;
			}

			line += 1;
			start = tok + 1;
			continue;
		}

		if (*tok == ';') {
			struct fwdtoken end = {
				.kind = FWDTOKEN_END,
				.line = line,
				.s    = NULL
			};

			if (!fwdtokens_append(tokens, end)) {
				fprintf(stderr, "failed appending end token\n");
				return -1;
			}

			start = tok + 1;
			continue;
		}

		if (*tok == '{') {
			struct fwdtoken open = {
				.kind = FWDTOKEN_OPEN,
				.line = line,
				.s    = NULL
			};

			if (!fwdtokens_append(tokens, open)) {
				fprintf(stderr, "failed appending open token\n");
				return -1;
			}

			start = tok + 1;
			continue;
		}

		if (*tok == '}') {
			struct fwdtoken close = {
				.kind = FWDTOKEN_CLOSE,
				.line = line,
				.s    = NULL
			};

			if (!fwdtokens_append(tokens, close)) {
				fprintf(stderr, "failed appending close token\n");
				return -1;
			}

			start = tok + 1;
			continue;
		}

		if (*tok == '=' && *(tok + 1) == '>') {
			struct fwdtoken to = {
				.kind = FWDTOKEN_TO,
				.line = line,
				.s    = NULL
			};

			if (!fwdtokens_append(tokens, to)) {
				fprintf(stderr, "failed appending to token\n");
				return -1;
			}

			start = tok + 2;
			continue;
		}

		if (*tok == '"') {
			size_t start_line = line;
			const char *end = tok + 1;
			while (*end && *end != '"') {
				if (*end == '\n') {
					line++;

					while (isblank(*end))
						end++;
				}

				if (*end == '\'')
					end++;

				end++;
			}

			if (!*end) {
				fprintf(stderr, "unterminated string at %zu\n", start_line);
				return -1;
			}

			char *s = strndup(tok + 1, end - tok - 1);
			if (!s) {
				fprintf(stderr, "failed duping str token\n");
				return -1;
			}

			struct fwdtoken str = {
				.kind = FWDTOKEN_STR,
				.line = start_line,
				.s    = s
			};

			if (!fwdtokens_append(tokens, str)) {
				fprintf(stderr, "failed appending str token\n");
				free(s);
				return -1;
			}

			start = end + 1;
			continue;
		}

		/* otherwise, read until next separation and check if we're
		 * maybe dealing with a number or a regular identifier */
		const char *end = next_separator(tok);

		char *dparse = NULL;
		double d = strtod(tok, &dparse);
		if (end == dparse) {
			/* all of the non-separators were consumed, so we have a
			 * number */
			struct fwdtoken num = {
				.kind = FWDTOKEN_NUM,
				.line = line,
				.d    = d
			};

			if (!fwdtokens_append(tokens, num)) {
				fprintf(stderr, "failed appending num token\n");
				return -1;
			}

			start = end + 1;
			continue;
		}

		char *s = strndup(tok, end - tok);
		if (!s) {
			fprintf(stderr, "failed duping id token\n");
			return -1;
		}

		struct fwdtoken id = {
			.kind = FWDTOKEN_ID,
			.line = line,
			.s    = s
		};

		if (!fwdtokens_append(tokens, id)) {
			fprintf(stderr, "failed appending id token\n");
			free(s);
			return -1;
		}

		start = end;
	}

	return 0;
}

static void free_tokens(struct fwdtokens *tokens)
{
	foreach(fwdtokens, token, tokens) {
		switch (token->kind) {
		case FWDTOKEN_STR:
		case FWDTOKEN_ID: free(token->s);
		default: break;
		}
	}

	fwdtokens_destroy(tokens);
}

struct fwdast {
	enum {
		FWDAST_ID,
		FWDAST_VAR,
		FWDAST_STR,
		FWDAST_NUM,
		FWDAST_CNT,
		FWDAST_CMD,
		FWDAST_TOP
	} kind;

	char *s;

	union {
		/* reference to local var > 0, global < 0 (= 0 is invalid)
		 * To get the actual index, calculate abs(var) - 1.
		 *
		 * Populated during analysis */
		ssize_t ref;

		/* number value */
		double d;

	};

	/* node type and status */
	bool moved;
	enum fwdtype t;

	/* cmd args and cnt params */
	size_t argc;
	struct fwdast *args;

	size_t bodyc;
	struct fwdast *body;
};

#define VEC_TYPE struct fwdast
#define VEC_NAME fwdasts
#include <conts/vec.h>

static struct fwdasts fwdast_body(struct fwdast *ast)
{
	return (struct fwdasts){.n = ast->bodyc, .s = 0, .buf = ast->body};
}

static struct fwdasts fwdast_args(struct fwdast *ast)
{
	return (struct fwdasts){.n = ast->argc, .s = 0, .buf = ast->args};
}

static void free_asts(struct fwdasts *asts);

static void free_ast(struct fwdast *ast)
{
	struct fwdasts args = fwdast_args(ast);
	struct fwdasts body = fwdast_body(ast);

	switch (ast->kind) {
	case FWDAST_TOP:
		free_asts(&body);
		break;

	case FWDAST_CMD:
		free(ast->s);
		free_asts(&args);
		break;

	case FWDAST_STR:
	case FWDAST_ID:
	case FWDAST_VAR:
		free(ast->s);
		break;

	case FWDAST_CNT:
		free(ast->s);
		free_asts(&args);
		free_asts(&body);
		break;

	default:
		break;
	}
}

static void free_asts(struct fwdasts *asts)
{
	foreach(fwdasts, n, asts) {
		free_ast(n);
	}

	fwdasts_destroy(asts);
}

static enum fwdtokentype fwdtoken_peek(struct fwdtokens *tokens, ssize_t pos)
{
	if ((size_t)pos >= fwdtokens_len(tokens))
		return FWDTOKEN_EOF;

	return fwdtokens_at(tokens, (size_t)pos)->kind;
}

static int fwdtoken_to_fwdtype(struct fwdtoken *token, enum fwdtype *type)
{
	assert(token->kind == FWDTOKEN_ID);
	if (strcmp(token->s, "/num") == 0) {
		*type = FWDSCRIPT_NUM;
		return 0;
	}

	if (strcmp(token->s, "/str") == 0) {
		*type = FWDSCRIPT_STR;
		return 0;
	}

	if (strcmp(token->s, "/cnt") == 0) {
		*type = FWDSCRIPT_CNT;
		return 0;
	}

	if (strcmp(token->s, "/tbl") == 0) {
		*type = FWDSCRIPT_TBL;
		return 0;
	}

	if (strcmp(token->s, "/opq") == 0) {
		*type = FWDSCRIPT_OPQ;
		return 0;
	}

	fprintf(stderr, "line %zu, unknown type %s\n", token->line, token->s);
	return -1;
}

static const char *fwdtoken_kind_str(enum fwdtokentype kind)
{
	switch (kind) {
	case FWDTOKEN_ID: return "id";
	case FWDTOKEN_STR: return "str";
	case FWDTOKEN_NUM: return "num";
	case FWDTOKEN_OPEN: return "{";
	case FWDTOKEN_CLOSE: return "}";
	case FWDTOKEN_TO: return "=>";
	case FWDTOKEN_END: return ";";
	case FWDTOKEN_EOL: return "end of line";
	case FWDTOKEN_EOF: return "end of file";
	default: break;
	}

	return "???";
}

static struct fwdtoken *fwdtokens_consume(struct fwdtokens *tokens, ssize_t *pos, enum fwdtokentype ex)
{
	if ((size_t)*pos >= fwdtokens_len(tokens)) {
		fprintf(stderr, "ran out of tokens\n");
		return NULL;
	}

	struct fwdtoken *tok = fwdtokens_at(tokens, *pos);
	if (tok->kind != ex) {
		fprintf(stderr, "line %zu, expected %s but found %s\n",
				tok->line,
				fwdtoken_kind_str(ex),
				fwdtoken_kind_str(tok->kind));
		return NULL;
	}

	*pos += 1;
	return tok;
}

static ssize_t fwdscript_parse_cmds(struct fwdtokens *tokens, ssize_t pos, struct fwdasts *cmds);
static ssize_t fwdscript_parse_body(struct fwdtokens *tokens, ssize_t pos, struct fwdasts *body);

static ssize_t fwdscript_parse_empty(struct fwdtokens *tokens, ssize_t pos)
{
	/* skip until first interesting feature */
	while (1) {
		if (fwdtoken_peek(tokens, pos) == FWDTOKEN_END) {
			pos++;
			continue;
		}

		if (fwdtoken_peek(tokens, pos) == FWDTOKEN_EOL) {
			pos++;
			continue;
		}

		break;
	}

	return pos;
}


static ssize_t fwdscript_parse_var(struct fwdtokens *tokens, ssize_t pos, struct fwdast *var)
{
	struct fwdtoken *type = NULL;
	struct fwdtoken *arg  = NULL;

	if ((type = fwdtokens_consume(tokens, &pos, FWDTOKEN_ID)) == NULL)
		return -1;

	if ((arg = fwdtokens_consume(tokens, &pos, FWDTOKEN_ID)) == NULL)
		return -1;

	enum fwdtype tk;
	if (fwdtoken_to_fwdtype(type, &tk))
		return -1;

	var->kind = FWDAST_VAR;
	var->s    = fwdscript_move((void **)&arg->s);
	var->t    = tk;
	return pos;
}

static ssize_t fwdscript_parse_closure(struct fwdtokens *tokens, ssize_t pos, struct fwdast *arg)
{
	if (fwdtokens_consume(tokens, &pos, FWDTOKEN_TO) == NULL)
		return -1;

	struct fwdasts vars = fwdasts_create(0);

	while (fwdtoken_peek(tokens, pos) == FWDTOKEN_ID) {
		struct fwdast var = {};
		if ((pos = fwdscript_parse_var(tokens, pos, &var)) < 0) {
			free_asts(&vars);
			return -1;
		}

		if (fwdasts_append(&vars, var) == NULL) {
			fprintf(stderr, "failed appending ast var to closure\n");
			free_asts(&vars);
			return -1;
		}
	}

	struct fwdasts body = fwdasts_create(0);
	if (fwdtoken_peek(tokens, pos) == FWDTOKEN_OPEN) {
		if ((pos = fwdscript_parse_body(tokens, pos, &body)) < 0) {
			free_asts(&vars);
			free_asts(&body);
			return -1;
		}

	} else if (fwdtoken_peek(tokens, pos) == FWDTOKEN_END) {
		if ((pos = fwdscript_parse_cmds(tokens, pos, &body)) < 0) {
			free_asts(&vars);
			free_asts(&body);
			return -1;
		}
	}

	arg->kind = FWDAST_CNT;

	arg->argc = vars.n;
	arg->args = vars.buf;

	arg->bodyc = body.n;
	arg->body  = body.buf;
	return pos;
}

static ssize_t fwdscript_parse_arg(struct fwdtokens *tokens, ssize_t pos, struct fwdast *arg)
{
	struct fwdtoken *tok = fwdtokens_at(tokens, pos);
	switch (tok->kind) {
	case FWDTOKEN_ID:
		arg->kind = FWDAST_ID;
		arg->s    = fwdscript_move((void **)&tok->s);
		return pos + 1;

	case FWDTOKEN_STR:
		arg->kind = FWDAST_STR;
		arg->s    = fwdscript_move((void **)&tok->s);
		return pos + 1;

	case FWDTOKEN_NUM:
		arg->kind = FWDAST_NUM;
		arg->d    = tok->d;
		return pos + 1;

	case FWDTOKEN_TO:
		return fwdscript_parse_closure(tokens, pos, arg);

	default:
		break;
	}

	/* let whoever called us handle other cases (?) */
	return pos;
}

static ssize_t fwdscript_parse_cmd(struct fwdtokens *tokens, ssize_t pos, struct fwdast *cmd)
{
	if ((pos = fwdscript_parse_empty(tokens, pos)) < 0)
		return -1;

	struct fwdasts parts = fwdasts_create(0);

	struct fwdtoken *name = NULL;
	if ((name = fwdtokens_consume(tokens, &pos, FWDTOKEN_ID)) == NULL) {
		free_asts(&parts);
		return -1;
	}

	/* parse args */
	while (1) {
		if (fwdtoken_peek(tokens, pos) == FWDTOKEN_EOL) {
			pos++;
			continue;
		}

		if (fwdtoken_peek(tokens, pos) == FWDTOKEN_END)
			break;


		if (fwdtoken_peek(tokens, pos) == FWDTOKEN_CLOSE)
			break;

		struct fwdast part = {};
		if ((pos = fwdscript_parse_arg(tokens, pos, &part)) < 0) {
			free_asts(&parts);
			return -1;
		}

		if (fwdasts_append(&parts, part) == NULL) {
			fprintf(stderr, "failed appending ast arg to parts\n");
			free_asts(&parts);
			return -1;
		}
	}

	cmd->kind = FWDAST_CMD;
	cmd->s    = fwdscript_move((void **)&name->s);
	cmd->argc = parts.n;
	cmd->args = parts.buf;
	return pos;
}

static ssize_t fwdscript_parse_cmds(struct fwdtokens *tokens, ssize_t pos, struct fwdasts *cmds)
{
	if ((pos = fwdscript_parse_empty(tokens, pos)) < 0) {
		return -1;
	}

	/* each command must start with an identifier */
	while (fwdtoken_peek(tokens, pos) == FWDTOKEN_ID) {
		struct fwdast cmd = {};
		if ((pos = fwdscript_parse_cmd(tokens, pos, &cmd)) < 0)
			return -1;

		if (fwdasts_append(cmds, cmd) == NULL) {
			fprintf(stderr, "failed appending at cmd to body\n");
			return -1;
		}

	}

	if ((pos = fwdscript_parse_empty(tokens, pos)) < 0)
		return -1;

	return pos;
}

static ssize_t fwdscript_parse_body(struct fwdtokens *tokens, ssize_t pos, struct fwdasts *body)
{
	if (fwdtokens_consume(tokens, &pos, FWDTOKEN_OPEN) == NULL)
		return -1;

	if ((pos = fwdscript_parse_cmds(tokens, pos, body)) < 0)
		return -1;

	if (fwdtokens_consume(tokens, &pos, FWDTOKEN_CLOSE) == NULL)
		return -1;

	return pos;
}

static ssize_t fwdscript_parse_def(struct fwdtokens *tokens, ssize_t pos, struct fwdast *def)
{
	struct fwdtoken *name = NULL;
	if ((name = fwdtokens_consume(tokens, &pos, FWDTOKEN_ID)) == NULL)
		return -1;

	struct fwdasts args = fwdasts_create(0);
	while (fwdtoken_peek(tokens, pos) == FWDTOKEN_ID) {
		struct fwdast var = {};
		if ((pos = fwdscript_parse_var(tokens, pos, &var)) < 0) {
			free_asts(&args);
			return -1;
		}

		if (fwdasts_append(&args, var) == NULL) {
			fprintf(stderr, "failed appending ast var to def\n");
			free_asts(&args);
			return -1;
		}
	}

	struct fwdasts body = fwdasts_create(0);
	if ((pos = fwdscript_parse_body(tokens, pos, &body)) < 0) {
		free_asts(&args);
		return -1;
	}

	/* move everything into ast node */
	def->kind = FWDAST_CNT,
	def->s    = fwdscript_move((void **)&name->s),

	def->argc = args.n,
	def->args = args.buf,

	def->bodyc = body.n,
	def->body  = body.buf;

	return pos;
}

static int fwdscript_parse_top(struct fwdtokens *tokens, struct fwdast *top)
{
	ssize_t pos = 0;
	struct fwdasts defs = fwdasts_create(1);

	while (1) {
		while (pos < (ssize_t)fwdtokens_len(tokens)
		    && fwdtokens_at(tokens, pos)->kind == FWDTOKEN_EOL)
			pos++;

		/* all input succesfully read */
		if (pos == (ssize_t)fwdtokens_len(tokens))
			break;

		struct fwdast tmp = {};
		struct fwdast *def = NULL;
		if ((def = fwdasts_append(&defs, tmp)) == NULL) {
			fprintf(stderr, "couldn't append temp ast def\n");
			free_asts(&defs);
			return -1;
		}

		if ((pos = fwdscript_parse_def(tokens, pos, def)) < 0) {
			free_asts(&defs);
			return -1;
		}
	}

	/* transfer ownership or array to top */
	top->kind = FWDAST_TOP;
	top->s = NULL;
	top->argc = 0;
	top->args = NULL;
	top->bodyc = defs.n;
	top->body = defs.buf;
	return 0;
}

static void fwdscript_debug_tokens(struct fwdtokens *tokens)
{
	foreach(fwdtokens, tok, tokens) {
		switch (tok->kind) {
		case FWDTOKEN_ID:
			fprintf(stderr, "line %4zu: %s (%s)\n",
					tok->line,
					fwdtoken_kind_str(tok->kind),
					tok->s);
			break;

		case FWDTOKEN_STR:
			fprintf(stderr, "line %4zu: %s \"%s\"\n",
					tok->line,
					fwdtoken_kind_str(tok->kind),
					tok->s);
			break;

		case FWDTOKEN_NUM:
			fprintf(stderr, "line %4zu: %s (%g)\n",
					tok->line,
					fwdtoken_kind_str(tok->kind),
					tok->d);
			break;

		case FWDTOKEN_OPEN:
			fprintf(stderr, "line %4zu: %s\n",
					tok->line,
					fwdtoken_kind_str(tok->kind));
			break;

		case FWDTOKEN_CLOSE:
			fprintf(stderr, "line %4zu: %s\n",
					tok->line,
					fwdtoken_kind_str(tok->kind));
			break;

		case FWDTOKEN_TO:
			fprintf(stderr, "line %4zu: %s\n",
					tok->line,
					fwdtoken_kind_str(tok->kind));
			break;

		case FWDTOKEN_END:
			fprintf(stderr, "line %4zu: %s\n",
					tok->line,
					fwdtoken_kind_str(tok->kind));
			break;

		case FWDTOKEN_EOL:
			fprintf(stderr, "line %4zu: %s\n",
					tok->line,
					fwdtoken_kind_str(tok->kind));
			break;

		case FWDTOKEN_EOF:
			fprintf(stderr, "line %4zu: %s\n",
					tok->line,
					fwdtoken_kind_str(tok->kind));
			break;
		}
	}

}

static void fwdscript_debug_ast(struct fwdast *ast, int depth)
{
	/** @todo line numbering disappears from ast, not fantastic I guess? */
	struct fwdasts args = fwdast_args(ast);
	struct fwdasts body = fwdast_body(ast);

	switch (ast->kind) {
	case FWDAST_ID:
		fprintf(stderr, "%*s+ id (%s)\n", depth, "|",
				ast->s);
		break;

	case FWDAST_VAR:
		fprintf(stderr, "%*s+ var (%s, %s)\n", depth, "|",
				ast->s, fwdtype_str(ast->t));
		break;

	case FWDAST_STR:
		fprintf(stderr, "%*s+ str \"%s\"\n", depth, "|",
				ast->s);
		break;

	case FWDAST_NUM:
		fprintf(stderr, "%*s+ num (%g)\n", depth, "|",
				ast->d);
		break;

	case FWDAST_CNT:
		fprintf(stderr, "%*s+ cnt (%s)\n", depth, "|",
				ast->s ? ast->s : "closure");

		foreach(fwdasts, n, &args) {
			fwdscript_debug_ast(n, depth + 1);
		}

		foreach(fwdasts, n, &body) {
			fwdscript_debug_ast(n, depth + 1);
		}
		break;

	case FWDAST_CMD:
		fprintf(stderr, "%*s+ cmd (%s)\n", depth, "|",
				ast->s);

		foreach(fwdasts, n, &args) {
			fwdscript_debug_ast(n, depth + 1);
		}
		break;

	case FWDAST_TOP:
		fprintf(stderr, "TOP\n");
		foreach(fwdasts, n, &body) {
			fwdscript_debug_ast(n, depth + 1);
		}
		break;
	}
}

static int fwdctx_add(struct fwdctx *ctx, struct fwdast *symb)
{
	assert(symb->s);
	if (fwdrefs_append(&ctx->refs, symb) == NULL) {
		fprintf(stderr, "couldn't append new ref to fwdctx\n");
		return -1;
	}

	size_t handle = fwdrefs_len(&ctx->refs) - 1;
	/* not really a func, should maybe rename structure to be more generic? */
	size_t *eh = fwdfuncs_insert(&ctx->defs, symb->s, handle);
	if (!eh) {
		fprintf(stderr, "couldn't insert ref to fwdfuncs\n");
		return -1;
	}

	/* overwrite possible existing data (shadowing) */
	if (*eh != handle)
		*eh = handle;

	return 0;
}

static int fwdscript_analyze_body(struct fwdscript *s, struct fwdasts *body, struct fwdctx *ctx, bool last);

static int fwdscript_analyze_id(struct fwdscript *s, struct fwdast *id, struct fwdctx *ctx)
{
	assert(id->kind == FWDAST_ID);
	struct fwdast *def = NULL;

	/* only look for variable in the local scope since we don't have global
	 * variables and we don't support callbacks to global defs */
	if ((def = fwdctx_lookup(NULL, ctx, id->s)) == NULL) {
		fprintf(stderr, "no such variable: %s\n", id->s);
		return -1;
	}

	assert(def->kind == FWDAST_VAR);

	if (def->t != FWDSCRIPT_NUM && def->moved) {
		fprintf(stderr, "trying to reuse moved var %s\n", def->s);
		return -1;
	}

	def->moved = true;
	id->t = def->t;
	return 0;
}

static int fwdscript_analyze_var(struct fwdscript *s, struct fwdast *var, struct fwdctx *ctx)
{
	if (fwdctx_add(ctx, var)) {
		fprintf(stderr, "failed adding %s to local context\n", var->s);
		return -1;
	}

	return 0;
}

static int fwdscript_analyze_cnt(struct fwdscript *s, struct fwdast *cnt, struct fwdctx *ctx, bool last)
{
	assert(cnt->kind == FWDAST_CNT);
	struct fwdasts params = fwdast_args(cnt);
	struct fwdasts body   = fwdast_body(cnt);

	foreach(fwdasts, p, &params) {
		if (fwdscript_analyze_var(s, p, ctx))
			return -1;
	}

	cnt->t = FWDSCRIPT_CNT;
	return fwdscript_analyze_body(s, &body, ctx, last);
}

static int fwdscript_analyze_arg(struct fwdscript *s, struct fwdast *arg, struct fwdctx *ctx, bool last)
{
	switch (arg->kind) {
	case FWDAST_NUM:
		arg->t = FWDSCRIPT_NUM;
		return 0;

	case FWDAST_STR:
		arg->t = FWDSCRIPT_STR;
		return 0;

	case FWDAST_CNT:
		/* for now, the body is handled in a separate pass later on so
		 * we don't overwrite local variable names */
		arg->t = FWDSCRIPT_CNT;
		return 0;

	case FWDAST_ID:
		return fwdscript_analyze_id(s, arg, ctx);

	default:
		break;
	}

	/* should never get here */
	abort();
	return -1;
}

static int fwdscript_analyze_builtin(struct fwdscript *s, struct fwdast *builtin, struct fwdctx *ctx, bool last)
{
	assert(builtin->kind == FWDAST_CMD && builtin->s);
	struct fwdbuiltin *def = NULL;
	if ((def = fwdbuiltins_find(&s->builtins, builtin->s)) == NULL) {
		fprintf(stderr, "no such cmd: %s\n", builtin->s);
		return -1;
	}


	bool must_be_last = false;
	struct fwdasts args = fwdast_args(builtin);
	struct fwdtypes types = fwdtypes_create(fwdasts_len(&args));
	foreach(fwdasts, arg, &args) {
		if (fwdscript_analyze_arg(s, arg, ctx, last))
			return -1;

		if (arg->kind == FWDAST_CNT)
			must_be_last = true;

		if (fwdtypes_append(&types, arg->t) == NULL) {
			fprintf(stderr, "failed appending type to builtin call\n");
			return -1;
		}
	}

	if (must_be_last && !last) {
		fprintf(stderr, "%s ref call should be last, but isn't\n", builtin->s);
		return -1;
	}

	if (def->tp(s, &types)) {
		fwdtypes_destroy(&types);
		return -1;
	}

	fwdtypes_destroy(&types);

	foreach(fwdasts, arg, &args) {
		if (arg->kind != FWDAST_CNT)
			continue;

		if (fwdscript_analyze_cnt(s, arg, ctx, last))
			return -1;
	}

	return 0;
}

static int fwdscript_analyze_ref_call(struct fwdscript *s, struct fwdast *cmd, struct fwdast *def, struct fwdctx *ctx, bool last)
{
	assert(def->kind == FWDAST_VAR);
	if (def->t != FWDSCRIPT_CNT) {
		fprintf(stderr, "trying to call %s %s\n",
				fwdtype_str(def->t),
				def->s);
		return -1;
	}

	/* needs to be checked at runtime, but we can at least do the move
	 * checks here */
	bool must_be_last = false;
	struct fwdasts args = fwdast_args(cmd);
	foreach(fwdasts, arg, &args) {
		if (fwdscript_analyze_arg(s, arg, ctx, last))
			return -1;

		if (arg->kind == FWDAST_CNT)
			must_be_last = true;
	}

	if (must_be_last && !last) {
		fprintf(stderr, "%s ref call should be last, but isn't\n", cmd->s);
		return -1;
	}

	foreach(fwdasts, arg, &args) {
		if (arg->kind != FWDAST_CNT)
			continue;

		if (fwdscript_analyze_cnt(s, arg, ctx, last))
			return -1;
	}

	return 0;
}

static int fwdscript_analyze_call(struct fwdscript *s, struct fwdast *cmd, struct fwdast *def, struct fwdctx *ctx, bool last)
{
	bool must_be_last = false;
	struct fwdasts params = fwdast_args(def);
	struct fwdasts args   = fwdast_args(cmd);

	size_t pl = fwdasts_len(&params);
	size_t al = fwdasts_len(&args);

	if (pl != al) {
		fprintf(stderr, "%s expects %zu args, found %zu\n",
				cmd->s, pl, al);
		return -1;
	}

	for (size_t i = 0; i < pl; ++i) {
		struct fwdast *param = fwdasts_at(&params, i);
		struct fwdast *arg   = fwdasts_at(&args, i);
		assert(param);
		assert(arg);

		if (fwdscript_analyze_arg(s, arg, ctx, last))
			return -1;

		if (arg->kind == FWDAST_CNT)
			must_be_last = true;

		if (param->t != arg->t) {
			fprintf(stderr, "%s %s expects %s, found %s\n",
					cmd->s, param->s,
					fwdtype_str(param->t),
					fwdtype_str(arg->t));
			return -1;
		}
	}

	if (must_be_last && !last) {
		fprintf(stderr, "%s call should be last, but isn't\n", cmd->s);
		return -1;
	}

	foreach(fwdasts, arg, &args) {
		if (arg->kind != FWDAST_CNT)
			continue;

		if (fwdscript_analyze_cnt(s, arg, ctx, last))
			return -1;
	}

	return 0;
}

static int fwdscript_analyze_cmd(struct fwdscript *s, struct fwdast *cmd, struct fwdctx *ctx, bool last)
{
	assert(cmd->kind == FWDAST_CMD && cmd->s);
	struct fwdast *def = NULL;
	if ((def = fwdctx_lookup(&s->ctx, ctx, cmd->s)) == NULL)
		return fwdscript_analyze_builtin(s, cmd, ctx, last);

	if (def->kind == FWDAST_VAR)
		return fwdscript_analyze_ref_call(s, cmd, def, ctx, last);

	return fwdscript_analyze_call(s, cmd, def, ctx, last);
}

static int fwdscript_analyze_body(struct fwdscript *s, struct fwdasts *body, struct fwdctx *ctx, bool last)
{
	size_t n = fwdasts_len(body);
	for (size_t i = 0; i < n; ++i) {
		bool last_cmd = false;
		if (i == n - 1)
			last_cmd = last;

		struct fwdast *cmd = fwdasts_at(body, i);
		if (fwdscript_analyze_cmd(s, cmd, ctx, last_cmd))
			return -1;
	}

	return 0;
}

static int fwdscript_analyze_def(struct fwdscript *s, struct fwdast *def)
{
	assert(def->kind == FWDAST_CNT && def->s);

	/* add us to the global symbol table */
	if (fwdctx_add(&s->ctx, def))
		return -1;

	/* create a local symbol table */
	struct fwdctx ctx = fwdctx_create();

	/* add local variables to local context */
	struct fwdasts params = fwdast_args(def);
	foreach(fwdasts, p, &params) {
		p->moved = false;

		if (fwdctx_add(&ctx, p)) {
			fwdctx_destroy(&ctx);
			return -1;
		}
	}

	struct fwdasts body = fwdast_body(def);
	if (fwdscript_analyze_body(s, &body, &ctx, true)) {
		fwdctx_destroy(&ctx);
		return -1;
	}

	fwdctx_destroy(&ctx);
	return 0;
}

static int fwdscript_analyze(struct fwdscript *s, struct fwdast *top)
{
	assert(top->kind == FWDAST_TOP);

	struct fwdasts defs = fwdast_body(top);
	foreach(fwdasts, def, &defs) {
		if (fwdscript_analyze_def(s, def))
			return -1;
	}

	return 0;
}

static int fwdscript_parse(struct fwdscript *s, const char *buf)
{
	struct fwdtokens tokens = fwdtokens_create(0);

	if (fwdscript_tokenize(&tokens, buf)) {
		free_tokens(&tokens);
		return -1;
	}

	fwdscript_debug_tokens(&tokens);

	/* convert tokens into some kind of structure */
	struct fwdast top = {};
	if (fwdscript_parse_top(&tokens, &top)) {
		free_tokens(&tokens);
		return -1;
	}

	fwdscript_debug_ast(&top, 0);

	free_tokens(&tokens);

	if (fwdscript_analyze(s, &top)) {
		free_ast(&top);
		return -1;
	}

	/** @todo this means that the global ctx in s will be illegal to
	 * reference after this point, which is presumably not ideal if we want
	 * to support modules. Will have to think of something better. */
	free_ast(&top);
	return 0;
}

static int fwdscript_run(struct fwdscript *s, const char *name, struct fwdvals *args)
{
	TODO();
}

static void fwdscript_destroy(struct fwdscript *s)
{
	foreach(fwdbuiltins, builtin, &s->builtins) {
		free(builtin.t->key);
	}

	fwdctx_destroy(&s->ctx);
	fwdinsns_destroy(&s->insns);
	fwdbuiltins_destroy(&s->builtins);
}

static const char *read_file(const char *fname)
{
	FILE *f = fopen(fname, "rb");
	if (!f)
		err(EXIT_FAILURE, "failed opening %s", fname);

	if (fseek(f, 0, SEEK_END) < 0)
		err(EXIT_FAILURE, "failed seeking %s", fname);

	long sz = ftell(f);
	if (sz < 0)
		err(EXIT_FAILURE, "failed telling %s", fname);

	if (fseek(f, 0, SEEK_SET) < 0)
		err(EXIT_FAILURE, "failed rewinding %s", fname);

	char *buf = malloc(sz + 1);
	if (!buf)
		err(EXIT_FAILURE, "failed allocating buf for %s", fname);

	if ((long)fread(buf, 1, sz, f) != sz)
		err(EXIT_FAILURE, "failed reading file %s", fname);

	/* trailing zero */
	buf[sz] = '\0';

	fclose(f);
	return buf;
}

int main(int argc, char *argv[])
{
	/* a bit silly but good enough for testing */
	const char *buf =
	"main /tbl args {\n"
	"	get args 0 => {abort \"impossible\"}\n"
	"	=> /str line;\n"
	"	print line\n"
	"}\n";

	if (argc == 2)
		buf = read_file(argv[1]);

	int ret = 0;
	struct fwdscript inst = fwdscript_create();

	ret = fwdscript_register_builtins(&inst);
	assert(ret == 0);

	ret = fwdscript_parse(&inst, buf);
	assert(ret == 0);

	struct fwdvals args = fwdvals_create(1);
	fwdvals_append(&args, FWDSTR(strdup("Hello world!\n")));

	fwdscript_run(&inst, "main", &args);

	/* string should've been consumed by main so we shouldn't try to free it */
	fwdvals_destroy(&args);
	fwdscript_destroy(&inst);

	if (argc == 2)
		free((void *)buf);
}
