// (CVN) - Candy Valley Network GmbH

#include "LovenseManager.h"
#include <Containers/Array.h>
// EXTERNAL INCLUDES
#include <Misc/ConfigCacheIni.h>

// INTERNAL INCLUDES
#include "LovenseIntegration.h"
#include "LovenseToy.h"
#include "Adapters/ILovenseAdapter.h"
#include "LovenseTypes.h"

FLovenseManager::FLovenseManager() {
	this->lovenseEvents = nullptr;
	// Active data
	this->adaptersResponseData = FLovenseGetAdaptersResponseData();
	this->lovenseAdapters = TArray<TSharedPtr<ILovenseAdapter>>();
	this->toys = TArray<ULovenseToy*>();

	// Temporary data
	this->numAdaptersWaitingForGetToysResponse = 0;
	this->numAdaptersFailedGetToys = 0;
	this->temporaryLovenseAdapters = TArray<TSharedPtr<ILovenseAdapter>>();
	this->temporaryToys = TArray<ULovenseToy*>();

	// State flags
	this->bIsLovenseRunning = false;
	this->bIsUpdatingAdapters = false;
	this->bIsUpdatingToys = false;

	// Timers
	this->updateAdaptersTimerHandle = FTimerHandle();
	this->currentUpdateAdaptersTimerInterval = 5.0f;
	this->updateToysTimerHandle = FTimerHandle();
	this->commandDelayTimers = { };

	// Config value cache
	this->toyDelay = 500;
	this->toyStrengthMultiplier = 1.0f;

	//InitToySupport();
	
}


void FLovenseManager::InitToySupport(TMap<FString, TArray<ELovenseCommandType>> typeSupportMap1) {

	this->typeSupportMap = typeSupportMap1;
}

TArray<ELovenseCommandType> FLovenseManager::GetSupportCommandsByType(const FString toyType) {
	if (typeSupportMap.Contains(toyType.ToLower().Replace(TEXT(" "), TEXT("")))) {
		return *typeSupportMap.Find(toyType.ToLower().Replace(TEXT(" "), TEXT("")));
	}
	TArray<ELovenseCommandType> temp;
	return temp;
}

bool FLovenseManager::ToyIsSupportThisCommand(FString toyType, ELovenseCommandType commandType) {
	if (typeSupportMap.Contains(toyType.ToLower().Replace(TEXT(" "),TEXT("")))) {
		if (typeSupportMap.Find(toyType.ToLower().Replace(TEXT(" "), TEXT("")))->Contains(commandType)) {
			return true;
		}
	}
	return false;
}

FLovenseManager* FLovenseManager::Get() {
	// The lovense manager instance resides on the lovense integration module and always gets created in StartupModule().
	return FLovenseIntegrationModule::Get().GetLovenseManager();
}

void FLovenseManager::StartLovense() {
#if WITH_LOVENSE
	this->bIsLovenseRunning = true;

	// Fetch config values relevant for commands.
	FString configFilePath = FString::Printf(TEXT("%s%s.ini"), *FPaths::GeneratedConfigDir(), *LovenseIntegration::configFileName);
	bool bResult = false;
	int32 delay = 0;
	bResult = GConfig->GetInt(*LovenseIntegration::configSectionName, *LovenseIntegration::configToyDelayKeyName, delay, configFilePath);
	this->toyDelay = bResult ? FMath::Clamp(delay, 0, 2000) : 500;
	float toyStrength = 0.0f;
	bResult = GConfig->GetFloat(*LovenseIntegration::configSectionName, *LovenseIntegration::configToyStrengthMultiplierKeyName, toyStrength, configFilePath);
	this->toyStrengthMultiplier = bResult ? FMath::Clamp(toyStrength, 0.0f, 1.0f) : 1.0f;

	// Start the heartbeat which will update the timer manager.
	FLovenseIntegrationModule::Get().StartHeartbeat();
	// Sanity check. Strictly speaking not necessary as this happens in StopLovense() as well.
	this->ResetUpdateAdaptersTimer();
	// Let everyone who's interested know that we just started the lovense integration.
	// This should happen before we update the adapters. Otherwise onLovenseStartedUpdatingAdapters will be called before onLovenseStarted.
	if (this->lovenseEvents->onLovenseStarted.IsBound())
		this->lovenseEvents->onLovenseStarted.Broadcast();
	// Poll for any active Lovense App on the local network and for toys.
	this->UpdateAdapters();
#endif
}

void FLovenseManager::StopLovense() {
	// These have to be called before setting bIsLovenseRunning to false, as they would do nothing if it was false.
	// Stop update timers and reset Update Adapters timer interval.
	this->StopTimer(this->updateToysTimerHandle);
	this->ResetUpdateAdaptersTimer();
	// Stop any commands that might currently be running.
	this->ClearAllCommands();
	this->bIsLovenseRunning = false;
	// These can and should be called after setting bIsLovenseRunning to false, in case there are any side effects calling one of our methods.
	// Clear active and temporary data.
	// Shutting down adapters after setting bIsLovenseRunning to false means the clearing of any running test commands will do nothing,
	// but that doesn't matter as we've already cleared all commands above.
	//this->ClearData(true);
	FLovenseIntegrationModule::Get().StopHeartbeat();
	// Let everyone who's interested know that we just stopped the lovense integration.
	if (this->lovenseEvents->onLovenseStopped.IsBound())
		this->lovenseEvents->onLovenseStopped.Broadcast();
}

void FLovenseManager::ClearAllCommands(ULovenseToy* toy) {
	// In an earlier implementation SendCommand_Stop() did not exist and ClearAllCommands() directly set all function speeds to 0.
	// Now this is just a wrapper to not break code using this integration.
	this->SendCommand_Stop(toy,NULL);
}

void FLovenseManager::ReloadLovenseConfig() {
#if WITH_LOVENSE
	GConfig->LoadFile(FString::Printf(TEXT("%s%s.ini"), *FPaths::GeneratedConfigDir(), *LovenseIntegration::configFileName));
#endif
}

