/*
 * SPDX-FileCopyrightText: Stone Tickle <lattis@mochiro.moe>
 * SPDX-FileCopyrightText: illiliti <illiliti@dimension.sh>
 * SPDX-FileCopyrightText: Owen Rafferty <owen@owenrafferty.com>
 * SPDX-FileCopyrightText: illiliti <illiliti@thunix.net>
 * SPDX-FileCopyrightText: Vincent Torri <vincent.torri@gmail.com>
 * SPDX-License-Identifier: GPL-3.0-only
 */

#include "compat.h"

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

#include "args.h"
#include "buf_size.h"
#include "compilers.h"
#include "error.h"
#include "guess.h"
#include "lang/object_iterators.h"
#include "lang/typecheck.h"
#include "lang/workspace.h"
#include "log.h"
#include "machines.h"
#include "options.h"
#include "platform/path.h"
#include "platform/run_cmd.h"

struct toolchain_id {
	const char *public_id;
	const char *id;
};
#define TOOLCHAIN_NAME(_, public_id, id) { public_id, id },
static const struct toolchain_id compiler_type_name[] = { FOREACH_TOOLCHAIN_COMPILER_TYPE(TOOLCHAIN_NAME) };
static const struct toolchain_id linker_type_name[] = { FOREACH_TOOLCHAIN_LINKER_TYPE(TOOLCHAIN_NAME) };
static const struct toolchain_id static_linker_type_name[] = { FOREACH_TOOLCHAIN_STATIC_LINKER_TYPE(TOOLCHAIN_NAME) };
#undef TOOLCHAIN_CASE

static bool
toolchain_id_lookup(const char *name, const struct toolchain_id names[], uint32_t len, uint32_t *res)
{
	uint32_t i;
	for (i = 0; i < len; ++i) {
		if (strcmp(name, names[i].id) == 0) {
			*res = i;
			return true;
		}
	}

	return false;
}

const char *
compiler_type_to_s(enum compiler_type t)
{
	return compiler_type_name[t].public_id;
}

bool
compiler_type_from_s(const char *name, uint32_t *res)
{
	return toolchain_id_lookup(name, compiler_type_name, ARRAY_LEN(compiler_type_name), res);
}

const char *
linker_type_to_s(enum linker_type t)
{
	return linker_type_name[t].public_id;
}

bool
linker_type_from_s(const char *name, uint32_t *res)
{
	return toolchain_id_lookup(name, linker_type_name, ARRAY_LEN(linker_type_name), res);
}

static const char *
static_linker_type_to_s(enum static_linker_type t)
{
	return static_linker_type_name[t].public_id;
}

bool
static_linker_type_from_s(const char *name, uint32_t *res)
{
	return toolchain_id_lookup(name, static_linker_type_name, ARRAY_LEN(static_linker_type_name), res);
}

static const char *compiler_language_names[compiler_language_count] = {
	[compiler_language_null] = "null",
	[compiler_language_c] = "c",
	[compiler_language_c_hdr] = "c_hdr",
	[compiler_language_cpp] = "cpp",
	[compiler_language_cpp_hdr] = "cpp_hdr",
	[compiler_language_c_obj] = "c_obj",
	[compiler_language_objc] = "objc",
	[compiler_language_objcpp] = "objcpp",
	[compiler_language_assembly] = "assembly",
	[compiler_language_llvm_ir] = "llvm_ir",
	[compiler_language_nasm] = "nasm",
};

const char *
compiler_language_to_s(enum compiler_language l)
{
	assert(l < compiler_language_count);
	return compiler_language_names[l];
}

bool
s_to_compiler_language(const char *_s, enum compiler_language *l)
{
	const struct str s = WKSTR(_s);
	uint32_t i;
	for (i = 0; i < compiler_language_count; ++i) {
		if (str_eqli(&s, &WKSTR(compiler_language_names[i]))) {
			*l = i;
			return true;
		}
	}

	return false;
}

static const char *compiler_language_exts[compiler_language_count][10] = {
	[compiler_language_c] = { "c" },
	[compiler_language_c_hdr] = { "h" },
	[compiler_language_cpp] = { "cc", "cpp", "cxx", "C" },
	[compiler_language_cpp_hdr] = { "hh", "hpp", "hxx" },
	[compiler_language_c_obj] = { "o", "obj" },
	[compiler_language_objc] = { "m", "M" },
	[compiler_language_objcpp] = { "mm" },
	[compiler_language_assembly] = { "S", "s" },
	[compiler_language_llvm_ir] = { "ll" },
	[compiler_language_nasm] = { "asm" },
};

bool
filename_to_compiler_language(const char *str, enum compiler_language *l)
{
	uint32_t i, j;
	const char *ext;

	if (!(ext = strrchr(str, '.'))) {
		return false;
	}
	++ext;

	for (i = 0; i < compiler_language_count; ++i) {
		for (j = 0; compiler_language_exts[i][j]; ++j) {
			if (strcmp(ext, compiler_language_exts[i][j]) == 0) {
				*l = i;
				return true;
			}
		}
	}

	return false;
}

const char *
compiler_language_extension(enum compiler_language l)
{
	return compiler_language_exts[l][0];
}

enum compiler_language
coalesce_link_languages(enum compiler_language cur, enum compiler_language new)
{
	switch (new) {
	case compiler_language_null:
	case compiler_language_c_hdr:
	case compiler_language_cpp_hdr:
	case compiler_language_llvm_ir: break;
	case compiler_language_assembly:
		if (!cur) {
			return compiler_language_assembly;
		}
		break;
	case compiler_language_nasm:
	case compiler_language_c:
	case compiler_language_c_obj:
	case compiler_language_objc:
		if (!cur) {
			return compiler_language_c;
		}
		break;
	case compiler_language_cpp:
	case compiler_language_objcpp:
		if (!cur || cur == compiler_language_c || cur == compiler_language_assembly) {
			return compiler_language_cpp;
		}
		break;
	case compiler_language_count: UNREACHABLE;
	}

	return cur;
}

static bool
run_cmd_arr(struct workspace *wk, struct run_cmd_ctx *cmd_ctx, obj cmd_arr, const char *arg)
{
	obj args;
	obj_array_dup(wk, cmd_arr, &args);
	obj_array_push(wk, args, make_str(wk, arg));

	const char *argstr;
	uint32_t argc;
	join_args_argstr(wk, &argstr, &argc, args);

	if (!run_cmd(cmd_ctx, argstr, argc, NULL, 0)) {
		run_cmd_ctx_destroy(cmd_ctx);
		return false;
	}

	return true;
}

static const char *
guess_version_arg(struct workspace *wk, bool msvc_like)
{
	if (msvc_like) {
		return "/?";
	} else {
		return "--version";
	}
}

