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

#include <iostream>
#include <fstream>
#include <cassert>
#include <algorithm>

#include "ctor.h"
#include "execute.h"
#include "util.h"
#include "tools.h"
#include "deps.h"

TaskCC::TaskCC(const ctor::build_configuration& config_, const ctor::settings& settings_,
               const std::string& sourceDir_, const ctor::source& source)
	: Task(config_, settings_, sourceDir_)
	, sourceFile(sourceDir_)
	, config(config_)
	, settings(settings_)
	, sourceDir(sourceDir_)
	, _source(source)
{
	sourceFile /= source.file;

	std::filesystem::path base = sourceFile.parent_path();

	base /= cleanUp(config.target);
	base += "-";
	base += sourceFile.stem();

	target_type = ctor::target_type::object;
	output_system = config.system;
	source_language = source.language;
	if(source_language == ctor::language::automatic)
	{
		source_language = languageFromExtension(sourceFile);
	}

	switch(source_language)
	{
	case ctor::language::c:
		base += "_c";
		break;
	case ctor::language::cpp:
		base += "_cc";
		break;
	case ctor::language::assembler:
		base += "_asm";
		break;
	case ctor::language::automatic:
		assert(0 && "This should never happen");
		break;
	}

	if(source.output.empty())
	{
		_targetFile = base;
	}
	else
	{
		_targetFile = source.output;
	}
	auto toolchain = getToolChain(config.system);
	_targetFile = extension(toolchain, target_type, config.system, _targetFile);

	depsFile = targetFile().parent_path() / targetFile().stem();
	depsFile += ".d";
	flagsFile = targetFile().parent_path() / targetFile().stem();
	flagsFile += ".flags";

	std::filesystem::create_directories(targetFile().parent_path());
}

int TaskCC::registerDepTasksInner(const std::vector<std::shared_ptr<Task>>& tasks)
{
	for(const auto& task : tasks)
	{
		if(*task == _source.file)
		{
			if(std::find(dependsTasks.begin(), dependsTasks.end(), task) == dependsTasks.end())
			{
				dependsTasks.push_back(task);
			}
		}
	}

	return 0;
}

bool TaskCC::dirtyInner()
{
	if(!std::filesystem::exists(sourceFile))
	{
		//std::cout << "Missing source file: " << std::string(sourceFile) << "\n";
		return true;
	}

	if(!std::filesystem::exists(targetFile()))
	{
		//std::cout << "Missing targetFile\n";
		return true;
	}

	if(!std::filesystem::exists(depsFile))
	{
		//std::cout << "Missing depsFile\n";
		return true;
	}

	if(!std::filesystem::exists(flagsFile))
	{
		//std::cout << "Missing flagsFile\n";
		return true;
	}

	if(std::filesystem::last_write_time(sourceFile) >
	   std::filesystem::last_write_time(depsFile))
	{
		//std::cout << "The sourceFile newer than depsFile\n";
		return true;
	}

	{
		auto lastFlags = readFile(flagsFile.string());
		if(flagsString() != lastFlags)
		{
			//std::cout << "The compiler flags changed\n";
			return true;
		}
	}

	auto toolchain = getToolChain(config.system);
	auto depList = readDeps(depsFile.string(), toolchain);
	for(const auto& dep : depList)
	{
		if(!std::filesystem::exists(dep) ||
		   std::filesystem::last_write_time(targetFile()) <
		   std::filesystem::last_write_time(dep))
		{
			//std::cout << "The targetFile older than " << std::string(dep) << "\n";
			return true;
		}
	}

	if(std::filesystem::last_write_time(sourceFile) >
	   std::filesystem::last_write_time(targetFile()))
	{
		//std::cout << "The targetFile older than sourceFile\n";
		return true;
	}

	return false;
}

int TaskCC::runInner()
{
	if(!std::filesystem::exists(sourceFile))
	{
		std::cout << "Missing source file: " << sourceFile.string() << "\n";
		return 1;
	}

	auto args = getCompilerArgs();

	{ // Write flags to file.
		std::ofstream flagsStream(flagsFile.string());
		flagsStream << flagsString();
	}

	if(settings.verbose == 0)
	{
		std::string output;
		switch(sourceLanguage())
		{
		case ctor::language::c:
			output = "CC ";
			break;
		case ctor::language::cpp:
			output = "CXX ";
			break;
		case ctor::language::automatic:
		case ctor::language::assembler:
			// Only c/c++ handled by this task type.
			break;
		}
		output += sourceFile.lexically_normal().string() + " => " +
			targetFile().lexically_normal().string() + '\n';
		std::cout << output << std::flush;
	}

	const auto& cfg = ctor::get_configuration();
	return execute(settings, compiler(), args, cfg.env);
}

