// (CVN) - Candy Valley Network GmbH

#include "LovenseIntegration.h"

// EXTERNAL INCLUDES
#include <HAL/IConsoleManager.h>
#include <Misc/ConfigCacheIni.h>
#include <Modules/ModuleManager.h>

// INTERNAL INCLUDES
#include "LovenseManager.h"
#include "LovenseTypes.h"


IMPLEMENT_MODULE(FLovenseIntegrationModule, LovenseIntegration);


DEFINE_LOG_CATEGORY(LogLovenseIntegration);


static TAutoConsoleVariable<bool> CVarAutoInitializeLovenseIntegration(
	TEXT("li.Initialization.Automatic"),
	true,
	TEXT("If true, the lovense integration will be initialized automatically on module startup.")
	TEXT("If false, initialize needs to be called manually, either via FLovenseIntegrationModule::Get().Initialize() or through ULovenseFunctionLibrary.")
);
static TAutoConsoleVariable<bool> CVarUseDirectIPForRequests(
	TEXT("li.Requests.UseDirectIP"),
	false,
	TEXT("If true, the lovense adapters will use the direct IP to create HTTP requests.")
	TEXT("If false, the lovense adapters will use the domain supplied by the V2 API.")
);


FLovenseIntegrationModule* FLovenseIntegrationModule::moduleInstance = nullptr;

void FLovenseIntegrationModule::StartupModule() {
	if (FLovenseIntegrationModule::moduleInstance) return;

	FLovenseIntegrationModule::moduleInstance = this;

	FModuleManager::Get().LoadModuleChecked(TEXT("WebSockets"));

	// Lovense manager should always exist regardless of whether the integration is initialized or not, so we won't get nullptr exceptions when using FLovenseManager::Get().
	this->lovenseManager = MakeShareable(new FLovenseManager());
	check(this->lovenseManager);
	this->lovenseManager->Initialize();

	// If you want manual initialization, set the cvar "li.Initialization.Automatic" to false and call "FLovenseIntegrationModule::Get().Initialize();" from your game's code.
	if (CVarAutoInitializeLovenseIntegration.GetValueOnGameThread())
		this->Initialize();
}

void FLovenseIntegrationModule::ShutdownModule() {
	check(this->lovenseManager);
	this->lovenseManager->StopLovense();
	this->lovenseManager->Shutdown();
	this->lovenseManager.Reset();

	this->StopHeartbeat();

	FLovenseIntegrationModule::moduleInstance = nullptr;
}

void FLovenseIntegrationModule::Initialize() {
#if WITH_LOVENSE
	FString configFilePath = FString::Printf(TEXT("%s%s.ini"), *FPaths::GeneratedConfigDir(), *LovenseIntegration::configFileName);

#if WITH_EDITOR
	// Reload config from disk in case it was edited manually.

	if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*configFilePath))
	{

		FFileHelper::SaveStringToFile(TEXT(""), *configFilePath);
	}
	GConfig->LoadFile(configFilePath);
