// (CVN) - Candy Valley Network GmbH

#include "Adapters/ILovenseAdapter.h"

// EXTERNAL INCLUDES
#include <HttpModule.h>
#include <IHttpRequest.h>
#include <IHttpResponse.h>
#include <Dom/JsonObject.h>
#include <Dom/JsonValue.h>
#include <HAL/IConsoleManager.h>
#include <Runtime/Launch/Resources/Version.h>
#include <Serialization/JsonReader.h>
#include <Serialization/JsonSerializer.h>
#include <UObject/Package.h>
#include <IPluginManager.h>
#include <RunnableThread.h>
THIRD_PARTY_INCLUDES_START
#include <curl/curl.h>
THIRD_PARTY_INCLUDES_END


// INTERNAL INCLUDES
#include "LovenseManager.h"
#include "LovenseToy.h"
#include "LovenseWebThread.h"
#include "LovenseToyEventsWebSocketHandler.h"
#include "Adapters/LovenseConnectDesktopAdapter.h"
#include "Adapters/LovenseConnectMobileAdapter.h"
#include "Adapters/LovenseRemoteDesktopAdapter.h"
#include "Adapters/LovenseRemoteMobileAdapter.h"


ILovenseAdapter::ILovenseAdapter() {
	this->lovenseToyEventsWebSocketHandler = nullptr;
	this->adapterDescription = FLovenseAdapterDescription();
	this->deviceIp = TEXT("");
	this->webProtocol = TEXT("https");
	this->responseData = FLovenseGetToysResponseData();
	this->toyStrongPointers = TArray<TStrongObjectPtr<ULovenseToy>>();
	this->toyTestTimerHandles = TArray<TPair<FString, FTimerHandle>>();
}

