#include "EmoShapeSubSystem/EmoShapeIOClient/Public/ESIONative.h"

#include "EmoShapeSubSystem/EmoShapeJson/Public/ESIOJConvert.h"
#include "EmoShapeSubSystem/EmoShapeJson/Public/ESJsonValue.h"
#include "EmoShapeSubSystem/EmoShapeWebUtility/Public/EmoShapeLambdaRunnable.h"

FESIONative::FESIONative(const bool bForceTLSMode, const bool bShouldVerifyTLSCertificate)
{
	PrivateClient = nullptr;
	SessionId = TEXT("Invalid");
	LastSessionId = TEXT("None");
	bIsConnected = false;
	MaxReconnectionAttempts = -1;
	ReconnectionDelay = 5000;
	bCallbackOnGameThread = true;
	bUnbindEventsOnDisconnect = false;
	bForceTLSUse = bForceTLSMode;
	InitPrivateClient(bForceTLSMode, bShouldVerifyTLSCertificate);

	ClearAllCallbacks();
}

void FESIONative::Connect(const FString& InAddressAndPort)
{
	FEmoShapeConnectParams Params;
	Params.AddressAndPort = InAddressAndPort;

	Connect(Params);
}

void FESIONative::Connect(const FEmoShapeConnectParams& InConnectParams)
{
	//Special case: reconnect on current settings if address is empty
	if (!InConnectParams.AddressAndPort.IsEmpty())
	{
		URLParams = InConnectParams;
	}

	SyncPrivateClientToTLSMode(URLParams.AddressAndPort);
	
	//Fill std types before going to background thread.

	std::string StdAddressString = UESMessageConvert::StdString(URLParams.AddressAndPort);
	std::string StdPathString = UESMessageConvert::StdString(URLParams.Path);
	std::map<std::string, std::string> QueryMap = {};
	std::map<std::string, std::string> HeadersMap = {};

	QueryMap = UESMessageConvert::FStringMapToStdStringMap(URLParams.Query);
	HeadersMap = UESMessageConvert::FStringMapToStdStringMap(URLParams.Headers);

	//Connect to the server on a background thread so it never blocks
	FEmoShapeLambdaRunnable::RunLambdaOnBackGroundThread([&, StdAddressString, StdPathString, QueryMap, HeadersMap]
	{
		PrivateClient->set_reconnect_attempts(MaxReconnectionAttempts);
		PrivateClient->set_reconnect_delay(ReconnectionDelay);
		PrivateClient->set_path(StdPathString);

		//close and reconnect if different url
		if(PrivateClient->opened())
		{
			if (PrivateClient->get_url() != StdAddressString)
			{
				//sync close to re-open
				PrivateClient->sync_close();
			}
			else
			{
				//we're already connected to the correct endpoint, ignore request
				UE_LOG(SocketIO, Warning, TEXT("Attempted to connect to %s when we're already connected. Request ignored."), UTF8_TO_TCHAR(StdAddressString.c_str()));
				return;
			}
		}
		PrivateClient->connect(StdAddressString, QueryMap, HeadersMap);
	});
}

void FESIONative::JoinNamespace(const FString& Namespace)
{
	//just referencing the namespace will join it
	PrivateClient->socket(UESMessageConvert::StdString(Namespace));
}

void FESIONative::LeaveNamespace(const FString& Namespace)
{
	PrivateClient->socket(UESMessageConvert::StdString(Namespace))->close();
}

void FESIONative::Disconnect()
{
	//Ensure disconnected callback is called first because we 
	//clear all callbacks for stability.
	if (OnDisconnectedCallback)
	{
		OnDisconnectedCallback(EEmoShapeIOConnectionCloseReason::CLOSE_REASON_NORMAL);
	}
	bIsConnected = false;

	if (bUnbindEventsOnDisconnect)
	{
		ClearAllCallbacks();
		PrivateClient->close();
	}
	else
	{
		//Clear and re-init map
		ClearInternalCallbacks();
		PrivateClient->close();
		RebindCurrentEventMap();
	}
}

void FESIONative::SyncDisconnect()
{
	//Ensure disconnected callback is called first because we 
	//clear all callbacks for stability.
	if (OnDisconnectedCallback)
	{
		OnDisconnectedCallback(EEmoShapeIOConnectionCloseReason::CLOSE_REASON_NORMAL);
	}
	bIsConnected = false;

	if (bUnbindEventsOnDisconnect)
	{
		ClearAllCallbacks();
		PrivateClient->sync_close();
	}
	else
	{
		//Clear and re-init map
		ClearInternalCallbacks();
		PrivateClient->sync_close();
		RebindCurrentEventMap();
	}
}

