// SPDX-License-Identifier: MPL-2.0
// (c) Hare authors <https://harelang.org>

use fmt;
use hare::ast;
use hare::lex;
use io;
use memio;
use strings;

// Unparses a [[hare::ast::decl]].
export fn decl(
	out: io::handle,
	syn: *synfunc,
	d: *ast::decl,
) (size | io::error) = {
	let n = 0z;
	let ctx = context {
		out = out,
		stack = &stack {
			cur = d,
			...
		},
		...
	};
	if (len(d.docs) > 0) {
		n += comment(&ctx, syn, d.docs)?;
	};
	if (d.exported) {
		n += syn(&ctx, "export", synkind::KEYWORD)?;
		n += space(&ctx)?;
	};
	match (d.decl) {
	case let c: ast::decl_const =>
		n += syn(&ctx, "def", synkind::KEYWORD)?;
		n += space(&ctx)?;
		n += _ident(&ctx, syn, c.ident, synkind::CONSTANT)?;
		match (c._type) {
		case null => void;
		case let ty: *ast::_type =>
			n += syn(&ctx, ":", synkind::PUNCTUATION)?;
			n += space(&ctx)?;
			n += __type(&ctx, syn, ty)?;
		};
		n += space(&ctx)?;
		n += syn(&ctx, "=", synkind::OPERATOR)?;
		n += space(&ctx)?;
		n += _expr(&ctx, syn, c.init)?;
	case let g: ast::decl_global =>
		if (len(g.symbol) != 0) {
			n += syn(&ctx, "@symbol(", synkind::ATTRIBUTE)?;
			n += literal(&ctx, syn, ast::plain_literal {
				storage = ast::builtin_type::STR,
				value = g.symbol,
			})?;
			n += syn(&ctx, ")", synkind::ATTRIBUTE)?;
			n += space(&ctx)?;
		} else if (g.is_threadlocal) {
			n += syn(&ctx, "@threadlocal",
				synkind::ATTRIBUTE)?;
			n += space(&ctx)?;
		};
		n += syn(&ctx,
			if (g.is_const) "const" else "let",
			synkind::KEYWORD)?;
		n += space(&ctx)?;
		n += _ident(&ctx, syn, g.ident, synkind::GLOBAL)?;
		match (g._type) {
		case null => void;
		case let ty: *ast::_type =>
			n += syn(&ctx, ":", synkind::PUNCTUATION)?;
			n += space(&ctx)?;
			n += __type(&ctx, syn, ty)?;
		};
		match (g.init) {
		case null => void;
		case let ex: *ast::expr =>
			n += space(&ctx)?;
			n += syn(&ctx, "=", synkind::OPERATOR)?;
			n += space(&ctx)?;
			n += _expr(&ctx, syn, ex)?;
		};
	case let t: ast::decl_type =>
		n += syn(&ctx, "type", synkind::KEYWORD)?;
		n += space(&ctx)?;
		n += _ident(&ctx, syn, t.ident, synkind::TYPEDEF)?;
		n += space(&ctx)?;
		n += syn(&ctx, "=", synkind::OPERATOR)?;
		n += space(&ctx)?;
		n += __type(&ctx, syn, t._type)?;
	case let f: ast::decl_func =>
		ctx.stack = &stack {
			cur = f.prototype,
			up = ctx.stack,
			...
		};
		defer {
			let stack = &(ctx.stack as *stack);
			match (stack.extra) {
			case let p: *opaque =>
				free(p);
			case null => void;
			};
			ctx.stack = stack.up;
		};

		switch (f.attrs) {
		case ast::fndecl_attr::NONE => void;
		case ast::fndecl_attr::FINI =>
			n += syn(&ctx, "@fini", synkind::ATTRIBUTE)?;
			n += space(&ctx)?;
		case ast::fndecl_attr::INIT =>
			n += syn(&ctx, "@init", synkind::ATTRIBUTE)?;
			n += space(&ctx)?;
		case ast::fndecl_attr::TEST =>
			n += syn(&ctx, "@test", synkind::ATTRIBUTE)?;
			n += space(&ctx)?;
		};
		let p = f.prototype.repr as ast::func_type;
		if (len(f.symbol) != 0) {
			n += syn(&ctx, "@symbol(", synkind::ATTRIBUTE)?;
			n += literal(&ctx, syn, ast::plain_literal {
				storage = ast::builtin_type::STR,
				value = f.symbol,
			})?;
			n += syn(&ctx, ")", synkind::ATTRIBUTE)?;
			n += space(&ctx)?;
		};
		n += syn(&ctx, "fn", synkind::KEYWORD)?;
		n += space(&ctx)?;
		n += _ident(&ctx, syn, f.ident, synkind::FUNCTION)?;
		const fntype = f.prototype.repr as ast::func_type;
		n += prototype(&ctx, syn, &fntype)?;
		match (f.body) {
		case null => void;
		case let e: *ast::expr =>
			n += space(&ctx)?;
			n += syn(&ctx, "=", synkind::OPERATOR)?;
			n += space(&ctx)?;
			n += _expr(&ctx, syn, e)?;
		};
	case let e: ast::assert_expr =>
		n += assert_expr(&ctx, syn, &e)?;
	};
	n += syn(&ctx, ";", synkind::PUNCTUATION)?;
	return n;
};