static bool
compiler_detect_c_or_cpp(struct workspace *wk, obj cmd_arr, obj comp_id)
{
	bool msvc_like = obj_array_in(wk, cmd_arr, make_str(wk, "cl"));

	// helpful: mesonbuild/compilers/detect.py:350
	struct run_cmd_ctx cmd_ctx = { 0 };
	if (!run_cmd_arr(wk, &cmd_ctx, cmd_arr, guess_version_arg(wk, msvc_like))) {
		run_cmd_ctx_destroy(&cmd_ctx);
		return false;
	}

	enum compiler_type type;
	bool unknown = true;
	obj ver;

	if (cmd_ctx.status != 0) {
		goto detection_over;
	}

	if (strstr(cmd_ctx.out.buf, "clang") || strstr(cmd_ctx.out.buf, "Clang")) {
		if (strstr(cmd_ctx.out.buf, "Apple") && strstr(cmd_ctx.out.buf, "clang")) {
			type = compiler_apple_clang;
		} else if (strstr(cmd_ctx.out.buf, "CL.EXE COMPATIBILITY")) {
			type = compiler_clang_cl;
		} else {
			type = compiler_clang;
		}
	} else if (strstr(cmd_ctx.out.buf, "Free Software Foundation")) {
		type = compiler_gcc;
	} else if (strstr(cmd_ctx.out.buf, "Microsoft") || strstr(cmd_ctx.err.buf, "Microsoft")) {
		type = compiler_msvc;
	} else {
		goto detection_over;
	}

	if (!guess_version(wk, (type == compiler_msvc) ? cmd_ctx.err.buf : cmd_ctx.out.buf, &ver)) {
		ver = make_str(wk, "unknown");
	}

	unknown = false;

detection_over:
	if (unknown) {
		LOG_W("unable to detect compiler type, falling back on posix compiler");
		type = compiler_posix;
		ver = make_str(wk, "unknown");
	}

	struct obj_compiler *comp = get_obj_compiler(wk, comp_id);
	comp->cmd_arr = cmd_arr;
	comp->type[toolchain_component_compiler] = type;
	comp->ver = ver;

	run_cmd_ctx_destroy(&cmd_ctx);
	return true;
}

static bool
compiler_detect_nasm(struct workspace *wk, obj cmd_arr, obj comp_id)
{
	struct run_cmd_ctx cmd_ctx = { 0 };
	if (!run_cmd_arr(wk, &cmd_ctx, cmd_arr, "--version")
		|| strstr(cmd_ctx.err.buf, "nasm: error: unable to find utility")) {
		run_cmd_ctx_destroy(&cmd_ctx);
		return false;
	}

	enum compiler_type type;
	obj ver;

	if (strstr(cmd_ctx.out.buf, "NASM")) {
		type = compiler_nasm;
	} else if (strstr(cmd_ctx.out.buf, "yasm")) {
		type = compiler_yasm;
	} else {
		// Just assume it is nasm
		type = compiler_nasm;
	}

	if (!guess_version(wk, cmd_ctx.out.buf, &ver)) {
		ver = make_str(wk, "unknown");
	}

	obj new_cmd;
	obj_array_dup(wk, cmd_arr, &new_cmd);

	{
		uint32_t addr_bits = host_machine.address_bits;

		const char *plat;
		SBUF(define);

		if (host_machine.is_windows) {
			plat = "win";
			sbuf_pushf(wk, &define, "WIN%d", addr_bits);
		} else if (host_machine.sys == machine_system_darwin) {
			plat = "macho";
			sbuf_pushs(wk, &define, "MACHO");
		} else {
			plat = "elf";
			sbuf_pushs(wk, &define, "ELF");
		}

		obj_array_push(wk, new_cmd, make_strf(wk, "-f%s%d", plat, addr_bits));
		obj_array_push(wk, new_cmd, make_strf(wk, "-D%s", define.buf));
		if (addr_bits == 64) {
			obj_array_push(wk, new_cmd, make_str(wk, "-D__x86_64__"));
		}
	}

	struct obj_compiler *comp = get_obj_compiler(wk, comp_id);
	comp->cmd_arr = new_cmd;
	comp->type[toolchain_component_compiler] = type;
	comp->ver = ver;
	comp->lang = compiler_language_nasm;

	run_cmd_ctx_destroy(&cmd_ctx);
	return true;
}

static bool
compiler_get_libdirs(struct workspace *wk, struct obj_compiler *comp)
{
	struct run_cmd_ctx cmd_ctx = { 0 };
	if (!run_cmd_arr(wk, &cmd_ctx, comp->cmd_arr, "-print-search-dirs") || cmd_ctx.status) {
		goto done;
	}

	const char *key = "libraries: ";
	char *s, *e;
	bool beginning_of_line = true;
	for (s = cmd_ctx.out.buf; *s; ++s) {
		if (beginning_of_line && strncmp(s, key, strlen(key)) == 0) {
			s += strlen(key);
			if (*s == '=') {
				++s;
			}

			e = strchr(s, '\n');

			struct str str = {
				.s = s,
				.len = e ? (uint32_t)(e - s) : strlen(s),
			};

			comp->libdirs = str_split(wk, &str, &WKSTR(ENV_PATH_SEP_STR));
			goto done;
		}

		beginning_of_line = *s == '\n';
	}

done:
	run_cmd_ctx_destroy(&cmd_ctx);

	if (!comp->libdirs) {
		const char *libdirs[] = { "/usr/lib", "/usr/local/lib", "/lib", NULL };

		make_obj(wk, &comp->libdirs, obj_array);

		uint32_t i;
		for (i = 0; libdirs[i]; ++i) {
			obj_array_push(wk, comp->libdirs, make_str(wk, libdirs[i]));
		}
	}

	return true;
}

static void
compiler_refine_host_machine(struct workspace *wk, struct obj_compiler *comp)
{
	struct run_cmd_ctx cmd_ctx = { 0 };
	if (run_cmd_arr(wk, &cmd_ctx, comp->cmd_arr, "-dumpmachine") && cmd_ctx.status == 0) {
		machine_parse_and_apply_triplet(&host_machine, cmd_ctx.out.buf);
	}
	run_cmd_ctx_destroy(&cmd_ctx);

	// TODO: check for macros like ILP32 and x86_64
}

static bool
compiler_detect_cmd_arr(struct workspace *wk, obj comp, enum compiler_language lang, obj cmd_arr)
{
	if (log_should_print(log_debug)) {
		obj_fprintf(wk, log_file(), "checking compiler %o\n", cmd_arr);
	}

	switch (lang) {
	case compiler_language_c:
	case compiler_language_cpp:
	case compiler_language_objc:
	case compiler_language_objcpp:
		if (!compiler_detect_c_or_cpp(wk, cmd_arr, comp)) {
			return false;
		}

		struct obj_compiler *compiler = get_obj_compiler(wk, comp);
		compiler_get_libdirs(wk, compiler);

		compiler_refine_host_machine(wk, compiler);

		compiler->lang = lang;

		compiler->linker_passthrough = compiler->type[toolchain_component_compiler] != compiler_msvc;
		return true;
	case compiler_language_nasm:
		if (!compiler_detect_nasm(wk, cmd_arr, comp)) {
			return false;
		}
		return true;
	default:
		LOG_E("tried to get a compiler for unsupported language '%s'", compiler_language_to_s(lang));
		return false;
	}
}