bool ILovenseAdapter::TryGetAdapterData(FOnLovenseGetAdaptersResponse callback) {
#if WITH_LOVENSE
	//FHttpRequestPtr httpRequest = FHttpModule::Get().CreateRequest();
	//httpRequest->SetVerb(TEXT("GET"));
	//httpRequest->SetURL(TEXT("https://api.lovense-api.com/api/lan/v2/app"));
	//httpRequest->OnProcessRequestComplete().BindLambda([callback](FHttpRequestPtr request, FHttpResponsePtr response, bool bWasSuccessful) ->void {
	//	// Do nothing if the integration is not running or we are not updating adapters.
	//	// The integration could have been stopped while the HTTP request was being processed.
	//	// IsUpdatingAdapters() shouldn't really be false at this point unless the integration was stopped, we check it just in case.
	//	if (!FLovenseManager::Get()->IsLovenseRunning() || !FLovenseManager::Get()->IsUpdatingAdapters()) return;

	//	// If request was unsuccessful, execute callback with empty response data.
	//	if (!bWasSuccessful) return (void)callback.ExecuteIfBound(FLovenseGetAdaptersResponseData());

	//	FLovenseGetAdaptersResponseData adaptersResponseData = FLovenseGetAdaptersResponseData();
	//	// Cache raw json string.
	//	adaptersResponseData.jsonString = response->GetContentAsString();

	//	// Parse response into a json object.
	//	TSharedPtr<FJsonObject> jsonObject = MakeShareable(new FJsonObject());
	//	TSharedRef<TJsonReader<TCHAR>> jsonReader = TJsonReaderFactory<TCHAR>::Create(adaptersResponseData.jsonString);
	//	FJsonSerializer::Deserialize(jsonReader, jsonObject);

	//	// If response does not have a "data" field it is invalid, execute callback with cached raw json string.
	//	if (!jsonObject->HasField(TEXT("data"))) return (void)callback.ExecuteIfBound(adaptersResponseData);

	//	// Retrieve response status.
	//	adaptersResponseData.code = jsonObject->GetIntegerField(TEXT("code"));
	//	adaptersResponseData.message = jsonObject->GetStringField(TEXT("message"));
	//	// Code 0 means success, if we get anything else something went wrong, execute callback with response status.
	//	if (adaptersResponseData.code != 0) return (void)callback.ExecuteIfBound(adaptersResponseData);

	//	// Retrieve "data" json array holding the toy json subobjects.
	//	TSharedPtr<FJsonValue> dataValue = jsonObject->GetField<EJson::None>(TEXT("data"));
	//	// If "data" is null there are no apps, execute callback with response status.
	//	if (dataValue->IsNull()) return (void)callback.ExecuteIfBound(adaptersResponseData);

	//	const TArray<TSharedPtr<FJsonValue>>& dataArray = dataValue->AsArray();
	//	// If "data" array is empty, execute callback with response status.
	//	// As we already check if the "data" field exists, this can only be the case if the "data" field is an empty json array.
	//	if (dataArray.Num() <= 0) return (void)callback.ExecuteIfBound(adaptersResponseData);
	//	// Iterate over all indices. Each index is an active Lovense App in the local network for which we will create adapters.
	//	for (const TSharedPtr<FJsonValue>& adapterResponse : dataArray) {
	//		// Convert the json value to a json object, which holds the adapter data.
	//		const TSharedPtr<FJsonObject>* adapterObjectPtr;
	//		if (!adapterResponse->TryGetObject(adapterObjectPtr)) continue;

	//		// Retrieve adapter data from the json object.
	//		TSharedPtr<FJsonObject> adapterObject = *adapterObjectPtr;
	//		FLovenseAdapterDescription parsedAdapterDescription = FLovenseAdapterDescription();
	//		parsedAdapterDescription.deviceCode = adapterObject->GetStringField(TEXT("deviceCode"));
	//		parsedAdapterDescription.online = adapterObject->GetBoolField(TEXT("online"));
	//		parsedAdapterDescription.domain = adapterObject->GetStringField(TEXT("domain"));
	//		parsedAdapterDescription.httpsPort = adapterObject->GetIntegerField(TEXT("httpsPort"));
	//		parsedAdapterDescription.wssPort = adapterObject->GetIntegerField(TEXT("wssPort"));
	//		parsedAdapterDescription.platform = adapterObject->GetStringField(TEXT("platform"));
	//		parsedAdapterDescription.appVersion = adapterObject->GetStringField(TEXT("appVersion"));
	//		parsedAdapterDescription.appType = adapterObject->GetStringField(TEXT("appType"));

	//		// Iterate over all toy json subobjects and retrieve their data.
	//		// This is strictly speaking not necessary as we'll get more complete toy data from polling GetToys directly from the Lovense Apps, we do it for completeness sake.
	//		const TArray<TSharedPtr<FJsonValue>>& toysArray = adapterObject->GetArrayField(TEXT("toyList"));
	//		if (toysArray.Num() <= 0) continue;
	//		for (const TSharedPtr<FJsonValue>& toyResponse : toysArray) {
	//			const TSharedPtr<FJsonObject>* toyObjectPtr;
	//			if (!toyResponse->TryGetObject(toyObjectPtr)) continue;

	//			TSharedPtr<FJsonObject> toyObject = *toyObjectPtr;
	//			FLovenseToyDescription toyDescription = FLovenseToyDescription();
	//			toyDescription.id = toyObject->GetStringField(TEXT("id"));
	//			toyDescription.name = toyObject->GetStringField(TEXT("toyType"));
	//			TSharedPtr<FJsonValue> nicknameValue = toyObject->GetField<EJson::None>(TEXT("nickName"));
	//			toyDescription.nickname = nicknameValue->IsNull() ? TEXT("") : nicknameValue->AsString();
	//			TSharedPtr<FJsonValue> batteryValue = toyObject->GetField<EJson::None>(TEXT("battery"));
	//			// Battery status of -1 means the status of the battery is unknown.
	//			toyDescription.battery = batteryValue->IsNull() ? -1 : FMath::RoundHalfFromZero(batteryValue->AsNumber());
	//			TSharedPtr<FJsonValue> versionValue = toyObject->GetField<EJson::None>(TEXT("hVersion"));
	//			toyDescription.version = versionValue->IsNull() ? TEXT("") : versionValue->AsString();
	//			toyDescription.status = toyObject->GetBoolField(TEXT("connected"));
	//			toyDescription.domain = parsedAdapterDescription.domain;

	//			parsedAdapterDescription.toys.Add(toyDescription);
	//		}

	//		adaptersResponseData.adapters.Add(parsedAdapterDescription);
	//	}

	//	// Execute callback with the retrieved adapter response data.
	//	(void)callback.ExecuteIfBound(adaptersResponseData);
	//});
	TMap<FString, FString> headers;
	GetRemote(TEXT("https://api.lovense-api.com/api/lan/v2/app"),false,TEXT("POST"),TEXT(""), headers,callback);
	//return httpRequest->ProcessRequest();
	return true;
#else
	return true;
#endif
}

