// -*- c++ -*-
// Distributed under the BSD 2-Clause License.
// See accompanying file LICENSE for details.
#include "configure.h"

#include <iostream>
#include <filesystem>
#include <fstream>
#include <optional>

#include <getoptpp/getoptpp.hpp>

#include "execute.h"
#include "ctor.h"
#include "tasks.h"
#include "rebuild.h"
#include "externals.h"
#include "tools.h"
#include "util.h"

std::filesystem::path configurationFile("configuration.cc");
std::filesystem::path configHeaderFile("config.h");

std::map<std::string, std::string> external_includedir;
std::map<std::string, std::string> external_libdir;

const ctor::configuration& __attribute__((weak)) ctor::get_configuration()
{
	static ctor::configuration cfg;
	static bool initialised{false};
	if(!initialised)
	{
		cfg.build_toolchain = getToolChain(cfg.get(ctor::cfg::build_cxx, "/usr/bin/g++"));
		initialised = true;
	}
	return cfg;
}

namespace ctor {
std::optional<std::string> includedir;
std::optional<std::string> libdir;
std::optional<std::string> builddir;
std::map<std::string, std::string> conf_values;
} // ctor::

bool ctor::configuration::has(const std::string& key) const
{
	if(key == ctor::cfg::ctor_includedir && ctor::includedir)
	{
		return true;
	}

	if(key == ctor::cfg::ctor_libdir && ctor::libdir)
	{
		return true;
	}

	if(key == ctor::cfg::builddir && ctor::builddir)
	{
		return true;
	}

	if(ctor::conf_values.find(key) != ctor::conf_values.end())
	{
		return true;
	}

	return tools.find(key) != tools.end();
}

const std::string& ctor::configuration::get(const std::string& key, const std::string& default_value) const
{
	if(key == ctor::cfg::ctor_includedir && ctor::includedir)
	{
		return *ctor::includedir;
	}

	if(key == ctor::cfg::ctor_libdir && ctor::libdir)
	{
		return *ctor::libdir;
	}

	if(key == ctor::cfg::builddir && ctor::builddir)
	{
		return *ctor::builddir;
	}

	if(ctor::conf_values.find(key) != ctor::conf_values.end())
	{
		return ctor::conf_values[key];
	}

	if(has(key))
	{
		return tools.at(key);
	}

	return default_value;
}

class Args
	: public std::vector<char*>
{
public:
	Args(const std::vector<std::string>& args)
	{
		resize(args.size() + 1);
		(*this)[0] = strdup("./ctor");
		for(std::size_t i = 0; i < size() - 1; ++i)
		{
			(*this)[i + 1] = strdup(args[i].data());
		}
	}

	~Args()
	{
		for(std::size_t i = 0; i < size(); ++i)
		{
			free((*this)[i]);
		}
	}
};

namespace {
std::ostream& operator<<(std::ostream& stream, const ctor::toolchain& toolchain)
{
	switch(toolchain)
	{
	case ctor::toolchain::any:
		stream << "ctor::toolchain::any";
		break;
	case ctor::toolchain::none:
		stream << "ctor::toolchain::none";
		break;
	case ctor::toolchain::gcc:
		stream << "ctor::toolchain::gcc";
		break;
	case ctor::toolchain::clang:
		stream << "ctor::toolchain::clang";
		break;
	}
	return stream;
}

std::ostream& operator<<(std::ostream& stream, const ctor::arch& arch)
{
	switch(arch)
	{
	case ctor::arch::unix:
		stream << "ctor::arch::unix";
		break;
	case ctor::arch::apple:
		stream << "ctor::arch::apple";
		break;
	case ctor::arch::windows:
		stream << "ctor::arch::windows";
		break;
	case ctor::arch::unknown:
		stream << "ctor::arch::unknown";
		break;
	}
	return stream;
}

std::ostream& operator<<(std::ostream& ostr, const ctor::c_flag& flag)
{
	for(const auto& s : to_strings(ctor::toolchain::any, flag))
	{
		ostr << s;
	}
	return ostr;
}

std::ostream& operator<<(std::ostream& ostr, const ctor::cxx_flag& flag)
{
	for(const auto& s : to_strings(ctor::toolchain::any, flag))
	{
		ostr << s;
	}
	return ostr;
}

std::ostream& operator<<(std::ostream& ostr, const ctor::ld_flag& flag)
{
	for(const auto& s : to_strings(ctor::toolchain::any, flag))
	{
		ostr << s;
	}
	return ostr;
}

std::ostream& operator<<(std::ostream& ostr, const ctor::asm_flag& flag)
{
	for(const auto& s : to_strings(ctor::toolchain::any, flag))
	{
		ostr << s;
	}
	return ostr;
}
}

