#include "PortaConnectionSubsystem.h"
#include "PortaInterface.h"
#include "PortaSettings.h"
#include "SocketIOClient.h"
#include "Serialization/JsonTypes.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "IRemoteControlModule.h"
#include "StructSerializer.h"
#include "JsonObjectConverter.h"
#include "PortaStructs.h"
#include "Backends/JsonStructDeserializerBackend.h"
#include "Blueprint/BlueprintSupport.h"
#include "StructDeserializer.h"
#include "Containers/StringConv.h"
#include "PortaJsonStructDeserializerBackend.h"
#include "AssetRegistry/AssetRegistryModule.h"
//TODO: Remove this
#include "PortaSequenceSubsystem.h"

// See docs/protocol.md for the protocol

//File wide TODO: There is a lot of going back and forth between buffers and json objects. It might be good to clean this up at some point.

class FPortaJsonStructSerializerBackend
	: public FJsonStructSerializerBackend
{
public:
	/** Default flags for serializing structures in Web RC. */
	static const EStructSerializerBackendFlags DefaultSerializerFlags = EStructSerializerBackendFlags::WriteByteArrayAsByteStream;

	/**
	 * Creates and initializes a new instance with the given flags.
	 *
	 * @param InArchive The archive to serialize into.
	 * @param InFlags The flags that control the serialization behavior (typically FRCJsonStructSerializerBackend::DefaultSerializerFlags).
	 */
	FPortaJsonStructSerializerBackend(FArchive& InArchive, const EStructSerializerBackendFlags InFlags = DefaultSerializerFlags)
		: FJsonStructSerializerBackend(InArchive, InFlags)
	{ }

	// IStructSerializerBackend interface
	virtual void WriteProperty(const FStructSerializerState& State, int32 ArrayIndex = 0) override
	{
		if (State.FieldType == FByteProperty::StaticClass())
		{
			FByteProperty* ByteProperty = CastFieldChecked<FByteProperty>(State.ValueProperty);

			if (ByteProperty->IsEnum())
			{
				const uint8 PropertyValue = ByteProperty->GetPropertyValue_InContainer(State.ValueData, ArrayIndex);
				const FText DisplayName = ByteProperty->Enum->GetDisplayNameTextByValue(PropertyValue);
				WritePropertyValue(State, DisplayName.ToString());
				return;
			}
		}
		else if (State.FieldType == FEnumProperty::StaticClass())
		{
			FEnumProperty* EnumProperty = CastFieldChecked<FEnumProperty>(State.ValueProperty);
			const void* PropertyValuePtr = EnumProperty->ContainerPtrToValuePtr<void>(State.ValueData, ArrayIndex);
			const int64 Value = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(PropertyValuePtr);
			const FText DisplayName = EnumProperty->GetEnum()->GetDisplayNameTextByValue(Value);
			WritePropertyValue(State, DisplayName.ToString());
			return;
		}
		else if (State.FieldType == FFloatProperty::StaticClass())
		{
			FFloatProperty* FloatProperty = CastFieldChecked<FFloatProperty>(State.ValueProperty);
			const float Value = FloatProperty->GetPropertyValue_InContainer(State.ValueData, ArrayIndex);
			if (Value >= std::numeric_limits<float>::max())
			{
				WritePropertyValue(State, std::numeric_limits<float>::max());
			}
			else
			{
				WritePropertyValue(State, Value);
			}
			return;
		}

		FJsonStructSerializerBackend::WriteProperty(State, ArrayIndex);
	}
};

bool FPortaRoute::Matches(const FString& MsgUrl, const FString& MsgVerb, TMap<FString, FString>& OutParams)
{
	if (MsgVerb != Verb)
		return false;

	OutParams.Empty();
	TArray<FString> MsgUrlSplit;
	MsgUrl.ParseIntoArray(MsgUrlSplit, TEXT("/"), 1);
	if (MsgUrlSplit.Num() != UrlSplit.Num())
		return false;

	for (int i = 0; i < UrlSplit.Num(); ++i)
	{
		if (UrlSplit[i].StartsWith(TEXT(":")))
		{
			OutParams.Add(UrlSplit[i].Mid(1), MsgUrlSplit[i]);
		}
		else if (UrlSplit[i] != MsgUrlSplit[i])
		{
			return false;
		}
	}

	return true;
}

void UPortaAsyncCallHolder::Callback()
{
	//this is a dummy function to trick the delegate to accepting us
}

void UPortaAsyncCallHolder::ProcessEvent(UFunction* Function, void* Parms)
{
	if (Subsystem)
		Subsystem->SendAsyncResponse(this, Parms);
}

const FString UPortaConnectionSubsystem::Namespace = TEXT("unreal");

void UPortaConnectionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	GEngine->Exec(GetWorld(), TEXT("WebControl.StartServer"));

	auto Config = GetMutableDefault<UPortaSettings>();
	PortaChannel = Config->PortaChannel;

	if (Config->ConnectionURL.IsEmpty() || PortaChannel.IsEmpty())
	{
		UE_LOG(LogPorta, Error, TEXT("Invalid Porta config options"));
		GEngine->AddOnScreenDebugMessage(-1, 100.f, FColor::Red, TEXT("Invalid Porta config options."));
		return;
	}

	auto& ConnectionUrl = Config->ConnectionURL;
	if (!ConnectionUrl.StartsWith(TEXT("http://")) && !ConnectionUrl.StartsWith(TEXT("https://")) && !ConnectionUrl.StartsWith(TEXT("ws://")) && !ConnectionUrl.StartsWith(TEXT("wss://")))
	{
		FString Err = FString::Printf(TEXT("Invalid Porta Connection URL: %s"), *ConnectionUrl);
		UE_LOG(LogPorta, Error, TEXT("%s"), *Err);
		GEngine->AddOnScreenDebugMessage(-1, 100.f, FColor::Red, Err);
		return;
	}

	SocketIoClient = NewObject<USocketIoClient>();
	SocketIoClient->OnConnected.AddUObject(this, &UPortaConnectionSubsystem::OnConnected);
	SocketIoClient->GetOnMessage(TEXT("message")).AddUObject(this, &UPortaConnectionSubsystem::HandleMessage);
	SocketIoClient->Connect(Config->ConnectionURL, Namespace);

	RegisterRoute(TEXT("/remote/presets"), TEXT("GET"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleGetPresetsRoute));
	RegisterRoute(TEXT("/remote/preset/:preset"), TEXT("GET"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleGetPresetRoute));
	RegisterRoute(TEXT("/remote/preset/:preset/property/:prop"), TEXT("GET"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleGetPresetPropertyRoute));
	RegisterRoute(TEXT("/remote/preset/:preset/property/:prop"), TEXT("PUT"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleSetPresetPropertyRoute));
	RegisterRoute(TEXT("/remote/preset/:preset/controller/:controller"), TEXT("PUT"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleSetPresetControllerRoute));
	RegisterRoute(TEXT("/remote/preset/:preset/function/:func"), TEXT("PUT"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleCallPresetFunctionRoute));
	RegisterRoute(TEXT("/remote/object/call"), TEXT("PUT"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleObjectCallRoute));
	RegisterRoute(TEXT("/remote/search/assets"), TEXT("PUT"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleSearchAssetsRoute));
	RegisterRoute(TEXT("/remote/batch"), TEXT("PUT"), FPortaRouteHandler::CreateUObject(this, &UPortaConnectionSubsystem::HandleBatchRoute));

	GetWorld()->GetTimerManager().SetTimer(Timer, FTimerDelegate::CreateUObject(this, &UPortaConnectionSubsystem::Heartbeat), 1.f, true);

}

