// (CVN) - Candy Valley Network GmbH

#include "Adapters/LovenseRemoteAdapterBase.h"
#include "LovenseWebThread.h"

// EXTERNAL INCLUDES
#include <HttpModule.h>
#include <IHttpRequest.h>
#include <Dom/JsonObject.h>
#include <Dom/JsonValue.h>
#include <Serialization/JsonReader.h>
#include <Serialization/JsonSerializer.h>

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

LovenseRemoteAdapterBase::LovenseRemoteAdapterBase()
	: ILovenseAdapter() {}

#if WITH_LOVENSE

bool LovenseRemoteAdapterBase::SendCommand_Stop(const FString& toyId, FOnLovenseResponse callback) {
	return this->SendCommand(
		FString::Printf(
			TEXT("{\"command\":\"Function\",\"action\":\"Stop\",\"timeSec\":0,\"toy\":\"%s\",\"apiVer\":1}"),
			*toyId
		),callback
	);
}

bool LovenseRemoteAdapterBase::SendCommands(TArray<class UELovenseCommands*> commands, const FString& toyId, float timeSec, float loopRunningSec, float loopPauseSec, FOnLovenseResponse callback) {
	bool onlyVibrate = false;
	if (commands.Num() <= 0) {
		onlyVibrate = true;
	}
	FString cmds = FString::Printf(
		TEXT("{\"command\":\"Function\",\"toy\":\"%s\",\"action\":\""),
		*toyId
	);
	bool bHaveVibrate = false, bHaveVibrate1 = false, bHaveVibrate2 = false, bHaveVibrate3 = false;
	for (const UELovenseCommands* command : commands) {
		if (command->GetCommandType() == ELovenseCommandType::VIBRATE) {
			bHaveVibrate = true;
		}
		else if(command->GetCommandType() == ELovenseCommandType::VIBRATE1) {
			bHaveVibrate1 = true;
		}
		else if(command->GetCommandType() == ELovenseCommandType::VIBRATE2) {
			bHaveVibrate2 = true;
		}
		else if(command->GetCommandType() == ELovenseCommandType::VIBRATE3) {
			bHaveVibrate3 = true;
		}
	}
	if (onlyVibrate) {
		cmds.Append("Vibrate:10");
	}
	else {
		for (UELovenseCommands* command : commands)
		{
			if (command->GetCommandType() == ELovenseCommandType::VIBRATE) {
				FString temp = FString::Printf(
					TEXT("Vibrate:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::VIBRATE1 && !bHaveVibrate) {
				FString temp = FString::Printf(
					TEXT("Vibrate1:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::VIBRATE2 && !bHaveVibrate) {
				FString temp = FString::Printf(
					TEXT("Vibrate2:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::VIBRATE3 && !bHaveVibrate) {
				FString temp = FString::Printf(
					TEXT("Vibrate3:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::ROTATE) {
				FString temp = FString::Printf(
					TEXT("Rotate:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::PUMP) {
				int value = 0;
				if (command->GetValue() > 3) {
					value = 3;
				}
				else if (command->GetValue() < 0) {
					value = 0;
				}
				else {
					value = command->GetValue();
				}
				FString temp = FString::Printf(
					TEXT("Pump:%d,"),
					value
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::THRUST) {
				FString temp = FString::Printf(
					TEXT("Thrusting:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::SUCTION) {
				FString temp = FString::Printf(
					TEXT("Suction:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::FINGERING) {
				FString temp = FString::Printf(
					TEXT("Fingering:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::DEPTH) {
				FString temp = FString::Printf(
					TEXT("Depth:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::POSITION) {
				UELovenseSolaceProCommands* cmd = static_cast<UELovenseSolaceProCommands*>(command);
				FString temp = FString::Printf(
					TEXT("Stroke:%d-%d,"),
					cmd->GetStrokeLowValue(),
					cmd->GetStrokeHighValue()
				);
				cmds.Append(temp);
			}
			if (command->GetCommandType() == ELovenseCommandType::OSCILLATE) {
				FString temp = FString::Printf(
					TEXT("Oscillate:%d,"),
					command->GetValue()
				);
				cmds.Append(temp);
			}

		}
		cmds.RemoveAt(cmds.Len() - 1, 1);
	}
	cmds.Append("\",");
	if (timeSec < 0) {
		timeSec = 0;
	}
	if (timeSec == static_cast<int>(timeSec)) {
		cmds.Append(
			FString::Printf(
				TEXT("\"timeSec\":%d,"),
				static_cast<int>(timeSec)
			)
		);
	}
	else {
		cmds.Append(
			FString::Printf(
				TEXT("\"timeSec\":%.2f,"),
				timeSec
			)
		);
	}

	if (loopRunningSec > 0) {
		if (loopRunningSec == static_cast<int>(loopRunningSec)) {
			cmds.Append(
				FString::Printf(
					TEXT("\"loopRunningSec\":%d,"),
					static_cast<int>(loopRunningSec)
				)
			);
		}
		else {
			cmds.Append(
				FString::Printf(
					TEXT("\"loopRunningSec\":%.2f,"),
					loopRunningSec
				)
			);
		}

		if (loopPauseSec == static_cast<int>(loopRunningSec)) {
			cmds.Append(
				FString::Printf(
					TEXT("\"loopPauseSec\":%d,"),
					static_cast<int>(loopRunningSec)
				)
			);
		}
		else {
			cmds.Append(
				FString::Printf(
					TEXT("\"loopPauseSec\":%.2f,"),
					loopPauseSec
				)
			);
		}

	}
	cmds.Append("\"apiVer\":1}");
	return this->SendCommand(cmds, callback);
}

bool LovenseRemoteAdapterBase::SendPosition(int position, const FString& toyId, FOnLovenseResponse callback) {
	return this->SendCommand(
		FString::Printf(
			TEXT("{\"command\":\"Position\",\"value\":\"%d\",\"toy\":\"%s\",\"apiVer\":1}"),
			position,
			*toyId
		), callback
	);
}

bool LovenseRemoteAdapterBase::SetUpPatternV2(TArray<class UEPositionPatternValue*> positionValues, const FString& toyId, FOnLovenseResponse callback) {
	int count = 0;
	FString actions = TEXT("[");
	//{"ts":0,"pos":10},
	for (const UEPositionPatternValue* pos : positionValues) {
		count++;
		FString temp = FString::Printf(TEXT("{\"ts\":%d,\"pos\":%d},"),
			pos->GetTime(),
			pos->GetPosition()
		);
		actions.Append(
			temp
		);
	}
	if (count == 0) {

	}
	actions.RemoveAt(actions.Len() - 1, 1);
	actions.Append("]");
	return this->SendCommand(
		FString::Printf(
			TEXT("{\"command\":\"PatternV2\",\"type\":\"Setup\",\"actions\":%s,\"toy\":\"%s\",\"apiVer\":1}"),
			*actions,
			*toyId
		), callback
	);
	return true;
}

bool LovenseRemoteAdapterBase::StartPatternV2(const FString& toyId, double startTime, double offsetTime, FOnLovenseResponse callback) {
	return this->SendCommand(
		FString::Printf(
			TEXT("{\"command\":\"PatternV2\",\"type\":\"Play\",\"startTime\":%d,\"offsetTime\":%d,\"toy\":\"%s\",\"apiVer\":1}"),
			startTime,
			offsetTime,
			*toyId
		), callback
	);
}

bool LovenseRemoteAdapterBase::StopPatternV2(const FString& toyId, FOnLovenseResponse callback) {
	return this->SendCommand(
		FString::Printf(
			TEXT("{\"command\":\"PatternV2\",\"type\":\"Stop\",\"toy\":\"%s\",\"apiVer\":1}"),
			*toyId
		), callback
	);
}

bool LovenseRemoteAdapterBase::GetPatternV2SyncTime(FOnLovenseResponse callback) {
	return this->SendCommand(
		FString::Printf(
			TEXT("{\"command\":\"PatternV2\",\"type\":\"SyncTime\",\"apiVer\":1}")
			), callback
	);
}

bool LovenseRemoteAdapterBase::SendCommand_Pattern(const FString& toyId, FLovensePattern& pattern, FOnLovenseResponse callback){
	TArray<FString> functions = TArray<FString>();
	functions.Reserve(9);
	if (pattern.bVibrate) functions.Add(TEXT("v"));
	if (pattern.bVibrate1) functions.Add(TEXT("v1"));
	if (pattern.bVibrate2) functions.Add(TEXT("v2"));
	if (pattern.bVibrate3) functions.Add(TEXT("v3"));
	if (pattern.bRotate) functions.Add(TEXT("r"));
	if (pattern.bPump) functions.Add(TEXT("p"));
	if (pattern.bThrust) functions.Add(TEXT("t"));
	if (pattern.bSuck) functions.Add(TEXT("s"));
	if (pattern.bFinger) functions.Add(TEXT("f"));
	if (pattern.bDepth) functions.Add(TEXT("d"));
	if (pattern.bOscillate) functions.Add(TEXT("o"));
	
	FString functionsString = FString::Join(functions, TEXT(","));

	FString timeStr = FString::Printf(TEXT("%f"), pattern.time);

	while (timeStr.EndsWith(TEXT("0")) || timeStr.EndsWith(TEXT(".")))
	{
		if (timeStr.EndsWith(TEXT("."))) {
			timeStr = timeStr.LeftChop(1);
			break;
		}
		timeStr = timeStr.LeftChop(1);
	}

	return this->SendCommand(
		FString::Printf(
			TEXT("{\"command\":\"Pattern\",\"rule\":\"V:1;F:%s;S:%d#\",\"strength\":\"%s\",\"timeSec\":%s,\"toy\":\"%s\",\"apiVer\":1}"),
			*functionsString,
			pattern.interval,
			*pattern.ParsePattern(),
			*timeStr,
			*toyId
		), callback
	);
}

bool LovenseRemoteAdapterBase::SendCommand_Preset(const FString& toyId, const FString& name, float timeSec, FOnLovenseResponse callback){
	FString cmds = FString::Printf(
		TEXT("{\"command\":\"Preset\",\"toy\":\"%s\",\"name\":\"%s\",\"apiVer\":1,\"timeSec\":"),
		*toyId,
		*name
	);
	if (timeSec == static_cast<int>(timeSec)) {
		cmds.Append(
			FString::Printf(
				TEXT("%d}"),
				static_cast<int>(timeSec)
			)
		);
	}
	else {
		cmds.Append(
			FString::Printf(
				TEXT("%.2f}"),
				timeSec
			)
		);
	}
	return this->SendCommand(
		cmds,callback
	);
}
#endif

void LovenseRemoteAdapterBase::GetToys(FOnLovenseGetToysResponse callback) {
#if WITH_LOVENSE
	TMap<FString, FString> headers;
	headers.Add(TEXT("Content-Type"), TEXT("application/json"));
	FString url = FString::Printf(TEXT("%s://%s:%d/command"), *this->webProtocol, *this->deviceIp, this->webProtocol.Equals(TEXT("https")) ? this->adapterDescription.httpsPort : this->adapterDescription.httpPort);
	RequestToys(url, true, TEXT("POST"), TEXT("{\"command\":\"GetToys\"}"), headers, callback);
#endif
}

void LovenseRemoteAdapterBase::WebThread_SendCommand(const FString& command, FOnLovenseResponse callback) {
//	FHttpRequestPtr httpRequest = FHttpModule::Get().CreateRequest();
//#if WITH_LOVENSE
//	httpRequest->SetVerb(TEXT("POST"));
//	httpRequest->SetURL(FString::Printf(TEXT("%s://%s:%d/command"), *this->webProtocol, *this->deviceIp, this->adapterDescription.httpsPort));
//	httpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
//	httpRequest->SetContentAsString(command);
//#endif
#if WITH_LOVENSE

	FString url = TEXT("");
	url = FString::Printf(TEXT("%s://%s:%d/command"), *this->webProtocol, *this->deviceIp, this->webProtocol.Equals(TEXT("https"))? this->adapterDescription.httpsPort: this->adapterDescription.httpPort);
	TMap<FString, FString> headers;
	headers.Add(TEXT("Content-Type"), TEXT("application/json"));
	LovenseWebThread* webThread = new LovenseWebThread(url, true, TEXT("POST"), command, headers, TEXT("thread_sendCommand"), callback, [&](const FString response, FOnLovenseResponse callback) {
		//if (!FLovenseManager::Get()->IsLovenseRunning()) return;

		// Parse response into a json object.
		TSharedPtr<FJsonObject> jsonObject = MakeShareable(new FJsonObject());
		TSharedRef<TJsonReader<TCHAR>> jsonReader = TJsonReaderFactory<TCHAR>::Create(response);
		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);

	});
	FRunnableThread* myThread = FRunnableThread::Create(webThread, TEXT("MyRunnableThread"));
	
#endif
}

void LovenseRemoteAdapterBase::GetToysDataJsonObject(TSharedPtr<FJsonObject> jsonObject, TSharedPtr<FJsonObject>& outToysJsonObject) {
#if WITH_LOVENSE
	const TSharedPtr<FJsonObject> dataObject = jsonObject->GetObjectField(TEXT("data"));
	if (!dataObject.IsValid()) return;
	// The remote api returns the toys data as a json string instead of a json subobject, so we need to deserialize the string into a json object.
	FString toysJsonObjectString = dataObject->GetStringField(TEXT("toys"));

	TSharedPtr<FJsonObject> toysJsonObject = MakeShareable(new FJsonObject());
	TSharedRef<TJsonReader<TCHAR>> jsonReader = TJsonReaderFactory<TCHAR>::Create(toysJsonObjectString);
	FJsonSerializer::Deserialize(jsonReader, toysJsonObject);
	outToysJsonObject = toysJsonObject;
#endif
}

void LovenseRemoteAdapterBase::ParseToyDescription(FLovenseToyDescription& toyDescription, TSharedPtr<FJsonObject> toyObject) {
#if WITH_LOVENSE
	toyDescription.id = toyObject->GetStringField(TEXT("id"));
	// The remote api returns toy names in all lower case (at least used to), so convert first character to upper case as we use this name string in the UI.
	FString name = toyObject->GetStringField(TEXT("name"));
	if (name.Len() > 0) name[0] = toupper(name[0]);
	toyDescription.name = name;
	toyDescription.status = toyObject->GetIntegerField(TEXT("status"));
	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());
	// The remote api does not return a "fVersion" field.
	toyDescription.firmwareVersion = TEXT("");
	// If there is no user defined nickname, the remote api returns the toy name as nickname (at least used to),
	// use empty nickname in that case, so we don't display the toy name as nickname in the UI.
	FString nickname = toyObject->GetStringField(TEXT("nickName"));
	toyDescription.nickname = name == nickname ? TEXT("") : nickname;
	// The remote api always returns the version (at least used to) as opposed to the connect API, which returns an empty version if the toy version is 1.
	// As we only want to display versions >1, use empty version if the returned version is 1.
	TSharedPtr<FJsonValue> versionValue = toyObject->GetField<EJson::None>(TEXT("version"));
	FString version = versionValue->IsNull() ? TEXT("") : versionValue->AsString();
	toyDescription.version = version;
	if (toyDescription.name == TEXT("gush") && toyDescription.version == TEXT("2")) {
		toyDescription.name = TEXT("gush2");
	}
#endif
}