bool ILovenseAdapter::Initialize(FLovenseAdapterDescription& description) {
#if WITH_LOVENSE
	this->adapterDescription = description;

	// No need to attempt a connection if the device is offline.
	//if (!this->adapterDescription.online) return false;

#/*if ENGINE_MAJOR_VERSION > 5 || ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2
	static TConsoleVariableData<bool>* CVarUseDirectIP = IConsoleManager::Get().FindTConsoleVariableDataBool(TEXT("li.Requests.UseDirectIP"));
#else
	static IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("li.Requests.UseDirectIP"));
	static TConsoleVariableData<bool>* CVarUseDirectIP = CVar ? CVar->AsVariableBool() : nullptr;
#endif*/
	if (!FLovenseManager::Get()->GetUseHttps()) {
		// Adapter descriptions returned from the lovense servers have a domain string in this format: "X-X-X-X.lovense.club".
		// while the offline adapter descriptions we create locally have the direct IP as the domain.
		// This means if the domain string doesn't end with ".lovense.club", we can just check if the string has a valid IP.
		if (!this->adapterDescription.domain.EndsWith(TEXT(".lovense.club"))) {
			if (!FLovenseManager::Get()->IsIPStringValid(this->adapterDescription.domain)) return false;

			// If domain is a valid IP, we can just use it as the device IP.
			this->deviceIp = this->adapterDescription.domain;
		} else {
			// If the domain string does end with ".lovense.club", convert the domain to an IP string.
			this->deviceIp = FLovenseManager::Get()->ConvertDomainStringToIPString(this->adapterDescription.domain);
			// We now should have an IP string, check if it is a valid IP.
			if (!FLovenseManager::Get()->IsIPStringValid(this->deviceIp)) return false;
		}

		// Override ports and protocol with HTTP as the SSL certificate does not work with direct IPs and we can't ignore SSL host verification without modifying the engine.
		//this->adapterDescription.httpsPort;
		//this->adapterDescription.wssPort;
		this->webProtocol = TEXT("http");
	} else {
		if (!FLovenseManager::Get()->IsDomainStringValid(this->adapterDescription.domain)) return false;

		// If domain is valid, we can just use it as the device IP.
		this->deviceIp = this->adapterDescription.domain;
		this->webProtocol = TEXT("https");
	}

	// Platform and appType should not be empty as those are used to identify what type of adapter this is.
	if (this->adapterDescription.platform.IsEmpty() || this->adapterDescription.appType.IsEmpty()) return false;

//	this->lovenseToyEventsWebSocketHandler = MakeShared<LovenseToyEventsWebSocketHandler>();

	return true;
#else
	return false;
#endif
}

void ILovenseAdapter::Shutdown() {
#if WITH_LOVENSE
	// Stop any test commands if they are running.
	for (TPair<FString, FTimerHandle>& timerHandle : this->toyTestTimerHandles) {
		/*TArray<class UELovenseCommands*> commands;
		UELovenseCommands command;
		command.SetCommandType(ELovenseCommandType::VIBRATE);
		command.SetValue(0);
		commands.Add(&command);*/
		//this->SendCommands(commands, toyStrongPointers. , timerHandle.Key, 0);
		FLovenseManager::Get()->StopTimer(timerHandle.Value);
	}
#endif
	this->toyTestTimerHandles.Empty();
	// Clean toys and mark them for destruction, so we can check if they are invalid in case the user fails to update cached toy pointers during ULovenseEvents::onLovenseUpdatedToys.
	for (TStrongObjectPtr<ULovenseToy> toy : this->toyStrongPointers) {
		if (!toy.IsValid()) continue;
		toy.Get()->lovenseAdapter = nullptr;
		// During engine exit, if the lovense integration is still running, the toy objects can already be marked as destroyed at this point.
		if (!IsValid(toy.Get())) continue;
		//if (toy.Get() != nullptr)
		//{
			//toy.Get()->ConditionalBeginDestroy();
		//}
	}
	this->toyStrongPointers.Empty();
//	if (this->lovenseToyEventsWebSocketHandler.IsValid())
//		this->lovenseToyEventsWebSocketHandler->Shutdown();
//	this->lovenseToyEventsWebSocketHandler.Reset();
	this->responseData = FLovenseGetToysResponseData();
}