static bool
static_linker_detect(struct workspace *wk, obj comp, enum compiler_language lang, obj cmd_arr)
{
	struct obj_compiler *compiler = get_obj_compiler(wk, comp);

	struct run_cmd_ctx cmd_ctx = { 0 };
	if (!run_cmd_arr(wk,
		    &cmd_ctx,
		    cmd_arr,
		    guess_version_arg(wk, compiler->type[toolchain_component_compiler] == compiler_msvc))) {
		run_cmd_ctx_destroy(&cmd_ctx);
		return false;
	}

	enum static_linker_type type = compiler->type[toolchain_component_compiler] == compiler_msvc ?
					       static_linker_msvc :
					       static_linker_ar_posix;

	if (cmd_ctx.status == 0 && strstr(cmd_ctx.out.buf, "Free Software Foundation")) {
		type = static_linker_ar_gcc;
	}

	run_cmd_ctx_destroy(&cmd_ctx);

	get_obj_compiler(wk, comp)->static_linker_cmd_arr = cmd_arr;
	get_obj_compiler(wk, comp)->type[toolchain_component_static_linker] = type;
	return true;
}

static bool
linker_detect(struct workspace *wk, obj comp, enum compiler_language lang, obj cmd_arr)
{
	struct obj_compiler *compiler = get_obj_compiler(wk, comp);

	bool msvc_like = compiler->type[toolchain_component_compiler] == compiler_msvc;

	enum linker_type type = compilers[compiler->type[toolchain_component_compiler]].default_linker;
	if (host_machine.sys == machine_system_windows) {
		if (compiler->type[toolchain_component_compiler] == compiler_clang) {
			type = linker_lld_link;
			msvc_like = true;
		}
	}

	struct run_cmd_ctx cmd_ctx = { 0 };
	if (!run_cmd_arr(wk, &cmd_ctx, cmd_arr, guess_version_arg(wk, msvc_like))) {
		run_cmd_ctx_destroy(&cmd_ctx);
		return false;
	}

	// TODO: do something with command output?

	run_cmd_ctx_destroy(&cmd_ctx);

	get_obj_compiler(wk, comp)->linker_cmd_arr = cmd_arr;
	get_obj_compiler(wk, comp)->type[toolchain_component_linker] = type;
	return true;
}

typedef bool((*toolchain_detect_cmd_arr_cb)(struct workspace *wk, obj comp, enum compiler_language lang, obj cmd_arr));

bool
toolchain_exe_detect(struct workspace *wk,
	const char *toolchain_exe_option_name,
	const char *exe_list[],
	obj comp,
	enum compiler_language lang,
	toolchain_detect_cmd_arr_cb cb)
{
	if (!toolchain_exe_option_name) {
		return false;
	}

	obj cmd_arr_opt;
	get_option(wk, NULL, &WKSTR(toolchain_exe_option_name), &cmd_arr_opt);
	struct obj_option *cmd_arr = get_obj_option(wk, cmd_arr_opt);

	if (cmd_arr->source > option_value_source_default) {
		return cb(wk, comp, lang, cmd_arr->val);
	}

	uint32_t i;
	for (i = 0; exe_list[i]; ++i) {
		obj cmd_arr;
		make_obj(wk, &cmd_arr, obj_array);
		obj_array_push(wk, cmd_arr, make_str(wk, exe_list[i]));

		if (cb(wk, comp, lang, cmd_arr)) {
			return true;
		}
	}

	return false;
}

static bool
toolchain_linker_detect(struct workspace *wk, obj comp, enum compiler_language lang)
{
	struct obj_compiler *compiler = get_obj_compiler(wk, comp);

	enum linker_type t = compilers[compiler->type[toolchain_component_compiler]].default_linker;
	if (host_machine.sys == machine_system_windows) {
		if (compiler->type[toolchain_component_compiler] == compiler_clang) {
			t = linker_lld_link;
		}
	}

	const char *exe_list[] = { linker_type_to_s(t), "ld", NULL };

	return toolchain_exe_detect(wk, "env.LD", exe_list, comp, lang, linker_detect);
}

static bool
toolchain_static_linker_detect(struct workspace *wk, obj comp, enum compiler_language lang)
{
	const char **exe_list = NULL;

	struct obj_compiler *compiler = get_obj_compiler(wk, comp);

	if (compiler->type[toolchain_component_compiler] == compiler_msvc) {
		static const char *msvc_list[] = { "lib", NULL };
		exe_list = msvc_list;
	} else {
		static const char *default_list[] = { "ar", "llvm-ar", NULL };
		exe_list = default_list;
	}

	return toolchain_exe_detect(wk, "env.AR", exe_list, comp, lang, static_linker_detect);
}

static bool
toolchain_compiler_detect(struct workspace *wk, obj comp, enum compiler_language lang)
{
	static const char *compiler_option[compiler_language_count] = {
		[compiler_language_c] = "env.CC",
		[compiler_language_cpp] = "env.CXX",
		[compiler_language_objc] = "env.OBJC",
		[compiler_language_objcpp] = "env.OBJCPP",
		[compiler_language_nasm] = "env.NASM",
	};

	const char **exe_list = NULL;

	if (host_machine.sys == machine_system_windows) {
		static const char *default_executables[][compiler_language_count] = {
			[compiler_language_c] = { "cl", "cc", "gcc", "clang", "clang-cl", NULL },
			[compiler_language_cpp] = { "cl", "c++", "g++", "clang++", "clang-cl", NULL },
			[compiler_language_objc] = { "cc", "gcc", NULL },
			[compiler_language_objcpp] = { "c++", "g++", "clang++", "clang-cl", NULL },
			[compiler_language_nasm] = { "nasm", "yasm", NULL },
		};

		exe_list = default_executables[lang];
	} else {
		static const char *default_executables[][compiler_language_count] = {
			[compiler_language_c] = { "cc", "gcc", "clang", NULL },
			[compiler_language_cpp] = { "c++", "g++", "clang++", NULL },
			[compiler_language_objc] = { "cc", "gcc", "clang", NULL },
			[compiler_language_objcpp] = { "c++", "g++", "clang++", NULL },
			[compiler_language_nasm] = { "nasm", "yasm", NULL },
		};

		exe_list = default_executables[lang];
	}

	return toolchain_exe_detect(wk, compiler_option[lang], exe_list, comp, lang, compiler_detect_cmd_arr);
}

bool
toolchain_detect(struct workspace *wk, obj *comp, enum compiler_language lang)
{
	make_obj(wk, comp, obj_compiler);

	if (!toolchain_compiler_detect(wk, *comp, lang)) {
		LOG_E("failed to detect compiler");
		return false;
	}

	if (!toolchain_linker_detect(wk, *comp, lang)) {
		LOG_E("failed to detect linker");
		return false;
	}

	if (!toolchain_static_linker_detect(wk, *comp, lang)) {
		LOG_E("failed to detect static linker");
		return false;
	}

	struct obj_compiler *compiler = get_obj_compiler(wk, *comp);

	LLOG_I("detected compiler %s ", compiler_type_to_s(compiler->type[toolchain_component_compiler]));
	obj_fprintf(wk,
		log_file(),
		"%o (%o), linker: %s (%o), static_linker: %s (%o)\n",
		compiler->ver,
		compiler->cmd_arr,
		linker_type_to_s(compiler->type[toolchain_component_linker]),
		compiler->linker_cmd_arr,
		static_linker_type_to_s(compiler->type[toolchain_component_static_linker]),
		compiler->static_linker_cmd_arr);

	return true;
}

/*******************************************************************************
 * Toolchain args
 ******************************************************************************/

#define TOOLCHAIN_ARGS(...)                      \
	static const char *argv[] = __VA_ARGS__; \
	static struct args args = { .args = argv, .len = ARRAY_LEN(argv) };