void FESIONative::ClearAllCallbacks()
{
	PrivateClient->clear_socket_listeners();
	SetupInternalCallbacks();					//if clear socket listeners cleared our internal callbacks. reset them
	EventFunctionMap.Empty();

	OnConnectedCallback = nullptr;
	OnDisconnectedCallback = nullptr;
	OnNamespaceConnectedCallback = nullptr;
	OnNamespaceDisconnectedCallback = nullptr;
	OnReconnectionCallback = nullptr;
	OnFailCallback = nullptr;
}

void FESIONative::Emit(const FString& EventName, const TSharedPtr<FJsonValue>& Message,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	TFunction<void(const emoshapeio::message::list&)> RawCallback = nullptr;

	//Only bind the raw callback if we pass in a callback ourselves;
	if (CallbackFunction)
	{
		RawCallback = [&, CallbackFunction](const emoshapeio::message::list& MessageList)
		{
			TArray<TSharedPtr<FJsonValue>> ValueArray;

			for (uint32 i = 0; i < MessageList.size(); i++)
			{
				auto ItemMessagePtr = MessageList[i];
				ValueArray.Add(UESMessageConvert::ToJsonValue(ItemMessagePtr));
			}
			if (CallbackFunction)
			{
				CallbackFunction(ValueArray);
			}
		};
	}

	EmitRaw(
		EventName,
		UESMessageConvert::ToSIOMessage(Message),
		RawCallback,
		Namespace);
}

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

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

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

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

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

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

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

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

void FESIONative::Emit(const FString& EventName,
	TFunction<void(const TArray<TSharedPtr<FJsonValue>>&)> CallbackFunction, const FString& Namespace)
{
	TSharedPtr<FJsonValue> NoneValue;
	Emit(EventName, NoneValue, CallbackFunction, Namespace);
}

void FESIONative::EmitRaw(const FString& EventName, const emoshapeio::message::list& MessageList,
	TFunction<void(const emoshapeio::message::list&)> CallbackFunction, const FString& Namespace)
{
	std::function<void(emoshapeio::message::list const&)> RawCallback = nullptr;

	//Only have non-null raw callback if we pass in a callback function
	if (CallbackFunction)
	{
		RawCallback = [&, CallbackFunction](const emoshapeio::message::list& response)
		{
			if (CallbackFunction != nullptr)
			{
				//Callback on game thread
				if (bCallbackOnGameThread)
				{
					FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, CallbackFunction, response]
					{
						if (CallbackFunction)
						{
							CallbackFunction(response);
						}
					});
				}
				else
				{
					CallbackFunction(response);
				}
			}
		};
	}

	PrivateClient->socket(UESMessageConvert::StdString(Namespace))->emit(
		UESMessageConvert::StdString(EventName),
		MessageList,
		RawCallback);
}

void FESIONative::EmitRawBinary(const FString& EventName, uint8* Data, int32 DataLength, const FString& Namespace)
{
	PrivateClient->socket(UESMessageConvert::StdString(Namespace))->emit(UESMessageConvert::StdString(EventName), std::make_shared<std::string>((char*)Data, DataLength));
}

void FESIONative::OnEvent(const FString& EventName,
	TFunction<void(const FString&, const TSharedPtr<FJsonValue>&)> CallbackFunction, const FString& Namespace,
	EEmoShapeIOThreadOverrideOption CallbackThread)
{
	//Keep track of all the bound native JsonValue functions
	FEmoShapeIOBoundEvent BoundEvent;
	BoundEvent.Function = CallbackFunction;
	BoundEvent.Namespace = Namespace;
	BoundEvent.ThreadOption = CallbackThread;
	EventFunctionMap.Add(EventName, BoundEvent);

	OnRawEvent(EventName, [&, CallbackFunction](const FString& Event, const emoshapeio::message::ptr& RawMessage) {
		CallbackFunction(Event, UESMessageConvert::ToJsonValue(RawMessage));
	}, Namespace, CallbackThread);
}

void FESIONative::OnRawEvent(const FString& EventName,
	TFunction<void(const FString&, const emoshapeio::message::ptr&)> CallbackFunction, const FString& Namespace,
	EEmoShapeIOThreadOverrideOption CallbackThread)
{
	if (CallbackFunction == nullptr)
	{
		PrivateClient->socket(UESMessageConvert::StdString(Namespace))->off(UESMessageConvert::StdString(EventName));
	}
	else
	{
		//determine thread override option
		bool bCallbackThisEventOnGameThread = bCallbackOnGameThread;
		switch (CallbackThread)
		{
		case USE_DEFAULT:
			break;
		case USE_GAME_THREAD:
			bCallbackThisEventOnGameThread = true;
			break;
		case USE_NETWORK_THREAD:
			bCallbackThisEventOnGameThread = false;
			break;
		default:
			break;
		}

		const TFunction< void(const FString&, const emoshapeio::message::ptr&)> SafeFunction = CallbackFunction;	//copy the function so it remains in context

		PrivateClient->socket(UESMessageConvert::StdString(Namespace))->on(
			UESMessageConvert::StdString(EventName),
			emoshapeio::socket::event_listener_aux(
			[&, SafeFunction, bCallbackThisEventOnGameThread](std::string const& name, emoshapeio::message::ptr const& data, bool isAck, emoshapeio::message::list &ack_resp)
			{
				if (SafeFunction != nullptr)
				{
					const FString SafeName = UESMessageConvert::FStringFromStd(name);

					if (bCallbackThisEventOnGameThread)
					{
						FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, SafeFunction, SafeName, data]
							{
								SafeFunction(SafeName, data);
							});
					}
					else
					{
						SafeFunction(SafeName, data);
					}
				}
			}));
	}
}

