// fetchUnityVersion.cpp : Defines the entry point for the console application.

#include <napi.h>
#include <optional>

#ifdef _WIN32
#include <windows.h>
#include <strsafe.h>

std::wstring StringToWString(const std::string &input)
{
    if (input.empty())
    {
        return std::wstring();
    }

    int requiredSize = MultiByteToWideChar(CP_UTF8, 0, input.c_str(), -1, nullptr, 0);
    if (requiredSize <= 0)
    {
        throw std::runtime_error("Failed to calculate buffer size for wide string conversion");
    }

    std::vector<wchar_t> buffer(requiredSize);
    int result = MultiByteToWideChar(CP_UTF8, 0, input.c_str(), -1, buffer.data(), requiredSize);
    if (result <= 0)
    {
        throw std::runtime_error("Failed to convert string to wide string");
    }

    return std::wstring(buffer.data(), result - 1);
}

std::string WStringToString(const std::wstring &wstr)
{
    if (wstr.empty())
    {
        return std::string();
    }

    int requiredSize = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr);
    if (requiredSize <= 0)
    {
        throw std::runtime_error("Failed to calculate buffer size for string conversion");
    }

    std::vector<char> buffer(requiredSize);
    int result = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, buffer.data(), requiredSize, nullptr, nullptr);
    if (result <= 0)
    {
        throw std::runtime_error("Failed to convert wide string to string");
    }

    return std::string(buffer.data(), result - 1);
}

// returns nullopt if the value could not be obtained
std::optional<std::wstring> GetUnityVersion(std::wstring szFilename)
{
	// allocate a block of memory for the version info
	DWORD dummy;
	DWORD dwSize = GetFileVersionInfoSizeW(szFilename.c_str(), &dummy);
	if (dwSize == 0)
	{
		return std::nullopt;
	}
	std::vector<BYTE> data(dwSize);

	// load the version info
	if (!GetFileVersionInfoW(szFilename.c_str(), NULL, dwSize, data.data()))
	{
		return std::nullopt;
	}

	wchar_t *pvUnityVersion = NULL;
	unsigned int iUnityVersionLen = 0;

	if (!VerQueryValueW(data.data(), L"\\StringFileInfo\\040904b0\\Unity Version", (LPVOID*) &pvUnityVersion, &iUnityVersionLen))
	{
		return std::nullopt;
	}

	if (iUnityVersionLen == 0)
	{
		return std::wstring{};
	}

	// Final length is determined to copy into the new wstring.
	// This code checks: if the last character is a null, we trim it.
	// If the string is not null-terminated we use the full length.
	wchar_t* pVersionString = static_cast<wchar_t*>(pvUnityVersion);
	size_t finalLength = iUnityVersionLen;
	if (pVersionString[iUnityVersionLen - 1] == L'\0')
	{
		finalLength--;
	}

	// Construct the string using the pointer and the calculated length.
	std::wstring ret(pVersionString, finalLength);
	
	return ret;
}

std::optional<std::wstring> GetUnityManufacturer(std::wstring szFilename)
{
	// allocate a block of memory for the version info
	DWORD dummy;
	DWORD dwSize = GetFileVersionInfoSizeW(szFilename.c_str(), &dummy);
	if (dwSize == 0)
	{
		return std::nullopt;
	}
	std::vector<BYTE> data(dwSize);

	// load the version info
	if (!GetFileVersionInfoW(szFilename.c_str(), NULL, dwSize, data.data()))
	{
		return std::nullopt;
	}

	wchar_t *pvUnityManufacturer = NULL;
	unsigned int iUnityManufacturerLen = 0;

	if (!VerQueryValueW(data.data(), L"\\StringFileInfo\\040904b0\\CompanyName", (LPVOID*)&pvUnityManufacturer, &iUnityManufacturerLen))
	{
		return std::nullopt;
	}

	if (iUnityManufacturerLen == 0)
	{
		return std::wstring{};
	}

	wchar_t* pUnityManufacturerString = static_cast<wchar_t*>(pvUnityManufacturer);
	size_t finalLength = iUnityManufacturerLen;
	if (pUnityManufacturerString[iUnityManufacturerLen - 1] == L'\0')
	{
		finalLength--;
	}

	std::wstring ret(pUnityManufacturerString, finalLength);
	
	return ret;
}
#endif

Napi::Value GetUnityVersion(const Napi::CallbackInfo &info) {
	Napi::Env env = info.Env();
#ifdef _WIN32
	if (info.Length() < 1 || !info[0].IsString()) {
		throw Napi::TypeError::New(env, "Expected a string argument");
	}

	std::string path = info[0].As<Napi::String>().Utf8Value();
	std::wstring wpath = StringToWString(path);
	std::wstring unityVersion = GetUnityVersion(wpath).value_or(L"");
	Napi::String retval = Napi::String::New(env, WStringToString(unityVersion));
	return retval;
#else
	throw Napi::Error::New(env, "getUnityVersion is not supported on this platform");
#endif
}

Napi::Value GetUnityManufacturer(const Napi::CallbackInfo &info) {
	Napi::Env env = info.Env();
#ifdef _WIN32
	if (info.Length() < 1 || !info[0].IsString()) {
		throw Napi::TypeError::New(env, "Expected a string argument");
	}

	std::string path = info[0].As<Napi::String>().Utf8Value();;
	std::wstring wpath = StringToWString(path);
	std::wstring unityManufacturer = GetUnityManufacturer(wpath).value_or(L"");
	Napi::String retval = Napi::String::New(env, WStringToString(unityManufacturer));
	return retval;
#else
	throw Napi::Error::New(env, "getUnityManufacturer is not supported on this platform");
#endif
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
	exports.Set(Napi::String::New(env, "getUnityVersion"), Napi::Function::New<GetUnityVersion>(env));
	exports.Set(Napi::String::New(env, "getUnityManufacturer"), Napi::Function::New<GetUnityManufacturer>(env));
	return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init);