#define TOOLCHAIN_ARGS_RETURN static const struct args *
#define TOOLCHAIN_PROTO_0(name) TOOLCHAIN_ARGS_RETURN name(TOOLCHAIN_SIG_0)
#define TOOLCHAIN_PROTO_1i(name) TOOLCHAIN_ARGS_RETURN name(TOOLCHAIN_SIG_1i)
#define TOOLCHAIN_PROTO_1s(name) TOOLCHAIN_ARGS_RETURN name(TOOLCHAIN_SIG_1s)
#define TOOLCHAIN_PROTO_2s(name) TOOLCHAIN_ARGS_RETURN name(TOOLCHAIN_SIG_2s)
#define TOOLCHAIN_PROTO_1s1b(name) TOOLCHAIN_ARGS_RETURN name(TOOLCHAIN_SIG_1s1b)
#define TOOLCHAIN_PROTO_ns(name) TOOLCHAIN_ARGS_RETURN name(TOOLCHAIN_SIG_ns)

/* empty functions */

TOOLCHAIN_PROTO_0(toolchain_arg_empty_0)
{
	TOOLCHAIN_ARGS({ NULL });
	args.len = 0;
	return &args;
}

TOOLCHAIN_PROTO_1i(toolchain_arg_empty_1i)
{
	TOOLCHAIN_ARGS({ NULL });
	args.len = 0;
	return &args;
}

TOOLCHAIN_PROTO_1s(toolchain_arg_empty_1s)
{
	TOOLCHAIN_ARGS({ NULL });
	args.len = 0;
	return &args;
}

TOOLCHAIN_PROTO_2s(toolchain_arg_empty_2s)
{
	TOOLCHAIN_ARGS({ NULL });
	args.len = 0;
	return &args;
}

TOOLCHAIN_PROTO_1s1b(toolchain_arg_empty_1s1b)
{
	TOOLCHAIN_ARGS({ NULL });
	args.len = 0;
	return &args;
}

TOOLCHAIN_PROTO_ns(toolchain_arg_empty_ns)
{
	return a;
}

/* posix compilers */

TOOLCHAIN_PROTO_0(compiler_posix_args_object_extension)
{
	TOOLCHAIN_ARGS({ ".o" });

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_posix_args_compile_only)
{
	TOOLCHAIN_ARGS({ "-c" });

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_posix_args_preprocess_only)
{
	TOOLCHAIN_ARGS({ "-E" });

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_posix_args_output)
{
	TOOLCHAIN_ARGS({ "-o", NULL });

	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_1i(compiler_posix_args_optimization)
{
	TOOLCHAIN_ARGS({ NULL });

	switch ((enum compiler_optimization_lvl)a) {
	case compiler_optimization_lvl_0: argv[0] = "-O0"; break;
	case compiler_optimization_lvl_1:
	case compiler_optimization_lvl_2:
	case compiler_optimization_lvl_3: argv[0] = "-O1"; break;
	case compiler_optimization_lvl_none:
	case compiler_optimization_lvl_g:
	case compiler_optimization_lvl_s: args.len = 0; break;
	}

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_posix_args_debug)
{
	TOOLCHAIN_ARGS({ "-g" });

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_posix_args_include)
{
	TOOLCHAIN_ARGS({ "-I", NULL });

	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_posix_args_define)
{
	TOOLCHAIN_ARGS({ "-D", NULL });

	argv[1] = a;

	return &args;
}

/* gcc compilers */

TOOLCHAIN_PROTO_ns(linker_args_passthrough)
{
	static char buf[BUF_SIZE_S], buf2[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf, NULL, buf2 });

	if (a->len == 0) {
		return a;
	} else if (a->len == 1) {
		const char *passthrough_blacklist[] = {
			"-shared",
			"-bundle",
			"-dynamiclib",
		};

		uint32_t i;
		for (i = 0; i < ARRAY_LEN(passthrough_blacklist); ++i) {
			if (strcmp(passthrough_blacklist[i], a->args[0]) == 0) {
				return a;
			}
		}

		snprintf(buf, BUF_SIZE_S, "-Wl,%s", a->args[0]);
		args.len = 1;
	} else if (a->len == 2) {
		snprintf(buf, BUF_SIZE_S, "-Wl,%s,%s", a->args[0], a->args[1]);
		args.len = 1;
	} else if (a->len == 3) {
		snprintf(buf, BUF_SIZE_S, "-Wl,%s", a->args[0]);
		argv[1] = a->args[1];
		snprintf(buf2, BUF_SIZE_S, "-Wl,%s", a->args[2]);
		args.len = 3;
	} else {
		UNREACHABLE;
	}

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_gcc_args_preprocess_only)
{
	TOOLCHAIN_ARGS({ "-E", "-P" });

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_gcc_args_include_system)
{
	TOOLCHAIN_ARGS({ "-isystem", NULL });

	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_2s(compiler_gcc_args_deps)
{
	TOOLCHAIN_ARGS({ "-MD", "-MQ", NULL, "-MF", NULL });

	argv[2] = a;
	argv[4] = b;

	return &args;
}

TOOLCHAIN_PROTO_1i(compiler_gcc_args_optimization)
{
	TOOLCHAIN_ARGS({ NULL });

	switch ((enum compiler_optimization_lvl)a) {
	case compiler_optimization_lvl_none: args.len = 0; break;
	case compiler_optimization_lvl_0: argv[0] = "-O0"; break;
	case compiler_optimization_lvl_1: argv[0] = "-O1"; break;
	case compiler_optimization_lvl_2: argv[0] = "-O2"; break;
	case compiler_optimization_lvl_3: argv[0] = "-O3"; break;
	case compiler_optimization_lvl_g: argv[0] = "-Og"; break;
	case compiler_optimization_lvl_s: argv[0] = "-Os"; break;
	}

	return &args;
}

TOOLCHAIN_PROTO_1i(compiler_gcc_args_warning_lvl)
{
	TOOLCHAIN_ARGS({ NULL, NULL, NULL });

	args.len = 0;

	switch ((enum compiler_warning_lvl)a) {
	case compiler_warning_lvl_everything: UNREACHABLE; break;
	case compiler_warning_lvl_3: argv[args.len] = "-Wpedantic"; ++args.len;
	/* fallthrough */
	case compiler_warning_lvl_2: argv[args.len] = "-Wextra"; ++args.len;
	/* fallthrough */
	case compiler_warning_lvl_1: argv[args.len] = "-Wall"; ++args.len;
	/* fallthrough */
	case compiler_warning_lvl_0: break;
	}

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_gcc_args_werror)
{
	TOOLCHAIN_ARGS({ "-Werror" });
	return &args;
}

TOOLCHAIN_PROTO_0(compiler_gcc_args_warn_everything)
{
	TOOLCHAIN_ARGS({ "-Wall",
		"-Winvalid-pch",
		"-Wextra",
		"-Wpedantic",
		"-Wcast-qual",
		"-Wconversion",
		"-Wfloat-equal",
		"-Wformat=2",
		"-Winline",
		"-Wmissing-declarations",
		"-Wredundant-decls",
		"-Wshadow",
		"-Wundef",
		"-Wuninitialized",
		"-Wwrite-strings",
		"-Wdisabled-optimization",
		"-Wpacked",
		"-Wpadded",
		"-Wmultichar",
		"-Wswitch-default",
		"-Wswitch-enum",
		"-Wunused-macros",
		"-Wmissing-include-dirs",
		"-Wunsafe-loop-optimizations",
		"-Wstack-protector",
		"-Wstrict-overflow=5",
		"-Warray-bounds=2",
		"-Wlogical-op",
		"-Wstrict-aliasing=3",
		"-Wvla",
		"-Wdouble-promotion",
		"-Wsuggest-attribute=const",
		"-Wsuggest-attribute=noreturn",
		"-Wsuggest-attribute=pure",
		"-Wtrampolines",
		"-Wvector-operation-performance",
		"-Wsuggest-attribute=format",
		"-Wdate-time",
		"-Wformat-signedness",
		"-Wnormalized=nfc",
		"-Wduplicated-cond",
		"-Wnull-dereference",
		"-Wshift-negative-value",
		"-Wshift-overflow=2",
		"-Wunused-const-variable=2",
		"-Walloca",
		"-Walloc-zero",
		"-Wformat-overflow=2",
		"-Wformat-truncation=2",
		"-Wstringop-overflow=3",
		"-Wduplicated-branches",
		"-Wattribute-alias=2",
		"-Wcast-align=strict",
		"-Wsuggest-attribute=cold",
		"-Wsuggest-attribute=malloc",
		"-Wanalyzer-too-complex",
		"-Warith-conversion",
		"-Wbidi-chars=ucn",
		"-Wopenacc-parallelism",
		"-Wtrivial-auto-var-init",
		"-Wbad-function-cast",
		"-Wmissing-prototypes",
		"-Wnested-externs",
		"-Wstrict-prototypes",
		"-Wold-style-definition",
		"-Winit-self",
		"-Wc++-compat",
		"-Wunsuffixed-float-constants" });
	return &args;
}

TOOLCHAIN_PROTO_0(compiler_clang_args_warn_everything)
{
	TOOLCHAIN_ARGS({ "-Weverything" });
	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_gcc_args_set_std)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });

	snprintf(buf, BUF_SIZE_S, "-std=%s", a);

	return &args;
}