void FESIONative::OnRawBinaryEvent(const FString& EventName,
	TFunction<void(const FString&, const TArray<uint8>&)> CallbackFunction, const FString& Namespace)
{
	const TFunction< void(const FString&, const TArray<uint8>&)> SafeFunction = CallbackFunction;	//copy the function so it remains in context

	PrivateClient->socket(UESMessageConvert::StdString(Namespace))->on(
		UESMessageConvert::StdString(EventName),
		emoshapeio::socket::event_listener_aux(
			[&, SafeFunction](std::string const& name, emoshapeio::message::ptr const& data, bool isAck, emoshapeio::message::list &ack_resp)
	{
		const FString SafeName = UESMessageConvert::FStringFromStd(name);

		//Construct raw buffer
		if (data->get_flag() == emoshapeio::message::flag_binary)
		{
			TArray<uint8> Buffer;
			int32 BufferSize = data->get_binary()->size();
			auto MessageBuffer = data->get_binary();
			Buffer.Append((uint8*)(MessageBuffer->data()), BufferSize);

			if (bCallbackOnGameThread)
			{
				FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, SafeFunction, SafeName, Buffer]
				{
					SafeFunction(SafeName, Buffer);
				});
			}
			else
			{
				SafeFunction(SafeName, Buffer);
			}
		}
		else
		{
			UE_LOG(SocketIO, Warning, TEXT("Non-binary message received to binary message lambda, check server message data!"));
		}
	}));
}

void FESIONative::UnbindEvent(const FString& EventName, const FString& Namespace)
{
	OnRawEvent(EventName, nullptr, Namespace);
	EventFunctionMap.Remove(EventName);
}

void FESIONative::ClearInternalCallbacks()
{
	PrivateClient->clear_socket_listeners();
}

