#include "EmoShapeSubSystem/EmoShapeIOClient/Public/ESIOClientComponent.h"
#include "EmoShapeSubSystem.h"
#include "EmoShapeSubSystem/EmoShapeJson/Public/ESIOJConvert.h"
#include "EmoShapeSubSystem/EmoShapeJson/Public/ESJsonObject.h"
#include "EmoShapeSubSystem/EmoShapeJson/Public/ESJsonValue.h"
#include "EmoShapeSubSystem/EmoShapeWebUtility/Public/EmoShapeLambdaRunnable.h"
#include "Engine.h"
#include "Engine/World.h"
#include "Components/ActorComponent.h"
#include "Dom/JsonValue.h"
#include "Engine/Engine.h"

UESIOClientComponent::UESIOClientComponent(const FObjectInitializer &init) : UActorComponent(init)
{
	bWantsInitializeComponent = true;
	bAutoActivate = true;

	bForceTLS = false;
	bUnbindEventsOnDisconnect = false;
	bShouldVerifyTLSCertificate = false;	//Until verification feature is implemented, this should default to false
	bShouldAutoConnect = true;
	NativeClient = nullptr;
	bLimitConnectionToGameWorld = true;
	SessionId = TEXT("Invalid");
	
	//Plugin scoped utilities
	bPluginScopedConnection = false;
	PluginScopedId = TEXT("Default");
	bVerboseConnectionLog = true;
	ReconnectionTimeout = 0.f;
	MaxReconnectionAttempts = -1.f;
	ReconnectionDelayInMs = 5000;

	bStaticallyInitialized = false;

	ClearCallbacks();
}

void UESIOClientComponent::Connect(const FString& InAddressAndPort, const FString& InPath, UESJsonObject* Query,
                                   UESJsonObject* Headers)
{
	//Check if we're limiting this component
	if (bLimitConnectionToGameWorld)
	{
		UWorld* World = GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull);
		if (World)
		{
			bool bIsGameWorld = (World->IsGameWorld() || World->IsPreviewWorld());
			if (!bIsGameWorld)
			{
				UE_LOG(SocketIO, Log, TEXT("UEmoShapeIOClientComponent::Connect attempt in non-game world blocked by bLimitConnectionToGameWorld."));
				return;
			}
		}
		else
		{
			UE_LOG(SocketIO, Warning, TEXT("UEmoShapeIOClientComponent::Connect attempt while in invalid world."));
			return;
		}
	}

	URLParams.AddressAndPort = InAddressAndPort;

	TSharedPtr<FJsonObject> QueryFJson;
	TSharedPtr<FJsonObject> HeadersFJson;

	if (Query != nullptr)
	{
		QueryFJson = Query->GetRootObject();
	}

	if (Headers != nullptr)
	{
		HeadersFJson = Headers->GetRootObject();
	}

	URLParams.Query = UESMessageConvert::JsonObjectToFStringMap(QueryFJson);
	URLParams.Headers = UESMessageConvert::JsonObjectToFStringMap(HeadersFJson);
	URLParams.Path = InPath;

	//Ensure we sync our native max/reconnection attempts before connecting
	NativeClient->MaxReconnectionAttempts = MaxReconnectionAttempts;
	NativeClient->ReconnectionDelay = ReconnectionDelayInMs;
	NativeClient->VerboseLog = bVerboseConnectionLog;
	NativeClient->bUnbindEventsOnDisconnect = bUnbindEventsOnDisconnect;
	NativeClient->bForceTLSUse = bForceTLS;

	ConnectWithParams(URLParams);
}

void UESIOClientComponent::ConnectWithParams(const FEmoShapeConnectParams& InURLParams)
{
	NativeClient->Connect(InURLParams);
}

void UESIOClientComponent::Disconnect()
{
	NativeClient->Disconnect();
}

void UESIOClientComponent::SyncDisconnect()
{
	NativeClient->SyncDisconnect();
}

void UESIOClientComponent::JoinNamespace(const FString& Namespace)
{
	NativeClient->JoinNamespace(Namespace);
}

void UESIOClientComponent::LeaveNamespace(const FString& Namespace)
{
	NativeClient->LeaveNamespace(Namespace);
}