#endif

	auto RenameConfigString = [&configFilePath](const FString& oldKeyName, const FString& newKeyName) {
		FString result = TEXT("");
		bool foundValue = GConfig->GetString(*LovenseIntegration::configSectionName, *oldKeyName, result, configFilePath);
		if (!foundValue) return;
		GConfig->SetString(*LovenseIntegration::configSectionName, *newKeyName, *result, configFilePath);
		GConfig->RemoveKey(*LovenseIntegration::configSectionName, *oldKeyName, configFilePath);
	};

	// Rename already existing deprecated config entries.
	PRAGMA_DISABLE_DEPRECATION_WARNINGS
	// ReSharper disable CppDeprecatedEntity
	RenameConfigString(LovenseIntegration::configMobileDeviceIpOverrideKeyName, LovenseIntegration::configDeviceIpOverrideKeyName);
	RenameConfigString(LovenseIntegration::configMobileDevicePortOverrideKeyName, LovenseIntegration::configDevicePortOverrideKeyName);
	// ReSharper restore CppDeprecatedEntity
	PRAGMA_ENABLE_DEPRECATION_WARNINGS

	auto GenerateConfigBool = [&configFilePath](const FString& keyName, bool value) {
		bool result = false;
		bool foundValue = GConfig->GetBool(*LovenseIntegration::configSectionName, *keyName, result, configFilePath);
		if (foundValue) return;
		GConfig->SetBool(*LovenseIntegration::configSectionName, *keyName, value, configFilePath);
	};

	auto GenerateConfigString = [&configFilePath](const FString& keyName, const FString& value) {
		FString result = TEXT("");
		bool foundValue = GConfig->GetString(*LovenseIntegration::configSectionName, *keyName, result, configFilePath);
		if (foundValue) return;
		GConfig->SetString(*LovenseIntegration::configSectionName, *keyName, *value, configFilePath);
	};

	auto GenerateConfigInt = [&configFilePath](const FString& keyName, int32 value) {
		int32 result = 0;
		bool foundValue = GConfig->GetInt(*LovenseIntegration::configSectionName, *keyName, result, configFilePath);
		if (foundValue) return;
		GConfig->SetInt(*LovenseIntegration::configSectionName, *keyName, value, configFilePath);
	};

	auto GenerateConfigFloat = [&configFilePath](const FString& keyName, float value) {
		float result = 0.0f;
		bool foundValue = GConfig->GetFloat(*LovenseIntegration::configSectionName, *keyName, result, configFilePath);
		if (foundValue) return;
		GConfig->SetFloat(*LovenseIntegration::configSectionName, *keyName, value, configFilePath);
	};

	// Generate default config values and write them to disk in case they are not present in the config.
	GenerateConfigBool(LovenseIntegration::configStartWithLovenseActiveKeyName, false);
	GenerateConfigString(LovenseIntegration::configDeviceIpOverrideKeyName, TEXT(""));
	GenerateConfigString(LovenseIntegration::configDevicePortOverrideKeyName, TEXT(""));
	GenerateConfigInt(LovenseIntegration::configToyDelayKeyName, 500);
	GenerateConfigFloat(LovenseIntegration::configToyStrengthMultiplierKeyName, 1.0f);
	GConfig->Flush(false, configFilePath);

	// Fetch "StartWithLovenseActive" from config and automatically start the integration if the value is true.
	bool bStartWithLovenseActive = false;
	bool bResult = GConfig->GetBool(*LovenseIntegration::configSectionName, *LovenseIntegration::configStartWithLovenseActiveKeyName, bStartWithLovenseActive, configFilePath);
	if (!bResult) bStartWithLovenseActive = false;

	if (!bStartWithLovenseActive) return;
	this->lovenseManager->StartLovense();
#endif
}

void FLovenseIntegrationModule::InitToySupport(TMap<FString, TArray<ELovenseCommandType>> typeSupportMap1) {
#if WITH_LOVENSE
	this->lovenseManager->InitToySupport(typeSupportMap1);
#endif
}

void FLovenseIntegrationModule::StartHeartbeat() {
#if WITH_LOVENSE
	// In case the heartbeat is already running, stop it.
	this->StopHeartbeat();

	// Create a timer manager which will manage our various timers.
	this->timerManager = MakeShareable(new FTimerManager());
	this->timerManagerEventsAPI = MakeShareable(new FTimerManager());

	// Register a ticker, so we can tick the timer manager.
	this->heartbeatTickerDelegate = FTickerDelegate::CreateRaw(this, &FLovenseIntegrationModule::HeartBeat);
#if ENGINE_MAJOR_VERSION >= 5
	this->heartbeatTickerDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(this->heartbeatTickerDelegate);
#else
	this->heartbeatTickerDelegateHandle = FTicker::GetCoreTicker().AddTicker(this->heartbeatTickerDelegate);
#endif
#endif
}

void FLovenseIntegrationModule::StopHeartbeat() {
	// If the handle is not set, our ticker is not registered -> Do nothing.
	if (!this->heartbeatTickerDelegateHandle.IsValid()) return;

	// Unregister our ticker.
#if ENGINE_MAJOR_VERSION >= 5
	FTSTicker::GetCoreTicker().RemoveTicker(this->heartbeatTickerDelegateHandle);
#else
	FTicker::GetCoreTicker().RemoveTicker(this->heartbeatTickerDelegateHandle);
#endif
	this->heartbeatTickerDelegateHandle.Reset();
	this->heartbeatTickerDelegate.Unbind();

	// Delete the timer manager.
	this->timerManager.Reset();
	this->timerManagerEventsAPI.Reset();
}

bool FLovenseIntegrationModule::HeartBeat(float deltaTime) {
	// Tick timer manager which will update our various timers.
	this->timerManager->Tick(deltaTime);
	this->timerManagerEventsAPI->Tick(deltaTime);
	return true;
}