void UPortaConnectionSubsystem::Deinitialize()
{
	GetWorld()->GetTimerManager().ClearTimer(Timer);
	GEngine->Exec(GetWorld(), TEXT("WebControl.StopServer"));
	SocketIoClient = nullptr;
}

void UPortaConnectionSubsystem::SendAsyncResponse(UPortaAsyncCallHolder* Async, void* Args)
{
	TSharedPtr<FJsonObject> ResponseJson = MakeShareable(new FJsonObject);

	if (Args)
	{
		TArray<uint8> OutPayload;
		FMemoryWriter Writer(OutPayload);
		TSharedPtr<TJsonWriter<TCHAR>> JsonWriter;

		FPortaJsonStructSerializerBackend Backend(Writer, EStructSerializerBackendFlags::WriteByteArrayAsByteStream);
		FStructSerializerPolicies Policies;
		Policies.PropertyFilter = [](const FProperty* CurrentProp, const FProperty* ParentProperty) -> bool
		{
			return (!CurrentProp->HasAnyPropertyFlags(CPF_ReturnParm)) || ParentProperty != nullptr;
		};

		FStructSerializer::Serialize(Args, *Async->DelegateSignature, Backend, Policies);
		FMemoryReader ResponseReader{ OutPayload };

		auto JsonResponseReader = TJsonReader<TCHAR>::Create(&ResponseReader);
		FJsonSerializer::Deserialize(JsonResponseReader, ResponseJson);
	}

	SendResponseMessage(Async->Sender, Async->ID, ResponseJson);
	if (!Async->Subscription)
		AsyncCalls.Remove(Async);
}

void UPortaConnectionSubsystem::GetComboItems(const FString& PresetName, const FString& Field, const FString& Getter, TArray<FString>& OutLabels, TArray<FString>& OutValues)
{
	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return;
	
	FGuid EntityId = Preset->GetExposedEntityId(*Field);
	TSharedPtr<FRemoteControlField> RemoteControlProperty = Preset->GetExposedEntity<FRemoteControlField>(EntityId).Pin();
	if (!RemoteControlProperty || !RemoteControlProperty->IsBound())
		return;

	auto Object = RemoteControlProperty->GetBoundObject();

	if (!Object)
		return;

	auto GetterFunc = FindUField<UFunction>(Object->GetClass(), FName(Getter));
	FStructOnScope ParamStruct = GetterFunc;

	Object->ProcessEvent(GetterFunc, ParamStruct.GetStructMemory());

	auto Struct = ParamStruct.GetStruct();
	bool LabelsFilled = false;
	for (auto Prop = Struct->ChildProperties; Prop; Prop = Prop->Next)
	{
		if (auto ArrayProp = CastField<FArrayProperty>(Prop))
		{
			if (auto InnerProp = CastField<FStrProperty>(ArrayProp->Inner))
			{
				FScriptArray* StructArray = ArrayProp->GetPropertyValuePtr_InContainer(ParamStruct.GetStructMemory());
				FScriptArrayHelper Helper(ArrayProp, StructArray);
				for (int i = 0; i < Helper.Num(); ++i)
				{
					if (!LabelsFilled)
						OutLabels.Add(*(FString*)Helper.GetRawPtr(i));
					else
						OutValues.Add(*(FString*)Helper.GetRawPtr(i));
				}
				LabelsFilled = true;
			}

		}
	}


}

void UPortaConnectionSubsystem::OnConnected()
{
	UE_LOG(LogPorta, Log, TEXT("Connected"));

	FChannelSubscribe subscribe;
	subscribe.channel = PortaChannel;
	SocketIoClient->Emit(TEXT("join channel"), MakeShared<FJsonValueObject>(FJsonObjectConverter::UStructToJsonObject(subscribe)));
}

void UPortaConnectionSubsystem::HandleMessage(const TSharedPtr<FJsonValue>& Message)
{
	const TSharedPtr<FJsonObject>* Object;
	if (!Message->TryGetObject(Object))
		return;

	FString OutputString;
	TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&OutputString);
	FJsonSerializer::Serialize(Object->ToSharedRef(), Writer);
	UE_LOG(LogPorta, Log, TEXT("Received: %s"), *OutputString);

	const TSharedPtr<FJsonObject>* Payload;
	if (!Object->Get()->TryGetObjectField(TEXT("message"), Payload))
		return;
	FString Sender;
	if (!Object->Get()->TryGetStringField(TEXT("sender"), Sender))
		return;

	FString Url;
	if (!Payload->Get()->TryGetStringField(TEXT("url"), Url))
		return;
	FString Verb;
	if (!Payload->Get()->TryGetStringField(TEXT("verb"), Verb))
		return;
	FString ID;
	if (!Payload->Get()->TryGetStringField(TEXT("message_id"), ID))
		return;


	FString Content;
	Payload->Get()->TryGetStringField(TEXT("content"), Content);
	TMap<FString, FString> UrlParams;
	UE_LOG(LogPorta, Log, TEXT("Content: %s"), *Content);

	for (const TUniquePtr<FPortaRoute>& Route : Routes)
	{
		if (Route->Matches(Url, Verb, UrlParams))
		{
			auto Response = Route->Handler.Execute(UrlParams, Content, Sender, ID);
			if (Response.IsValid())
				SendResponseMessage(Sender, ID, Response);
			return;
		}
	}
}