void UESIOClientComponent::Emit(const FString& EventName, UESJsonValue* Message, const FString& Namespace)
{
	//Set the message is not null
	TSharedPtr<FJsonValue> JsonMessage = nullptr;
	if (Message != nullptr)
	{
		JsonMessage = Message->GetRootValue();
	}
	else
	{
		JsonMessage = MakeShareable(new FJsonValueNull);
	}

	NativeClient->Emit(EventName, JsonMessage, nullptr, Namespace);
}

void UESIOClientComponent::EmitWithCallBack(const FString& EventName, UESJsonValue* Message,
	const FString& CallbackFunctionName, UObject* Target, const FString& Namespace, UObject* WorldContextObject)
{
	if (!CallbackFunctionName.IsEmpty())
	{
		if (Target == nullptr)
		{
			Target = WorldContextObject;
		}

		//Set the message is not null
		TSharedPtr<FJsonValue> JsonMessage = nullptr;
		if (Message != nullptr)
		{
			JsonMessage = Message->GetRootValue();
		}
		else
		{
			JsonMessage = MakeShareable(new FJsonValueNull);
		}

		EmitNative(EventName, JsonMessage, [&, Target, CallbackFunctionName, this](const TArray<TSharedPtr<FJsonValue>>& Response)
		{
			CallBPFunctionWithResponse(Target, CallbackFunctionName, Response);
		}, Namespace);
	}
	else 
	{
		EmitNative(EventName, Message->GetRootValue(),nullptr,Namespace);
	}
}

void UESIOClientComponent::EmitWithGraphCallBack(const FString& EventName, FLatentActionInfo LatentInfo,
	UESJsonValue*& Result, UESJsonValue* Message, const FString& Namespace)
{
	//Set the message is not null
	TSharedPtr<FJsonValue> JsonMessage = nullptr;
	if (Message != nullptr)
	{
		JsonMessage = Message->GetRootValue();
	}
	else
	{
		JsonMessage = MakeShareable(new FJsonValueNull);
	}
	FEmoShapeLatentAction *LatentAction = FEmoShapeLatentAction::CreateLatentAction(LatentInfo, this);

	if (LatentAction)
	{
		//emit the message and pass the LatentAction, we also pass the result reference through lambda capture
		NativeClient->Emit(EventName, JsonMessage, [this, LatentAction, &Result](const TArray<TSharedPtr<FJsonValue>>& Response)
		{
			// Finish the latent action
			if (LatentAction)
			{
				TSharedPtr<FJsonValue> FirstResponseValue = Response[0];
				UESJsonValue* ResultObj = NewObject<UESJsonValue>();
				ResultObj->SetRootValue(FirstResponseValue);
				Result = ResultObj;		//update the output value
				LatentAction->Call();	//resume the latent action
			}
		}, Namespace);
	}
}

void UESIOClientComponent::BindEventToDelegate(const FString& EventName,
	const FEmoShapeJsonValueSignature& CallbackDelegate, const FString& Namespace,
	EEmoShapeIOThreadOverrideOption ThreadOverride)
{
	const FEmoShapeJsonValueSignature SafeCallback = CallbackDelegate;	//copy for lambda ref
	OnNativeEvent(EventName, [&, SafeCallback](const FString& Event, const TSharedPtr<FJsonValue>& Message)
	{
		UESJsonValue* Value = NewObject<UESJsonValue>();
		TSharedPtr<FJsonValue> NonConstValue = Message;
		Value->SetRootValue(NonConstValue);

		SafeCallback.ExecuteIfBound(Value);
	}, Namespace, ThreadOverride);
}

void UESIOClientComponent::BindEventToGenericEvent(const FString& EventName, const FString& Namespace)
{
	NativeClient->OnEvent(EventName, [&](const FString& Event, const TSharedPtr<FJsonValue>& EventValue)
	{
		UESJsonValue* NewValue = NewObject<UESJsonValue>();
		TSharedPtr<FJsonValue> NonConstValue = EventValue;
		NewValue->SetRootValue(NonConstValue);
		OnGenericEvent.Broadcast(Event, NewValue);
	}, Namespace);
}

void UESIOClientComponent::BindEventToFunction(const FString& EventName, const FString& FunctionName, UObject* Target,
	const FString& Namespace, EEmoShapeIOThreadOverrideOption ThreadOverride, UObject* WorldContextObject)
{
	if (!FunctionName.IsEmpty())
	{
		if (Target == nullptr)
		{
			Target = WorldContextObject;
		}
		OnNativeEvent(EventName, [&, FunctionName, Target](const FString& Event, const TSharedPtr<FJsonValue>& Message)
		{
			CallBPFunctionWithMessage(Target, FunctionName, Message);
		}, Namespace, ThreadOverride);
	}
	else
	{
		//if we forgot our function name, fallback to regular bind event
		BindEventToGenericEvent(EventName, Namespace);
	}
}