bool FLovenseManager::IsIPStringValid(const FString& ipString, const TCHAR* delimiter) {
/*
	// This solution is so much neater, but it's 2 times slower... :(
	static const FRegexPattern regexPattern(TEXT("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$"));
	return !ipString.IsEmpty() && FRegexMatcher(regexPattern, ipString).FindNext();
/*/
	// If supplied string is empty there's no IP.
	if (ipString.IsEmpty()) return false;

	// Split up supplied string into IP component value strings. "123.123.123.123" -> { "123", "123", "123", "123" }
	TArray<FString> ipElements = TArray<FString>();
	ipElements.Reserve(4);
	ipString.ParseIntoArray(ipElements, delimiter, false);
	// If there are not exactly 4 component value strings, it's not a valid IP.
	if (ipElements.Num() != 4) return false;

	// Iterate over component value strings to check if they are valid values.
	for (FString& ipElement : ipElements) {
		// Not a number -> IP is invalid. (Atoi() below ignores any non-numeric characters, so we have to explicitly check this)
		if (!FCString::IsNumeric(*ipElement)) return false;
		// Component value string is "0" -> valid value, check next component value.
		if (ipElement == TEXT("0")) continue;
		// First character is 0 which we do not accept unless it's a single digit value (checked above) -> IP is invalid.
		if (ipElement.StartsWith(TEXT("0"))) return false;
		// Convert value string to int32.
		// We can safely ignore a leading '+' as it gets stripped by Atoi(). A leading '-' will lead to the value falling out of the valid value range.
		int32 ipNumber = FCString::Atoi(*ipElement);
		// Atoi() returns 0 if no conversion can be performed, which is why we check for 0 above. If converted value falls within 1-255, check next component value.
		if (ipNumber > 0 && ipNumber < 256) continue;
		// If converted value does not fall within 1-255 -> IP is invalid.
		return false;
	}

	return true;
/**/
}

bool FLovenseManager::IsDomainStringValid(const FString& domainString) {
	// If supplied string is empty there's no domain.
	if (domainString.IsEmpty()) return false;

	CONSTEXPR int32 numTopLevelDomainCharacters = 13;

	// Extract top level domain from domain string. "123-123-123-123.lovense.club" -> ".lovense.club"
	FString topLevelDomainString = domainString.Right(numTopLevelDomainCharacters);
	// If the top level domain string is not ".lovense.club", the domain is invalid.
	if (topLevelDomainString != TEXT(".lovense.club")) return false;

	// Extract ip from domain string. "123-123-123-123.lovense.club" -> "123-123-123-123"
	FString ipString = domainString.LeftChop(numTopLevelDomainCharacters);

	// Check if ip part of the domain string is valid.
	return this->IsIPStringValid(ipString, TEXT("-"));
}

bool FLovenseManager::IsPortStringValid(const FString& portString) {
	// If supplied string is empty there's no Port.
	if (portString.IsEmpty()) return false;

	// Supplied string contains at least 1 period -> Port is invalid. (IsNumeric() below interprets periods as a decimal separator, so we have to explicitly check this)
	if (portString.Contains(TEXT("."))) return false;
	// Not a number -> Port is invalid. (Atoi() below ignores any non-numeric characters, so we have to explicitly check this)
	if (!FCString::IsNumeric(*portString)) return false;
	// First character is 0 which we do not accept -> Port is invalid.
	if (portString.StartsWith(TEXT("0"))) return false;
	// Convert value string to int32.
	// We can safely ignore a leading '+' as it gets stripped by Atoi(). A leading '-' will lead to the value falling out of the valid value range.
	int32 portNumber = FCString::Atoi(*portString);
	// If converted value falls within 1-65534, the Port is valid.
	return portNumber > 0 && portNumber < 65535;
}


bool FLovenseManager::IsPortIntValid(const int32& port) {
	// If supplied string is empty there's no Port.
	return port > 0 && port < 65535;
}

FString FLovenseManager::ConvertIPStringToDomainString(const FString& ipString) {
	FString domainString = ipString;
	// First, replace all occurrences of '.' with '-'. "123.123.123.123" -> "123-123-123-123"
	domainString = domainString.Replace(TEXT("."), TEXT("-"));
	// Second, add the ".lovense.club" part to the string. "123-123-123-123" -> "123-123-123-123.lovense.club"
	return domainString.Append(TEXT(".lovense.club"));
}

FString FLovenseManager::ConvertDomainStringToIPString(const FString& domainString) {
	FString ipString = domainString;
	// First, remove the ".lovense.club" part from the string as we don't care about that.
	// If the domain string was e.g. "123-123-123-123.lovense.club", this will leave us with "123-123-123-123".
	ipString.RemoveFromEnd(TEXT(".lovense.club"));
	// Second, replace all occurrences of '-' with '.'. "123-123-123-123" -> "123.123.123.123"
	return ipString.Replace(TEXT("-"), TEXT("."));
}

void FLovenseManager::StartTimer(FTimerHandle& timerHandle, float timerRate, bool bIsLooped) {
	// Timer manager does not exist if the integration is not running -> early return.
	if (!this->bIsLovenseRunning) return;
	FLovenseIntegrationModule::Get().GetTimerManager()->SetTimer(timerHandle, timerRate, bIsLooped);
}

void FLovenseManager::StartTimer(FTimerHandle& timerHandle, const FTimerDelegate& delegate, float timerRate, bool bIsLooped) {
	// Timer manager does not exist if the integration is not running -> early return.
	if (!this->bIsLovenseRunning) return;
	FLovenseIntegrationModule::Get().GetTimerManager()->SetTimer(timerHandle, delegate, timerRate, bIsLooped);
}

void FLovenseManager::StartTimerByEventsAPI(FTimerHandle& timerHandle, const FTimerDelegate& delegate, float timerRate, bool bIsLooped) {
	// Timer manager does not exist if the integration is not running -> early return.
	FLovenseIntegrationModule::Get().GetTimerManagerEventsAPI()->SetTimer(timerHandle, delegate, timerRate, bIsLooped);
}

void FLovenseManager::StartTimer(FTimerHandle& timerHandle, const FTimerDynamicDelegate& delegate, float timerRate, bool bIsLooped) {
	// Timer manager does not exist if the integration is not running -> early return.
	if (!this->bIsLovenseRunning) return;
	FLovenseIntegrationModule::Get().GetTimerManager()->SetTimer(timerHandle, delegate, timerRate, bIsLooped);
}

void FLovenseManager::StopTimer(FTimerHandle& timerHandle) {
	// Timer manager does not exist if the integration is not running -> early return.
	if (!this->bIsLovenseRunning) return;
	FLovenseIntegrationModule::Get().GetTimerManager()->ClearTimer(timerHandle);
}

bool FLovenseManager::IsTimerRunning(FTimerHandle timerHandle) {
	// Timer manager does not exist if the integration is not running -> early return.
	if (!this->bIsLovenseRunning) return false;
	return FLovenseIntegrationModule::Get().GetTimerManager()->IsTimerActive(timerHandle);
}

bool FLovenseManager::IsTimerExecuting(FTimerHandle timerHandle) {
	// Timer manager does not exist if the integration is not running -> early return.
	if (!this->bIsLovenseRunning) return false;
	// GetTimerRemaining() will only return 0.0f if the timer has just finished and is executing it's delegate.
	// Before the timer is finished GetTimerRemaining() will return a value >0.0f.
	// After the timer's delegate was executed GetTimerRemaining() will return -1.0f, as the timer will have been removed.
	return FLovenseIntegrationModule::Get().GetTimerManager()->GetTimerRemaining(timerHandle) == 0.0f;
}

