// -*- 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 "settings.h"
#include "execute.h"
#include "libctor.h"
#include "tasks.h"
#include "rebuild.h"

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

const Configuration default_configuration{};
const Configuration& __attribute__((weak)) configuration()
{
	return default_configuration;
}

namespace ctor
{
std::optional<std::string> includedir;
std::optional<std::string> libdir;
}

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

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

	const auto& c = configuration();
	return c.tools.find(key) != c.tools.end();
}

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

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

	const auto& c = configuration();
	if(hasConfiguration(key))
	{
		return c.tools.at(key);
	}

	return defaultValue;
}

std::string locate(const std::string& arch, const std::string& app)
{
	std::string path_env = std::getenv("PATH");
	std::cout << path_env << "\n";

	std::string program = app;
	if(!arch.empty())
	{
		program = arch + "-" + app;
	}
	std::cout << "Looking for: " << program << "\n";
	std::vector<std::string> paths;

	{
		std::stringstream ss(path_env);
		std::string path;
		while (std::getline(ss, path, ':'))
		{
			paths.push_back(path);
		}
	}
	for(const auto& path_str : paths)
	{
		std::filesystem::path path(path_str);
		auto prog_path = path / program;
		if(std::filesystem::exists(prog_path))
		{
			std::cout << "Found file " << app << " in path: " << path << "\n";
			auto perms = std::filesystem::status(prog_path).permissions();
			if((perms & std::filesystem::perms::owner_exec) != std::filesystem::perms::none)
			{
				std::cout << " - executable by owner\n";
			}
			if((perms & std::filesystem::perms::group_exec) != std::filesystem::perms::none)
			{
				std::cout << " - executable by group\n";
			}
			if((perms & std::filesystem::perms::others_exec) != std::filesystem::perms::none)
			{
				std::cout << " - executable by others\n";
			}

			return prog_path.string();
		}
	}

	std::cerr << "Could not locate " << app << " for the " << arch << " architecture\n";
	exit(1);
	return {};
}

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]);
		}
	}
};

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

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

	std::string build_arch;
	std::string build_path;
	std::string host_arch;
	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;

	opt.add("build-dir", required_argument, 'b',
	        "Set output directory for build files (default: '" +
	        settings.builddir + "').",
	        [&]() {
		        settings.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 = 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 = 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;
	        });

	opt.add("help", no_argument, 'h',
	        "Print this help text.",
	        [&]() {
		        std::cout << "configure usage stuff\n";
		        opt.help();
		        exit(0);
		        return 0;
	        });

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

	if(host_arch.empty())
	{
		host_arch = build_arch;
	}

	auto tasks = getTasks(settings);
/*
	bool needs_cpp{false};
	bool needs_c{false};
	bool needs_ar{false};
	bool needs_asm{false};
	for(const auto& task :tasks)
	{
		switch(task->sourceLanguage())
		{
		case Language::Auto:
			std::cerr << "TargetLanguage not deduced!\n";
			exit(1);
			break;
		case Language::C:
			needs_cpp = false;
			break;
		case Language::Cpp:
			needs_c = true;
			break;
		case Language::Asm:
			needs_asm = true;
			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;
	}

	std::string host_cc = locate(host_arch, cc_prog);
	std::string host_cxx = locate(host_arch, cxx_prog);
	std::string host_ar = locate(host_arch, ar_prog);
	std::string host_ld = locate(host_arch, ld_prog);
	std::string build_cc = locate(build_arch, cc_prog);
	std::string build_cxx = locate(build_arch, cxx_prog);
	std::string build_ar = locate(build_arch, ar_prog);
	std::string build_ld = locate(build_arch, ld_prog);

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

	std::cout << "Writing results to: " << configurationFile.string() << "\n";
	{
		std::ofstream istr(configurationFile);
		istr << "#include <libctor.h>\n\n";
		istr << "const Configuration& configuration()\n";
		istr << "{\n";
		istr << "	static Configuration cfg =\n";
		istr << "	{\n";
		istr << "		.args = {";
		for(const auto& arg : args)
		{
			istr << "\"" << arg << "\",";
		}
		istr << "},\n";
		istr << "		.env = {";
		for(const auto& e : env)
		{
			istr << "{\"" << e.first << "\", \"" << e.second << "\"}, ";
		}
		istr << "},\n";

		istr << "		.tools = {\n";
		istr << "			{ \"" << cfg::builddir << "\", \"" << settings.builddir << "\" },\n";
		istr << "			{ \"" << cfg::host_cc << "\", \"" << host_cc << "\" },\n";
		istr << "			{ \"" << cfg::host_cxx << "\", \"" << host_cxx << "\" },\n";
		istr << "			{ \"" << cfg::host_ar << "\", \"" << host_ar << "\" },\n";
		istr << "			{ \"" << cfg::host_ld << "\", \"" << host_ld << "\" },\n";
		istr << "			{ \"" << cfg::build_cc << "\", \"" << build_cc << "\" },\n";
		istr << "			{ \"" << cfg::build_cxx << "\", \"" << build_cxx << "\" },\n";
		istr << "			{ \"" << cfg::build_ar << "\", \"" << build_ar << "\" },\n";
		istr << "			{ \"" << cfg::build_ld << "\", \"" << build_ld << "\" },\n";
		if(!ctor_includedir.empty())
		{
			istr << "			{ \"" << cfg::ctor_includedir << "\", \"" << ctor_includedir << "\" },\n";
			ctor::includedir = ctor_includedir;
		}
		if(!ctor_libdir.empty())
		{
			istr << "			{ \"" << cfg::ctor_libdir << "\", \"" << ctor_libdir << "\" },\n";
			ctor::libdir = ctor_libdir;
		}

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

		for(const auto& externalConfig : externalConfigs)
		{
			istr << "			{ \"" << externalConfig.name << "\", {\n";

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

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

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

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

		istr << "		},\n";
		istr << "	};\n";
		istr << "	return cfg;\n";
		istr << "}\n\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 Settings& global_settings, int argc, char* argv[])
{
	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 ret = regenerateCache(settings, args, env);
	if(ret != 0)
	{
		return ret;
	}

	recompileCheck(settings, argc, argv, false);

	return 0;
}

int reconfigure(const Settings& settings, int argc, char* argv[])
{
	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 = 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, 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);
}