void UESIOClientComponent::UnbindEvent(const FString& EventName, const FString& Namespace)
{
	NativeClient->UnbindEvent(EventName, Namespace);
}

void UESIOClientComponent::ConnectNative(const FString& InAddressAndPort, const FString& InPath,
	const TSharedPtr<FJsonObject>& Query, const TSharedPtr<FJsonObject>& Headers)
{
	FEmoShapeConnectParams Params;
	Params.AddressAndPort = InAddressAndPort;
	Params.Path = InPath;

	Params.Query = UESMessageConvert::JsonObjectToFStringMap(Query);
	Params.Headers = UESMessageConvert::JsonObjectToFStringMap(Headers);

	ConnectWithParams(Params);
}

void UESIOClientComponent::EmitNative(const FString& EventName, const TSharedPtr<FJsonValue>& Message,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	NativeClient->Emit(EventName, Message, CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, const TSharedPtr<FJsonObject>& ObjectMessage,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, MakeShareable(new FJsonValueObject(ObjectMessage)), CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, const FString& StringMessage,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, MakeShareable(new FJsonValueString(StringMessage)), CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, const SIO_TEXT_TYPE StringMessage,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, MakeShareable(new FJsonValueString(FString(StringMessage))), CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, double NumberMessage,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, MakeShareable(new FJsonValueNumber(NumberMessage)), CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, bool BooleanMessage,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, MakeShareable(new FJsonValueBoolean(BooleanMessage)), CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, const TArray<uint8>& BinaryMessage,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, MakeShareable(new FJsonValueBinary(BinaryMessage)), CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, const TArray<TSharedPtr<FJsonValue>>& ArrayMessage,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, MakeShareable(new FJsonValueArray(ArrayMessage)), CallbackFunction, Namespace);
}

void UESIOClientComponent::EmitNative(const FString& EventName, UStruct* Struct, const void* StructPtr,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	EmitNative(EventName, UESIOJConvert::ToJsonObject(Struct, (void*)StructPtr), CallbackFunction, Namespace);
}

void UESIOClientComponent::OnNativeEvent(const FString& EventName,
	TFunction<void(const FString&, const TSharedPtr<FJsonValue>&)> CallbackFunction, const FString& Namespace,
	EEmoShapeIOThreadOverrideOption ThreadOverride)
{
	NativeClient->OnEvent(EventName, CallbackFunction, Namespace, ThreadOverride);
}

void UESIOClientComponent::StaticInitialization(UObject* WorldContextObject, bool bValidOwnerWorld)
{
	bStaticallyInitialized = true;

	if (!bValidOwnerWorld)
	{
		//Need to allow connections to non-owner worlds.
		bLimitConnectionToGameWorld = false;

		//The auto-connect will never happen in this case, so disable for clarity
		bShouldAutoConnect = false;
	}

	//We statically initialize for all cases
	InitializeNative();
}

void UESIOClientComponent::InitializeComponent()
{
	Super::InitializeComponent();

	if (!bStaticallyInitialized)
	{
		InitializeNative();
	}
}

void UESIOClientComponent::UninitializeComponent()
{
	//Because our connections can last longer than game world 
	//end, we let plugin-scoped structures manage our memory.
	//We must ensure we set our pointer to null however.

	ClearCallbacks();

	//If we're a regular connection we should close and release when we quit
	/*if (!bPluginScopedConnection)
	{
		IEmoShapeIOClientModule::Get().ReleaseNativePointer(NativeClient);
		NativeClient = nullptr;
	}*/

	Super::UninitializeComponent();
}

void UESIOClientComponent::BeginPlay()
{
	Super::BeginPlay();

	//Auto-connect to default address if supported and not already connected
	if (bShouldAutoConnect && !bIsConnected)
	{
		ConnectWithParams(URLParams);
	}
}

UESIOClientComponent::~UESIOClientComponent()
{
	
}