bool ILovenseAdapter::RemoveToy(const FString toyId) {
	TStrongObjectPtr<class ULovenseToy>* removeToy = this->toyStrongPointers.FindByPredicate([toyId](TStrongObjectPtr<class ULovenseToy> rmToy) {
			return rmToy.Get()->GetToyID() == toyId;
		});
	if (removeToy != nullptr && removeToy->IsValid()) {
		this->toyStrongPointers.Remove(*removeToy);
	}
	return true;
}

void ILovenseAdapter::GetToys(FOnLovenseGetToysResponse callback) {
#if WITH_LOVENSE
	//this->GetToys(callback);
#else
#endif
}

void ILovenseAdapter::RequestToys(FString url, bool bIgnoreSSL, FString method, FString body, TMap<FString, FString> headers, FOnLovenseGetToysResponse callback) {
	this->requestURL = url;
	thread = new LovenseWebThread(url, bIgnoreSSL, method, body, headers, TEXT("thread"), callback,[this](const FString response,const FOnLovenseGetToysResponse callback) {
		this->responseData = FLovenseGetToysResponseData();
		// Cache raw json string
		this->responseData.jsonString = response;

		// Parse response into a json object.
		TSharedPtr<FJsonObject> jsonObject = MakeShareable(new FJsonObject());
		TSharedRef<TJsonReader<TCHAR>> jsonReader = TJsonReaderFactory<TCHAR>::Create(this->responseData.jsonString);
		FJsonSerializer::Deserialize(jsonReader, jsonObject);

		// If response does not have a "data" field it is invalid, execute callback with cached raw json string.
		if (!jsonObject->HasField(TEXT("data"))) return (void)callback.ExecuteIfBound(this->responseData, TArray<TStrongObjectPtr<ULovenseToy>>());

		// Retrieve response status.
		this->responseData.type = jsonObject->GetStringField(TEXT("type"));
		this->responseData.code = jsonObject->GetIntegerField(TEXT("code"));
		// Code 200 means success, if we get anything else something went wrong, execute callback with response status and empty toys array.
		if (this->responseData.code != 200) return (void)callback.ExecuteIfBound(this->responseData, TArray<TStrongObjectPtr<ULovenseToy>>());

		// Retrieve "data" json subobject holding the toy json subobjects.
		TSharedPtr<FJsonObject> dataObject;
		this->GetToysDataJsonObject(jsonObject, dataObject);
		// If we couldn't retrieve the "data" subobject, execute callback with response status and empty toys array.
		// As we already check if the "data" field exists, this can only be the case if the "data" field is not a json subobject,
		// or in the case of Lovense Remote (which returns a json string in the "data" field) if the json string is invalid.
		if (!dataObject.IsValid()) return (void)callback.ExecuteIfBound(this->responseData, TArray<TStrongObjectPtr<ULovenseToy>>());
		// Iterate over all json subobjects. Each subobject is a toy for which we will create a ULovenseToy.
		for (TPair<FString, TSharedPtr<FJsonValue>>& toyResponse : dataObject->Values) {
			// Convert the json value to a json object, which holds the toy data.
			const TSharedPtr<FJsonObject>* toyObjectPtr;
			if (!toyResponse.Value->TryGetObject(toyObjectPtr)) continue;

			// Retrieve toy data and create the lovense toy.
			TSharedPtr<FJsonObject> toyObject = *toyObjectPtr;
			FLovenseToyDescription toyDescription = FLovenseToyDescription();
			TArray<FString> Tokens;
			this->requestURL.ParseIntoArray(Tokens, TEXT(":"), true);

			if (Tokens.Num() >= 2)
			{
				FString SecondToken = Tokens[1];
				FString Domain = SecondToken.Mid(2);
				toyDescription.domain = Domain;
			}

			this->ParseToyDescription(toyDescription, toyObject);
			this->CreateLovenseToy(toyDescription);
		}

		// Execute the callback with the response data and the created toys.
		(void)callback.ExecuteIfBound(this->responseData, this->toyStrongPointers);
		});
	myThread = FRunnableThread::Create(thread, TEXT("MyRunnableThread"));
}

