#include "Codepack.h"

FilePath GetCommonFolder(
	const List<FilePath>& paths
)
{
	auto folder = paths[0].GetFolder();
	while (true)
	{
		if (From(paths).All([&](const FilePath& path)
		{
			return INVLOC.StartsWith(path.GetFullPath(), folder.GetFullPath() + WString::FromChar(folder.Delimiter), Locale::IgnoreCase);
		}))
		{
			return folder;
		}
		folder = folder.GetFolder();
	}
	CHECK_FAIL(L"Cannot process files across multiple drives.");
}

void CollectConditions(
	Group<WString, WString>& categorizedConditions,
	Group<FilePath, Tuple<WString, FilePath>>& conditions,
	const FilePath& file,
	const Dictionary<FilePath, WString>& inputFileToOutputFiles
)
{
	vint index = conditions.Keys().IndexOf(file);
	if (index != -1)
	{
		for (auto [condition, path] : conditions.GetByIndex(index))
		{
			auto includeFile = inputFileToOutputFiles[path];
			if (!categorizedConditions.Contains(condition, includeFile))
			{
				categorizedConditions.Add(condition, includeFile);
			}
		}
	}
}

void CombineWriteHeader(
	List<WString>& lines,
	const Dictionary<FilePath, WString>& inputFileToOutputFiles,
	Group<FilePath, Tuple<WString, FilePath>>& conditionOns,
	Group<FilePath, Tuple<WString, FilePath>>& conditionOffs,
	const List<FilePath>& files,
	LazyList<WString> externalIncludes
)
{
	lines.Add(L"/***********************************************************************");
	lines.Add(L"THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY");
	lines.Add(L"DEVELOPER: Zihan Chen(vczh)");
	lines.Add(L"***********************************************************************/");
	for (auto path : externalIncludes)
	{
		lines.Add(L"#include \"" + path + L"\"");
	}
	{
		Group<WString, WString> categorizedConditionOns, categorizedConditionOffs;
		for (auto file : files)
		{
			CollectConditions(categorizedConditionOns, conditionOns, file, inputFileToOutputFiles);
			CollectConditions(categorizedConditionOffs, conditionOffs, file, inputFileToOutputFiles);
		}

		for (vint i = 0; i < categorizedConditionOns.Count(); i++)
		{
			lines.Add(L"#ifdef " + categorizedConditionOns.Keys()[i]);
			const auto& onFiles = categorizedConditionOns.GetByIndex(i);
			for (auto onFile : onFiles)
			{
				lines.Add(L"#include \"" + inputFileToOutputFiles[onFile] + L"\"");
			}
			lines.Add(L"#endif");
		}

		for (vint i = 0; i < categorizedConditionOffs.Count(); i++)
		{
			lines.Add(L"#ifndef " + categorizedConditionOffs.Keys()[i]);
			const auto& offFiles = categorizedConditionOffs.GetByIndex(i);
			for (auto offFile : offFiles)
			{
				lines.Add(L"#include \"" + offFile + L".h\"");
			}
			lines.Add(L"#endif");
		}
	}
}

void Combine(
	const Dictionary<FilePath, WString>& inputFileToOutputFiles,		// (in)
	const Dictionary<WString, FilePath>& skippedImportFiles,			// (in)
	Dictionary<FilePath, LazyList<FilePath>>& cachedFileToIncludes,		// (cache)
	Group<FilePath, Tuple<WString, FilePath>>& conditionOns,			// (out)
	Group<FilePath, Tuple<WString, FilePath>>& conditionOffs,			// (out)
	const List<FilePath>& files,										// (in)
	FilePath outputFilePath,											//
	FilePath outputIncludeFilePath,										//
	SortedList<WString>& systemIncludes,								//
	LazyList<WString> externalIncludes									//
)
{
	auto workingDir = outputFilePath.GetFolder();

	List<FilePath> sortedFiles;
	{
		PartialOrderingProcessor popFiles;
		Group<FilePath, FilePath> depGroup;
		for (auto file : files)
		{
			for (auto dep : GetIncludedFiles(file, skippedImportFiles, cachedFileToIncludes, conditionOns, conditionOffs))
			{
				if (files.Contains(dep))
				{
					depGroup.Add(file, dep);
				}
			}
		}

		popFiles.InitWithGroup(files, depGroup);
		popFiles.Sort();

		bool needExit = false;
		for (vint i = 0; i < popFiles.components.Count(); i++)
		{
			auto& component = popFiles.components[i];
			sortedFiles.Add(files[component.firstNode[0]]);

			if (component.nodeCount > 1)
			{
				Console::SetColor(true, false, false, true);
				Console::WriteLine(
					L"Error: Cycle dependency found in categories: "
					+ From(component.firstNode, component.firstNode + component.nodeCount)
					.Select([&](vint nodeIndex) { return L"\r\n" + files[nodeIndex].GetFullPath(); })
					.Aggregate([](const WString& a, const WString& b) {return a + b; })
					+ L"\r\n.");
				Console::SetColor(true, true, true, false);
				needExit = true;
			}
		}
		CHECK_ERROR(!needExit, L"Cycle dependency is not allowed");
	}
	{
		List<WString> lines;
		CombineWriteHeader(lines, inputFileToOutputFiles, conditionOns, conditionOffs, files, externalIncludes);
		{
			auto prefix = GetCommonFolder(files);
			for (auto file : From(sortedFiles).Intersect(files))
			{
				lines.Add(L"");
				lines.Add(L"/***********************************************************************");
				lines.Add(wupper(prefix.GetRelativePathFor(file)));
				lines.Add(L"***********************************************************************/");

				StringReader reader(ReadFile(file));
				bool skip = false;
				while (!reader.IsEnd())
				{
					auto line = reader.ReadLine();
					Ptr<RegexMatch> match;
					if ((match = regexInstruction.MatchHead(line)))
					{
						auto name = match->Groups()[instruction_name][0].Value();
						if (name == L"BeginIgnore")
						{
							skip = true;
						}
						else if (name == L"EndIgnore")
						{
							skip = false;
						}
					}
					else if (!skip)
					{
						if ((match = regexSystemInclude.MatchHead(line)))
						{
							auto systemFile = match->Groups()[systemInclude_path][0].Value();
							if (skippedImportFiles.Keys().Contains(systemFile)) continue;
							if (systemIncludes.Contains(systemFile)) continue;
							systemIncludes.Add(systemFile);
							lines.Add(line);
						}
						else if (!regexInclude.MatchHead(line))
						{
							lines.Add(line);
						}
					}
				}
			}
		}
		File(outputFilePath).WriteAllLines(lines, true, BomEncoder::Utf8);
	}
	{
		List<WString> lines;
		CombineWriteHeader(lines, inputFileToOutputFiles, conditionOns, conditionOffs, files, externalIncludes);
		lines.Add(L"");
		{
			auto prefix = outputIncludeFilePath.GetFolder();
			for (auto file : From(sortedFiles).Intersect(files))
			{
				lines.Add(L"#include \"" + prefix.GetRelativePathFor(file) + L"\"");
			}
		}
		File(outputIncludeFilePath).WriteAllLines(lines, true, BomEncoder::Utf8);
	}
	Console::SetColor(false, true, false, true);
	Console::WriteLine(L"Succeeded to write: " + outputFilePath.GetFullPath());
	Console::SetColor(true, true, true, false);
}