TOOLCHAIN_PROTO_1i(compiler_gcc_args_pgo)
{
	TOOLCHAIN_ARGS({ NULL, NULL });

	args.len = 1;

	switch ((enum compiler_pgo_stage)a) {
	case compiler_pgo_generate: argv[0] = "-fprofile-generate"; break;
	case compiler_pgo_use:
		argv[1] = "-fprofile-correction";
		++args.len;
		argv[0] = "-fprofile-use";
		break;
	}

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_gcc_args_pic)
{
	TOOLCHAIN_ARGS({ "-fpic" });

	if (host_machine.is_windows) {
		args.len = 0;
	} else {
		args.len = 1;
	}
	return &args;
}

TOOLCHAIN_PROTO_0(compiler_gcc_args_pie)
{
	TOOLCHAIN_ARGS({ "-fpie" });
	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_gcc_args_sanitize)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });

	snprintf(buf, BUF_SIZE_S, "-fsanitize=%s", a);

	return &args;
}

TOOLCHAIN_PROTO_1i(compiler_gcc_args_visibility)
{
	TOOLCHAIN_ARGS({ NULL, NULL });

	args.len = 1;

	switch ((enum compiler_visibility_type)a) {
	case compiler_visibility_default: argv[0] = "-fvisibility=default"; break;
	case compiler_visibility_internal: argv[0] = "-fvisibility=internal"; break;
	case compiler_visibility_protected: argv[0] = "-fvisibility=protected"; break;
	case compiler_visibility_inlineshidden: argv[1] = "-fvisibility-inlines-hidden"; ++args.len;
	// fallthrough
	case compiler_visibility_hidden: argv[0] = "-fvisibility=hidden"; break;
	default: assert(false && "unreachable");
	}

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_gcc_args_specify_lang)
{
	TOOLCHAIN_ARGS({
		"-x",
		NULL,
	});

	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_gcc_args_color_output)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });

	snprintf(buf, BUF_SIZE_S, "-fdiagnostics-color=%s", a);

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_gcc_args_lto)
{
	TOOLCHAIN_ARGS({ "-flto" });

	return &args;
}

/* cl compilers
 * see mesonbuild/compilers/mixins/visualstudio.py for reference
 */

TOOLCHAIN_PROTO_0(compiler_cl_args_always)
{
	TOOLCHAIN_ARGS({ "/nologo" });

	return &args;
}

TOOLCHAIN_PROTO_1s1b(compiler_cl_args_crt)
{
	TOOLCHAIN_ARGS({ NULL });

	if (strcmp(a, "from_buildtype") == 0) {
		argv[0] = b ? "/MDd" : "/MD";
	} else if (strcmp(a, "static_from_buildtype") == 0) {
		argv[0] = b ? "/MTd" : "/MT";
	} else {
		argv[0] = a;
	}

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_cl_args_debugfile)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });

	snprintf(buf, BUF_SIZE_S, "/Fd%s.pdb", a);

	return &args;
}

TOOLCHAIN_PROTO_2s(compiler_cl_args_deps)
{
	TOOLCHAIN_ARGS({ "/showIncludes" });

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_cl_args_compile_only)
{
	TOOLCHAIN_ARGS({ "/c" });

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_cl_args_preprocess_only)
{
	TOOLCHAIN_ARGS({ "/EP" });

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_cl_args_output)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });

	if (str_endswithi(&WKSTR(a), &WKSTR(".exe"))) {
		snprintf(buf, BUF_SIZE_S, "/Fe%s", a);
	} else {
		snprintf(buf, BUF_SIZE_S, "/Fo%s", a);
	}

	return &args;
}

TOOLCHAIN_PROTO_1i(compiler_cl_args_optimization)
{
	TOOLCHAIN_ARGS({ NULL, NULL });
	switch ((enum compiler_optimization_lvl)a) {
	case compiler_optimization_lvl_none:
	case compiler_optimization_lvl_g: args.len = 0; break;
	case compiler_optimization_lvl_0:
		args.len = 1;
		argv[0] = "/Od";
		break;
	case compiler_optimization_lvl_1:
		args.len = 1;
		argv[0] = "/O1";
		break;
	case compiler_optimization_lvl_2:
		args.len = 1;
		argv[0] = "/O2";
		break;
	case compiler_optimization_lvl_3:
		args.len = 2;
		argv[0] = "/O2";
		argv[1] = "/Gw";
		break;
	case compiler_optimization_lvl_s:
		args.len = 2;
		argv[0] = "/O1";
		argv[1] = "/Gw";
		break;
	}

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_cl_args_debug)
{
	TOOLCHAIN_ARGS({ "/Zi" });

	return &args;
}

TOOLCHAIN_PROTO_1i(compiler_cl_args_warning_lvl)
{
	/* meson uses nothing instead of /W0, but it's the same warning level
	 * see: https://mesonbuild.com/Builtin-options.html#details-for-warning_level
	 */

	TOOLCHAIN_ARGS({ NULL });

	switch ((enum compiler_warning_lvl)a) {
	case compiler_warning_lvl_0: args.len = 0; break;
	case compiler_warning_lvl_1: argv[0] = "/W2"; break;
	case compiler_warning_lvl_2: argv[0] = "/W3"; break;
	case compiler_warning_lvl_everything:
	case compiler_warning_lvl_3: argv[0] = "/W4"; break;
	}

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_cl_args_warn_everything)
{
	TOOLCHAIN_ARGS({ "/Wall" });
	return &args;
}

