// -*- c++ -*-
// Distributed under the BSD 2-Clause License.
// See accompanying file LICENSE for details.
#include <vector>
#include <string>
#include <filesystem>
#include <iostream>
#include <utility>
#include <list>
#include <thread>
#include <memory>
#include <algorithm>
#include <list>
#include <array>
#include <deque>
#include <fstream>
#include <cstdlib>
#include <set>

#include <getoptpp/getoptpp.hpp>

#include "libctor.h"
#include "settings.h"
#include "configure.h"
#include "rebuild.h"
#include "tasks.h"
#include "build.h"
#include "unittest.h"

int main(int argc, char* argv[])
{
	Settings settings{};

	settings.builddir = getConfiguration(cfg::builddir, settings.builddir);
	settings.parallel_processes =
		std::max(1u, std::thread::hardware_concurrency()) * 2 - 1;
	if(argc > 1 && std::string(argv[1]) == "configure")
	{
		return configure(settings, argc, argv);
	}

	bool write_compilation_database{false};
	std::string compilation_database;
	bool print_configure_cmd{false};
	bool print_configure_db{false};
	std::vector<std::string> add_files;
	std::vector<std::string> remove_files;
	bool no_default_build{false}; // set to true to prevent empty arg list building 'all' target.
	bool list_files{false};
	bool list_targets{false};
	bool no_relaunch{false}; // true means no re-launch after rebuild.
	bool run_unittests{false};

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

	opt.add("jobs", required_argument, 'j',
	        "Number of parallel jobs. (default: cpucount * 2 - 1)",
	        [&]() {
		        try
		        {
			        settings.parallel_processes = std::stoi(optarg);
		        }
		        catch(...)
		        {
			        std::cerr << "Not a number\n";
			        return 1;
		        }
		        return 0;
	        });

	opt.add("build-dir", required_argument, 'b',
	        "Overload 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("quiet", no_argument, 'q',
	        "Be completely silent.",
	        [&]() {
		        settings.verbose = -1;
		        return 0;
	        });

	opt.add("add", required_argument, 'a',
	        "Add specified file to the build configurations.",
	        [&]() {
		        no_relaunch = true;
		        add_files.push_back(optarg);
		        return 0;
	        });

	opt.add("remove", required_argument, 'r',
	        "Remove specified file from the build configurations.",
	        [&]() {
		        no_relaunch = true;
		        remove_files.push_back(optarg);
		        return 0;
	        });

	opt.add("list-files", no_argument, 'L',
	        "List files in the build configurations.",
	        [&]() {
		        no_relaunch = true;
		        list_files = true;
		        return 0;
	        });

	opt.add("list-targets", no_argument, 'l',
	        "List targets.",
	        [&]() {
		        no_relaunch = true;
		        list_targets = true;
		        return 0;
	        });

	opt.add("configure-cmd", no_argument, key++,
	        "Print commandline for last configure.",
	        [&]() {
		        no_relaunch = true;
		        print_configure_cmd = true;
		        return 0;
	        });

	opt.add("configure-db", no_argument, key++,
	        "Print entire configure parameter database.",
	        [&]() {
		        no_relaunch = true;
		        print_configure_db = true;
		        return 0;
	        });

	opt.add("database", required_argument, 'd',
	        "Write compilation database json file.",
	        [&]() {
		        no_relaunch = true;
		        write_compilation_database = true;
		        compilation_database = optarg;
		        return 0;
	        });

	opt.add("help", no_argument, 'h',
	        "Print this help text.",
	        [&]() {
		        std::cout << "Usage: " << argv[0] << " [options] [target] ...\n";
		        std::cout <<
R"_( where target can be either:
   configure - run configuration step (cannot be used with other targets).
   clean     - clean all generated files.
   all       - build all targets (default)
 or the name of a target which will be built along with its dependencies.
 Use '-l' to see a list of possible target names.

Options:
)_";
		        opt.help();
		        exit(0);
		        return 0;
	        });

	opt.process(argc, argv);

	auto verbose_env = std::getenv("V");
	if(verbose_env)
	{
		settings.verbose = std::atoi(verbose_env);
	}

	if(list_files)
	{
		no_default_build = true;
		std::set<std::string> files;
		for(std::size_t i = 0; i < numConfigFiles; ++i)
		{
			files.insert(configFiles[i].file);
		}

		for(std::size_t i = 0; i < numExternalConfigFiles; ++i)
		{
			files.insert(externalConfigFiles[i].file);
		}

		for(const auto& file : files)
		{
			std::cout << file << "\n";
		}
	}

	if(!add_files.empty() || !remove_files.empty())
	{
		no_default_build = true;
		for(const auto& add_file : add_files)
		{
			reg(add_file.data(), [](){ return std::vector<BuildConfiguration>{};});
		}

		for(const auto& remove_file : remove_files)
		{
			unreg(remove_file.data());
		}

		// Force rebuild if files were added
		recompileCheck(settings, 1, argv, true, no_relaunch == false);
	}

	recompileCheck(settings, argc, argv);

	std::filesystem::path builddir(settings.builddir);
	std::filesystem::create_directories(builddir);

	auto all_tasks = getTasks(settings);

	if(list_targets)
	{
		no_default_build = true;
		auto& targets = getTargets(settings);
		for(const auto& target : targets)
		{
			std::cout << target.config.target << "\n";
		}
	}

	if(write_compilation_database)
	{
		std::ofstream istr(compilation_database);
		istr << "[";
		bool first{true};
		for(auto task : all_tasks)
		{
			auto s = task->toJSON();
			if(!s.empty())
			{
				if(!first)
				{
					istr << ",\n";
				}
				else
				{
					istr << "\n";
				}
				first = false;
				istr << s;
			}
		}
		istr << "\n]\n";
	}

	if(print_configure_cmd)
	{
		no_default_build = true;
		std::cout << getConfiguration("cmd") << "\n";
	}

	if(print_configure_db)
	{
		no_default_build = true;
		const auto& c = configuration();
		for(const auto& config : c)
		{
			std::cout << config.first << ": " << config.second << "\n";
		}
	}

	for(auto task : all_tasks)
	{
		if(task->registerDepTasks(all_tasks))
		{
			return 1;
		}
	}

	bool build_all{!no_default_build};
	for(const auto& arg : opt.arguments())
	{
		if(arg == "configure")
		{
			std::cerr << "The 'configure' target must be the first argument.\n";
			return 1;
		}

		if(arg == "clean")
		{
			build_all = false;

			std::cout << "Cleaning\n";
			for(auto& task : all_tasks)
			{
				if(task->clean() != 0)
				{
					return 1;
				}
			}
		}
		else if(arg == "check")
		{
			build_all = false;
			run_unittests = true;

			std::vector<Target> unittest_targets;
			auto& targets = getTargets(settings);
			for(const auto& target : targets)
			{
				if(target.config.type == TargetType::UnitTest)
				{
					unittest_targets.push_back(target);
				}
			}

			auto ret = build(settings, "check", unittest_targets, all_tasks);
			if(ret != 0)
			{
				return ret;
			}
		}
		else
		{
			build_all = false;

			if(arg == "all")
			{
				std::vector<Target> non_unittest_targets;
				auto& targets = getTargets(settings);
				for(const auto& target : targets)
				{
					if(target.config.type != TargetType::UnitTest)
					{
						non_unittest_targets.push_back(target);
					}
				}

				auto ret = build(settings, "all", non_unittest_targets, all_tasks);
				if(ret != 0)
				{
					return ret;
				}
			}
			else
			{
				auto ret = build(settings, arg, all_tasks);
				if(ret != 0)
				{
					return ret;
				}
			}
		}
	}

	if(build_all)
	{
		std::vector<Target> non_unittest_targets;
		auto& targets = getTargets(settings);
		for(const auto& target : targets)
		{
			if(target.config.type != TargetType::UnitTest)
			{
				non_unittest_targets.push_back(target);
			}
		}

		auto ret = build(settings, "all", non_unittest_targets, all_tasks);
		if(ret != 0)
		{
			return ret;
		}
	}

	if(run_unittests)
	{
		return runUnitTests(all_tasks, settings);
	}

	return 0;
}