bool FLovenseManager::SendCommands(
	TArray<class UELovenseCommands*> commands,
	ULovenseToy* toy,
	float timeSec,
	float loopRunningSec,
	float loopPauseSec,
	FOnLovenseResponse callback
) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration is disabled by config, there's nothing to do.
	if (this->lovenseAdapters.Num() <= 0) return false;

	// Check command delay timers if vibrationSpeed is >0. If any are running, return false, otherwise start the timers.
	// We check for vibrationSpeed, so we can always stop the toys even if a command delay timer is running.
	/*if (vibrationSpeed > 0) COMMAND_DELAY_TIMER_HANDLER_1(Vibrate);*/

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// Generate parameters string for the Lovense Connect adapter implementations.

	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->SendCommands(commands, toy->GetToyID(), timeSec, loopRunningSec, loopPauseSec, callback);

	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	bool bResult = false;
	this->ForEachAdapter([ &commands,&timeSec, &loopRunningSec, &loopPauseSec, &bResult,&callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->SendCommands(commands, TEXT(""), timeSec, loopRunningSec, loopPauseSec, callback);
		});
	return bResult;
#else
	return true;
#endif
}

bool FLovenseManager::SendPosition(
	int position,
	ULovenseToy* toy,
	FOnLovenseResponse callback
) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration is disabled by config, there's nothing to do.
	if ( this->lovenseAdapters.Num() <= 0) return false;

	// Check command delay timers if vibrationSpeed is >0. If any are running, return false, otherwise start the timers.
	// We check for vibrationSpeed, so we can always stop the toys even if a command delay timer is running.
	/*if (vibrationSpeed > 0) COMMAND_DELAY_TIMER_HANDLER_1(Vibrate);*/

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// Generate parameters string for the Lovense Connect adapter implementations.

	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->SendPosition(position, toy->GetToyID(),  callback);

	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	bool bResult = false;
	this->ForEachAdapter([&position,  &bResult, &callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->SendPosition(position, TEXT(""), callback);
		});
	return bResult;
#else
	return true;
#endif
}

bool FLovenseManager::SetUpPatternV2(
	TArray<class UEPositionPatternValue*> positionValues,
	ULovenseToy* toy,
	FOnLovenseResponse callback
) {
	#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration is disabled by config, there's nothing to do.
	if ( this->lovenseAdapters.Num() <= 0) return false;

	// Check command delay timers if vibrationSpeed is >0. If any are running, return false, otherwise start the timers.
	// We check for vibrationSpeed, so we can always stop the toys even if a command delay timer is running.
	/*if (vibrationSpeed > 0) COMMAND_DELAY_TIMER_HANDLER_1(Vibrate);*/

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// Generate parameters string for the Lovense Connect adapter implementations.

	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->SetUpPatternV2(positionValues,toy->GetToyID(), callback);

	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	bool bResult = false;
	this->ForEachAdapter([&positionValues, &bResult, &callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->SetUpPatternV2(positionValues, TEXT(""),  callback);
		});
	return bResult;
#else
	return true;
#endif
}

bool FLovenseManager::StartPatternV2(
	ULovenseToy * toy,
	int startTime,
	int offsetTime,
	FOnLovenseResponse callback
) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration is disabled by config, there's nothing to do.
	if ( this->lovenseAdapters.Num() <= 0) return false;

	// Check command delay timers if vibrationSpeed is >0. If any are running, return false, otherwise start the timers.
	// We check for vibrationSpeed, so we can always stop the toys even if a command delay timer is running.
	/*if (vibrationSpeed > 0) COMMAND_DELAY_TIMER_HANDLER_1(Vibrate);*/

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// Generate parameters string for the Lovense Connect adapter implementations.

	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->StartPatternV2(toy->GetToyID(), startTime, offsetTime, callback);

	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	bool bResult = false;
	this->ForEachAdapter([ &startTime, &offsetTime, &bResult, &callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->StartPatternV2(TEXT(""), startTime, offsetTime, callback);
		});
	return bResult;
#else
	return true;
#endif
}

bool FLovenseManager::StopPatternV2(
	ULovenseToy* toy,
	FOnLovenseResponse callback
) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration is disabled by config, there's nothing to do.
	if ( this->lovenseAdapters.Num() <= 0) return false;

	// Check command delay timers if vibrationSpeed is >0. If any are running, return false, otherwise start the timers.
	// We check for vibrationSpeed, so we can always stop the toys even if a command delay timer is running.
	/*if (vibrationSpeed > 0) COMMAND_DELAY_TIMER_HANDLER_1(Vibrate);*/

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// Generate parameters string for the Lovense Connect adapter implementations.

	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->StopPatternV2(toy->GetToyID(),  callback);

	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	bool bResult = false;
	this->ForEachAdapter([ &bResult, &callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->StopPatternV2(TEXT(""), callback);
		});
	return bResult;
#else
	return true;
#endif
}

bool FLovenseManager::GetPatternV2SyncTime(
	ULovenseToy* toy,
	FOnLovenseResponse callback
) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration is disabled by config, there's nothing to do.
	if (this->lovenseAdapters.Num() <= 0) return false;

	// Check command delay timers if vibrationSpeed is >0. If any are running, return false, otherwise start the timers.
	// We check for vibrationSpeed, so we can always stop the toys even if a command delay timer is running.
	/*if (vibrationSpeed > 0) COMMAND_DELAY_TIMER_HANDLER_1(Vibrate);*/

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// Generate parameters string for the Lovense Connect adapter implementations.

	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	//if (bIsToyValid) 
	return toy->lovenseAdapter->GetPatternV2SyncTime(callback);

	//// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	//bool bResult = false;
	//this->ForEachAdapter([&toy, &bResult, &callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
	//	bResult |= lovenseAdapter->GetPatternV2SyncTime( callback);
	//	});
	//return bResult;
#else
	return true;
#endif
}


/**
 * @brief Macro used in SendCommand_*() to check or start a single command delay timer.
 * \n If the global timer is running, always return false.
 * \n If toy is valid and its timer is running, return false, otherwise start that timer.
 * \n If no toy is specified and global timer is not running, start global timer.
 */