TOOLCHAIN_PROTO_0(compiler_cl_args_werror)
{
	TOOLCHAIN_ARGS({ "/WX" });
	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_cl_args_set_std)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });
	snprintf(buf, BUF_SIZE_S, "/std:%s", a);

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_cl_args_include)
{
	TOOLCHAIN_ARGS({ "/I", NULL });

	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_cl_args_sanitize)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });
	snprintf(buf, BUF_SIZE_S, "-fsanitize=%s", a);

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_cl_args_define)
{
	TOOLCHAIN_ARGS({ "/D", NULL });

	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_cl_args_object_extension)
{
	TOOLCHAIN_ARGS({ ".obj" });

	return &args;
}

TOOLCHAIN_PROTO_1s(compiler_clang_cl_args_color_output)
{
	TOOLCHAIN_ARGS({ "-fcolor-diagnostics" });

	return &args;
	(void)a;
}

TOOLCHAIN_PROTO_0(compiler_clang_cl_args_lto)
{
	TOOLCHAIN_ARGS({ "-flto" });

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_deps_gcc)
{
	TOOLCHAIN_ARGS({ "gcc" });

	return &args;
}

TOOLCHAIN_PROTO_0(compiler_deps_msvc)
{
	TOOLCHAIN_ARGS({ "msvc" });

	return &args;
}

