#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 <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>

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

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

	if(targetFile.extension() == ".a")
	{
		// static lib
		tasks.emplace_back(std::make_shared<TaskAR>(config, settings, config.target,
		                                            objects));
	}
	else if(targetFile.extension() == ".so")
	{
		if(targetFile.stem().string().substr(0, 3) != "lib")
		{
			std::cerr << "Dynamic library target must have 'lib' prefix\n";
			exit(1);
		}
		// dynamic lib
		tasks.emplace_back(std::make_shared<TaskSO>(config, settings, config.target,
		                                            objects));
	}
	else
	{
		// executable
		tasks.emplace_back(std::make_shared<TaskLD>(config, settings, config.target,
		                                            objects));
	}

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

struct Config
{
	const char* file;
	std::vector<BuildConfiguration> (*cb)();
};
std::array<Config, 100> 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
	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(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 ret = execute("/usr/bin/g++", args);
		if(ret != 0)
		{
			std::cerr << "Failed.\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));
		}
	}
}

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

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

	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',
	        "Set output directory for build files (default: build).",
	        [&]() {
		        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("help", no_argument, 'h',
	        "Print this help text.",
	        [&]() {
		        std::cout << "usage stuff\n";
		        opt.help();
		        return 0;
	        });

	opt.process(argc, argv);

	recompileCheck(settings, argc, argv);

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

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

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

	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";
			//std::filesystem::remove_all(builddir);
			for(auto& task : tasks)
			{
				if(task->clean() != 0)
				{
					return 1;
				}
			}

			return 0;
		}
	}

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