void UESIOClientComponent::SetupCallbacks()
{
	//Sync current connected state
	bIsConnected = NativeClient->bIsConnected;
	if (bIsConnected)
	{
		SessionId = NativeClient->SessionId;
		URLParams = NativeClient->URLParams;
	}

	NativeClient->OnConnectedCallback = [this](const FString& InSessionId)
	{
		if (NativeClient.IsValid())
		{
			bIsConnected = true;
			SessionId = InSessionId;
			bool bIsReconnection = bIsHavingConnectionProblems;
			bIsHavingConnectionProblems = false;
			OnConnected.Broadcast(SessionId, bIsReconnection);
			
		}
	};

	const FEmoShapeCloseEventSignature OnDisconnectedSafe = OnDisconnected;

	NativeClient->OnDisconnectedCallback = [OnDisconnectedSafe, this](const EEmoShapeIOConnectionCloseReason Reason)
	{
		if (NativeClient.IsValid())
		{
			bIsConnected = false;
			OnDisconnectedSafe.Broadcast(Reason);
		}
	};

	NativeClient->OnNamespaceConnectedCallback = [this](const FString& Namespace)
	{
		if (NativeClient.IsValid())
		{
			OnSocketNamespaceConnected.Broadcast(Namespace);
		}
	};

	const FEmoShapeSocketEventSignature OnSocketNamespaceDisconnectedSafe = OnSocketNamespaceDisconnected;

	NativeClient->OnNamespaceDisconnectedCallback = [this, OnSocketNamespaceDisconnectedSafe](const FString& Namespace)
	{

		if (NativeClient.IsValid())
		{
			OnSocketNamespaceDisconnectedSafe.Broadcast(Namespace);
		}
	};
	NativeClient->OnReconnectionCallback = [this](const uint32 AttemptCount, const uint32 DelayInMs)
	{
		if (NativeClient.IsValid())
		{
			//First time we know about this problem?
			if (!bIsHavingConnectionProblems)
			{
				TimeWhenConnectionProblemsStarted = FDateTime::Now();
				bIsHavingConnectionProblems = true;
			}

			FTimespan Difference = FDateTime::Now() - TimeWhenConnectionProblemsStarted;
			float ElapsedInSec = Difference.GetTotalSeconds();

			if (ReconnectionTimeout > 0 && ElapsedInSec > ReconnectionTimeout)
			{
				//Let's stop trying and disconnect if we're using timeouts
				Disconnect();
			}
			OnConnectionProblems.Broadcast(AttemptCount, DelayInMs, ElapsedInSec);
		}
	};

	NativeClient->OnFailCallback = [this]()
	{
		if(NativeClient.IsValid())
		{
			OnFail.Broadcast();
		};
	};
}

void UESIOClientComponent::ClearCallbacks()
{
	if (NativeClient.IsValid())
	{
		NativeClient->ClearAllCallbacks();
	}
}

void UESIOClientComponent::InitializeNative()
{
	if (bPluginScopedConnection)
	{
		NativeClient = FEmoShapeSubSystemModule::Get().ValidSharedNativePointer(PluginScopedId, bForceTLS, bShouldVerifyTLSCertificate);

		//Enforcement: This is the default FEmoShapeIONative option value, but this component depends on it being true.
		NativeClient->bCallbackOnGameThread = true;
	}
	else
	{
		NativeClient = FEmoShapeSubSystemModule::Get().NewValidNativePointer(bForceTLS, bShouldVerifyTLSCertificate);
	}

	SetupCallbacks();
}

