#include "InstallationFolder.h"

#include <fstream>
#include <set>
#include <Windows.h>
#include <common/utils/winapi_error.h>

using namespace std;
using std::filesystem::directory_iterator;
using std::filesystem::path;

wstring GetVersion(path filePath)
{
	DWORD  verHandle = 0;
	UINT   size = 0;
	LPVOID lpBuffer = nullptr;
	DWORD  verSize = GetFileVersionInfoSize(filePath.c_str(), &verHandle);
	wstring version = L"None";

	if (verSize != 0)
	{
		LPSTR verData = new char[verSize];

		if (GetFileVersionInfo(filePath.c_str(), verHandle, verSize, verData))
		{
			if (VerQueryValue(verData, L"\\", &lpBuffer, &size))
			{
				if (size)
				{
					VS_FIXEDFILEINFO* verInfo = (VS_FIXEDFILEINFO*)lpBuffer;
					if (verInfo->dwSignature == 0xfeef04bd)
					{
						version =
							std::to_wstring((verInfo->dwFileVersionMS >> 16) & 0xffff) + L"." +
							std::to_wstring((verInfo->dwFileVersionMS >> 0) & 0xffff) + L"." +
							std::to_wstring((verInfo->dwFileVersionLS >> 16) & 0xffff) + L"." +
							std::to_wstring((verInfo->dwFileVersionLS >> 0) & 0xffff);
					}
				}
			}
		}

		delete[] verData;
	}

	return version;
}

optional<path> GetRootPath()
{
	WCHAR modulePath[MAX_PATH];
	if (!GetModuleFileName(NULL, modulePath, MAX_PATH))
	{
		return nullopt;
	}

	path rootPath = path(modulePath);
	rootPath = rootPath.remove_filename();
	rootPath = rootPath.append("..");
	return std::filesystem::canonical(rootPath);
}

wstring GetChecksum(path filePath)
{
	DWORD dwStatus = 0;
	BOOL bResult = FALSE;
	HCRYPTPROV hProv = 0;
	HCRYPTHASH hHash = 0;
	HANDLE hFile = NULL;
	constexpr int bufferSize = 1024;
	BYTE rgbFile[bufferSize];
	DWORD cbRead = 0;
	constexpr int md5Length = 16;
	BYTE rgbHash[md5Length];
	DWORD cbHash = 0;
	CHAR rgbDigits[] = "0123456789abcdef";
	LPCWSTR filename = filePath.c_str();
	hFile = CreateFile(filename,
		GENERIC_READ,
		FILE_SHARE_READ,
		NULL,
		OPEN_EXISTING,
		FILE_FLAG_SEQUENTIAL_SCAN,
		NULL);

	if (INVALID_HANDLE_VALUE == hFile)
	{
		return L"CreateFile() failed. " + get_last_error_or_default(GetLastError());
	}

	// Get handle to the crypto provider
	if (!CryptAcquireContext(&hProv,
		NULL,
		NULL,
		PROV_RSA_FULL,
		CRYPT_VERIFYCONTEXT))
	{
		CloseHandle(hFile);
		return L"CryptAcquireContext() failed. " + get_last_error_or_default(GetLastError());
	}

	if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
	{
		CloseHandle(hFile);
		CryptReleaseContext(hProv, 0);
		return L"CryptCreateHash() failed. " + get_last_error_or_default(GetLastError());
	}

	while (bResult = ReadFile(hFile, rgbFile, bufferSize, &cbRead, NULL))
	{
		if (0 == cbRead)
		{
			break;
		}

		if (!CryptHashData(hHash, rgbFile, cbRead, 0))
		{
			CryptReleaseContext(hProv, 0);
			CryptDestroyHash(hHash);
			CloseHandle(hFile);
			return L"CryptHashData() failed. " + get_last_error_or_default(GetLastError());;
		}
	}

	if (!bResult)
	{
		CryptReleaseContext(hProv, 0);
		CryptDestroyHash(hHash);
		CloseHandle(hFile);
		return L"ReadFile() failed. " + get_last_error_or_default(GetLastError());;
	}

	cbHash = md5Length;
	std::wstring result = L"";
	if (CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &cbHash, 0))
	{
		for (DWORD i = 0; i < cbHash; i++)
		{
			result += rgbDigits[rgbHash[i] >> 4];
			result += rgbDigits[rgbHash[i] & 0xf];
		}
	}
	else
	{
		std::wstring result = L"CryptGetHashParam() failed. " + get_last_error_or_default(GetLastError());;
	}

	CryptDestroyHash(hHash);
	CryptReleaseContext(hProv, 0);
	CloseHandle(hFile);

	return result;
}

class Reporter
{
private:
	std::wofstream os;
	std::wofstream GetOutputStream(const path& tmpDir)
	{
		auto path = tmpDir;
		path += "installationFolderStructure.txt";
		std::wofstream os = std::wofstream(path);
		return os;
	}
public:
	Reporter(const path& tmpDir)
	{
		os = GetOutputStream(tmpDir);
	}

	void Report(path dirPath, int indentation = 0)
	{
		set<pair<path, bool>> paths;
		try
		{
			directory_iterator end_it;
			for (directory_iterator it(dirPath); it != end_it; ++it)
			{
				paths.insert({ it->path(), it->is_directory() });
			}
		}
		catch (filesystem::filesystem_error err)
		{
			os << err.what() << endl;
		}

		for (auto filePair : paths)
		{
			auto filePath = filePair.first;
			auto isDirectory = filePair.second;

			auto fileName = filePath.wstring().substr(dirPath.wstring().size() + 1);
			os << wstring(indentation, ' ') << fileName << " ";
			if (!isDirectory)
			{
				os << GetVersion(filePath) << " " << GetChecksum(filePath);
			}

			os << endl;
			if (isDirectory)
			{
				Report(filePath, indentation + 2);
			}
		}
	}
};

void InstallationFolder::ReportStructure(const path& tmpDir)
{
	auto rootPath = GetRootPath();
	if (rootPath)
	{
		Reporter(tmpDir).Report(rootPath.value());
	}
}
