// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license.

#include "SimplygonModule.h"
#include "Misc/CommandLine.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/App.h"
#include "HAL/Platform.h"
#include "HAL/PlatformProcess.h"
#include "Logging/MessageLog.h"
#include "SimplygonSettings.h"

#if PLATFORM_WINDOWS
THIRD_PARTY_INCLUDES_START
#include "Windows\AllowWindowsPlatformTypes.h"
#include "Simplygon.h"
#include <Windows.h>
#include "Windows\HideWindowsPlatformTypes.h"
THIRD_PARTY_INCLUDES_END
#endif // PLATFORM_WINDOWS

#include "Interfaces/IPluginManager.h"
#include "Misc/EngineVersionBase.h"
#include "Misc/EngineVersion.h"

#if WITH_UNREAL_DEVELOPER_TOOLS
#include "MessageLogModule.h"
#endif

DEFINE_LOG_CATEGORY_STATIC( LogSimplygon, VeryVerbose, All );

#define LOCTEXT_NAMESPACE "Simplygon"

class FDefaultErrorHandler : public Simplygon::ErrorHandler
{
	public:
	virtual void HandleError( Simplygon::spObject Object, const char* InterfaceName,
	                          const char* MethodName, Simplygon::rid ErrorType, const char* ErrorText ) override
	{
		FString ErrorString =
		    FString::Printf( TEXT( "Simplygon Error:\n\nInterface: %s\nMethod: %s\nError: (%d) %s" ),
		                     ANSI_TO_TCHAR( InterfaceName ), ANSI_TO_TCHAR( MethodName ), ErrorType,
		                     ANSI_TO_TCHAR( ErrorText ) );
 
		if (IsInGameThread())
		{
			FMessageLog MessageLog("Simplygon");
			MessageLog.Error(FText::FromString(ErrorString));
		}
		else
		{
			UE_LOG(LogSimplygon, Error, TEXT("%s"), *ErrorString);
		}
	}
};

class FSimplygonModule : public ISimplygonModule
{
	public:
	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
	virtual bool SupportsDynamicReloading() override { return true; }
	Simplygon::ISimplygon* GetSDK(bool bValidate = false) override;
	virtual bool IsUsingLocalSdkPath() override { return bUsingLocalSdkPath; }
	virtual FString GetSimplygonSDKPath() override { return SimplygonSdkPath; }
	virtual void SendTelemetryData(const FString& Json, const FString& Category) override;
	virtual void DumpMemoryStatistics() const override;
	private:
	void InitializeSimplygon();
	void InitializeSimplygonSDKPath();
	void* SimplygonDLL{};
	Simplygon::ISimplygon* SDK{};
	FDefaultErrorHandler ErrorHandler{};
	bool bUsingLocalSdkPath{};
	FString SimplygonSdkPath{};
};

IMPLEMENT_MODULE( FSimplygonModule, Simplygon )

TUniquePtr<FSimplygonModule> GSimplygonSDKModule;

void FSimplygonModule::StartupModule()
{
#if WITH_UNREAL_DEVELOPER_TOOLS
	//Register message log used by all Simplygon plugin modules
	FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
	MessageLogModule.RegisterLogListing("Simplygon", LOCTEXT("SimplygonLogLabel", "Simplygon"));
#endif

	InitializeSimplygon();
}

void FSimplygonModule::ShutdownModule()
{
	if( SimplygonDLL )
	{
		typedef int ( *DeinitializeSimplygonSDKPtr )();
		DeinitializeSimplygonSDKPtr deinitializeSimplygonSDK =
		    (DeinitializeSimplygonSDKPtr)FPlatformProcess::GetDllExport( SimplygonDLL,
		                                                                 TEXT( "DeinitializeSimplygonSDK" ) );
		if( deinitializeSimplygonSDK )
			deinitializeSimplygonSDK();
		FPlatformProcess::FreeDllHandle( SimplygonDLL );
		SimplygonDLL = nullptr;
	}

#if WITH_UNREAL_DEVELOPER_TOOLS
	// Unregister message log
	if (FModuleManager::Get().IsModuleLoaded("MessageLog"))
	{
		FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
		MessageLogModule.UnregisterLogListing("Simplygon");
	}
#endif
}