// helper constant for the visitor
template<class> inline constexpr bool always_false_v = false;

int regenerateCache(ctor::settings& settings,
                    const std::string& name,
                    const std::vector<std::string>& args,
                    const std::map<std::string, std::string>& env)
{
	Args vargs(args);

	dg::Options opt;
	int key{128};

	std::string build_arch_prefix;
	std::string build_path;
	std::string host_arch_prefix;
	std::string host_path;
	std::string cc_prog = "gcc";
	std::string cxx_prog = "g++";
	std::string ar_prog = "ar";
	std::string ld_prog = "ld";
	std::string ctor_includedir;
	std::string ctor_libdir;
	std::string builddir;

	opt.add("build-dir", required_argument, 'b',
	        "Set output directory for build files (default: '" +
	        settings.builddir + "').",
	        [&]() {
		        settings.builddir = optarg;
		        builddir = optarg;
		        return 0;
	        });

	opt.add("verbose", no_argument, 'v',
	        "Be verbose. Add multiple times for more verbosity.",
	        [&]() {
		        settings.verbose++;
		        return 0;
	        });

	opt.add("cc", required_argument, key++,
	        "Use specified c-compiler instead of gcc.",
	        [&]() {
		        cc_prog = optarg;
		        return 0;
	        });

	opt.add("cxx", required_argument, key++,
	        "Use specified c++-compiler instead of g++.",
	        [&]() {
		        cxx_prog = optarg;
		        return 0;
	        });

	opt.add("ar", required_argument, key++,
	        "Use specified archiver instead of ar.",
	        [&]() {
		        ar_prog = optarg;
		        return 0;
	        });

	opt.add("ld", required_argument, key++,
	        "Use specified linker instead of ld.",
	        [&]() {
		        ld_prog = optarg;
		        return 0;
	        });

	opt.add("build", required_argument, key++,
	        "Configure for building on specified architecture.",
	        [&]() {
		        build_arch_prefix = optarg;
		        return 0;
	        });

	opt.add("build-path", required_argument, key++,
	        "Set path to build tool-chain.",
	        [&]() {
		        build_path = optarg;
		        return 0;
	        });

	opt.add("host", required_argument, key++,
	        "Cross-compile to build programs to run on specified architecture.",
	        [&]() {
		        host_arch_prefix = optarg;
		        return 0;
	        });

	opt.add("host-path", required_argument, key++,
	        "Set path to cross-compile tool-chain.",
	        [&]() {
		        host_path = optarg;
		        return 0;
	        });

	opt.add("ctor-includedir", required_argument, key++,
	        "Set path to ctor header file, used for re-compiling.",
	        [&]() {
		        ctor_includedir = optarg;
		        return 0;
	        });

	opt.add("ctor-libdir", required_argument, key++,
	        "Set path to ctor library file, used for re-compiling.",
	        [&]() {
		        ctor_libdir = optarg;
		        return 0;
	        });

	// Resolv externals
	ctor::external_configurations externalConfigs;
	for(std::size_t i = 0; i < numExternalConfigFiles; ++i)
	{
		auto newExternalConfigs = externalConfigFiles[i].cb(settings);
		externalConfigs.insert(externalConfigs.end(),
		                       newExternalConfigs.begin(),
		                       newExternalConfigs.end());
	}

	auto add_path_args =
		[&](const std::string& name)
		{
			opt.add(name + "-includedir", required_argument, key++,
			        "Set path to " + name + " header file.",
			        [&]() {
				        external_includedir[name] = optarg;
				        return 0;
			        });

			opt.add(name + "-libdir", required_argument, key++,
			        "Set path to " + name + " libraries.",
			        [&]() {
				        external_libdir[name] = optarg;
				        return 0;
			        });
		};

	for(const auto& ext : externalConfigs)
	{
		std::visit([&](auto&& arg)
		           {
			           using T = std::decay_t<decltype(arg)>;
			           if constexpr (std::is_same_v<T, ctor::external_manual>)
			           {
				           add_path_args(ext.name);
			           }
			           else
			           {
				           static_assert(always_false_v<T>, "non-exhaustive visitor!");
			           }
		           }, ext.external);

	}

	opt.add("help", no_argument, 'h',
	        "Print this help text.",
	        [&]() {
		        std::cout << "Configure how to build with " << name << "\n";
		        std::cout << "Usage: " << name << " configure [options]\n\n";
		        std::cout << "Options:\n";
		        opt.help();
		        exit(0);
		        return 0;
	        });

	opt.process(vargs.size(), vargs.data());

	if(host_arch_prefix.empty())
	{
		host_arch_prefix = build_arch_prefix;
	}

	auto tasks = getTasks(settings, {}, false);

	bool needs_build{true}; // we always need to compile ctor itself
	bool needs_build_c{false};
	bool needs_build_cxx{true}; // we always need to compile ctor itself
	bool needs_build_ld{true}; // we always need to compile ctor itself
	bool needs_build_ar{false};
	bool needs_build_asm{false};

	bool needs_host_c{false};
	bool needs_host{false};
	bool needs_host_cxx{false};
	bool needs_host_ld{false};
	bool needs_host_ar{false};
	bool needs_host_asm{false};

	for(const auto& task :tasks)
	{
		switch(task->outputSystem())
		{
		case ctor::output_system::build:
			needs_build = true;
			switch(task->targetType())
			{
			case ctor::target_type::executable:
			case ctor::target_type::unit_test:
			case ctor::target_type::dynamic_library:
				needs_build_ld = true;
				break;
			case ctor::target_type::static_library:
			case ctor::target_type::unit_test_library:
				needs_build_ar = true;
				break;
			case ctor::target_type::object:
				switch(task->sourceLanguage())
				{
				case ctor::language::automatic:
					std::cerr << "TargetLanguage not deduced!\n";
					exit(1);
					break;
				case ctor::language::c:
					needs_build_c = true;
					break;
				case ctor::language::cpp:
					needs_build_cxx = true;
					break;
				case ctor::language::assembler:
					needs_build_asm = true;
					break;
				}
				break;
			case ctor::target_type::function:
			case ctor::target_type::automatic:
			case ctor::target_type::unknown:
				break;
			}
			break;
		case ctor::output_system::host:
			needs_host = true;
			switch(task->targetType())
			{
			case ctor::target_type::executable:
			case ctor::target_type::unit_test:
			case ctor::target_type::dynamic_library:
				needs_host_ld = true;
				break;
			case ctor::target_type::static_library:
			case ctor::target_type::unit_test_library:
				needs_host_ar = true;
				break;
			case ctor::target_type::object:
				switch(task->sourceLanguage())
				{
				case ctor::language::automatic:
					std::cerr << "TargetLanguage not deduced!\n";
					exit(1);
					break;
				case ctor::language::c:
					needs_host_c = true;
					break;
				case ctor::language::cpp:
					needs_host_cxx = true;
					break;
				case ctor::language::assembler:
					needs_host_asm = true;
					break;
				}
				break;
			case ctor::target_type::function:
			case ctor::target_type::automatic:
			case ctor::target_type::unknown:
				break;
			}
			break;
		}
	}

	auto cc_env = env.find("CC");
	if(cc_env != env.end())
	{
		cc_prog = cc_env->second;
	}

	auto cxx_env = env.find("CXX");
	if(cxx_env != env.end())
	{
		cxx_prog = cxx_env->second;
	}

	auto ar_env = env.find("AR");
	if(ar_env != env.end())
	{
		ar_prog = ar_env->second;
	}

	auto ld_env = env.find("LD");
	if(ld_env != env.end())
	{
		ld_prog = ld_env->second;
	}

	auto paths = get_paths();

	auto path_env = env.find("PATH");
	if(path_env != env.end())
	{
		paths = get_paths(path_env->second);
	}

	std::string host_cc;
	std::string host_cxx;
	std::string host_ld;
	std::string host_ar;
	ctor::toolchain host_toolchain{ctor::toolchain::none};
	ctor::arch host_arch{ctor::arch::unknown};
	if(needs_host)
	{
		// Host detection
		if(needs_host_c)
		{
			host_cc = locate(cc_prog, paths, host_arch_prefix);
			if(host_cc.empty())
			{
				std::cerr << "Could not locate host_cc prog" << std::endl;
				return 1;
			}
		}

		if(needs_host_cxx)
		{
			host_cxx = locate(cxx_prog, paths, host_arch_prefix);
			if(host_cxx.empty())
			{
				std::cerr << "Could not locate host_cxx prog" << std::endl;
				return 1;
			}
		}

		if(needs_host_ar)
		{
			host_ar = locate(ar_prog, paths, host_arch_prefix);
			if(host_ar.empty())
			{
				std::cerr << "Could not locate host_ar prog" << std::endl;
				return 1;
			}
		}

		if(needs_host_ld)
		{
			host_ld = locate(ld_prog, paths, host_arch_prefix);
			if(host_ld.empty())
			{
				std::cerr << "Could not locate host_ld prog" << std::endl;
				return 1;
			}
		}

		if(needs_host_asm)
		{
			// TODO
		}

		host_toolchain = getToolChain(host_cxx);
		auto host_arch_str = get_arch(ctor::output_system::host);
		host_arch = get_arch(ctor::output_system::host, host_arch_str);

		std::cout << "** Host architecture '" << host_arch_str << "': " << host_arch << std::endl;

		if(host_arch == ctor::arch::unknown)
		{
			std::cerr << "Could not detect host architecture" << std::endl;
			return 1;
		}
	}

	std::string build_cc;
	std::string build_cxx;
	std::string build_ld;
	std::string build_ar;
	ctor::toolchain build_toolchain{ctor::toolchain::none};
	ctor::arch build_arch{ctor::arch::unknown};
	if(needs_build)
	{
		// Build detection
		if(needs_build_c)
		{
			build_cc = locate(cc_prog, paths, build_arch_prefix);
			if(build_cc.empty())
			{
				std::cerr << "Could not locate build_cc prog" << std::endl;
				return 1;
			}
		}

		if(needs_build_cxx)
		{
			build_cxx = locate(cxx_prog, paths, build_arch_prefix);
			if(build_cxx.empty())
			{
				std::cerr << "Could not locate build_cxx prog" << std::endl;
				return 1;
			}
		}

		if(needs_build_ar)
		{
			build_ar = locate(ar_prog, paths, build_arch_prefix);
			if(build_ar.empty())
			{
				std::cerr << "Could not locate build_ar prog" << std::endl;
				return 1;
			}
		}

		if(needs_build_ld)
		{
			build_ld = locate(ld_prog, paths, build_arch_prefix);
			if(build_ld.empty())
			{
				std::cerr << "Could not locate build_ld prog" << std::endl;
				return 1;
			}
		}

		if(needs_build_asm)
		{
			// TODO
		}

		build_toolchain = getToolChain(build_cxx);
		auto build_arch_str = get_arch(ctor::output_system::build);
		build_arch = get_arch(ctor::output_system::build, build_arch_str);

		std::cout << "** Build architecture '" << build_arch_str << "': " << build_arch << std::endl;

		if(build_arch == ctor::arch::unknown)
		{
			std::cerr << "Could not detect build architecture" << std::endl;
			return 1;
		}
	}


	// Store current values for execution in this execution context.
	if(!ctor_includedir.empty())
	{
		ctor::conf_values[ctor::cfg::ctor_includedir] = ctor_includedir;
	}
	if(!ctor_libdir.empty())
	{
		ctor::conf_values[ctor::cfg::ctor_libdir] = ctor_libdir;
	}
	if(!builddir.empty())
	{
		ctor::conf_values[ctor::cfg::builddir] = builddir;
	}
	ctor::conf_values[ctor::cfg::host_cxx] = host_cxx;
	ctor::conf_values[ctor::cfg::build_cxx] = build_cxx;

	std::cout << "Writing results to: " << configurationFile.string() << "\n";
	{
		std::ofstream istr(configurationFile);
		istr << "#include <ctor.h>\n\n";
		istr << "const ctor::configuration& ctor::get_configuration()\n";
		istr << "{\n";
		istr << "	static ctor::configuration cfg =\n";
		istr << "	{\n";
		if(needs_host)
		{
			istr << "		.host_toolchain = " << host_toolchain << ",\n";
			istr << "		.host_arch = " << host_arch << ",\n";
		}
		if(needs_build)
		{
			istr << "		.build_toolchain = " << build_toolchain << ",\n";
			istr << "		.build_arch = " << build_arch << ",\n";
		}
		istr << "		.args = {";
		for(const auto& arg : args)
		{
			istr << "\"" << esc(arg) << "\",";
		}
		istr << "},\n";
		istr << "		.env = {\n";
		for(const auto& e : env)
		{
			istr << "			{\"" << esc(e.first) << "\", \"" << esc(e.second) << "\"},\n";
		}
		istr << "		},\n";

		istr << "		.tools = {\n";
		if(!builddir.empty())
		{
			istr << "			{ \"" << ctor::cfg::builddir << "\", \"" << esc(builddir) << "\" },\n";
			ctor::builddir = builddir;
		}
		if(needs_host)
		{
			if(needs_host_c)
			{
				istr << "			{ \"" << ctor::cfg::host_cc << "\", \"" << esc(host_cc) << "\" },\n";
			}
			if(needs_host_cxx)
			{
				istr << "			{ \"" << ctor::cfg::host_cxx << "\", \"" << esc(host_cxx) << "\" },\n";
			}
			if(needs_host_ar)
			{
				istr << "			{ \"" << ctor::cfg::host_ar << "\", \"" << esc(host_ar) << "\" },\n";
			}
			if(needs_host_ld)
			{
				istr << "			{ \"" << ctor::cfg::host_ld << "\", \"" << esc(host_ld) << "\" },\n";
			}
		}
		if(needs_build)
		{
			if(needs_build_c)
			{
				istr << "			{ \"" << ctor::cfg::build_cc << "\", \"" << esc(build_cc) << "\" },\n";
			}
			if(needs_build_cxx)
			{
				istr << "			{ \"" << ctor::cfg::build_cxx << "\", \"" << esc(build_cxx) << "\" },\n";
			}
			if(needs_build_ar)
			{
				istr << "			{ \"" << ctor::cfg::build_ar << "\", \"" << esc(build_ar) << "\" },\n";
			}
			if(needs_build_ld)
			{
				istr << "			{ \"" << ctor::cfg::build_ld << "\", \"" << esc(build_ld) << "\" },\n";
			}
		}
		if(!ctor_includedir.empty())
		{
			istr << "			{ \"" << ctor::cfg::ctor_includedir << "\", \"" << esc(ctor_includedir) << "\" },\n";
			ctor::includedir = ctor_includedir;
		}
		if(!ctor_libdir.empty())
		{
			istr << "			{ \"" << ctor::cfg::ctor_libdir << "\", \"" << esc(ctor_libdir) << "\" },\n";
			ctor::libdir = ctor_libdir;
		}

		istr << "		},\n";
		istr << "		.externals = {\n";

		for(const auto& ext : externalConfigs)
		{
			istr << "			{ \"" << esc(ext.name) << "\", {\n";
			ctor::flags resolved_flags;
			if(std::holds_alternative<ctor::external_manual>(ext.external))
			{
				if(auto ret = resolv(settings, ext,
				                     std::get<ctor::external_manual>(ext.external),
				                     resolved_flags))
				{
					return ret;
				}
			}
			else
			{
				std::cout << "Unknown external type\n";
				return 1;
			}

			if(!resolved_flags.cflags.empty())
			{
				istr << "			  .cflags = {";
				for(const auto& flag : resolved_flags.cflags)
				{
					istr << flag << ",";
				}
				istr << "},\n";
			}

			if(!resolved_flags.cxxflags.empty())
			{
				istr << "			  .cxxflags = {";
				for(const auto& flag : resolved_flags.cxxflags)
				{
					istr << flag << ",";
				}
				istr << "},\n";
			}

			if(!resolved_flags.ldflags.empty())
			{
				istr << "			  .ldflags = {";
				for(const auto& flag : resolved_flags.ldflags)
				{
					istr << flag << ",";
				}
				istr << "},\n";
			}

			if(!resolved_flags.asmflags.empty())
			{
				istr << "			  .asmflags = {";
				for(const auto& flag : resolved_flags.asmflags)
				{
					istr << flag << ",";
				}
				istr << "},\n";
			}
			istr << "			}},\n";
		}

		istr << "		},\n";
		istr << "	};\n";
		istr << "	return cfg;\n";
		istr << "}\n";
	}

	{
		std::ofstream istr(configHeaderFile);
		istr << "#pragma once\n\n";
		istr << "#define HAS_FOO 1\n";
		istr << "//#define HAS_BAR 1\n";
	}

	return 0;
}