void ILovenseAdapter::ResetAdapter() {
	thread->Reset();
	//myThread->Kill();
}

void ILovenseAdapter::UnResetAdapter() {
	thread->UnReset();
	//myThread->Kill();
}

void ILovenseAdapter::GetRemote(FString url, bool bIgnoreSSL, FString method, FString body, TMap<FString, FString> headers, FOnLovenseGetAdaptersResponse& callback)
{
		LovenseWebThread* thread = new LovenseWebThread(url, bIgnoreSSL, method, body, headers, TEXT("thread"), callback,[&](const FString response,const FOnLovenseGetAdaptersResponse callback) {
		FLovenseGetAdaptersResponseData adaptersResponseData = FLovenseGetAdaptersResponseData();
		// Cache raw json string.
		adaptersResponseData.jsonString = response;

		// Parse response into a json object.
		TSharedPtr<FJsonObject> jsonObject = MakeShareable(new FJsonObject());
		TSharedRef<TJsonReader<TCHAR>> jsonReader = TJsonReaderFactory<TCHAR>::Create(adaptersResponseData.jsonString);
		FJsonSerializer::Deserialize(jsonReader, jsonObject);

		// If response does not have a "data" field it is invalid, execute callback with cached raw json string.
		if (!jsonObject->HasField(TEXT("data"))) return (void)callback.ExecuteIfBound(adaptersResponseData);

		// Retrieve response status.
		adaptersResponseData.code = jsonObject->GetIntegerField(TEXT("code"));
		adaptersResponseData.message = jsonObject->GetStringField(TEXT("message"));
		// Code 0 means success, if we get anything else something went wrong, execute callback with response status.
		if (adaptersResponseData.code != 0) return (void)callback.ExecuteIfBound(adaptersResponseData);

		// Retrieve "data" json array holding the toy json subobjects.
		TSharedPtr<FJsonValue> dataValue = jsonObject->GetField<EJson::None>(TEXT("data"));
		// If "data" is null there are no apps, execute callback with response status.
		if (dataValue->IsNull()) return (void)callback.ExecuteIfBound(adaptersResponseData);

		const TArray<TSharedPtr<FJsonValue>>& dataArray = dataValue->AsArray();
		// If "data" array is empty, execute callback with response status.
		// As we already check if the "data" field exists, this can only be the case if the "data" field is an empty json array.
		if (dataArray.Num() <= 0) return (void)callback.ExecuteIfBound(adaptersResponseData);
		// Iterate over all indices. Each index is an active Lovense App in the local network for which we will create adapters.
		for (const TSharedPtr<FJsonValue>& adapterResponse : dataArray) {
			// Convert the json value to a json object, which holds the adapter data.
			const TSharedPtr<FJsonObject>* adapterObjectPtr;
			if (!adapterResponse->TryGetObject(adapterObjectPtr)) continue;

			// Retrieve adapter data from the json object.
			TSharedPtr<FJsonObject> adapterObject = *adapterObjectPtr;
			FLovenseAdapterDescription parsedAdapterDescription = FLovenseAdapterDescription();
			parsedAdapterDescription.deviceCode = adapterObject->GetStringField(TEXT("deviceCode"));
			parsedAdapterDescription.online = adapterObject->GetBoolField(TEXT("online"));
			parsedAdapterDescription.domain = adapterObject->GetStringField(TEXT("domain"));
			parsedAdapterDescription.httpsPort = adapterObject->GetIntegerField(TEXT("httpsPort"));
			parsedAdapterDescription.httpPort = adapterObject->GetIntegerField(TEXT("httpPort"));
			parsedAdapterDescription.wssPort = adapterObject->GetIntegerField(TEXT("wssPort"));
			parsedAdapterDescription.platform = adapterObject->GetStringField(TEXT("platform"));
			parsedAdapterDescription.appVersion = adapterObject->GetStringField(TEXT("appVersion"));
			parsedAdapterDescription.appType = adapterObject->GetStringField(TEXT("appType"));

			// Iterate over all toy json subobjects and retrieve their data.
			// This is strictly speaking not necessary as we'll get more complete toy data from polling GetToys directly from the Lovense Apps, we do it for completeness sake.
			const TArray<TSharedPtr<FJsonValue>>& toysArray = adapterObject->GetArrayField(TEXT("toyList"));
			if (toysArray.Num() <= 0) continue;
			for (const TSharedPtr<FJsonValue>& toyResponse : toysArray) {
				const TSharedPtr<FJsonObject>* toyObjectPtr;
				if (!toyResponse->TryGetObject(toyObjectPtr)) continue;

				TSharedPtr<FJsonObject> toyObject = *toyObjectPtr;
				FLovenseToyDescription toyDescription = FLovenseToyDescription();
				toyDescription.id = toyObject->GetStringField(TEXT("id"));
				toyDescription.name = toyObject->GetStringField(TEXT("toyType"));
				TSharedPtr<FJsonValue> nicknameValue = toyObject->GetField<EJson::None>(TEXT("nickName"));
				toyDescription.nickname = nicknameValue->IsNull() ? TEXT("") : nicknameValue->AsString();
				TSharedPtr<FJsonValue> batteryValue = toyObject->GetField<EJson::None>(TEXT("battery"));
				// Battery status of -1 means the status of the battery is unknown.
				toyDescription.battery = batteryValue->IsNull() ? -1 : FMath::RoundHalfFromZero(batteryValue->AsNumber());
				TSharedPtr<FJsonValue> versionValue = toyObject->GetField<EJson::None>(TEXT("hVersion"));
				toyDescription.version = versionValue->IsNull() ? TEXT("") : versionValue->AsString();
				toyDescription.status = toyObject->GetBoolField(TEXT("connected"));
				toyDescription.domain = parsedAdapterDescription.domain;
				parsedAdapterDescription.toys.Add(toyDescription);
			}
			adaptersResponseData.adapters.Add(parsedAdapterDescription);
		}

		// Execute callback with the retrieved adapter response data.
		(void)callback.ExecuteIfBound(adaptersResponseData);
		});

	FRunnableThread* myThread = FRunnableThread::Create(thread, TEXT("MyRunnableThread"));
	
}