bool UESIOClientComponent::CallBPFunctionWithResponse(UObject* Target, const FString& FunctionName,
	TArray<TSharedPtr<FJsonValue>> Response)
{
	if (!Target->IsValidLowLevel())
	{
		UE_LOG(SocketIO, Warning, TEXT("CallFunctionByNameWithArguments: Target not found for '%s'"), *FunctionName);
		return false;
	}
	UWorld* World = GEngine->GetWorldFromContextObject(Target, EGetWorldErrorMode::LogAndReturnNull);
	if (World && World->bIsTearingDown)
	{
		UE_LOG(SocketIO, Log, TEXT("World tearing down, %s BP function call ignored."), *FunctionName);
		return false;
	}

	UFunction* Function = Target->FindFunction(FName(*FunctionName));
	if (nullptr == Function)
	{
		UE_LOG(SocketIO, Warning, TEXT("CallFunctionByNameWithArguments: Function not found '%s'"), *FunctionName);
		return false;
	}

	//Check function signature
	TFieldIterator<FProperty> Iterator(Function);

	TArray<FProperty*> Properties;
	while (Iterator && (Iterator->PropertyFlags & CPF_Parm))
	{
		FProperty* Prop = *Iterator;
		Properties.Add(Prop);
		++Iterator;
	}

	auto ResponseJsonValue = UESIOJConvert::ToEmoShapeJsonValue(Response);

	bool bResponseNumNotZero = Response.Num() > 0;
	bool bNoFunctionParams = Properties.Num() == 0;
	bool bNullResponse = ResponseJsonValue->IsNull();

	if (bNullResponse && bNoFunctionParams)
	{
		Target->ProcessEvent(Function, nullptr);
		return true;
	}
	else if (bResponseNumNotZero)
	{
		//function has too few params
		if (bNoFunctionParams)
		{
			UE_LOG(SocketIO, Warning, TEXT("CallFunctionByNameWithArguments: Function '%s' has too few parameters, callback parameters ignored : <%s>"), *FunctionName, *ResponseJsonValue->EncodeJson());
			Target->ProcessEvent(Function, nullptr);
			return true;
		}
		struct FDynamicArgs
		{
			void* Arg01 = nullptr;
			UESJsonValue* Arg02 = nullptr;
		};
		//create the container
		FDynamicArgs Args = FDynamicArgs();

		//add the full response array as second param
		Args.Arg02 = ResponseJsonValue;
		const FString& FirstParam = Properties[0]->GetCPPType();
		auto FirstFJsonValue = Response[0];

		//Is first param...
		//EmoShapeJsonValue?
		if (FirstParam.Equals("UESJsonValue*"))
		{
			//convenience wrapper, response is a single object
			UESJsonValue* Value = NewObject<UESJsonValue>();
			Value->SetRootValue(FirstFJsonValue);
			Args.Arg01 = Value;
			Target->ProcessEvent(Function, &Args);
			return true;
		}
		//EmoShapeJsonObject?
		else if (FirstParam.Equals("UESJsonObject*"))
		{
			//convenience wrapper, response is a single object
			UESJsonObject* ObjectValue = NewObject<UESJsonObject>();
			ObjectValue->SetRootObject(FirstFJsonValue->AsObject());
			Args.Arg01 = ObjectValue;
			Target->ProcessEvent(Function, &Args);
			return true;
		}
		//String?
		else if (FirstParam.Equals("FString"))
		{
			FString	StringValue = UESIOJConvert::ToJsonString(FirstFJsonValue);
			
			Target->ProcessEvent(Function, &StringValue);
			return true;
		}
		//Float?
		else if (FirstParam.Equals("float"))
		{
			float NumberValue = (float)FirstFJsonValue->AsNumber();
			Target->ProcessEvent(Function, &NumberValue);
			return true;
		}
		//Int?
		else if (FirstParam.Equals("int32"))
		{
			int NumberValue = (int)FirstFJsonValue->AsNumber();
			Target->ProcessEvent(Function, &NumberValue);
			return true;
		}
		//bool?
		else if (FirstParam.Equals("bool"))
		{
			bool BoolValue = FirstFJsonValue->AsBool();
			Target->ProcessEvent(Function, &BoolValue);
			return true;
		}
		//array?
		else if (FirstParam.Equals("TArray"))
		{
			FArrayProperty* ArrayProp = CastField<FArrayProperty>(Properties[0]);

			FString Inner;
			ArrayProp->GetCPPMacroType(Inner);

			//byte array is the only supported version
			if (Inner.Equals("uint8"))
			{
				TArray<uint8> Bytes = ResponseJsonValue->AsArray()[0]->AsBinary();
				Target->ProcessEvent(Function, &Bytes);
				return true;
			}
		}
	}

	UE_LOG(SocketIO, Warning, TEXT("CallFunctionByNameWithArguments: Function '%s' signature not supported expected <%s>"), *FunctionName, *ResponseJsonValue->EncodeJson());
	return false;
}

bool UESIOClientComponent::CallBPFunctionWithMessage(UObject* Target, const FString& FunctionName,
	TSharedPtr<FJsonValue> Message)
{
	TArray<TSharedPtr<FJsonValue>> Response;
	Response.Add(Message);

	return CallBPFunctionWithResponse(Target, FunctionName, Response);
}