TOOLCHAIN_PROTO_1s(linker_posix_args_lib)
{
	TOOLCHAIN_ARGS({ "-l", NULL });
	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_2s(linker_posix_args_input_output)
{
	TOOLCHAIN_ARGS({ NULL, NULL });
	argv[0] = b;
	argv[1] = a;
	return &args;
}

/* technically not a posix linker argument, but include it here since it is so
 * common
 */

TOOLCHAIN_PROTO_0(linker_posix_args_shared)
{
	TOOLCHAIN_ARGS({ "-shared" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_ld_args_as_needed)
{
	TOOLCHAIN_ARGS({ "--as-needed" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_ld_args_no_undefined)
{
	TOOLCHAIN_ARGS({ "--no-undefined" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_ld_args_start_group)
{
	TOOLCHAIN_ARGS({ "--start-group" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_ld_args_end_group)
{
	TOOLCHAIN_ARGS({ "--end-group" });
	return &args;
}

TOOLCHAIN_PROTO_1s(linker_ld_args_soname)
{
	TOOLCHAIN_ARGS({ "-soname", NULL });
	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_1s(linker_ld_args_rpath)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });

	snprintf(buf, BUF_SIZE_S, "-rpath,%s", a);

	return &args;
}

TOOLCHAIN_PROTO_0(linker_ld_args_allow_shlib_undefined)
{
	TOOLCHAIN_ARGS({ "--allow-shlib-undefined" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_ld_args_export_dynamic)
{
	TOOLCHAIN_ARGS({ "-export-dynamic" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_ld_args_fatal_warnings)
{
	TOOLCHAIN_ARGS({ "--fatal-warnings" });
	return &args;
}

TOOLCHAIN_PROTO_1s(linker_ld_args_whole_archive)
{
	TOOLCHAIN_ARGS({ "--whole-archive", NULL, "--no-whole-archive" });
	argv[1] = a;
	return &args;
}

/* cl linkers */

TOOLCHAIN_PROTO_1s(linker_link_args_lib)
{
	TOOLCHAIN_ARGS({ NULL });
	argv[0] = a;

	return &args;
}

TOOLCHAIN_PROTO_0(linker_link_args_debug)
{
	TOOLCHAIN_ARGS({ "/DEBUG" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_link_args_shared)
{
	TOOLCHAIN_ARGS({ "/DLL" });
	return &args;
}

TOOLCHAIN_PROTO_1s(linker_link_args_soname)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf });

	snprintf(buf, BUF_SIZE_S, "/OUT:%s", a);

	return &args;
}

TOOLCHAIN_PROTO_2s(linker_link_args_input_output)
{
	static char buf[BUF_SIZE_S];
	TOOLCHAIN_ARGS({ buf, NULL });

	snprintf(buf, BUF_SIZE_S, "/out:%s", b);

	argv[1] = a;

	return &args;
}

TOOLCHAIN_PROTO_1s(linker_lld_link_args_whole_archive)
{
	TOOLCHAIN_ARGS({ "/whole-archive", NULL });
	argv[1] = a;
	return &args;
}

/* apple linker */

TOOLCHAIN_PROTO_1s(linker_apple_args_whole_archive)
{
	TOOLCHAIN_ARGS({ "-force_load", NULL });
	argv[1] = a;
	return &args;
}

TOOLCHAIN_PROTO_0(linker_apple_args_allow_shlib_undefined)
{
	TOOLCHAIN_ARGS({ "-undefined", "dynamic_lookup" });
	return &args;
}

TOOLCHAIN_PROTO_0(linker_apple_args_shared_module)
{
	TOOLCHAIN_ARGS({ "-bundle" });
	return &args;
}

/* static linkers */

TOOLCHAIN_PROTO_0(static_linker_ar_posix_args_base)
{
	TOOLCHAIN_ARGS({ "csr" });
	return &args;
}

TOOLCHAIN_PROTO_0(static_linker_ar_gcc_args_base)
{
	TOOLCHAIN_ARGS({ "csrD" });
	return &args;
}

struct compiler compilers[compiler_type_count];
struct linker linkers[linker_type_count];
struct static_linker static_linkers[static_linker_type_count];

const struct language languages[compiler_language_count] = {
	[compiler_language_null] = { 0 },
	[compiler_language_c] = { .is_header = false },
	[compiler_language_c_hdr] = { .is_header = true },
	[compiler_language_cpp] = { .is_header = false },
	[compiler_language_cpp_hdr] = { .is_header = true },
	[compiler_language_c_obj] = { .is_linkable = true },
	[compiler_language_assembly] = { 0 },
	[compiler_language_llvm_ir] = { 0 },
};

#define TOOLCHAIN_ARG_MEMBER_(name, __type, params, names) .name = toolchain_arg_empty_##__type,
#define TOOLCHAIN_ARG_MEMBER(name, comp, type) TOOLCHAIN_ARG_MEMBER_(name, type)

static void
build_compilers(void)
{
	struct compiler empty = {
		.args = { FOREACH_COMPILER_ARG(TOOLCHAIN_ARG_MEMBER) },
	};

	empty.args.object_ext = compiler_posix_args_object_extension;

	struct compiler clang_llvm_ir = empty;
	clang_llvm_ir.args.compile_only = compiler_posix_args_compile_only;
	clang_llvm_ir.args.output = compiler_posix_args_output;

	struct compiler posix = empty;
	posix.args.compile_only = compiler_posix_args_compile_only;
	posix.args.preprocess_only = compiler_posix_args_preprocess_only;
	posix.args.output = compiler_posix_args_output;
	posix.args.optimization = compiler_posix_args_optimization;
	posix.args.debug = compiler_posix_args_debug;
	posix.args.include = compiler_posix_args_include;
	posix.args.include_system = compiler_posix_args_include;
	posix.args.define = compiler_posix_args_define;
	posix.default_linker = linker_posix;
	posix.default_static_linker = static_linker_ar_posix;

	struct compiler gcc = posix;
	gcc.args.linker_passthrough = linker_args_passthrough;
	gcc.args.preprocess_only = compiler_gcc_args_preprocess_only;
	gcc.args.deps = compiler_gcc_args_deps;
	gcc.args.optimization = compiler_gcc_args_optimization;
	gcc.args.warning_lvl = compiler_gcc_args_warning_lvl;
	gcc.args.warn_everything = compiler_gcc_args_warn_everything;
	gcc.args.werror = compiler_gcc_args_werror;
	gcc.args.set_std = compiler_gcc_args_set_std;
	gcc.args.include_system = compiler_gcc_args_include_system;
	gcc.args.pgo = compiler_gcc_args_pgo;
	gcc.args.pic = compiler_gcc_args_pic;
	gcc.args.pie = compiler_gcc_args_pie;
	gcc.args.sanitize = compiler_gcc_args_sanitize;
	gcc.args.visibility = compiler_gcc_args_visibility;
	gcc.args.specify_lang = compiler_gcc_args_specify_lang;
	gcc.args.color_output = compiler_gcc_args_color_output;
	gcc.args.enable_lto = compiler_gcc_args_lto;
	gcc.args.deps_type = compiler_deps_gcc;
	gcc.default_linker = linker_ld;
	gcc.default_static_linker = static_linker_ar_gcc;

	struct compiler clang = gcc;
	clang.args.warn_everything = compiler_clang_args_warn_everything;
	clang.default_linker = linker_clang;

	struct compiler apple_clang = clang;
	apple_clang.default_linker = linker_apple;
	apple_clang.default_static_linker = static_linker_ar_posix;

	struct compiler msvc = empty;
	msvc.args.deps = compiler_cl_args_deps;
	msvc.args.compile_only = compiler_cl_args_compile_only;
	msvc.args.preprocess_only = compiler_cl_args_preprocess_only;
	msvc.args.output = compiler_cl_args_output;
	msvc.args.optimization = compiler_cl_args_optimization;
	msvc.args.debug = compiler_cl_args_debug;
	msvc.args.warning_lvl = compiler_cl_args_warning_lvl;
	msvc.args.warn_everything = compiler_cl_args_warn_everything;
	msvc.args.werror = compiler_cl_args_werror;
	msvc.args.set_std = compiler_cl_args_set_std;
	msvc.args.include = compiler_cl_args_include;
	msvc.args.sanitize = compiler_cl_args_sanitize;
	msvc.args.define = compiler_cl_args_define;
	msvc.args.always = compiler_cl_args_always;
	msvc.args.crt = compiler_cl_args_crt;
	msvc.args.debugfile = compiler_cl_args_debugfile;
	msvc.default_linker = linker_msvc;
	msvc.default_static_linker = static_linker_msvc;
	msvc.args.object_ext = compiler_cl_args_object_extension;
	msvc.args.deps_type = compiler_deps_msvc;

	struct compiler clang_cl = msvc;
	clang_cl.args.color_output = compiler_clang_cl_args_color_output;
	clang_cl.args.enable_lto = compiler_clang_cl_args_lto;
	clang_cl.default_linker = linker_lld_link;

	compilers[compiler_posix] = posix;
	compilers[compiler_gcc] = gcc;
	compilers[compiler_clang] = clang;
	compilers[compiler_apple_clang] = apple_clang;
	compilers[compiler_clang_llvm_ir] = clang_llvm_ir;
	compilers[compiler_clang_cl] = clang_cl;
	compilers[compiler_msvc] = msvc;

	struct compiler nasm = empty;
	nasm.args.output = compiler_posix_args_output;
	nasm.args.optimization = compiler_posix_args_optimization;
	nasm.args.debug = compiler_posix_args_debug;
	nasm.args.include = compiler_posix_args_include;
	nasm.args.include_system = compiler_posix_args_include;
	nasm.args.define = compiler_posix_args_define;
	nasm.default_linker = linker_posix;
	nasm.default_static_linker = static_linker_ar_posix;

	compilers[compiler_nasm] = nasm;
	compilers[compiler_yasm] = nasm;
}

static void
build_linkers(void)
{
	/* linkers */
	struct linker empty = { .args = { FOREACH_LINKER_ARG(TOOLCHAIN_ARG_MEMBER) } };

	struct linker posix = empty;
	posix.args.lib = linker_posix_args_lib;
	posix.args.shared = linker_posix_args_shared;
	posix.args.input_output = linker_posix_args_input_output;

	struct linker ld = posix;
	ld.args.as_needed = linker_ld_args_as_needed;
	ld.args.no_undefined = linker_ld_args_no_undefined;
	ld.args.start_group = linker_ld_args_start_group;
	ld.args.end_group = linker_ld_args_end_group;
	ld.args.soname = linker_ld_args_soname;
	ld.args.rpath = linker_ld_args_rpath;
	ld.args.pgo = compiler_gcc_args_pgo;
	ld.args.sanitize = compiler_gcc_args_sanitize;
	ld.args.allow_shlib_undefined = linker_ld_args_allow_shlib_undefined;
	ld.args.shared_module = linker_posix_args_shared;
	ld.args.export_dynamic = linker_ld_args_export_dynamic;
	ld.args.fatal_warnings = linker_ld_args_fatal_warnings;
	ld.args.whole_archive = linker_ld_args_whole_archive;
	ld.args.enable_lto = compiler_gcc_args_lto;

	struct linker lld = ld;

	struct linker apple = posix;
	posix.args.shared = linker_posix_args_shared;
	apple.args.sanitize = compiler_gcc_args_sanitize;
	apple.args.enable_lto = compiler_gcc_args_lto;
	apple.args.allow_shlib_undefined = linker_apple_args_allow_shlib_undefined;
	apple.args.shared_module = linker_apple_args_shared_module;
	apple.args.whole_archive = linker_apple_args_whole_archive;

	struct linker link = empty;
	link.args.lib = linker_link_args_lib;
	link.args.debug = linker_link_args_debug;
	link.args.shared = linker_link_args_shared;
	link.args.soname = linker_link_args_soname;
	link.args.input_output = linker_link_args_input_output;
	link.args.always = compiler_cl_args_always;

	struct linker lld_link = link;
	lld_link.args.whole_archive = linker_lld_link_args_whole_archive;
	lld_link.args.always = toolchain_arg_empty_0;

	linkers[linker_posix] = posix;
	linkers[linker_ld] = ld;
	linkers[linker_clang] = lld;
	linkers[linker_apple] = apple;
	linkers[linker_lld_link] = lld_link;
	linkers[linker_msvc] = link;
}

static void
build_static_linkers(void)
{
	struct static_linker empty = { .args = { FOREACH_STATIC_LINKER_ARG(TOOLCHAIN_ARG_MEMBER) } };

	struct static_linker posix = empty;
	posix.args.base = static_linker_ar_posix_args_base;
	posix.args.input_output = linker_posix_args_input_output;

	struct static_linker gcc = posix;
	gcc.args.base = static_linker_ar_gcc_args_base;

	struct static_linker msvc = empty;
	msvc.args.input_output = linker_link_args_input_output;
	msvc.args.always = compiler_cl_args_always;

	static_linkers[static_linker_ar_posix] = posix;
	static_linkers[static_linker_ar_gcc] = gcc;
	static_linkers[static_linker_msvc] = msvc;
}

#undef TOOLCHAIN_ARG_MEMBER
#undef TOOLCHAIN_ARG_MEMBER_

void
compilers_init(void)
{
	build_compilers();
	build_linkers();
	build_static_linkers();
}

#define TOOLCHAIN_ARG_MEMBER_(name, comp, __type, params, names) { #name, toolchain_arg_arity_##__type },
#define TOOLCHAIN_ARG_MEMBER(name, comp, type) TOOLCHAIN_ARG_MEMBER_(name, comp, type)
static struct toolchain_arg_handler toolchain_compiler_arg_handlers[] = { FOREACH_COMPILER_ARG(TOOLCHAIN_ARG_MEMBER) };
static struct toolchain_arg_handler toolchain_linker_arg_handlers[] = { FOREACH_LINKER_ARG(TOOLCHAIN_ARG_MEMBER) };
static struct toolchain_arg_handler toolchain_static_linker_arg_handlers[]
	= { FOREACH_STATIC_LINKER_ARG(TOOLCHAIN_ARG_MEMBER) };
#undef TOOLCHAIN_ARG_MEMBER
#undef TOOLCHAIN_ARG_MEMBER_

static struct {
	struct toolchain_arg_handler *handlers;
	uint32_t len;
} toolchain_arg_handlers[] = {
	[toolchain_component_compiler]
	= { toolchain_compiler_arg_handlers, ARRAY_LEN(toolchain_compiler_arg_handlers) },
	[toolchain_component_linker] = { toolchain_linker_arg_handlers, ARRAY_LEN(toolchain_linker_arg_handlers) },
	[toolchain_component_static_linker]
	= { toolchain_static_linker_arg_handlers, ARRAY_LEN(toolchain_static_linker_arg_handlers) },
};

const struct toolchain_arg_handler *
get_toolchain_arg_handler_info(enum toolchain_component component, const char *name)
{
	uint32_t i;
	for (i = 0; i < toolchain_arg_handlers[component].len; ++i) {
		if (strcmp(toolchain_arg_handlers[component].handlers[i].name, name) == 0) {
			return &toolchain_arg_handlers[component].handlers[i];
		}
	}

	return 0;
}

void
toolchain_arg_arity_to_sig(enum toolchain_arg_arity arity, type_tag signature[2], uint32_t *len)
{
	switch (arity) {
	case toolchain_arg_arity_0: {
		*len = 0;
		break;
	}
	case toolchain_arg_arity_1i: {
		signature[0] = tc_number;
		*len = 1;
		break;
	}
	case toolchain_arg_arity_1s: {
		signature[0] = tc_string;
		*len = 1;
		break;
	}
	case toolchain_arg_arity_2s: {
		signature[0] = tc_string;
		signature[1] = tc_string;
		*len = 2;
		break;
	}
	case toolchain_arg_arity_1s1b: {
		signature[0] = tc_string;
		signature[1] = tc_bool;
		*len = 2;
		break;
	}
	case toolchain_arg_arity_ns: {
		signature[0] = TYPE_TAG_GLOB | tc_string;
		*len = 1;
		break;
	}
	}
}

static obj
lookup_toolchain_arg_override(struct workspace *wk,
	struct obj_compiler *c,
	enum toolchain_component component,
	uint32_t arg)
{
	obj overrides = c->overrides[component], override;
	if (overrides
		&& obj_dict_index_str(wk, overrides, toolchain_arg_handlers[component].handlers[arg].name, &override)) {
		return override;
	}

	return 0;
}

enum toolchain_arg_by_component {
#define TOOLCHAIN_ARG_MEMBER_(comp, _name) toolchain_arg_by_component_##comp##_name,
#define TOOLCHAIN_ARG_MEMBER(name, comp, type) TOOLCHAIN_ARG_MEMBER_(comp, _##name)
	FOREACH_COMPILER_ARG(TOOLCHAIN_ARG_MEMBER) toolchain_arg_by_component_reset_0 = -1,
	FOREACH_LINKER_ARG(TOOLCHAIN_ARG_MEMBER) toolchain_arg_by_component_reset_1 = -1,
	FOREACH_STATIC_LINKER_ARG(TOOLCHAIN_ARG_MEMBER)
#undef TOOLCHAIN_ARG_MEMBER
#undef TOOLCHAIN_ARG_MEMBER_
};

static obj handle_toolchain_arg_override;

static const struct args *
handle_toolchain_arg_override_convert_to_args(struct workspace *wk, obj list)
{
	static const char *argv[32];
	static struct args args = { .args = argv, .len = 0 };

	obj v;
	obj_array_for(wk, list, v) {
		assert(args.len < ARRAY_LEN(argv) && "increase size of argv");

		++args.len;
		argv[args.len] = get_cstr(wk, v);
	}

	return &args;
}

static const struct args *
handle_toolchain_arg_override_check_const(struct workspace *wk)
{
	if (get_obj_type(wk, handle_toolchain_arg_override) == obj_array) {
		return handle_toolchain_arg_override_convert_to_args(wk, handle_toolchain_arg_override);
	}

	return 0;
}

#define constant_override_check()                                                        \
	{                                                                                \
		const struct args *args = handle_toolchain_arg_override_check_const(wk); \
		if (args) {                                                              \
			return args;                                                     \
		}                                                                        \
	}

static const struct args *
handle_toolchain_arg_override_0(TOOLCHAIN_SIG_0)
{
	constant_override_check();

	return 0;
}

static const struct args *
handle_toolchain_arg_override_1i(TOOLCHAIN_SIG_1i)
{
	constant_override_check();
	return 0;
}

static const struct args *
handle_toolchain_arg_override_1s(TOOLCHAIN_SIG_1s)
{
	constant_override_check();
	return 0;
}

static const struct args *
handle_toolchain_arg_override_2s(TOOLCHAIN_SIG_2s)
{
	constant_override_check();
	return 0;
}

static const struct args *
handle_toolchain_arg_override_1s1b(TOOLCHAIN_SIG_1s1b)
{
	constant_override_check();
	return 0;
}

static const struct args *
handle_toolchain_arg_override_ns(TOOLCHAIN_SIG_ns)
{
	constant_override_check();
	return 0;
}

#define TOOLCHAIN_ARG_MEMBER_(name, _name, component, _type, params, names)                                        \
	const struct args *toolchain_##component##_name params                                                     \
	{                                                                                                          \
		handle_toolchain_arg_override = lookup_toolchain_arg_override(                                     \
			wk, comp, toolchain_component_##component, toolchain_arg_by_component_##component##_name); \
		if (handle_toolchain_arg_override) {                                                               \
			return handle_toolchain_arg_override_##_type names;                                        \
		}                                                                                                  \
                                                                                                                   \
		return component##s[comp->type[toolchain_component_##component]].args.name names;                  \
	}

#define TOOLCHAIN_ARG_MEMBER(name, comp, type) TOOLCHAIN_ARG_MEMBER_(name, _##name, comp, type)

FOREACH_COMPILER_ARG(TOOLCHAIN_ARG_MEMBER)
FOREACH_LINKER_ARG(TOOLCHAIN_ARG_MEMBER)
FOREACH_STATIC_LINKER_ARG(TOOLCHAIN_ARG_MEMBER)

#undef TOOLCHAIN_ARG_MEMBER
#undef TOOLCHAIN_ARG_MEMBER_