int TaskCC::clean()
{
	if(std::filesystem::exists(targetFile()))
	{
		std::cout << "Removing " << targetFile().string() << "\n";
		std::filesystem::remove(targetFile());
	}

	if(std::filesystem::exists(depsFile))
	{
		std::cout << "Removing " << depsFile.string() << "\n";
		std::filesystem::remove(depsFile);
	}

	if(std::filesystem::exists(flagsFile))
	{
		std::cout << "Removing " << flagsFile.string() << "\n";
		std::filesystem::remove(flagsFile);
	}

	return 0;
}

std::vector<std::string> TaskCC::depends() const
{
	return {};
}

std::string TaskCC::target() const
{
	return _targetFile.string();
}

std::filesystem::path TaskCC::targetFile() const
{
	return std::filesystem::path(settings.builddir) / _targetFile;
}

bool TaskCC::derived() const
{
	return true;
}

std::string TaskCC::toJSON() const
{
	std::string json;
	json += "\t{\n";
	json += "\t\t\"directory\": \"" + sourceDir.string() + "\",\n";
	json += "\t\t\"file\": \"" + sourceFile.lexically_normal().string() + "\",\n";
	json += "\t\t\"output\": \"" + targetFile().string() + "\",\n";
	json += "\t\t\"arguments\": [ \"" + compiler() + "\"";
	auto args = getCompilerArgs();
	for(const auto& arg : args)
	{
		json += ", \"" + arg + "\"";
	}
	json += " ]\n";
	json += "\t}";
	return json;
}

std::string TaskCC::source() const
{
	return sourceFile.string();
}

std::vector<std::string> TaskCC::flags() const
{
	std::vector<std::string> flags;
	auto toolchain = getToolChain(config.system);

	switch(sourceLanguage())
	{
	case ctor::language::c:
		for(const auto& flag : config.flags.cflags)
		{
			append(flags, to_strings(toolchain, flag));
		}
		return flags;
	case ctor::language::cpp:
		for(const auto& flag : config.flags.cxxflags)
		{
			append(flags, to_strings(toolchain, flag));
		}
		return flags;
	default:
		std::cerr << "Unknown CC target type\n";
		exit(1);
		break;
	}

}

std::string TaskCC::flagsString() const
{
	std::string flagsStr = compiler();
	for(const auto& flag : flags())
	{
		flagsStr += " " + flag;
	}
	return flagsStr;
}

std::vector<std::string> TaskCC::getCompilerArgs() const
{
	auto toolchain = getToolChain(config.system);
	auto compiler_flags = flags();

	std::vector<std::string> args;

	switch(sourceLanguage())
	{
	case ctor::language::c:
		{
			append(args, c_option(toolchain, ctor::c_opt::generate_dep_tree, depsFile.string()));

			if(std::filesystem::path(config.target).extension() == ".so")
			{
				// Add -fPIC arg to all contained object files
				append(args, c_option(toolchain,
				                      ctor::c_opt::position_independent_code));
			}

			append(args, c_option(toolchain, ctor::c_opt::no_link));
			args.push_back(sourceFile.string());
			append(args, c_option(toolchain,
			                      ctor::c_opt::output, targetFile().string()));

			// Relative include paths has to be altered to be relative to sourceDir
			for(const auto& flag : compiler_flags)
			{
				auto option = c_option(flag, toolchain);
				switch(option.opt)
				{
				case ctor::c_opt::include_path:
					{
						std::filesystem::path path(option.arg);
						if(path.is_relative())
						{
							path = (sourceDir / path).lexically_normal();
							append(args, c_option(toolchain,
							                      ctor::c_opt::include_path, path.string()));
						}
					}
					break;
				default:
					break;
				}

				args.push_back(flag);
			}
		}
		break;

	case ctor::language::cpp:
		{
			append(args, cxx_option(toolchain, ctor::cxx_opt::generate_dep_tree, depsFile.string()));

			if(std::filesystem::path(config.target).extension() == ".so")
			{
				// Add -fPIC arg to all contained object files
				append(args, cxx_option(toolchain,
				                        ctor::cxx_opt::position_independent_code));
			}

			append(args, cxx_option(toolchain, ctor::cxx_opt::no_link));
			args.push_back(sourceFile.string());
			append(args, cxx_option(toolchain,
			                        ctor::cxx_opt::output, targetFile().string()));

			// Relative include paths has to be altered to be relative to sourceDir
			for(const auto& flag : compiler_flags)
			{
				auto option = cxx_option(flag, toolchain);
				switch(option.opt)
				{
				case ctor::cxx_opt::include_path:
					{
						std::filesystem::path path(option.arg);
						if(path.is_relative())
						{
							path = (sourceDir / path).lexically_normal();
							append(args, cxx_option(toolchain,
							                        ctor::cxx_opt::include_path, path.string()));
						}
					}
					break;
				default:
					break;
				}

				args.push_back(flag);
			}

		}
		break;

	default:
		break;
	}

	return args;
}