#include <vector>
#include <string>
#include <filesystem>
#include <iostream>
#include <utility>
#include <list>
#include <chrono>
#include <thread>
#include <memory>
#include <algorithm>
#include <list>
#include <array>
#include <deque>
#include <fstream>

#include <getoptpp/getoptpp.hpp>

#include "libcppbuild.h"
#include "task_cc.h"
#include "task_ld.h"
#include "task_ar.h"
#include "task_so.h"
#include "settings.h"
#include "execute.h"

#include <unistd.h>


namespace
{
std::filesystem::path configurationFile("configuration.cc");
const std::map<std::string, std::string> default_configuration{};
}
const std::map<std::string, std::string>& __attribute__((weak)) configuration()
{
	return default_configuration;
}

bool hasConfiguration(const std::string& key)
{
	const auto& c = configuration();
	return c.find(key) != c.end();
}

const std::string& getConfiguration(const std::string& key,
                                    const std::string& defaultValue)
{
	const auto& c = configuration();
	if(hasConfiguration(key))
	{
		return c.at(key);
	}

	return defaultValue;
}

using namespace std::chrono_literals;

std::list<std::shared_ptr<Task>> taskFactory(const BuildConfiguration& config,
                                             const Settings& settings,
                                             const std::string& sourceDir)
{
	std::filesystem::path targetFile(config.target);

	TargetType target_type{config.type};
	if(target_type == TargetType::Auto)
	{
		if(targetFile.extension() == ".a")
		{
			target_type = TargetType::StaticLibrary;
		}
		else if(targetFile.extension() == ".so")
		{
			target_type = TargetType::DynamicLibrary;
		}
		else if(targetFile.extension() == "")
		{
			target_type = TargetType::Executable;
		}
		else
		{
			std::cerr << "Could not deduce target type from target " <<
				targetFile.string() << " please specify.\n";
			exit(1);
		}
	}

	std::vector<std::string> objects;
	std::list<std::shared_ptr<Task>> tasks;
	for(const auto& file : config.sources)
	{
		tasks.emplace_back(std::make_shared<TaskCC>(config, settings,
		                                            sourceDir, file));
		objects.push_back(tasks.back()->target());
	}

	switch(target_type)
	{
	case TargetType::StaticLibrary:
		tasks.emplace_back(std::make_shared<TaskAR>(config, settings, config.target,
		                                            objects));
		break;

	case TargetType::DynamicLibrary:
		if(targetFile.stem().string().substr(0, 3) != "lib")
		{
			std::cerr << "Dynamic library target must have 'lib' prefix\n";
			exit(1);
		}
		tasks.emplace_back(std::make_shared<TaskSO>(config, settings, config.target,
		                                            objects));
		break;

	case TargetType::Executable:
		tasks.emplace_back(std::make_shared<TaskLD>(config, settings, config.target,
		                                            objects));
		break;
	}

	return tasks;
}

std::shared_ptr<Task> getNextTask(const std::list<std::shared_ptr<Task>>& allTasks,
                                  std::list<std::shared_ptr<Task>>& dirtyTasks)
{
	for(auto dirtyTask = dirtyTasks.begin();
	    dirtyTask != dirtyTasks.end();
	    ++dirtyTask)
	{
		//std::cout << "Examining target " << (*dirtyTask)->target() << "\n";
		if((*dirtyTask)->ready())
		{
			dirtyTasks.erase(dirtyTask);
			return *dirtyTask;
		}
	}

	//std::cout << "No task ready ... \n";
	return nullptr;
}

namespace
{
struct BuildConfigurationEntry
{
	const char* file;
	std::vector<BuildConfiguration> (*cb)();
};
std::array<BuildConfigurationEntry, 1024> configFiles;
int numConfigFiles{0};
}

// TODO: Use c++20 when ready, somehing like this:
//int reg(const std::source_location location = std::source_location::current())
int reg(const char* location, std::vector<BuildConfiguration> (*cb)())
{
	// NOTE: std::cout cannot be used here
	if(numConfigFiles >= configFiles.size())
	{
		fprintf(stderr, "Max %d build configurations currently supported.\n",
		        (int)configFiles.size());
		exit(1);
	}

	configFiles[numConfigFiles].file = location;
	configFiles[numConfigFiles].cb = cb;
	++numConfigFiles;
	return 0;
}

void recompileCheck(const Settings& settings, int argc, char* argv[])
{
	bool dirty{false};

	std::vector<std::string> args;
	args.push_back("-s");
	args.push_back("-O3");
	args.push_back("-std=c++17");
	args.push_back("-pthread");

	std::filesystem::path binFile("cppbuild");

	if(std::filesystem::exists(configurationFile))
	{
		args.push_back(configurationFile.string());

		if(std::filesystem::last_write_time(binFile) <=
		   std::filesystem::last_write_time(configurationFile))
		{
			dirty = true;
		}

		const auto& c = configuration();
		if(&c == &default_configuration)
		{
			// configuration.cc exists, but currently compiled with the default one.
			dirty = true;
		}
	}

	if(settings.verbose > 1)
	{
		std::cout << "Recompile check (" << numConfigFiles << "):\n";
	}

	for(int i = 0; i < numConfigFiles; ++i)
	{
		std::string location = configFiles[i].file;
		if(settings.verbose > 1)
		{
			std::cout << " - " << location << "\n";
		}
		std::filesystem::path configFile(location);
		if(std::filesystem::last_write_time(binFile) <=
		   std::filesystem::last_write_time(configFile))
		{
			dirty = true;
		}

		// Support adding multiple config functions from the same file
		if(std::find(args.begin(), args.end(), location) == std::end(args))
		{
			args.push_back(location);
		}
	}
	args.push_back("libcppbuild.a");
	args.push_back("-o");
	args.push_back(binFile.string());

	if(dirty)
	{
		std::cout << "Rebuilding config\n";
		auto tool = getConfiguration("host-cpp", "/usr/bin/g++");
		auto ret = execute(tool, args, settings.verbose > 0);
		if(ret != 0)
		{
			std::cerr << "Failed: ." << ret << "\n";
			exit(1);
		}
		else
		{
			std::cout << "Re-launch\n";
			std::vector<std::string> args;
			for(int i = 1; i < argc; ++i)
			{
				args.push_back(argv[i]);
			}
			exit(execute(argv[0], args, settings.verbose > 0));
		}
	}
}