fn comment(ctx: *context, syn: *synfunc, s: str) (size | io::error) = {
	let n = 0z;
	let s = strings::trimsuffix(s, "\n");
	let s = strings::tokenize(s, "\n");

	for (let line => strings::next_token(&s)) {
		for (let i = 0z; i < ctx.indent; i += 1) {
			n += syn(ctx, "\t", synkind::COMMENT)?;
			ctx.linelen += 8;
		};
		n += syn(ctx, "//", synkind::COMMENT)?;
		n += syn(ctx, line, synkind::COMMENT)?;
		n += syn(ctx, "\n", synkind::COMMENT)?;
		ctx.linelen = 0;
	};
	return n;
};

fn decl_test(d: *ast::decl, expected: str) bool = {
	let buf = memio::dynamic();
	decl(&buf, &syn_nowrap, d)!;
	let s = memio::string(&buf)!;
	defer free(s);
	fmt::println(s)!;
	return s == expected;
};

@test fn decl() void = {
	let lloc = lex::location {
		path = "<test>",
		line = 0,
		col = 0,
		off = 0,
	};
	let loc = ast::location { start = lloc, end = lloc };
	let type_int = ast::_type {
		loc = loc,
		repr = ast::builtin_type::INT,
	};
	let type_fn = ast::_type {
		loc = loc,
		repr = ast::func_type {
			result = &type_int,
			variadism = ast::variadism::HARE,
			params = [
				ast::func_param {
					loc = loc,
					name = "foo",
					_type = &type_int,
					default_value = void,
				},
				ast::func_param {
					loc = loc,
					name = "bar",
					_type = &type_int,
					default_value = void,
				},
			],
		},
	};
	let expr_void = ast::expr {
		loc = loc,
		expr = ast::plain_literal {
			storage = ast::builtin_type::VOID,
			value = void,
		},
	};

	let d = ast::decl {
		exported = false,
		loc = loc,
		decl = ast::decl_global {
			is_const = false,
			is_threadlocal = false,
			symbol = "",
			ident = ["foo", "bar"],
			_type = &type_int,
			init = &expr_void,
		},
		...
	};
	assert(decl_test(&d, "let foo::bar: int = void;"));

	d.decl = ast::decl_global {
		is_const = false,
		is_threadlocal = true,
		symbol = "",
		ident = ["boo"],
		_type = &type_int,
		init = &expr_void,
	};
	assert(decl_test(&d, "@threadlocal let boo: int = void;"));

	d.decl = ast::decl_global {
		is_const = false,
		is_threadlocal = false,
		symbol = "foobar",
		ident = ["baz"],
		_type = &type_int,
		init = &expr_void,
	};
	assert(decl_test(&d, "@symbol(\"foobar\") let baz: int = void;"));

	d.exported = true;
	d.decl = ast::decl_const {
		ident = ["foo"],
		_type = &type_int,
		init = &expr_void,
	};
	assert(decl_test(&d, "export def foo: int = void;"));

	d.exported = false;
	d.decl = ast::decl_type {
		ident = ["foo"],
		_type = &type_int,
	};
	assert(decl_test(&d, "type foo = int;"));

	d.decl = ast::decl_func {
		symbol = "foo",
		ident = ["foo"],
		prototype = &type_fn,
		body = null,
		attrs = ast::fndecl_attr::FINI,
	};
	assert(decl_test(&d, "@fini @symbol(\"foo\") fn foo(foo: int, bar: int...) int;"));

	type_fn.repr = ast::func_type {
		result = &type_int,
		variadism = ast::variadism::NONE,
		params = [
			ast::func_param {
				loc = loc,
				name = "",
				_type = &type_int,
				default_value = ast::expr {
					expr = ast::plain_literal {
						storage = ast::builtin_type::ICONST,
						value = 4u64,
					},
					...
				},
			},
		],
	};
	d.decl = ast::decl_func {
		symbol = "",
		ident = ["foo"],
		prototype = &type_fn,
		body = &expr_void,
		attrs = 0,
	};
	assert(decl_test(&d, "fn foo(_: int = 4) int = void;"));
};