#define COMMAND_DELAY_TIMER_HANDLER_1(Timer) { \
		if (this->IsTimerRunning(this->commandDelayTimers.commandDelayTimerHandle_##Timer)) return false; \
		if (toy) { \
			if (this->IsTimerRunning(toy->commandDelayTimers.commandDelayTimerHandle_##Timer)) return false; \
			this->StartTimer(toy->commandDelayTimers.commandDelayTimerHandle_##Timer, toy->commandDelayTimers.commandDelay_##Timer, false); \
		} else { \
			this->StartTimer(this->commandDelayTimers.commandDelayTimerHandle_##Timer, this->commandDelayTimers.commandDelay_##Timer, false); \
		} \
	}

/**
 * @brief Macro used in SendCommand_*() to check or start two command delay timers.
 * \n If any of the two global timers are running, always return false.
 * \n If toy is valid and any of its timers are running, return false, otherwise start both timers.
 * \n If no toy is specified and global timers are not running, start global timers.
 */
#define COMMAND_DELAY_TIMER_HANDLER_2(Timer1, Timer2) { \
		if (this->IsTimerRunning(this->commandDelayTimers.commandDelayTimerHandle_##Timer1) || this->IsTimerRunning(this->commandDelayTimers.commandDelayTimerHandle_##Timer2)) return false; \
		if (toy) { \
			if (this->IsTimerRunning(toy->commandDelayTimers.commandDelayTimerHandle_##Timer1) || this->IsTimerRunning(toy->commandDelayTimers.commandDelayTimerHandle_##Timer2)) return false; \
			this->StartTimer(toy->commandDelayTimers.commandDelayTimerHandle_##Timer1, toy->commandDelayTimers.commandDelay_##Timer1, false); \
			this->StartTimer(toy->commandDelayTimers.commandDelayTimerHandle_##Timer2, toy->commandDelayTimers.commandDelay_##Timer2, false); \
		} else { \
			this->StartTimer(this->commandDelayTimers.commandDelayTimerHandle_##Timer1, this->commandDelayTimers.commandDelay_##Timer1, false); \
			this->StartTimer(this->commandDelayTimers.commandDelayTimerHandle_##Timer2, this->commandDelayTimers.commandDelay_##Timer2, false); \
		} \
	}

bool FLovenseManager::SendCommand_Test(ULovenseToy* toy) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters, there's nothing to do.
	if ( this->lovenseAdapters.Num() <= 0) return false;

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	//if (bIsToyValid) return toy->lovenseAdapter->SendCommand_Test(toy->GetToyDescription().id);

	// Otherwise run the test command on all active toys.
	// We iterate of all toys instead of all adapters as the test command requires a toy id.
	bool bResult = false;
	for (ULovenseToy* lovenseToy : this->toys) {
		//bResult |= lovenseToy->lovenseAdapter->SendCommand_Test(lovenseToy->GetToyDescription().id);
	}
	return bResult;
#else
	return true;
#endif
}

bool FLovenseManager::SendCommand_Stop(ULovenseToy* toy, FOnLovenseResponse callback) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters, there's nothing to do.
	if (this->lovenseAdapters.Num() <= 0) return false;
	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// As this command is not available for Lovense Connect, we can leave the parameters string empty.
	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->SendCommand_Stop(toy->GetToyID(),callback);

	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	bool bResult = false;
	this->ForEachAdapter([&bResult, &callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->SendCommand_Stop(TEXT(""), callback);
	});
	return bResult;
#else
	return true;
#endif
}

bool FLovenseManager::SendCommand_Pattern(ULovenseToy* toy, FLovensePattern& pattern, FOnLovenseResponse callback) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration, rotation and air are disabled by config, there's nothing to do.
	if ( this->lovenseAdapters.Num() <= 0 ) return false;

	// Check command delay timers. If any are running, return false, otherwise start the timers.
	COMMAND_DELAY_TIMER_HANDLER_1(Pattern);

	// If not all of these functions are disabled by config, we can still execute the command with only the enabled functions,
	// so set the speed for the disabled functions to 0 to disable it.
	

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// As this command is not available for Lovense Connect, we can leave the parameters string empty.
	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->SendCommand_Pattern(toy->GetToyID(), pattern,nullptr);

	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
	bool bResult = false;
	this->ForEachAdapter([&bResult, &pattern](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->SendCommand_Pattern(TEXT(""), pattern, nullptr);
	});
	return bResult;
#else
	return true;
#endif
}


bool FLovenseManager::SendCommand_Preset(ULovenseToy* toy, const FString& name,float timeSec, FOnLovenseResponse callback) {
#if WITH_LOVENSE
	// If the integration is not running or there are no adapters or toy vibration, rotation and air are disabled by config, there's nothing to do.
	if ( this->lovenseAdapters.Num() <= 0 ) return false;

	// Check command delay timers. If any are running, return false, otherwise start the timers.
	COMMAND_DELAY_TIMER_HANDLER_1(Pattern);

	// Check if we have a valid toy.
	bool bIsToyValid = false;
	if (toy) {
		// Check if the toy object pointed to is actually valid.
		// This will be invalid if the user passes in cached pointers and fails to update them during ULovenseEvents::onLovenseUpdatedToys.
		if (!toy->IsValidToy()) return false;
		bIsToyValid = true;
	}

	// As this command is not available for Lovense Connect, we can leave the parameters string empty.
	// If a valid toy was specified, use the id of its toy description to run the command only on that toy.
	if (bIsToyValid) return toy->lovenseAdapter->SendCommand_Preset(toy->GetToyID(), name,timeSec,callback);
	bool bResult = false;
	this->ForEachAdapter([&bResult, &name,&timeSec, &callback](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void {
		bResult |= lovenseAdapter->SendCommand_Preset(TEXT(""), name,timeSec, callback);
		});
	return bResult;
	// Otherwise iterate over all active adapters and run the command without a toy id, which will execute the command on all toys connected to the adapter.
#else
	return true;
#endif
}

bool FLovenseManager::StartEventsAPI(FString ip, int32 port, FOnEventCode& resultCodeCallback, FOnGetLovenseEventToys& toysCallback) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	//if(mapEventDictionary.Find(ip)) 
	
		TSharedPtr<LovenseToyEventsWebSocketHandler> handlerValue = MakeShareable(new LovenseToyEventsWebSocketHandler());
		handlerValue->Initialize(ip, port, static_cast<FOnEventCode&>(resultCodeCallback), static_cast<FOnGetLovenseEventToys&>(toysCallback));
		if (handler) {
			handler->Get()->Shutdown();
			mapEventDictionary.Remove(ip);
			mapEventDictionary.Add(ip, handlerValue);
		}
		else {
			mapEventDictionary.Add(ip, handlerValue);
		}
	
	return true;
}

void FLovenseManager::SetConnectEventCallback(const FString ip, FOnLovenseConnectStatusEvent& connectCallback) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	if (handler) {
		handler->Get()->SetConnectEventCallback(connectCallback);
	}
}

void FLovenseManager::SetBatteryEventCallback(const FString ip, FOnLovenseBatteryEvent& batteryCallback) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	if (handler) {
		handler->Get()->SetBatteryEventCallback( batteryCallback);
	}
}

void FLovenseManager::SetFunctionsValueEventCallback(const FString ip, FOnFunctionsEvent& functionsValueCallback) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	if (handler) {
		handler->Get()->SetFunctionsValueEventCallback( functionsValueCallback);
	}
}

void FLovenseManager::SetEveryShakeEventCallback(const FString ip, FOnEveryShakeEvent& everyShakeCallback) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	if (handler) {
		handler->Get()->SetEveryShakeEventCallback( everyShakeCallback);
	}
}

void FLovenseManager::SetButtonEventCallback(const FString ip, FOnButtonEvent& buttonCallback) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	if (handler) {
		handler->Get()->SetButtonEventCallback( buttonCallback);
	}
}

void FLovenseManager::SetMotionChangedEventCallback(const FString ip, FOnMotionChangedEvent& motionChangedCallback) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	if (handler) {
		handler->Get()->SetMotionChangedEventCallback(motionChangedCallback);
	}
}



bool FLovenseManager::StopEventsAPI(const FString ip) {
	TSharedPtr<LovenseToyEventsWebSocketHandler>* handler = mapEventDictionary.Find(ip);
	if (handler) {
		handler->Get()->Shutdown();
		mapEventDictionary.Remove(ip);
	}

	return true;
}

void FLovenseManager::OnCommandFailed() {
	// Something went wrong, reset Update Adapters timer loop and try polling for active Lovense App instances on the local network.
	this->ResetUpdateAdaptersTimer();
	this->UpdateAdapters();
}

void FLovenseManager::Initialize() {
	// Create the lovense events object instance.
	this->lovenseEvents = TStrongObjectPtr<ULovenseEvents>(NewObject<ULovenseEvents>());
}

void FLovenseManager::Shutdown() {
	// Destroy the lovense events object instance.
this->lovenseEvents.Reset();
	for (auto It = mapEventDictionary.CreateIterator(); It; ++It)
	{
		const FString& Key = It->Key;
		TSharedPtr<LovenseToyEventsWebSocketHandler>& Value = It->Value;
		Value->Shutdown();
	}
	mapEventDictionary.Reset();
}

void FLovenseManager::UpdateAdapters() {
#if WITH_LOVENSE
	// Don't do anything if the integration is not running or we are already updating adapters.
	// We don't need to check if we are currently updating toys here, as UpdateToys_Internal() checks for that already.
	// The only effect calling this while update toys is in progress would be that the toy information might be out of date,
	// as UpdateToys_Internal() was called with the currently active adapters response data, which gets updated with TryGetAdapterData() below.
	// But the Update Toys timer ticks every 5 seconds, so it's not a big deal. And if there is any invalid data received or there was a failed command,
	// the Update Adapters timer loop kicks on bringing everything up to speed.
	if (!this->bIsLovenseRunning || this->bIsUpdatingAdapters) return;

	this->bIsUpdatingAdapters = true;

	// Stop Update Adapters timer, so it doesn't finish and start the next iteration. Will be restarted if update fails.
	this->StopTimer(this->updateAdaptersTimerHandle);
	// This is strictly speaking not necessary as UpdateToys() does nothing if we are updating adapters or toys,
	// but since the timer gets restarted when the update is finished, might as well do it to be clean.
	this->StopTimer(this->updateToysTimerHandle);

	UE_LOG(LogLovenseIntegration, Log, TEXT("#################### Start Update Lovense Adapters #######################"));

	// Poll the Lovense Servers for any active Lovense Apps in the local network.
	bool bResult = ILovenseAdapter::TryGetAdapterData(FOnLovenseGetAdaptersResponse::CreateLambda([this](FLovenseGetAdaptersResponseData responseData) ->void {
		// Copy the response data. We work with adaptersResponseData in the methods below.
		this->adaptersResponseData = responseData;

		// Parse the response data and update toys.
		this->FilterAdaptersAndAddOfflineAdapters();
		this->PruneAdapters();
		this->UpdateToys_Internal();

		// Debug prints.
		UE_LOG(LogLovenseIntegration, Log, TEXT("#################### Finished Update Lovense Adapters ####################"));
		UE_LOG(LogLovenseIntegration, Log, TEXT("Current Active Adapters:"));
		for (TSharedPtr<ILovenseAdapter> adapter : this->lovenseAdapters) {
			UE_LOG(LogLovenseIntegration, Log, TEXT("%s"), *adapter->GetAdapterDescription().ToString());
		}
		UE_LOG(LogLovenseIntegration, Log, TEXT("New Adapters:"));
		for (TSharedPtr<ILovenseAdapter> adapter : this->temporaryLovenseAdapters) {
			UE_LOG(LogLovenseIntegration, Log, TEXT("%s"), *adapter->GetAdapterDescription().ToString());
		}
		UE_LOG(LogLovenseIntegration, Log, TEXT("##########################################################################"));
	}));

	if (bResult) {
		// If HTTP request was sent successfully, let listeners know we just started updating adapters.
		if (this->lovenseEvents->onLovenseStartedUpdatingAdapters.IsBound())
			this->lovenseEvents->onLovenseStartedUpdatingAdapters.Broadcast();
		return;
	}

	// If HTTP request failed, start/continue Update Adapters timer loop.
	this->StartUpdateAdaptersTimer();
	this->bIsUpdatingAdapters = false;
	UE_LOG(LogLovenseIntegration, Warning, TEXT("Failed to get adapters!"));
#endif
}

void FLovenseManager::UpdateToys() {
#if WITH_LOVENSE
	// Do nothing if the integration is not running.
	// Don't let external code update toys if we are updating adapters, as both use the same code and would interfere with each other due to its asynchronous nature.
	if (!this->bIsLovenseRunning || this->bIsUpdatingAdapters) return;

	// The update adapters sequence can't call UpdateToys() as bIsUpdatingAdapters will be true,
	// so we do the work in this internal method which does not check bIsUpdatingAdapters and which the update adapters sequence calls instead.
	this->UpdateToys_Internal();
#endif
}

void FLovenseManager::InitializeAdapter(TSharedPtr<ILovenseAdapter> adapter, FLovenseAdapterDescription& adapterDescription) {
#if WITH_LOVENSE
	// If the adapter has no toy descriptions, we don't need to bother with it.
	// This also prevents the Update Adapters timer loop from starting if there is an active Lovense App with no toys connected.
	if (adapterDescription.toys.Num() <= 0) return;
	// If passed in adapter is invalid or the adapter initialization fails, avoid starting timer loop as well.
	// Adapter initialization fails if the platform is empty or the adapter could not deduce a valid IP from the domain string.
	if (!adapter || !adapter->Initialize(adapterDescription)) {
		UE_LOG(LogLovenseIntegration, Warning, TEXT("Failed to initialize adapter: %s"), *adapterDescription.ToString());
		return;
	}

	// Poll the Lovense App for toys.
	adapter->GetToys(FOnLovenseGetToysResponse::CreateLambda([this, adapter](FLovenseGetToysResponseData responseData, const TArray<TStrongObjectPtr<ULovenseToy>>& toyStrongObjects) ->void {
		// If no toy objects were return, the adapter is useless, so we remove it.
		// This can only really happen with offline adapters as we check the response data from the Lovense servers for toys above.
		bool bNeedsToRemoveTemporaryAdapter = toyStrongObjects.Num() <= 0;
		// Code 200 means success, if we get anything else something went wrong, remove adapter and increment numAdaptersFailedGetToys to start Update Adapters timer loop.
		if (responseData.code != 200) {
			// As we always add the Remote_OfflineDesktop adapter, we don't want it to cause the Update Adapters timer loop to run indefinitely.
			// If there's only 1 adapter, run loop anyways, though, as there probably is no active Lovense app on the local network.
			if (this->adaptersResponseData.adapters.Num() <= 1 || adapter->adapterDescription.deviceCode != TEXT("Remote_OfflineDesktop"))
				++this->numAdaptersFailedGetToys;
			bNeedsToRemoveTemporaryAdapter = true;
		}

		if (bNeedsToRemoveTemporaryAdapter) {
			// Shutdown adapter and remove from temporary data.
			adapter->Shutdown();
			this->temporaryLovenseAdapters.Remove(adapter);
		}

		// Add passed in toys to temporary data.
		for (TStrongObjectPtr<ULovenseToy> toyStrongObject : toyStrongObjects) {
			this->temporaryToys.Add(toyStrongObject.Get());
		}

		// The adapter is no longer waiting for a GetToys response, so decrement.
		--this->numAdaptersWaitingForGetToysResponse;
		if (this->numAdaptersWaitingForGetToysResponse > 0) return;

		// All adapters have received their GetToys response.
		// If no adapter has failed to get their toys, reset the Update Adapters timer.
		// Otherwise start/continue the timer loop.
		if (this->numAdaptersFailedGetToys == 0) this->ResetUpdateAdaptersTimer();
		else this->StartUpdateAdaptersTimer();

		// Start the Update Toys timer, which can run regardless of whether there was a failed adapter, as it works with the currently active adapters response data.
		this->StartUpdateToysTimer();

		// Shutdown all active adapters, copy temporary data to active data and clear temporary data.
		this->ForEachAdapter([](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void { lovenseAdapter->Shutdown(); });
		this->lovenseAdapters = this->temporaryLovenseAdapters;
		this->temporaryLovenseAdapters.Empty();
		this->toys = this->temporaryToys;
		this->temporaryToys.Empty();
		this->numAdaptersFailedGetToys = 0;
		this->numAdaptersWaitingForGetToysResponse = 0;
		this->bIsUpdatingAdapters = false;
		this->bIsUpdatingToys = false;

		// Notify any listeners that the active data has changed.
		if (this->lovenseEvents && this->lovenseEvents->onLovenseUpdatedAdapters.IsBound())
			this->lovenseEvents->onLovenseUpdatedAdapters.Broadcast();
		if (this->lovenseEvents && this->lovenseEvents->onLovenseUpdatedToys.IsBound())
			this->lovenseEvents->onLovenseUpdatedToys.Broadcast();

		// Debug prints.
		UE_LOG(LogLovenseIntegration, Log, TEXT("##########################################################################"));
		UE_LOG(LogLovenseIntegration, Log, TEXT("New Active Adapters:"));
		for (TSharedPtr<ILovenseAdapter> lovenseAdapter : this->lovenseAdapters) {
			UE_LOG(LogLovenseIntegration, Log, TEXT("%s"), *lovenseAdapter->GetAdapterDescription().ToString());
		}
		UE_LOG(LogLovenseIntegration, Log, TEXT("Active Toys:"));
		for (ULovenseToy* toy : this->toys) {
			UE_LOG(LogLovenseIntegration, Log, TEXT("%s"), *toy->GetToyDescription().ToString());
		}
		UE_LOG(LogLovenseIntegration, Log, TEXT("##########################################################################"));
	}));

	//if (!bResult) {
	//	// If the GetToys HTTP request failed, increment numAdaptersFailedGetToys
	//	// which will start the Update Adapters timer loop once all adapters have finished polling for toys.
	//	UE_LOG(LogLovenseIntegration, Warning, TEXT("GetToys failed on adapter: %s"), *adapterDescription.ToString());
	//	++this->numAdaptersFailedGetToys;
	//	return;
	//}

	// Add adapter to temporary data.
	++this->numAdaptersWaitingForGetToysResponse;
	this->temporaryLovenseAdapters.AddUnique(adapter);
#endif
}

#define IS_MOBILE_PLATFORM (!PLATFORM_DESKTOP && !PLATFORM_HOLOLENS)

void FLovenseManager::FilterAdaptersAndAddOfflineAdapters() {
#if WITH_LOVENSE
	// Fetch IP and Port override configs.
	FString configFilePath = FString::Printf(TEXT("%s%s.ini"), *FPaths::GeneratedConfigDir(), *LovenseIntegration::configFileName);
	bool bResult = false;
	FString deviceIpOverride = TEXT("");
	bResult = GConfig->GetString(*LovenseIntegration::configSectionName, *LovenseIntegration::configDeviceIpOverrideKeyName, deviceIpOverride, configFilePath);
	if (!bResult) deviceIpOverride = TEXT("");
	FString devicePortOverride = TEXT("");
	bResult = GConfig->GetString(*LovenseIntegration::configSectionName, *LovenseIntegration::configDevicePortOverrideKeyName, devicePortOverride, configFilePath);
	if (!bResult || !this->IsPortStringValid(devicePortOverride)) devicePortOverride = TEXT("30010");

	FString deviceDomainOverride = this->ConvertIPStringToDomainString(deviceIpOverride);
	static IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("li.Requests.UseDirectIP"));
	static TConsoleVariableData<bool>* CVarUseDirectIP = CVar ? CVar->AsVariableBool() : nullptr;
	bool bUseDirectIP = CVarUseDirectIP && CVarUseDirectIP->GetValueOnGameThread();

	// If there is a valid IP override, we use that IP to filter the adapters that were returned by the Lovense servers.
	// We also use the IP override to create an offline adapter further down in case we don't have an internet connection or can't reach the Lovense servers for some other reason.
	// This is so users can connect to their mobile device without an internet connection, or if there are multiple devices found and the user only wants one specific device.
	if (!deviceIpOverride.IsEmpty()) {
#if ENGINE_MAJOR_VERSION > 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
		CVarUseDirectIP = IConsoleManager::Get().FindTConsoleVariableDataBool(TEXT("li.Requests.UseDirectIP"));
#else
		
#endif

		// Remove any adapters that do not match the IP/domain override.
		for (int32 i = this->adaptersResponseData.adapters.Num() - 1; i >= 0; --i) {
			LovenseTestAdapter testAdapter = LovenseTestAdapter();

			bool bIsValid =
				testAdapter.Initialize(this->adaptersResponseData.adapters[i])
				&& (bUseDirectIP && testAdapter.deviceIp == deviceIpOverride
					|| !bUseDirectIP && testAdapter.deviceIp == deviceDomainOverride);

			if (bIsValid) continue;

			this->adaptersResponseData.adapters.RemoveAt(i);
		}
	}

	// If there are no adapters at this point it means one of three things:
	//	 1. We don't have an internet connection or can't reach the Lovense servers for some other reason.
	//	 2. There are no active Lovense Apps in the local network.
	//	 3. The user has specified an IP which is either not found in the network or the device under that IP doesn't have an active Lovense App.
	// In case we don't have an internet connection,
	// we create offline adapters with the information we have available to see if there are any active Lovense Apps in the local network.
	FLovenseAdapterDescription adapterDescription = FLovenseAdapterDescription();
	if (this->adaptersResponseData.adapters.Num() <= 0 && this->IsDomainStringValid(deviceDomainOverride)) {
		int32 port = FCString::Atoi(*devicePortOverride);

		// No need to check for mobile device if we are on a desktop platform and ip is localhost as it would just lead to the same app being found twice.
//#if IS_MOBILE_PLATFORM
//		if CONSTEXPR (true) {
//#else
		if (deviceIpOverride != TEXT("127.0.0.1")) {
//#endif
			adapterDescription.online = 1;					// Needs to be online or initialization will fail.
			adapterDescription.domain = deviceDomainOverride;
			// The Lovense Remote app for Android or iOS (at least used to) have random ports assigned, so the user needs to be able to override the port in this case as well. 
			if (bUseDirectIP) {
				adapterDescription.httpPort = port;
				adapterDescription.httpsPort = port + 10000;
			}
			else {
				adapterDescription.httpsPort = port;
				adapterDescription.httpPort = port - 10000;

			}
			if (GetDevicePlatformIsMobile()) {
				adapterDescription.platform = TEXT("mobile");	// Important for adapter factory. This will also be displayed in the UI.
				adapterDescription.deviceCode = TEXT("Remote_OfflineMobile");

			}
			else {
				adapterDescription.platform = TEXT("pc");	// Important for adapter factory. This will also be displayed in the UI.
				adapterDescription.deviceCode = TEXT("Remote_OfflineDesktop");

			}
			adapterDescription.appType = TEXT("remote");	// Important for adapter factory. This will also be displayed in the UI.
			// Add a dummy toy to fool InitializeAdapter() as it checks for that.
			adapterDescription.toys.Add(FLovenseToyDescription());
			bool exists = false;
			//for (size_t i = 0; i < length; i++)
			//{
			//	//this->adaptersResponseData.adapters[i]
			//}
			this->adaptersResponseData.adapters.Add(adapterDescription);
		}

		// No need to check for pc device if we are on a mobile platform and ip is localhost as it would just lead to the same app being found twice.
//#if !IS_MOBILE_PLATFORM
//		if CONSTEXPR (true) {
//#else
//		if (deviceIpOverride != TEXT("127.0.0.1")) {
//#endif
//			adapterDescription.deviceCode = TEXT("Remote_OfflineDesktop");
//			adapterDescription.online = 1;					// Needs to be online or initialization will fail.
//			adapterDescription.domain = deviceDomainOverride;
//			adapterDescription.httpsPort = port;
//			adapterDescription.platform = TEXT("pc");		// Important for adapter factory. This will also be displayed in the UI.
//			adapterDescription.appType = TEXT("remote");	// Important for adapter factory. This will also be displayed in the UI.
//			// Add a dummy toy to fool InitializeAdapter() as it checks for that.
//			adapterDescription.toys.Add(FLovenseToyDescription());
//
//			this->adaptersResponseData.adapters.Add(adapterDescription);
//		}
	}

#if IS_MOBILE_PLATFORM
	static const FString platformString = TEXT("mobile");
	bool bHasCurrentPlatformAdapter = this->adaptersResponseData.adapters.ContainsByPredicate(
		[](const FLovenseAdapterDescription& adapterDescription) ->bool {
			return adapterDescription.platform == platformString || adapterDescription.platform == TEXT("android") || adapterDescription.platform == TEXT("ios");
		}
	);
#else
	static const FString platformString = TEXT("pc");
	bool bHasCurrentPlatformAdapter = this->adaptersResponseData.adapters.ContainsByPredicate(
		[](const FLovenseAdapterDescription& adapterDescription) ->bool { return adapterDescription.platform == platformString; }
	);
#endif
	if (!bHasCurrentPlatformAdapter) {
		// If there are no adapters of the current platform, check the local host as we have all necessary information and don't need the Lovense servers for it.
		adapterDescription.deviceCode = TEXT("Remote_OfflineDesktop");
		adapterDescription.online = 1;									// Needs to be online or initialization will fail.
		adapterDescription.domain = TEXT("127-0-0-1.lovense.club");		// Adapter initialization will fail if domain is invalid.
		adapterDescription.httpsPort = 30010;
		adapterDescription.httpPort = 20010;
		adapterDescription.platform = platformString;					// Important for adapter factory. This will also be displayed in the UI.
		adapterDescription.appType = TEXT("remote");					// Important for adapter factory. This will also be displayed in the UI.
		// Add a dummy toy to fool InitializeAdapter() as it checks for that.
		adapterDescription.toys.Add(FLovenseToyDescription());

		this->adaptersResponseData.adapters.Add(adapterDescription);
	}
#endif
}

#undef IS_MOBILE_PLATFORM

bool FLovenseManager::GetDevicePlatformIsMobile() {
#if WITH_LOVENSE
	bool devicePlatformIsMobile = false;
	bool bResult = GConfig->GetBool(
		*LovenseIntegration::configSectionName,
		*LovenseIntegration::configDevicePlatformIsMobileKeyName,
		devicePlatformIsMobile,
		FString::Printf(TEXT("%s%s.ini"), *FPaths::GeneratedConfigDir(), *LovenseIntegration::configFileName)
	);
	return bResult;
#else
	return false;
#endif
}

void FLovenseManager::PruneAdapters() {
#if WITH_LOVENSE
	bool bHaveAdaptersChanged = false;

	// Look for any active adapters that are not found in adaptersResponseData anymore and shut them down.
	for (int32 i = this->lovenseAdapters.Num() - 1; i >= 0; --i) {
		TSharedPtr<ILovenseAdapter> adapterToRemove = this->lovenseAdapters[i];

		// Check if this adapter is found in adaptersResponseData.
		bool bHasDataForAdapter = false;
		for (FLovenseAdapterDescription& adapterDescription : this->adaptersResponseData.adapters) {
			if (adapterDescription.deviceCode != adapterToRemove->GetAdapterDescription().deviceCode) continue;
			bHasDataForAdapter = true;
			break;
		}
		if (bHasDataForAdapter) continue;

		// If this adapter is not found in adaptersResponseData, remove all active toys of that adapter and shut the adapter down.
		bHaveAdaptersChanged = true;
		for (TStrongObjectPtr<ULovenseToy> toy : this->lovenseAdapters[i]->toyStrongPointers) this->toys.Remove(toy.Get());
		this->lovenseAdapters[i]->Shutdown();
		this->lovenseAdapters.RemoveAt(i);
	}

	if (!bHaveAdaptersChanged || !this->lovenseEvents) return;

	// Notify any listeners if the adapters have changed.
	if (this->lovenseEvents->onLovenseUpdatedAdapters.IsBound())
		this->lovenseEvents->onLovenseUpdatedAdapters.Broadcast();
	if (this->lovenseEvents->onLovenseUpdatedToys.IsBound())
		this->lovenseEvents->onLovenseUpdatedToys.Broadcast();
#endif
}

void FLovenseManager::UpdateToys_Internal() {
#if WITH_LOVENSE
	// Don't do anything if the integration is not running or we are already updating toys.
	if (!this->bIsLovenseRunning || this->bIsUpdatingToys) return;

	this->bIsUpdatingToys = true;

	UE_LOG(LogLovenseIntegration, Log, TEXT("#################### Update Lovense Toys #######################"));

	// Make sure temporary data is cleared before adding new temporary data.
	this->ClearData(false);

	// Create and initialize adapters.
	// Doing this here in UpdateToys() makes the code a little easier, but means that all adapters and toys are recreated every time we update the toys.
	for (FLovenseAdapterDescription& adapterDescription : this->adaptersResponseData.adapters) {
		TSharedPtr<ILovenseAdapter> adapter = ILovenseAdapter::CreateLovenseAdapter(adapterDescription);
		this->InitializeAdapter(adapter, adapterDescription);
	}

	// In InitializeAdapter() above, if an adapter was successfully initialized, numAdaptersWaitingForGetToysResponse will be incremented.
	if (this->numAdaptersWaitingForGetToysResponse > 0) return;

	// If no adapter was successfully initialized, something is wrong and we need to start the update Update Adapters timer loop.
	// Cache whether we were updating adapters as the IsUpdating flags get cleared with ClearData(true).
	bool bWasUpdatingAdapters = this->bIsUpdatingAdapters;
	// Clear both temporary and active data as it is invalid.
	this->ClearData(true);
	// If we were updating adapters, just start the Update Adapters timer again as we currently might be in the timer loop,
	// in which case we don't want to reset the timer interval. Otherwise, start the timer loop from the beginning by calling OnCommandFailed().
	if (bWasUpdatingAdapters) this->StartUpdateAdaptersTimer();
	else this->OnCommandFailed();
#endif
}

void FLovenseManager::ClearData(bool bClearCurrentData) {
	if (bClearCurrentData) {
		// Clear toys and fire onLovenseUpdatedToys before shutting down adapters in case a listener needs to access the adapters.
		this->toys.Empty();
		if (this->lovenseEvents.IsValid() && IsValid(this->lovenseEvents.Get()) && this->lovenseEvents->onLovenseUpdatedToys.IsBound())
			this->lovenseEvents->onLovenseUpdatedToys.Broadcast();
		// Shutdown and clear adapters and fire onLovenseUpdatedAdapters.
		this->ForEachAdapter([](TSharedPtr<ILovenseAdapter> lovenseAdapter) ->void { if (lovenseAdapter.IsValid()) lovenseAdapter->Shutdown(); });
		this->lovenseAdapters.Empty();
		if (this->lovenseEvents.IsValid() && IsValid(this->lovenseEvents.Get()) && this->lovenseEvents->onLovenseUpdatedAdapters.IsBound())
			this->lovenseEvents->onLovenseUpdatedAdapters.Broadcast();
		// Also clear IsUpdating flags and response data as we call ClearData(true) in StopLovense().
		this->bIsUpdatingAdapters = false;
		this->bIsUpdatingToys = false;
		this->adaptersResponseData = FLovenseGetAdaptersResponseData();
	}

	// Clear temporary adapter and toy data used during UpdatedAdapters() and UpdateToys().
	this->numAdaptersWaitingForGetToysResponse = 0;
	this->numAdaptersFailedGetToys = 0;
	for (TSharedPtr<ILovenseAdapter> adapter : this->temporaryLovenseAdapters) adapter->Shutdown();
	this->temporaryLovenseAdapters.Empty();
	this->temporaryToys.Empty();
}

void FLovenseManager::OnUpdateAdaptersTimerFinished() {
#if WITH_LOVENSE
	// Double the interval for the next iteration if UpdateAdapters() fails again.
	this->currentUpdateAdaptersTimerInterval *= 2.0f;
	UE_LOG(LogLovenseIntegration, Log, TEXT("[FLovenseManager::OnUpdateAdaptersTimerFinished] [Interval: %.2f]"), this->currentUpdateAdaptersTimerInterval);
	// Try updating adapters again.
	this->UpdateAdapters();
#endif
}

void FLovenseManager::StartUpdateAdaptersTimer() {
#if WITH_LOVENSE
	// If the timer is already running, we just ignore this call.
	if (this->IsTimerRunning(this->updateAdaptersTimerHandle)) return;
	UE_LOG(LogLovenseIntegration, Log, TEXT("[FLovenseManager::StartUpdateAdaptersTimer] [Interval: %.2f]"), this->currentUpdateAdaptersTimerInterval);
	// Start the timer with the current interval.
	FTimerDelegate timerDelegate = FTimerDelegate::CreateRaw(this, &FLovenseManager::OnUpdateAdaptersTimerFinished);
	this->StartTimer(this->updateAdaptersTimerHandle, timerDelegate, this->currentUpdateAdaptersTimerInterval, false);
#endif
}

void FLovenseManager::ResetUpdateAdaptersTimer() {
#if WITH_LOVENSE
	// Stop the timer and reset the interval. This is the exit point for the Update Adapters timer loop.
	this->StopTimer(this->updateAdaptersTimerHandle);
	this->currentUpdateAdaptersTimerInterval = 5.01f;
	UE_LOG(LogLovenseIntegration, Log, TEXT("[FLovenseManager::ResetUpdateAdaptersTimer] [Interval: %.2f]"), this->currentUpdateAdaptersTimerInterval);
#endif
}

void FLovenseManager::OnUpdateToysTimerLoop() {
	UE_LOG(LogLovenseIntegration, Log, TEXT("[FLovenseManager::OnUpdateToysTimerLoop]"));
	// Try updating toys. Will do nothing if we are updating adapters or toys.
	this->UpdateToys();
}

void FLovenseManager::StartUpdateToysTimer() {
	// If the timer is already running, we just ignore this call.
	if (this->IsTimerRunning(this->updateToysTimerHandle)) return;
	UE_LOG(LogLovenseIntegration, Log, TEXT("[FLovenseManager::StartUpdateToysTimer]"));
	// Start the timer.
	FTimerDelegate timerDelegate = FTimerDelegate::CreateRaw(this, &FLovenseManager::OnUpdateToysTimerLoop);
	this->StartTimer(this->updateToysTimerHandle, timerDelegate, 5.0f, true);
}