std::list<std::shared_ptr<Task>> getTasks(const Settings& settings)
{
	static std::deque<BuildConfiguration> build_configs;
	std::list<std::shared_ptr<Task>> tasks;
	for(int i = 0; i < numConfigFiles; ++i)
	{
		std::string path =
			std::filesystem::path(configFiles[i].file).parent_path();
		if(settings.verbose > 1)
		{
			std::cout << configFiles[i].file << " in path " << path << "\n";
		}
		auto configs = configFiles[i].cb();
		for(const auto& config : configs)
		{
			build_configs.push_back(config);
			const auto& build_config = build_configs.back();
			std::vector<std::string> objects;
			auto t = taskFactory(build_config, settings, path);
			tasks.insert(tasks.end(), t.begin(), t.end());
		}
	}

	return tasks;
}

int configure(int argc, char* argv[],const Settings& settings)
{
	auto tasks = getTasks(settings);

	bool needs_cpp{false};
	bool needs_c{false};
	bool needs_ar{false};
	for(const auto& task :tasks)
	{
		switch(task->language())
		{
		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;
		}
	}
	std::ofstream istr(configurationFile);
	istr << "#include \"libcppbuild.h\"\n\n";
	istr << "const std::map<std::string, std::string>& configuration()\n";
	istr << "{\n";
	istr << "	static std::map<std::string, std::string> c =\n";
	istr << "	{\n";
	istr << "		{ \"builddir\", \"build\" },\n";
	istr << "		{ \"host-cc\", \"/usr/bin/gcc\" },\n";
	istr << "		{ \"host-cpp\", \"/usr/bin/g++\" },\n";
	istr << "		{ \"host-ar\", \"/usr/bin/ar\" },\n";
	istr << "		{ \"host-ld\", \"/usr/bin/ld\" },\n";
	istr << "		{ \"target-cc\", \"/usr/bin/gcc\" },\n";
	istr << "		{ \"target-cpp\", \"/usr/bin/g++\" },\n";
	istr << "		{ \"target-ar\", \"/usr/bin/ar\" },\n";
	istr << "		{ \"target-ld\", \"/usr/bin/ld\" },\n";
	istr << "	};\n";
	istr << "	return c;\n";
	istr << "}\n";

	return 0;
}

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

	settings.builddir = getConfiguration("builddir", "build");
	settings.parallel_processes =
		std::max(1u, std::thread::hardware_concurrency() * 2 - 1);
	settings.verbose = 0;

	bool write_compilation_database{false};
	std::string compilation_database;

	if(argc > 1 && std::string(argv[1]) == "configure")
	{
		return configure(argc, argv, settings);
	}

	dg::Options opt;

	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("database", required_argument, 'd',
	        "Write compilation database json file.",
	        [&]() {
		        write_compilation_database = true;
		        compilation_database = optarg;
		        return 0;
	        });

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

	opt.process(argc, argv);

	recompileCheck(settings, argc, argv);

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

	auto tasks = getTasks(settings);

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

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

	std::list<std::shared_ptr<Task>> dirtyTasks;
	for(auto task : tasks)
	{
		if(task->dirty())
		{
			dirtyTasks.push_back(task);
		}
	}

	for(auto const &arg : opt.arguments())
	{
		if(arg == "clean")
		{
			std::cout << "Cleaning\n";
			for(auto& task : tasks)
			{
				if(task->clean() != 0)
				{
					return 1;
				}
			}

			return 0;
		}

		if(arg == "configure")
		{
			std::cerr << "The 'configure' target must be the first argument.\n";
			return 1;
		}
	}

	if(dirtyTasks.empty())
	{
		return 0;
	}

	std::cout << "Building\n";
	std::list<std::future<int>> processes;

	// Start all tasks
	bool done{false};
	while(!done)
	{
		bool started_one{false};
		while(processes.size() < settings.parallel_processes)
		{
			if(dirtyTasks.empty())
			{
				done = true;
				break;
			}

			auto task = getNextTask(tasks, dirtyTasks);
			if(task == nullptr)
			{
				break;
				//return 1;
			}

			processes.emplace_back(
				std::async(std::launch::async,
				           [task]()
				           {
					           return task->run();
				           }));
			started_one = true;
			std::this_thread::sleep_for(2ms);
		}

		for(auto process = processes.begin();
		    process != processes.end();
		    ++process)
		{
			if(process->valid())
			{
				if(process->get() != 0)
				{
					return 1;
				}
				processes.erase(process);
				break;
			}
		}

		if(started_one)
		{
			std::this_thread::sleep_for(2ms);
		}
		else
		{
			std::this_thread::sleep_for(200ms);
		}
	}

	for(auto process = processes.begin();
	    process != processes.end();
	    ++process)
	{
		process->wait();
		auto ret = process->get();
		if(ret != 0)
		{
			return 1;
		}
	}

	return 0;
}