Simplygon::ISimplygon* FSimplygonModule::GetSDK(bool bValidate)
{
	if (bValidate && SDK == nullptr)
	{
		UE_LOG(LogSimplygon, Error, TEXT("Simplygon API has not been initialized. This can be caused by several reasons. Please make sure the license is valid or check log for errors or warnings."));
	}

	return SDK;
}

void FSimplygonModule::InitializeSimplygon()
{
	if (FParse::Param(FCommandLine::Get(), TEXT("NoSimplygon")))
	{
		// The user specified that simplygon should not be used
		UE_LOG(LogSimplygon, Log, TEXT("Simplygon is disabled with -NoSimplygon flag"));
		return;
	}

	const FString SUPPORTED_SIMPLYGON_VERSION = "9";

	InitializeSimplygonSDKPath();
	UE_LOG(LogSimplygon, Display, TEXT("Simplygon SDK path: %s"), *GetSimplygonSDKPath());

	// Load D3D compiler first
	FString dllPath;
#if !PLATFORM_64BITS
	dllPath = FPaths::Combine(*FPaths::EngineDir(),
		TEXT("Binaries/ThirdParty/Windows/DirectX/x86/d3dcompiler_47.dll"));
#else
	dllPath = FPaths::Combine(*FPaths::EngineDir(),
		TEXT("Binaries/ThirdParty/Windows/DirectX/x64/d3dcompiler_47.dll"));
#endif
	if (!FPaths::FileExists(dllPath))
	{
		UE_LOG(LogSimplygon, Log,
			TEXT("Could not find d3dcompiler_47.dll, which is required for loading Simplygon"));
		return;
	}
	else
	{
		void* D3DHandle = FPlatformProcess::GetDllHandle(*dllPath);
	}

	FString dllFilename(FPaths::Combine(SimplygonSdkPath, TEXT("Simplygon.dll")));

	if (FParse::Param(FCommandLine::Get(), TEXT("SimplygonDebug")))
	{
		dllFilename = FPaths::Combine(SimplygonSdkPath, TEXT("SimplygonDebugRelease.dll"));
	}

	// If the DLL just doesn't exist, fail gracefully. Licensees and Subscribers will not necessarily have
	// Simplygon.
	if (!FPaths::FileExists(dllFilename))
	{
		UE_LOG(LogSimplygon, Error, TEXT("Simplygon DLL not present(%s) - disabling"), *dllFilename);
		return;
	}

	// Otherwise fail
	SimplygonDLL = FPlatformProcess::GetDllHandle(*dllFilename);
	if (!SimplygonDLL)
	{
		int32 ErrorNum = FPlatformMisc::GetLastError();
		TCHAR ErrorMsg[1024];
		FPlatformMisc::GetSystemErrorMessage(ErrorMsg, 1024, ErrorNum);
		UE_LOG(LogSimplygon, Error, TEXT("Failed to get Simplygon DLL handle: %s (%d)"), ErrorMsg,
			ErrorNum);
		return;
	}

	SDK = Simplygon::ISimplygon::CreateAPI( SimplygonDLL );

	// Get API function pointers of interest
	typedef void (*GetInterfaceVersionSimplygonSDKPtr)(char*);
	GetInterfaceVersionSimplygonSDKPtr getInterfaceVersionSimplygonSDK =
		(GetInterfaceVersionSimplygonSDKPtr)FPlatformProcess::GetDllExport(
			SimplygonDLL, TEXT("GetInterfaceVersionSimplygon"));

	typedef int (*InitializeSimplygonSDKPtr)(const char* license, Simplygon::ISimplygon** iface);
	InitializeSimplygonSDKPtr initializeSimplygonSDK =
		(InitializeSimplygonSDKPtr)FPlatformProcess::GetDllExport(SimplygonDLL,
			TEXT("InitializeSimplygon"));

	if (!getInterfaceVersionSimplygonSDK || !initializeSimplygonSDK)
	{
		// Couldn't find the functions we need.
		UE_LOG(LogSimplygon, Log, TEXT("Failed to acquire Simplygon DLL exports"));
		FPlatformProcess::FreeDllHandle(SimplygonDLL);
		delete SDK;
		SDK = nullptr;
		SimplygonDLL = nullptr;
		return;
	}

	// NOTE: Only major version check is required. This removes the dependency to update each time the SDK is
	// updated to a minor release with bug fixes
	// TODO : Tokenize the version string. This would allow for better granularity when a tight coupling to
	// the dll is required. (i.e Custom Dll Version)
	FString majorVersion, minorAndBuildRevision;
	FString simplygonHeaderVersionString = ANSI_TO_TCHAR(Simplygon::GetHeaderVersion());
	simplygonHeaderVersionString.Split(TEXT("."), &majorVersion, &minorAndBuildRevision);

	if (SUPPORTED_SIMPLYGON_VERSION.Compare(majorVersion) != 0)
	{
		UE_LOG(
			LogSimplygon, Log,
			TEXT("Simplygon version doesn't match the version expected by the Simplygon UE4 integration"));
		UE_LOG(LogSimplygon, Log, TEXT("Min version %s, found version %s"), *SUPPORTED_SIMPLYGON_VERSION,
			ANSI_TO_TCHAR(Simplygon::GetHeaderVersion()));
		FPlatformProcess::FreeDllHandle(SimplygonDLL);
		delete SDK;
		SDK = nullptr;
		SimplygonDLL = nullptr;
		return;
	}

	ANSICHAR VersionHash[200];
	getInterfaceVersionSimplygonSDK(VersionHash);
	int32 InterfaceHashLength = FCStringAnsi::Strlen(Simplygon::GetInterfaceVersionHash());
	if ((InterfaceHashLength > 0) && FCStringAnsi::Strcmp(VersionHash, Simplygon::GetInterfaceVersionHash()) != 0)
	{
		UE_LOG(LogSimplygon, Error, TEXT("Library version mismatch. Header=%s Lib=%s"),
			ANSI_TO_TCHAR(Simplygon::GetInterfaceVersionHash()), ANSI_TO_TCHAR(VersionHash));
		FPlatformProcess::FreeDllHandle(SimplygonDLL);
		delete SDK;
		SDK = nullptr;
		SimplygonDLL = nullptr;
		return;
	}

	// Get license file path from simplygon settings file.
	FString simplygonLicensePath = GetDefault<USimplygonSettings>()->GetLicensePath();
	if (simplygonLicensePath.IsEmpty())
	{
		FPlatformProcess::FreeDllHandle(SimplygonDLL);
		SimplygonDLL = nullptr;
		delete SDK;
		SDK = nullptr;
		return;
	}

	// Workaround for the fact Simplygon stomps memory
	class FSimplygonLicenseData
	{
		uint8* LicenseDataMem;
		int32 RealDataOffset;

	public:
		FSimplygonLicenseData(const TCHAR* InLicenseFilePath)
			: LicenseDataMem(nullptr)
			, RealDataOffset(0)
		{
			TArray<uint8> licenseFileContents;
			if (FFileHelper::LoadFileToArray(licenseFileContents, InLicenseFilePath))
			{
				if (licenseFileContents.Num() > 0)
				{
					// Allocate with a big slack at the beginning and end to workaround the fact Simplygon
					// stomps memory
					const int32 LicenseDataSizeWithSlack = licenseFileContents.Num() * 3 * sizeof(uint8);
					LicenseDataMem = (uint8*)FMemory::Malloc(LicenseDataSizeWithSlack);
					FMemory::Memzero(LicenseDataMem, LicenseDataSizeWithSlack);
					// Copy data to somewhere in the middle of allocated memory
					RealDataOffset = licenseFileContents.Num();
					FMemory::Memcpy(LicenseDataMem + RealDataOffset, licenseFileContents.GetData(),
						licenseFileContents.Num() * sizeof(uint8));
				}
			}
		}
		~FSimplygonLicenseData() { FMemory::Free(LicenseDataMem); }
		const char* GetLicenseData() const { return (const char*)(LicenseDataMem + RealDataOffset); }
		bool IsValid() const { return !!LicenseDataMem; }
	} licenseDataContainer(*simplygonLicensePath);

	if (licenseDataContainer.IsValid())
	{
		int32 initResult = initializeSimplygonSDK(licenseDataContainer.GetLicenseData(), nullptr);

		if (initResult != (int32)Simplygon::EErrorCodes::NoError &&
			initResult != (int32)Simplygon::EErrorCodes::AlreadyInitialized)
		{
			UE_LOG(LogSimplygon, Log, TEXT("Failed to initialize Simplygon. Return code: %d"),
				initResult);
			typedef int (*DeinitializeSimplygonSDKPtr)();
			DeinitializeSimplygonSDKPtr deinitializeSimplygonSDK =
				(DeinitializeSimplygonSDKPtr)FPlatformProcess::GetDllExport(
					SimplygonDLL, TEXT("DeinitializeSimplygonSDK"));
			if (deinitializeSimplygonSDK)
				deinitializeSimplygonSDK();
			FPlatformProcess::FreeDllHandle(SimplygonDLL);
			SimplygonDLL = nullptr;
			delete SDK;
			SDK = nullptr;
			return;
		}
		else
		{
			FString UEVersionString = LexToString(FApp::GetBuildConfiguration());
			SDK->SetErrorHandler(&ErrorHandler);
			SendTelemetryData(TEXT("{}"), TEXT("IntegrationInit"));
		}

		SDK->SetGlobalIntSetting("DefaultTBNType", (int32)Simplygon::ETangentSpaceMethod::OrthonormalLeftHanded);
		SDK->SetGlobalBoolSetting("AllowDirectX", true);
	}
	else
	{
		UE_LOG(LogSimplygon, Log, TEXT("Failed to load Simplygon license file"));
		FPlatformProcess::FreeDllHandle(SimplygonDLL);
		SimplygonDLL = nullptr;
	}
}