int configure(const ctor::settings& global_settings, int argc, char* argv[])
{
	ctor::settings settings{global_settings};

	std::vector<std::string> args;
	for(int i = 2; i < argc; ++i) // skip command and the first 'configure' arg
	{
		args.push_back(argv[i]);
	}

	std::map<std::string, std::string> env;
	auto cc_env = getenv("CC");
	if(cc_env)
	{
		env["CC"] = cc_env;
	}

	auto cxx_env = getenv("CXX");
	if(cxx_env)
	{
		env["CXX"] = cxx_env;
	}

	auto ar_env = getenv("AR");
	if(ar_env)
	{
		env["AR"] = ar_env;
	}

	auto ld_env = getenv("LD");
	if(ld_env)
	{
		env["LD"] = ld_env;
	}

	auto path_env = getenv("PATH");
	if(path_env)
	{
		env["PATH"] = path_env;
	}

	auto ret = regenerateCache(settings, argv[0], args, env);
	if(ret != 0)
	{
		return ret;
	}

	recompileCheck(settings, argc, argv, false);

	return 0;
}

int reconfigure(const ctor::settings& global_settings, int argc, char* argv[])
{
	ctor::settings settings{global_settings};

	bool no_rerun{false};

	std::vector<std::string> args;
	for(int i = 2; i < argc; ++i) // skip executable name and 'reconfigure' arg
	{
		if(i == 2 && std::string(argv[i]) == "--no-rerun")
		{
			no_rerun = true;
			continue;
		}
		args.push_back(argv[i]);
	}

	const auto& cfg = ctor::get_configuration();

	std::cout << "Re-running configure:\n";
	for(const auto& e : cfg.env)
	{
		std::cout << e.first << "=\"" << e.second << "\" ";
	}
	std::cout << argv[0] << " configure ";
	for(const auto& arg : cfg.args)
	{
		std::cout << arg << " ";
	}
	std::cout << "\n";

	auto ret = regenerateCache(settings, argv[0], cfg.args, cfg.env);
	if(ret != 0)
	{
		return ret;
	}

	recompileCheck(settings, 1, argv, false);

	if(no_rerun)
	{
		return 0; // this was originally invoked by configure, don't loop
	}

	return execute(argv[0], args);
}