#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(FilePath::GetPathDelimiter()), 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,
	const Dictionary<WString, vint>& externalIncludeOrder
)
{
	lines.Add(L"/***********************************************************************");
	lines.Add(L"THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY");
	lines.Add(L"DEVELOPER: Zihan Chen(vczh)");
	lines.Add(L"***********************************************************************/");
	List<WString> sortedExternalIncludes;
	CopyFrom(
		sortedExternalIncludes,
		externalIncludes.OrderBy([&](const WString& a, const WString& b)
		{
			auto aIndex = externalIncludeOrder.Keys().IndexOf(a);
			auto bIndex = externalIncludeOrder.Keys().IndexOf(b);
			if (aIndex != -1 && bIndex != -1)
			{
				return externalIncludeOrder.Values()[aIndex] <=> externalIncludeOrder.Values()[bIndex];
			}
			else if (aIndex != -1)
			{
				return std::strong_ordering::less;
			}
			else if (bIndex != -1)
			{
				return std::strong_ordering::greater;
			}
			else
			{
				return a <=> b;
			}
		})
		);
	for (auto path : sortedExternalIncludes)
	{
		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 LoadExistingIncludeOrder(
	FilePath outputIncludeFilePath,
	const List<FilePath>& files,
	Dictionary<FilePath, vint>& fileOrder,
	Dictionary<WString, vint>& externalIncludeOrder
)
{
	vint order = 0;
	StringReader reader(ReadFile(outputIncludeFilePath));
	while (!reader.IsEnd())
	{
		auto line = reader.ReadLine();
		if (auto match = regexInclude.MatchHead(line))
		{
			auto path = match->Groups()[include_path][0].Value();
			auto filePath = NormalizePathForFileSystem(outputIncludeFilePath.GetFolder() / path);
			if (files.Contains(filePath))
			{
				if (!fileOrder.Keys().Contains(filePath))
				{
					fileOrder.Add(filePath, order++);
				}
			}
			else if (!externalIncludeOrder.Keys().Contains(path))
			{
				externalIncludeOrder.Add(path, order++);
			}
		}
	}
}

std::strong_ordering CompareFilePathWithExistingOrder(
	const FilePath& a,
	const FilePath& b,
	const Dictionary<FilePath, vint>& fileOrder
)
{
	auto aIndex = fileOrder.Keys().IndexOf(a);
	auto bIndex = fileOrder.Keys().IndexOf(b);
	if (aIndex != -1 && bIndex != -1)
	{
		return fileOrder.Values()[aIndex] <=> fileOrder.Values()[bIndex];
	}
	else if (aIndex != -1)
	{
		return std::strong_ordering::less;
	}
	else if (bIndex != -1)
	{
		return std::strong_ordering::greater;
	}
	else
	{
		return GetCodePackPath(a.GetFullPath()) <=> GetCodePackPath(b.GetFullPath());
	}
}

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();
	Dictionary<FilePath, vint> fileOrder;
	Dictionary<WString, vint> externalIncludeOrder;
	LoadExistingIncludeOrder(outputIncludeFilePath, files, fileOrder, externalIncludeOrder);

	List<FilePath> orderedFiles;
	CopyFrom(
		orderedFiles,
		From(files).OrderBy([&](const FilePath& a, const FilePath& b)
		{
			return CompareFilePathWithExistingOrder(a, b, fileOrder);
		})
		);
	List<FilePath> sortedFiles;
	{
		PartialOrderingProcessor popFiles;
		Group<FilePath, FilePath> depGroup;
		for (auto file : orderedFiles)
		{
			for (auto dep : GetIncludedFiles(file, skippedImportFiles, cachedFileToIncludes, conditionOns, conditionOffs))
			{
				if (orderedFiles.Contains(dep))
				{
					depGroup.Add(file, dep);
				}
			}
		}

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

		bool needExit = false;
		for (vint i = 0; i < popFiles.components.Count(); i++)
		{
			auto& component = popFiles.components[i];
			sortedFiles.Add(orderedFiles[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" + orderedFiles[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, orderedFiles, externalIncludes, externalIncludeOrder);
		{
			auto prefix = GetCommonFolder(orderedFiles);
			for (auto file : From(sortedFiles).Intersect(orderedFiles))
			{
				lines.Add(L"");
				lines.Add(L"/***********************************************************************");
				lines.Add(wupper(GetCodePackCommentPath(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(NormalizePathForFileSystem(outputFilePath)).WriteAllLines(lines, true, BomEncoder::Utf8);
	}
	{
		List<WString> lines;
		CombineWriteHeader(lines, inputFileToOutputFiles, conditionOns, conditionOffs, orderedFiles, externalIncludes, externalIncludeOrder);
		lines.Add(L"");
		{
			auto prefix = outputIncludeFilePath.GetFolder();
			for (auto file : From(sortedFiles).Intersect(orderedFiles))
			{
				lines.Add(L"#include \"" + GetCodePackPath(prefix.GetRelativePathFor(file)) + L"\"");
			}
		}
		File(NormalizePathForFileSystem(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);
}