void UPortaConnectionSubsystem::DoOldHttpMessage(const TSharedPtr<FJsonValue>& Message)
{
	const TSharedPtr<FJsonObject>* Object;
	if (!Message->TryGetObject(Object))
		return;

	const TSharedPtr<FJsonObject>* Payload;
	if (!Object->Get()->TryGetObjectField(TEXT("message"), Payload))
		return;
	FString Sender;
	if (!Object->Get()->TryGetStringField(TEXT("sender"), Sender))
		return;

	FString Url;
	if (!Payload->Get()->TryGetStringField(TEXT("url"), Url))
		return;
	FString Verb;
	if (!Payload->Get()->TryGetStringField(TEXT("verb"), Verb))
		return;
	FString ID;
	if (!Payload->Get()->TryGetStringField(TEXT("message_id"), ID))
		return;

	Url = Url.Replace(TEXT(" "), TEXT("%20"));
	FString Content;
	Payload->Get()->TryGetStringField(TEXT("content"), Content);

	auto Request = FHttpModule::Get().CreateRequest();
	Request->OnProcessRequestComplete().BindLambda([this, ID, Sender](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) {
		OnHttpResponseReceived(Request, Response, bWasSuccessful, ID, Sender);
		});
	Request->SetURL(TEXT("http://localhost:30010") + Url);
	Request->SetVerb(Verb);
	Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
	Request->SetHeader("Content-Type", TEXT("application/json"));
	// the remote/batch command will crash if there is no Passphrase in the header
	Request->SetHeader("Passphrase", "random-junk");
	if (Payload->Get()->TryGetStringField(TEXT("content"), Content))
	{
		UE_LOG(LogPorta, Log, TEXT("Content: %s"), *Content);
		Request->SetContentAsString(Content);
	}
	Request->ProcessRequest();
}

void UPortaConnectionSubsystem::OnHttpResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, const FString& MessageID, const FString& Sender)
{
	UE_LOG(LogPorta, Log, TEXT("%s"), *Response->GetContentAsString());

	auto ResponseObject = TSharedPtr<FJsonObject>();
	auto Reader = TJsonReaderFactory<TCHAR>::Create(Response->GetContentAsString());
	if (!FJsonSerializer::Deserialize(Reader, ResponseObject))
		return;

	SendResponseMessage(Sender, MessageID, ResponseObject);
}

void UPortaConnectionSubsystem::OnEventHttpResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	UE_LOG(LogPorta, Log, TEXT("Send event result: %s"), *Response->GetContentAsString());
}

void UPortaConnectionSubsystem::SendResponseMessage(const FString& Channel, const FString& MessageID, const TSharedPtr<FJsonObject>& Response)
{
	auto DataObject = TSharedPtr<FJsonObject>(new FJsonObject);
	DataObject->SetStringField(TEXT("channel"), Channel);

	auto MessageObject = TSharedPtr<FJsonObject>(new FJsonObject);
	MessageObject->SetObjectField(TEXT("response"), Response);
	MessageObject->SetStringField(TEXT("message_id"), MessageID);

	DataObject->SetObjectField(TEXT("message"), MessageObject);

	SocketIoClient->Emit(TEXT("message"), MakeShareable(new FJsonValueObject(DataObject)));
}

void UPortaConnectionSubsystem::RegisterRoute(const FString& Url, const FString& Verb, FPortaRouteHandler Handler)
{
	TUniquePtr<FPortaRoute> Route = MakeUnique<FPortaRoute>(Url, Verb, Handler);
	Routes.Emplace(MoveTemp(Route));
}

//we're not using FJsonObjectConverter to stay closer web remote control. Specifically FJsonObjectConverter messes with letter cases
template <typename T>
static TSharedPtr<FJsonObject> StructToJson(const T& InStruct)
{
	TArray<uint8> WorkingBuffer;
	FMemoryWriter Writer(WorkingBuffer);
	FPortaJsonStructSerializerBackend SerializeBackend{ Writer, EStructSerializerBackendFlags::WriteByteArrayAsByteStream };
	FStructSerializer::Serialize(InStruct, SerializeBackend, FStructSerializerPolicies());
	auto ResponseObject = TSharedPtr<FJsonObject>();
	FString Temp(WorkingBuffer.Num() / sizeof(TCHAR), (TCHAR*)WorkingBuffer.GetData());
	auto Reader = TJsonReaderFactory<TCHAR>::Create(Temp);
	FJsonSerializer::Deserialize(Reader, ResponseObject);
	return ResponseObject;
}

static TSharedPtr<FJsonObject> CreateErrorResponse(const FString& Error)
{
	return FJsonObjectConverter::UStructToJsonObject(FPortaErrorResponse(Error));
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleGetPresetsRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	TArray<FAssetData> PresetAssets;
	IRemoteControlModule::Get().GetPresetAssets(PresetAssets);

	return StructToJson(FPortaListPresetsResponse{ PresetAssets });
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleGetPresetRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	const FString& PresetName = UrlParams[TEXT("preset")];
	if (auto Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName))
	{
		FPortaPresetResponse PresetData(Preset);

		return StructToJson(PresetData);
	}
	else
	{
		return CreateErrorResponse(FString::Printf(TEXT("Preset %s could not be found."), *PresetName));
	}
}

static void ConvertToUTF8(const FString& InString, TArray<uint8>& OutUTF8Payload)
{
	int32 StartIndex = OutUTF8Payload.Num();
	OutUTF8Payload.AddUninitialized(FTCHARToUTF8_Convert::ConvertedLength(*InString, InString.Len()) * sizeof(ANSICHAR));
	FTCHARToUTF8_Convert::Convert((ANSICHAR*)(OutUTF8Payload.GetData() + StartIndex), (OutUTF8Payload.Num() - StartIndex) / sizeof(ANSICHAR), *InString, InString.Len());
}