static bool ValidSimplygonSdkDir(const FString& Dir)
{
	auto HasFile = [](const FString& Dir, const FString& Filename)
	{
		if (!FPaths::FileExists(FPaths::Combine(Dir, Filename)))
		{
			return false;
		}
		return true;
	};

	return 
		HasFile(Dir, TEXT("Simplygon.h")) &&
		HasFile(Dir, TEXT("Simplygon.dll")) &&
		HasFile(Dir, TEXT("SimplygonBatch.exe")) &&
		HasFile(Dir, TEXT("SimplygonGridAgent.exe"));
}

void FSimplygonModule::InitializeSimplygonSDKPath()
{
	SimplygonSdkPath.Empty();
	bUsingLocalSdkPath = false;

	const FString PluginDir = IPluginManager::Get().FindPlugin(TEXT("Simplygon"))->GetBaseDir();
	FString SimplygonLocalSdkPath = FPaths::Combine(PluginDir, TEXT("Source/ThirdParty/SimplygonSDK"));

	// Use local path as primary path if it is valid
	if (ValidSimplygonSdkDir(SimplygonLocalSdkPath))
	{
		bUsingLocalSdkPath = true;
		SimplygonSdkPath = SimplygonLocalSdkPath;
	}
	else
	{
		// Fallback to global environment variable
		FString SimplygonGlobalEnvPath = FPlatformMisc::GetEnvironmentVariable(TEXT("SIMPLYGON_9_PATH"));
#if PLATFORM_WINDOWS
		if (!SimplygonGlobalEnvPath.IsEmpty())
		{
			FString ExpandedPath;
			TArray<TCHAR>& CharArray = ExpandedPath.GetCharArray();
			CharArray.SetNumUninitialized(FPlatformMisc::GetMaxPathLength());

			uint32 ExpandedLength = ::ExpandEnvironmentStringsW(SimplygonGlobalEnvPath.GetCharArray().GetData(), CharArray.GetData(), CharArray.Num());
			if (ExpandedLength && ExpandedLength < (uint32)CharArray.Num())
			{
				ExpandedPath.TrimToNullTerminator();
				SimplygonGlobalEnvPath = ExpandedPath;
			}
		}
#endif
		SimplygonSdkPath = SimplygonGlobalEnvPath;
	}
}

void FSimplygonModule::DumpMemoryStatistics() const
{
	if (SDK)
	{
		SDK->SetGlobalBoolSetting("DumpMemoryStatistics",true);
	}
}

void FSimplygonModule::SendTelemetryData(const FString& Json, const FString& Category)
{
	FString IntegrationName = TEXT("UnrealEngine");
	FString IntegrationVersion = FString::Printf(TEXT("%d.%d"), FEngineVersion::Current().GetMajor(), FEngineVersion::Current().GetMinor());

	if (SDK)
	{
		SDK->SendTelemetry(TCHAR_TO_ANSI(*Category), TCHAR_TO_ANSI(*IntegrationName), TCHAR_TO_ANSI(*IntegrationVersion), TCHAR_TO_ANSI(*Json));
	}
}
#undef LOCTEXT_NAMESPACE