void FESIONative::SetupInternalCallbacks()
{
	PrivateClient->set_open_listener(emoshapeio::client::con_listener([&]() 
	{
		//too early to get session id here so we defer the connection event until we connect to a namespace
	}));

	PrivateClient->set_close_listener(emoshapeio::client::close_listener([&](emoshapeio::client::close_reason const& reason)
	{
		bIsConnected = false;

		EEmoShapeIOConnectionCloseReason DisconnectReason = (EEmoShapeIOConnectionCloseReason)reason;
		FString DisconnectReasonString = UESIOJConvert::EnumToString(TEXT("EEmoShapeIOConnectionCloseReason"), DisconnectReason);
		if (VerboseLog)
		{
			UE_LOG(SocketIO, Log, TEXT("SocketIO Disconnected %s reason: %s"), *SessionId, *DisconnectReasonString);
		}
		LastSessionId = SessionId;
		SessionId = TEXT("Invalid");

		if (OnDisconnectedCallback)
		{
			if (bCallbackOnGameThread)
			{
				FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, DisconnectReason]
				{
					if (OnDisconnectedCallback)
					{
						OnDisconnectedCallback(DisconnectReason);
					}
				});
			}
			else
			{
				OnDisconnectedCallback(DisconnectReason);
			}
		}
	}));

	PrivateClient->set_socket_open_listener(emoshapeio::client::socket_listener([&](std::string const& nsp)
	{
		//Special case, we have a latent connection after already having been disconnected
		if (!PrivateClient.IsValid())
		{
			return;
		}
		if (!bIsConnected)
		{
			bIsConnected = true;
			SessionId = UESMessageConvert::FStringFromStd(PrivateClient->get_sessionid());

			if (VerboseLog)
			{
				UE_LOG(SocketIO, Log, TEXT("SocketIO Connected with session: %s"), *SessionId);
			}
			if (OnConnectedCallback)
			{
				if (bCallbackOnGameThread)
				{
					const FString SafeSessionId = SessionId;
					FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, SafeSessionId]
					{
						if (OnConnectedCallback)
						{
							OnConnectedCallback(SessionId);
						}
					});
				}
				else
				{
					OnConnectedCallback(SessionId);
				}
			}
		}

		const FString Namespace = UESMessageConvert::FStringFromStd(nsp);

		if (VerboseLog)
		{
			UE_LOG(SocketIO, Log, TEXT("SocketIO %s connected to namespace: %s"), *SessionId, *Namespace);
		}
		if (OnNamespaceConnectedCallback)
		{
			if (bCallbackOnGameThread)
			{
				FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, Namespace]
				{
					if (OnNamespaceConnectedCallback)
					{
						OnNamespaceConnectedCallback(Namespace);
					}
				});
			}
			else
			{
				OnNamespaceConnectedCallback(Namespace);
			}
		}
	}));

	PrivateClient->set_socket_close_listener(emoshapeio::client::socket_listener([&](std::string const& nsp)
	{
		const FString Namespace = UESMessageConvert::FStringFromStd(nsp);
		FString NamespaceSession = SessionId;
		if (NamespaceSession.Equals(TEXT("Invalid")))
		{
			NamespaceSession = LastSessionId;
		}
		if (VerboseLog)
		{
			UE_LOG(SocketIO, Log, TEXT("SocketIO %s disconnected from namespace: %s"), *NamespaceSession, *Namespace);
		}
		if (OnNamespaceDisconnectedCallback)
		{
			if (bCallbackOnGameThread)
			{
				FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, Namespace]
				{
					if (OnNamespaceDisconnectedCallback)
					{
						OnNamespaceDisconnectedCallback(Namespace);
					}
				});
			}
			else
			{
				OnNamespaceDisconnectedCallback(Namespace);
			}
		}
	}));

	PrivateClient->set_fail_listener(emoshapeio::client::con_listener([&]()
	{
		if (VerboseLog)
		{
			UE_LOG(SocketIO, Log, TEXT("SocketIO failed to connect."));
		}
		if (OnFailCallback)
		{
			if (bCallbackOnGameThread)
			{
				FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&]
				{
					if (OnFailCallback)
					{
						OnFailCallback();
					}
				});
			}
			else
			{
				OnFailCallback();
			}
		}
	}));

	PrivateClient->set_reconnect_listener(emoshapeio::client::reconnect_listener([&](unsigned num, unsigned delay)
	{
		bIsConnected = false;

		if (VerboseLog)
		{
			UE_LOG(SocketIO, Log, TEXT("SocketIO %s appears to have lost connection, reconnecting attempt %d with delay %d"), *SessionId, num, delay);
		}
		if (OnReconnectionCallback)
		{
			if (bCallbackOnGameThread)
			{
				FEmoShapeLambdaRunnable::RunShortLambdaOnGameThread([&, num, delay]
				{
					if (OnReconnectionCallback)
					{
						OnReconnectionCallback(num, delay);
					}
				});
			}
			else
			{
				OnReconnectionCallback(num, delay);
			}
		}
	}));
}

void FESIONative::RebindCurrentEventMap()
{
	ClearInternalCallbacks();

	for (auto& EventPair : EventFunctionMap)
	{
		const FString& EventName = EventPair.Key;
		const FEmoShapeIOBoundEvent EventBind = EventPair.Value;

		OnRawEvent(EventName, [&, EventBind](const FString& Event, const emoshapeio::message::ptr& RawMessage) {
			EventBind.Function(Event, UESMessageConvert::ToJsonValue(RawMessage));
		}, EventBind.Namespace, EventBind.ThreadOption);
	}

	SetupInternalCallbacks();
}

bool FESIONative::IsTLSURL(const FString& URL)
{
	return URL.StartsWith(TEXT("https://")) || URL.StartsWith(TEXT("wss://"));
}

void FESIONative::SyncPrivateClientToTLSMode(const FString& URL)
{
	//Should be in TLS mode?
	if (IsTLSURL(URL) || bForceTLSUse)
	{
		//needs to swap to TLS
		if (!bIsSetupForTLS)
		{
			if (PrivateClient->opened())
			{
				PrivateClient->sync_close();
			}
			ClearInternalCallbacks();
			InitPrivateClient(true, bUsingTLSCertVerification);
			RebindCurrentEventMap();
		}
	}
	//Should not be in TLS mode
	else
	{
		//URL is not TLS, but we are setup for it
		if (bIsSetupForTLS)
		{
			if (PrivateClient->opened())
			{
				PrivateClient->sync_close();
			}
			ClearInternalCallbacks();
			InitPrivateClient(false, bUsingTLSCertVerification);
			RebindCurrentEventMap();
		}
	}
}

void FESIONative::InitPrivateClient(const bool bShouldUseTlsLibraries, const bool bShouldVerifyTLSCertificate)
{
	bIsSetupForTLS = bShouldUseTlsLibraries;
	bUsingTLSCertVerification = bShouldVerifyTLSCertificate;
	PrivateClient = MakeShareable(new emoshapeio::client(bShouldUseTlsLibraries, bUsingTLSCertVerification));
}