static bool SerializePartial(TFunctionRef<bool(FPortaJsonStructSerializerBackend&)> SerializeFunction, FMemoryWriter& SerializedPayloadWriter)
{
	TArray<uint8> WorkingBuffer;
	FMemoryWriter TemporaryBufferWriter(WorkingBuffer);
	FPortaJsonStructSerializerBackend TemporaryBackend(TemporaryBufferWriter, FPortaJsonStructSerializerBackend::DefaultSerializerFlags);

	int32 ColonPosition = -1;
	int32 LastBracketPosition = -1;

	bool bSuccess = SerializeFunction(TemporaryBackend);

	FStringView BufferView = FStringView((TCHAR*)WorkingBuffer.GetData(), WorkingBuffer.Num() / sizeof(TCHAR));
	if (bSuccess)
	{
		BufferView.FindChar(TEXT(':'), ColonPosition);
		BufferView.FindLastChar(TEXT('}'), LastBracketPosition);
		ColonPosition++;
	}

	if (LastBracketPosition > ColonPosition)
	{
		BufferView.MidInline(ColonPosition, LastBracketPosition - ColonPosition);
		SerializedPayloadWriter.Serialize((uint8*)BufferView.GetData(), BufferView.Len() * sizeof(TCHAR) / sizeof(uint8));
	}
	else
	{
		bSuccess = false;
	}

	return bSuccess;
}

static const UWorld* GetPIEWorld()
{
	if (GEngine)
	{
		for (const FWorldContext& WorldContext : GEngine->GetWorldContexts())
		{
			const UWorld* CurrentWorld = WorldContext.World();
			if (CurrentWorld && CurrentWorld->WorldType == EWorldType::PIE)
			{
				return CurrentWorld;
			}
		}
	}
	return nullptr;
}



TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleGetPresetPropertyRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	const FString& PresetName = UrlParams[TEXT("preset")];
	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return CreateErrorResponse(FString::Printf(TEXT("Preset %s could not be found."), *PresetName));

	const FString& PropertyName = UrlParams[TEXT("prop")];
	TSharedPtr<FRemoteControlProperty> RemoteControlProperty = Preset->GetExposedEntity<FRemoteControlProperty>(Preset->GetExposedEntityId(*PropertyName)).Pin();
	if (!RemoteControlProperty)
		return CreateErrorResponse(TEXT("Unable to resolve the exposed entity."));

	if (!RemoteControlProperty->IsBound())
		return CreateErrorResponse(TEXT("Exposed entity was found but not bound to any object."));

	TArray<uint8> WorkingBuffer;
	FMemoryWriter Writer(WorkingBuffer);
	TSharedPtr<TJsonWriter<TCHAR>> JsonWriter = TJsonWriter<TCHAR>::Create(&Writer);
	FRCObjectReference ObjectRef;

	bool bSuccess = true;

	JsonWriter->WriteObjectStart();
	{
		{
			JsonWriter->WriteIdentifierPrefix(TEXT("ExposedPropertyDescription"));
			FPortaJsonStructSerializerBackend SerializeBackend{ Writer };
			FStructSerializer::Serialize(FPortaExposedPropertyDescription{ *RemoteControlProperty }, SerializeBackend, FStructSerializerPolicies());
		}

		// Temporary workaround, reset the state of the json writer
		JsonWriter->WriteRawJSONValue(TEXT(""));

		JsonWriter->WriteIdentifierPrefix("PropertyValues");
		JsonWriter->WriteArrayStart();

		for (UObject* Object : RemoteControlProperty->GetBoundObjects())
		{
			IRemoteControlModule::Get().ResolveObjectProperty(ERCAccess::READ_ACCESS, Object, RemoteControlProperty->FieldPathInfo.ToString(), ObjectRef);

			JsonWriter->WriteObjectStart();
			{
				JsonWriter->WriteValue(TEXT("ObjectPath"), Object->GetPathName());
				JsonWriter->WriteIdentifierPrefix(TEXT("PropertyValue"));

				bSuccess &= SerializePartial(
					[&ObjectRef](FJsonStructSerializerBackend& SerializerBackend)
					{
						return IRemoteControlModule::Get().GetObjectProperties(ObjectRef, SerializerBackend);
					}
				, Writer);
			}
			JsonWriter->WriteObjectEnd();
		}

		JsonWriter->WriteArrayEnd();
	}
	JsonWriter->WriteObjectEnd();

	if (bSuccess)
	{
		TSharedPtr<FJsonObject> ResponseJson = MakeShareable(new FJsonObject);
		FMemoryReader ResponseReader{ WorkingBuffer };

		auto JsonResponseReader = TJsonReader<TCHAR>::Create(&ResponseReader);
		FJsonSerializer::Deserialize(JsonResponseReader, ResponseJson);

		return ResponseJson;
	}
	else
	{
		return CreateErrorResponse(FString::Printf(TEXT("Error while trying to read property %s."), *PropertyName));
	}
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleSetPresetPropertyRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	const FString& PresetName = UrlParams[TEXT("preset")];
	const FString& PropertyName = UrlParams[TEXT("prop")];
	return SetPresetProperty(PresetName, PropertyName, Content);
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleSetPresetControllerRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
#if ENGINE_MINOR_VERSION > 0
	const FString& PresetName = UrlParams[TEXT("preset")];
	const FString& ControllerName = UrlParams[TEXT("controller")];

	UE_LOG(LogPorta, Warning, TEXT("HandleSetPresetControllerRoute: %s %s"), *ControllerName, *Content);

	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return CreateErrorResponse(FString::Printf(TEXT("Preset %s could not be found."), *PresetName));

#if ENGINE_MINOR_VERSION > 4 && WITH_EDITOR
	if (auto PIEWorld = GetPIEWorld())
	{
		Preset->SelectedWorld = PIEWorld;
	}
