// -*- 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 "ctor.h"
#include "execute.h"
#include "util.h"
#include "tools.h"

using namespace ctor;

TaskCC::TaskCC(const BuildConfiguration& config, const Settings& settings,
               const std::string& sourceDir, const Source& source)
	: Task(config, settings, sourceDir)
	, config(config)
	, settings(settings)
	, sourceDir(sourceDir)
	, _source(source)
{
	sourceFile = sourceDir;
	sourceFile /= source.file;

	std::filesystem::path base = sourceFile.parent_path();
	std::filesystem::create_directories(std::filesystem::path(settings.builddir) / base);

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

	target_type = ctor::target_type::object;
	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;
		_targetFile += ".o";
	}
	else
	{
		_targetFile = source.output;
	}
	depsFile = targetFile().parent_path() / targetFile().stem();
	depsFile += ".d";
	flagsFile = targetFile().parent_path() / targetFile().stem();
	flagsFile += ".flags";
}

int TaskCC::registerDepTasksInner(const std::set<std::shared_ptr<Task>>& tasks)
{
	for(const auto& task : tasks)
	{
		if(*task == _source.file)
		{
			dependsTasks.insert(task);
		}
	}

	return 0;
}

std::string TaskCC::name() const
{
	return {};
}

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 depList = readDeps(depsFile.string());
	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)
	{
		switch(sourceLanguage())
		{
		case ctor::language::c:
			std::cout << "CC ";
			break;
		case ctor::language::cpp:
			std::cout << "CXX ";
			break;
		case ctor::language::automatic:
		case ctor::language::assembler:
			// Only c/c++ handled by this task type.
			break;
		}
		std::cout <<
			sourceFile.lexically_normal().string() << " => " <<
			targetFile().lexically_normal().string() << std::endl;
	}

	return execute(compiler(), args, settings.verbose > 0);
}

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
{
	switch(sourceLanguage())
	{
	case ctor::language::c:
		return config.flags.cflags;
	case ctor::language::cpp:
		return config.flags.cxxflags;
	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 tool_chain = getToolChain(config.system);

	auto compiler_flags = flags();

	std::vector<std::string> args;
	append(args, getOption(tool_chain, opt::generate_dep_tree));

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

	append(args, getOption(tool_chain, opt::no_link));
	args.push_back(sourceFile.string());
	append(args, getOption(tool_chain, opt::output, targetFile().string()));

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

		args.push_back(flag);
	}

	return args;
}