TSharedPtr<ILovenseAdapter> ILovenseAdapter::CreateLovenseAdapter(const FLovenseAdapterDescription& adapterDescription) {
#if WITH_LOVENSE
	if (adapterDescription.appType.Equals(TEXT("remote"))) {
		if (adapterDescription.platform.Equals(TEXT("pc"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseRemoteDesktopAdapter());
		if (adapterDescription.platform.Equals(TEXT("android"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseRemoteMobileAdapter());
		if (adapterDescription.platform.Equals(TEXT("ios"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseRemoteMobileAdapter());
		if (adapterDescription.platform.Equals(TEXT("mobile"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseRemoteMobileAdapter());
		if (adapterDescription.platform.Equals(TEXT("remote"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseRemoteMobileAdapter());
	}
	if (adapterDescription.appType.Equals(TEXT("connect"))) {
		if (adapterDescription.platform.Equals(TEXT("pc"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseConnectDesktopAdapter());
		if (adapterDescription.platform.Equals(TEXT("android"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseConnectAndroidAdapter());
		if (adapterDescription.platform.Equals(TEXT("ios"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseConnectIOSAdapter());
		if (adapterDescription.platform.Equals(TEXT("mobile"), ESearchCase::IgnoreCase)) return MakeShareable(new LovenseConnectAndroidAdapter());
	}
#endif
	return nullptr;
}

void ILovenseAdapter::CreateLovenseToy(FLovenseToyDescription& toyDescription) {
#if WITH_LOVENSE
	// Generate a unique name for the toy object.
	const FName NAME_Toy(*FString::Printf(TEXT("%s_%s"), *ULovenseToy::StaticClass()->GetName(), *toyDescription.name));
	FName toyName = MakeUniqueObjectName(GetTransientPackage(), ULovenseToy::StaticClass(), NAME_Toy);
	// Toy objects should never be saved, so put them in the transient package.
	ULovenseToy* toy = NewObject<ULovenseToy>(GetTransientPackage(), toyName);
	toy->lovenseAdapter = this;
	toy->SetToyDescription(toyDescription);
	this->toyStrongPointers.Add(TStrongObjectPtr<ULovenseToy>(toy));

	this->responseData.toys.Add(toyDescription);
#endif
}

void ILovenseAdapter::GetToysDataJsonObject(TSharedPtr<FJsonObject> jsonObject, TSharedPtr<FJsonObject>& outToysJsonObject) {
#if WITH_LOVENSE
	// Lovense Connect returns the "data" field as a json subobject, so just retrieve the field as an object.
	outToysJsonObject = jsonObject->GetObjectField(TEXT("data"));
#endif
}

bool ILovenseAdapter::SendCommand(const FString& parameters, FOnLovenseResponse callback) {
#if WITH_LOVENSE
	// If there are no toys connected, there's nothing to do.
	if (this->toyStrongPointers.Num() <= 0) {
		// Execute callback with -1 to indicate that we couldn't determine the battery status.
		(void)callback.ExecuteIfBound(-1);
		return false;
	}

	this->WebThread_SendCommand(parameters,callback);



	// The callback is used to retrieve the battery status of the toy, so if it isn't bound, we don't need to bother trying to retrieve the data.
	//if (callback.IsBound()) {
	//	httpRequest->OnProcessRequestComplete().BindLambda([callback](FHttpRequestPtr request, FHttpResponsePtr response, bool bWasSuccessful) ->void {
	//		// Do nothing if the integration is not running.
	//		// The integration could have been stopped while the HTTP request was being processed.
	//		if (!FLovenseManager::Get()->IsLovenseRunning()) return;

	//		if (!bWasSuccessful) {
	//			// If request was unsuccessful, execute callback with -1 to indicate that we couldn't determine the battery status and start Update Adapters timer loop.
	//			(void)callback.ExecuteIfBound(-1);
	//			FLovenseManager::Get()->OnCommandFailed();
	//			return;
	//		}

	//		// Parse response into a json object.
	//		TSharedPtr<FJsonObject> jsonObject = MakeShareable(new FJsonObject());
	//		TSharedRef<TJsonReader<TCHAR>> jsonReader = TJsonReaderFactory<TCHAR>::Create(response->GetContentAsString());
	//		FJsonSerializer::Deserialize(jsonReader, jsonObject);

	//		// Retrieve response status.
	//		TSharedPtr<FJsonValue> codeValue = jsonObject->GetField<EJson::None>(TEXT("code"));
	//		// If there is no "code" field or the code is not 200 (which means success), execute callback with -1 to indicate that we couldn't determine the battery status.
	//		if (codeValue->IsNull()) return (void)callback.ExecuteIfBound(-1);
	//		int32 code = FMath::RoundHalfFromZero(codeValue->AsNumber());
	//		return (void)callback.ExecuteIfBound(code);

	//		// Retrieve response data, which contains battery status.
	//		//TSharedPtr<FJsonValue> dataValue = jsonObject->GetField<EJson::None>(TEXT("data"));
	//		// If there is no "data" field or the field is empty, execute callback with -1 to indicate that we couldn't determine the battery status.
	//		// Otherwise execute callback with the battery status (0-100).
	//		//if (dataValue->IsNull()) (void)callback.ExecuteIfBound(-1);
	//		//else (void)callback.ExecuteIfBound(FMath::RoundHalfFromZero(dataValue->AsNumber()));
	//	});
	//} else {
	//	httpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr request, FHttpResponsePtr response, bool bWasSuccessful) ->void {
	//		// Do nothing if the integration is not running or the request was successful.
	//		// The integration could have been stopped while the HTTP request was being processed.
	//		if (!FLovenseManager::Get()->IsLovenseRunning() || bWasSuccessful) return;
	//		// If integration is running and request was unsuccessful, start Update Adapters timer loop.
	//		FLovenseManager::Get()->OnCommandFailed();
	//	});
	//}

	//return httpRequest->ProcessRequest();
	return true;
#else
	return true;
#endif
}

void ILovenseAdapter::OnToyTestTimerFinished() {
#if WITH_LOVENSE
	for (int32 i = this->toyTestTimerHandles.Num() - 1; i >= 0; --i) {
		TPair<FString, FTimerHandle>& timerHandle = this->toyTestTimerHandles[i];
		// Find the timer that fired the finished delegate.
		if (!FLovenseManager::Get()->IsTimerExecuting(timerHandle.Value)) continue;
		// Stop vibrating.
		//this->SendCommand_Vibrate(FString::Printf(TEXT("?t=%s&v=0"), *timerHandle.Key), timerHandle.Key, 0);
		// Remove timer handle.
		this->toyTestTimerHandles.RemoveAtSwap(i);
		break;
	}
#endif
}