#endif

	URCVirtualPropertyBase* Controller = Preset->GetControllerByDisplayName(*ControllerName);
	if (!Controller)
		return CreateErrorResponse(TEXT("Unable to resolve the exposed entity."));

	const FString PropValueStr = TEXT("PropertyValue");
	int SearchIndex = Content.Find(PropValueStr, ESearchCase::CaseSensitive);
	FString WorkingContent;
	if (SearchIndex > -1)
	{
		WorkingContent = Content.Left(SearchIndex) + Controller->GetPropertyName().ToString() + Content.Mid(SearchIndex + PropValueStr.Len());
	}

	TArray<uint8> NewPayload;
	NewPayload.AddUninitialized(WorkingContent.GetCharArray().Num() * sizeof(TCHAR));
	FMemory::Memcpy(NewPayload.GetData(), *WorkingContent, WorkingContent.GetCharArray().Num() * sizeof(TCHAR));

	FMemoryReader NewPayloadReader(NewPayload);
	FPortaJsonStructDeserializerBackend Backend(NewPayloadReader);

	if (!IRemoteControlModule::Get().SetPresetController(*PresetName, Controller, Backend, NewPayload, true))
		return CreateErrorResponse(TEXT("Error while trying to set controller."));
#endif

	return TSharedPtr<FJsonObject>();
}

void UPortaConnectionSubsystem::SetPresetPropertyFromJson(const FString& PresetName, const FString& PropertyName, const FString& ValueJsonStr)
{
	SetPresetProperty(PresetName, PropertyName, ValueJsonStr);
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::SetPresetProperty(const FString& PresetName, const FString& PropertyName, const FString& ValueJsonStr)
{
	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return CreateErrorResponse(FString::Printf(TEXT("Preset %s could not be found."), *PresetName));

#if ENGINE_MINOR_VERSION > 4 && WITH_EDITOR
	if (auto PIEWorld = GetPIEWorld())
	{
		Preset->SelectedWorld = PIEWorld;
	}
#endif

	TSharedPtr<FRemoteControlProperty> RemoteControlProperty = Preset->GetExposedEntity<FRemoteControlProperty>(Preset->GetExposedEntityId(*PropertyName)).Pin();
	if (!RemoteControlProperty)
		return CreateErrorResponse(TEXT("Unable to resolve the exposed entity."));

	if (!RemoteControlProperty->IsBound())
		return CreateErrorResponse(TEXT("Exposed entity was found but not bound to any object."));


	FRCObjectReference ObjectRef;
	ObjectRef.Property = RemoteControlProperty->GetProperty();
	ObjectRef.Access = ERCAccess::WRITE_ACCESS;
	bool bSuccess = true;


	const FString PropValueStr = TEXT("PropertyValue");
	int SearchIndex = ValueJsonStr.Find(PropValueStr, ESearchCase::CaseSensitive);
	FString WorkingContent;
	if (SearchIndex > -1)
	{
		WorkingContent = ValueJsonStr.Left(SearchIndex) + RemoteControlProperty->FieldName.ToString() + ValueJsonStr.Mid(SearchIndex + PropValueStr.Len());
	}

	TSharedPtr<TJsonReader<TCHAR>> ValueJsonReader = TJsonReaderFactory<TCHAR>::Create(WorkingContent);
	TSharedPtr<FJsonObject> ValueJson;
	FJsonSerializer::Deserialize(ValueJsonReader.ToSharedRef(), ValueJson);

	if (ObjectRef.Property->GetCPPType() == TEXT("FLinearColor"))
	{
		const TSharedPtr<FJsonObject>* ColorJson;
		if (ValueJson->TryGetObjectField(RemoteControlProperty->FieldName.ToString(), ColorJson))
		{
			FColor Color;
			(*ColorJson)->TryGetNumberField(TEXT("R"), Color.R);
			(*ColorJson)->TryGetNumberField(TEXT("G"), Color.G);
			(*ColorJson)->TryGetNumberField(TEXT("B"), Color.B);
			(*ColorJson)->TryGetNumberField(TEXT("A"), Color.A);
			FLinearColor LinearColor(Color);

			(*ColorJson)->SetNumberField(TEXT("R"), LinearColor.R);
			(*ColorJson)->SetNumberField(TEXT("G"), LinearColor.G);
			(*ColorJson)->SetNumberField(TEXT("B"), LinearColor.B);
			(*ColorJson)->SetNumberField(TEXT("A"), LinearColor.A);

			TSharedRef< TJsonWriter<> > ValueWriter = TJsonWriterFactory<>::Create(&WorkingContent);
			FJsonSerializer::Serialize(ValueJson.ToSharedRef(), ValueWriter);
		}
	}

	FString ValueJsonStringReparsed;
	TSharedRef< TJsonWriter<> > ValueWriter = TJsonWriterFactory<>::Create(&ValueJsonStringReparsed);
	FJsonSerializer::Serialize(ValueJson.ToSharedRef(), ValueWriter);

	UE_LOG(LogPorta, Log, TEXT("%s"), *WorkingContent);

	TArray<uint8> WorkingBuffer;
	FMemoryWriter Writer(WorkingBuffer);
	TSharedPtr<TJsonWriter<TCHAR>> JsonWriter = TJsonWriter<TCHAR>::Create(&Writer);
	FRCObjectReference ReaderObjectRef;
	FPortaJsonStructSerializerBackend SerializeBackend{ Writer };

	bool PropertyDifferent = false;
	bool AnyPropertyChecked = false;

	for (UObject* Object : RemoteControlProperty->GetBoundObjects())
	{
		IRemoteControlModule::Get().ResolveObjectProperty(ERCAccess::READ_ACCESS, Object, RemoteControlProperty->FieldPathInfo.ToString(), ReaderObjectRef);
		if (!IRemoteControlModule::Get().GetObjectProperties(ReaderObjectRef, SerializeBackend))
			continue;

		FMemoryReader ObjReader(WorkingBuffer);
		TSharedPtr<TJsonReader<TCHAR>> ObjJsonReader = TJsonReader<TCHAR>::Create(&ObjReader);
		TSharedPtr<FJsonObject> ObjJson;
		FJsonSerializer::Deserialize(ObjJsonReader.ToSharedRef(), ObjJson);
		if (!ObjJson)
			continue;

		AnyPropertyChecked = true;
		FString ObjJsonString;
		TSharedRef< TJsonWriter<> > ObjWriter = TJsonWriterFactory<>::Create(&ObjJsonString);
		FJsonSerializer::Serialize(ObjJson.ToSharedRef(), ObjWriter);

		if (ObjJsonString != ValueJsonStringReparsed)
		{
			PropertyDifferent = true;
		}
	}

	if (!PropertyDifferent && AnyPropertyChecked)
	{
		UE_LOG(LogPorta, Log, TEXT("Not setting due to the values being the same"));
		return MakeShared<FJsonObject>();
	}

	RemoteControlProperty->EnableEditCondition();

	TArray<uint8> NewPayload;
	NewPayload.AddUninitialized(WorkingContent.GetCharArray().Num() * sizeof(TCHAR));
	FMemory::Memcpy(NewPayload.GetData(), *WorkingContent, WorkingContent.GetCharArray().Num() * sizeof(TCHAR));

	FMemoryReader NewPayloadReader(NewPayload);
	FPortaJsonStructDeserializerBackend Backend(NewPayloadReader);
	for (UObject* Object : RemoteControlProperty->GetBoundObjects())
	{
		IRemoteControlModule::Get().ResolveObjectProperty(ObjectRef.Access, Object, RemoteControlProperty->FieldPathInfo.ToString(), ObjectRef);
		NewPayloadReader.Seek(0);
		bSuccess &= IRemoteControlModule::Get().SetObjectProperties(ObjectRef, Backend, ERCPayloadType::Json, NewPayload);
	}

	return MakeShared<FJsonObject>();
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::CallPresetFunction(const FString& PresetName, const FString& FuncName, TSharedPtr<FJsonObject> Params)
{
	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return CreateErrorResponse(FString::Printf(TEXT("Preset %s could not be found."), *PresetName));

#if ENGINE_MINOR_VERSION > 4 && WITH_EDITOR
	if (auto PIEWorld = GetPIEWorld())
	{
		Preset->SelectedWorld = PIEWorld;
	}
#endif

	auto RCFunction = Preset->GetExposedEntity<FRemoteControlFunction>(Preset->GetExposedEntityId(*FuncName)).Pin();

	if (!RCFunction || !RCFunction->GetFunction() || !RCFunction->FunctionArguments || !RCFunction->FunctionArguments->IsValid())
	{
		return CreateErrorResponse(TEXT("Unable to resolve the preset field."));
	}


	TArray<uint8> OutputBuffer;
	FMemoryWriter Writer{ OutputBuffer };
	TSharedPtr<TJsonWriter<TCHAR>> JsonWriter = TJsonWriter<TCHAR>::Create(&Writer);
	FPortaJsonStructSerializerBackend WriterBackend{ Writer };

	JsonWriter->WriteObjectStart();
	JsonWriter->WriteIdentifierPrefix("ReturnedValues");
	JsonWriter->WriteArrayStart();

	// Copy the default arguments.
	FStructOnScope FunctionArgs{ RCFunction->GetFunction() };
	for (TFieldIterator<FProperty> It(RCFunction->GetFunction()); It; ++It)
	{
		if (It->HasAnyPropertyFlags(CPF_Parm) && !It->HasAnyPropertyFlags(CPF_ReturnParm | CPF_OutParm))
		{
			It->CopyCompleteValue_InContainer(FunctionArgs.GetStructMemory(), RCFunction->FunctionArguments->GetStructMemory());
		}
	}


	TArray<uint8> FunctionPayload;
	FMemoryWriter FuncWriter{ FunctionPayload };
	TSharedPtr<TJsonWriter<TCHAR>> JsonFuncWriter = TJsonWriter<TCHAR>::Create(&FuncWriter);
	FJsonSerializer::Serialize(Params.ToSharedRef(), JsonFuncWriter.ToSharedRef());

	FMemoryReader Reader{ FunctionPayload };
	FPortaJsonStructDeserializerBackend ReaderBackend{ Reader };

	bool bSuccess = true;
	// Deserialize the arguments passed from the user onto the copy of default arguments.
	if (FStructDeserializer::Deserialize((void*)FunctionArgs.GetStructMemory(), *const_cast<UStruct*>(FunctionArgs.GetStruct()), ReaderBackend, FStructDeserializerPolicies()))
	{
		bSuccess = true;
		for (UObject* Object : RCFunction->GetBoundObjects())
		{
			FRCCallReference CallRef;
			CallRef.Object = Object;
			CallRef.Function = RCFunction->GetFunction();

			FRCCall Call;
			Call.CallRef = MoveTemp(CallRef);
			Call.TransactionMode = ERCTransactionMode::NONE;
			Call.ParamStruct = FStructOnScope(FunctionArgs.GetStruct(), FunctionArgs.GetStructMemory());

			// Invoke call with replication payload
			bSuccess &= IRemoteControlModule::Get().InvokeCall(Call, ERCPayloadType::Json, FunctionPayload);

			if (bSuccess)
			{
				FStructOnScope ReturnedStruct{ FunctionArgs.GetStruct() };
				TSet<FProperty*> OutProperties;

				// Only copy the out/return parameters from the StructOnScope resulting from the call.
				for (TFieldIterator<FProperty> It(RCFunction->GetFunction()); It; ++It)
				{
					if (It->HasAnyPropertyFlags(CPF_ReturnParm | CPF_OutParm))
					{
						OutProperties.Add(*It);
						// Copy the out/return values into the returned struct.
						It->CopyCompleteValue_InContainer(ReturnedStruct.GetStructMemory(), FunctionArgs.GetStructMemory());

						// Then clear the out/return values.
						It->ClearValue_InContainer(FunctionArgs.GetStructMemory());
					}
				}

				FStructSerializerPolicies Policies;
				Policies.PropertyFilter = [&OutProperties](const FProperty* CurrentProp, const FProperty* ParentProp) { return OutProperties.Contains(CurrentProp); };
				FStructSerializer::Serialize((void*)ReturnedStruct.GetStructMemory(), *const_cast<UStruct*>(ReturnedStruct.GetStruct()), WriterBackend, Policies);
			}
		}
	}

	JsonWriter->WriteArrayEnd();
	JsonWriter->WriteObjectEnd();

	if (bSuccess)
	{
		TSharedPtr<FJsonObject> ResponseJson = MakeShareable(new FJsonObject);
		FMemoryReader ResponseReader{ OutputBuffer };

		auto JsonResponseReader = TJsonReader<TCHAR>::Create(&ResponseReader);
		FJsonSerializer::Deserialize(JsonResponseReader, ResponseJson);

		return ResponseJson;
	}
	else
	{
		return CreateErrorResponse(TEXT("Error calling function."));
	}
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleCallPresetFunctionRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	const FString& PresetName = UrlParams[TEXT("preset")];
	const FString& FuncName = UrlParams[TEXT("func")];
	TSharedPtr<FJsonObject> ContentJson = MakeShareable(new FJsonObject);
	auto ContentReader = TJsonReaderFactory<>::Create(Content);
	if (!FJsonSerializer::Deserialize(ContentReader, ContentJson))
	{
		return CreateErrorResponse(TEXT("Unable to parse json content."));
	}

	const TSharedPtr<FJsonObject>* ParamObjectJson;
	if (!ContentJson->TryGetObjectField(TEXT("Parameters"), ParamObjectJson))
	{
		return CreateErrorResponse(TEXT("No Parameters object found."));
	}

	return CallPresetFunction(PresetName, FuncName, *ParamObjectJson);
}


TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleObjectCallRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	TSharedPtr<FJsonObject> ContentJson = MakeShareable(new FJsonObject);
	auto ContentReader = TJsonReaderFactory<>::Create(Content);
	if (!FJsonSerializer::Deserialize(ContentReader, ContentJson))
	{
		return CreateErrorResponse(TEXT("Unable to parse json content."));
	}

	bool Unsubscribe = false;
	ContentJson->TryGetBoolField(TEXT("unsubscribe"), Unsubscribe);

	if (Unsubscribe)
	{
		FString SubscriptionID;
		if (ContentJson->TryGetStringField(TEXT("subscriptionId"), SubscriptionID))
		{
			for (auto Call : AsyncCalls)
			{
				if (Call->Sender == Sender && Call->ID == SubscriptionID)
				{
					Call->Subsystem = nullptr;
					AsyncCalls.Remove(Call);
					break;
				}
			}
		}
		return nullptr;
	}

	FString ObjectPath;
	if (!ContentJson->TryGetStringField(TEXT("objectPath"), ObjectPath))
	{
		return CreateErrorResponse(TEXT("Missing object path"));
	}

	FString FunctionName;
	if (!ContentJson->TryGetStringField(TEXT("functionName"), FunctionName))
	{
		return CreateErrorResponse(TEXT("Missing function name"));
	}

	bool Async = false;
	ContentJson->TryGetBoolField(TEXT("async"), Async);

	bool Subscribe = false;
	ContentJson->TryGetBoolField(TEXT("subscribe"), Subscribe);

	FRCCall Call;

	FString ErrorText;
	if (!IRemoteControlModule::Get().ResolveCall(ObjectPath, FunctionName, Call.CallRef, &ErrorText))
	{
		return CreateErrorResponse(ErrorText);
	}

	// Initialize the param struct with default parameters
	Call.ParamStruct = Call.CallRef.Function.Get();

	const TSharedPtr<FJsonObject>* ParamObjectJson;
	if (ContentJson->TryGetObjectField(TEXT("parameters"), ParamObjectJson))
	{
		TArray<uint8> FunctionPayload;
		FMemoryWriter FuncWriter{ FunctionPayload };
		TSharedPtr<TJsonWriter<TCHAR>> JsonFuncWriter = TJsonWriter<TCHAR>::Create(&FuncWriter);
		FJsonSerializer::Serialize(ParamObjectJson->ToSharedRef(), JsonFuncWriter.ToSharedRef());

		FMemoryReader Reader{ FunctionPayload };

		FPortaJsonStructDeserializerBackend Backend(Reader);
		// If some parameters were provided, deserialize them
		if (!FStructDeserializer::Deserialize((void*)Call.ParamStruct.GetStructMemory(), *const_cast<UStruct*>(Call.ParamStruct.GetStruct()), Backend, FStructDeserializerPolicies()))
		{
			return CreateErrorResponse(TEXT("Parameters object improperly formatted."));
		}
	}

	if ((Async || Subscribe) && !Sender.IsEmpty() && !MsgID.IsEmpty())
	{
		auto Struct = Call.ParamStruct.GetStruct();
		for (auto Prop = Struct->ChildProperties; Prop; Prop = Prop->Next)
		{
			if (auto Delegate = CastField<FDelegateProperty>(Prop))
			{
				auto Holder = NewObject<UPortaAsyncCallHolder>(this);
				Holder->Subsystem = this;
				Holder->Sender = Sender;
				Holder->ID = MsgID;
				Holder->Subscription = Subscribe;
				AsyncCalls.Add(Holder);
				Holder->DelegateSignature = Delegate->SignatureFunction;
				FScriptDelegate* StructDelegate = Delegate->GetPropertyValuePtr_InContainer(Call.ParamStruct.GetStructMemory());
				StructDelegate->BindUFunction(Holder, GET_FUNCTION_NAME_CHECKED(UPortaAsyncCallHolder, Callback));
			}
		}
	}

	IRemoteControlModule::Get().InvokeCall(Call);

	if (!Async)
	{
		TArray<uint8> OutPayload;
		FMemoryWriter Writer(OutPayload);
		TSharedPtr<TJsonWriter<TCHAR>> JsonWriter;

		FPortaJsonStructSerializerBackend Backend(Writer, EStructSerializerBackendFlags::WriteByteArrayAsByteStream);
		FStructSerializerPolicies Policies;
		Policies.PropertyFilter = [](const FProperty* CurrentProp, const FProperty* ParentProperty) -> bool
		{
			return CurrentProp->HasAnyPropertyFlags(CPF_ReturnParm | CPF_OutParm) || ParentProperty != nullptr;
		};

		FStructSerializer::Serialize((void*)Call.ParamStruct.GetStructMemory(), *const_cast<UStruct*>(Call.ParamStruct.GetStruct()), Backend, Policies);
		TSharedPtr<FJsonObject> ResponseJson = MakeShareable(new FJsonObject);
		FMemoryReader ResponseReader{ OutPayload };

		auto JsonResponseReader = TJsonReader<TCHAR>::Create(&ResponseReader);
		FJsonSerializer::Deserialize(JsonResponseReader, ResponseJson);

		return ResponseJson;
	}
	else
	{
		return nullptr;
	}
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleSearchAssetsRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
	TArray<uint8> Payload;
	Payload.AddUninitialized(Content.GetCharArray().Num() * sizeof(TCHAR));
	FMemory::Memcpy(Payload.GetData(), *Content, Content.GetCharArray().Num() * sizeof(TCHAR));

	FMemoryReader Reader(Payload);
	FPortaJsonStructDeserializerBackend DeserializerBackend(Reader);
	FPortaSearchAssetRequest SearchAssetRequest;
	if (!FStructDeserializer::Deserialize(&SearchAssetRequest, *FPortaSearchAssetRequest::StaticStruct(), DeserializerBackend, FStructDeserializerPolicies()))
	{
		return CreateErrorResponse(TEXT("Unable to deserialize request."));
	}

	FARFilter Filter = SearchAssetRequest.Filter.ToARFilter();
	TArray<FAssetData> Assets;
	AssetRegistryModule.Get().GetAssets(Filter, Assets);

	//Do advanced blueprint filtering if required
	if (SearchAssetRequest.Filter.EnableBlueprintNativeClassFiltering && SearchAssetRequest.Filter.NativeParentClasses.Num() > 0)
	{
		TSet<UClass*> NativeClasses;
		NativeClasses.Reserve(SearchAssetRequest.Filter.NativeParentClasses.Num());
		for (FName NativeClassName : SearchAssetRequest.Filter.NativeParentClasses)
		{
			if (UClass* Class = FindObject<UClass>(FTopLevelAssetPath{ NativeClassName.ToString() }))
			{
				NativeClasses.Add(Class);
			}
		}

		for (auto It = Assets.CreateIterator(); It; ++It)
		{
			const FAssetData& AssetData = *It;
			const FString NativeParentClassPath = AssetData.GetTagValueRef<FString>(FBlueprintTags::NativeParentClassPath);
			const FSoftClassPath ClassPath(NativeParentClassPath);
			UClass* NativeParentClass = ClassPath.ResolveClass();
			if (NativeParentClass)
			{
				for (UClass* ClassFilter : NativeClasses)
				{
					if (!NativeParentClass->IsChildOf(ClassFilter))
					{
						It.RemoveCurrent();
					}
				}
			}
		}
	}

	TArrayView<FAssetData> AssetsView{ Assets };
	int32 ArrayEnd = FMath::Min(SearchAssetRequest.Limit, Assets.Num());

	TArray<FAssetData> FilteredAssets;
	FilteredAssets.Reserve(SearchAssetRequest.Limit);

	for (const FAssetData& AssetData : Assets)
	{
		if (!SearchAssetRequest.Query.IsEmpty())
		{
			if (AssetData.AssetName.ToString().Contains(*SearchAssetRequest.Query))
			{
				FilteredAssets.Add(AssetData);
			}
		}
		else
		{
			FilteredAssets.Add(AssetData);
		}

		if (FilteredAssets.Num() >= SearchAssetRequest.Limit)
		{
			break;
		}
	}

	return StructToJson(FPortaSearchAssetResponse{ FilteredAssets });
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::RunBatch(const FString& Content)
{
	return HandleBatchRoute(TMap<FString, FString>(), Content, FString(), FString());
}

TSharedPtr<FJsonObject> UPortaConnectionSubsystem::HandleBatchRoute(const TMap<FString, FString>& UrlParams, const FString& Content, const FString& Sender, const FString& MsgID)
{
	auto ContentObject = TSharedPtr<FJsonObject>();
	auto Reader = TJsonReaderFactory<TCHAR>::Create(Content);
	if (!FJsonSerializer::Deserialize(Reader, ContentObject))
		return CreateErrorResponse(TEXT("Error parsing content."));

	const TArray<TSharedPtr<FJsonValue>>* Requests;
	if (!ContentObject->TryGetArrayField(TEXT("Requests"), Requests))
		return CreateErrorResponse(TEXT("No Requests sent"));

	TSharedPtr<FJsonObject> BatchResponse = MakeShared<FJsonObject>();
	TArray<TSharedPtr<FJsonValue>> Responses;

	for (auto Request : *Requests)
	{
		const TSharedPtr<FJsonObject>* Object;
		if (!Request->TryGetObject(Object))
			continue;

		FString Url;
		if (!Object->Get()->TryGetStringField(TEXT("URL"), Url))
			continue;

		FString Verb;
		if (!Object->Get()->TryGetStringField(TEXT("Verb"), Verb))
			continue;

		int RequestId;
		if (!Object->Get()->TryGetNumberField(TEXT("RequestId"), RequestId))
			continue;

		FString BodyString;
		const TSharedPtr<FJsonObject>* Body;
		if (Object->Get()->TryGetObjectField(TEXT("Body"), Body))
		{
			TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&BodyString);
			FJsonSerializer::Serialize(Body->ToSharedRef(), Writer);
		}

		TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
		Response->SetNumberField(TEXT("RequestId"), RequestId);

		TMap<FString, FString> CallUrlParams;
		for (const TUniquePtr<FPortaRoute>& Route : Routes)
		{
			if (Route->Matches(Url, Verb, CallUrlParams))
			{
				Response->SetObjectField(TEXT("ResponseBody"), Route->Handler.Execute(CallUrlParams, BodyString, Sender, MsgID));
				break;
			}
		}

		Responses.Add(MakeShareable(new FJsonValueObject(Response)));
	}
	BatchResponse->SetArrayField(TEXT("Responses"), Responses);

	return BatchResponse;
}

void UPortaConnectionSubsystem::Heartbeat()
{
	if (SocketIoClient && SocketIoClient->IsConnected())
	{
		//send some garbage data to probe the connection
		SocketIoClient->Emit(TEXT("heartbeat"), nullptr);
	}
}
