#include "SocketIoClient.h"
#include "Async/Async.h"
#include "Dom/JsonObject.h"
#include "PortaInterface.h"

void USocketIoClient::Connect(const FString& Url, const FString& Namespace)
{
	if (!Client)
	{
		sio::client_options options;
		options.uri = TCHAR_TO_ANSI(*Url);
		Client = MakeShareable<sio::client>(new sio::client(options));
	}
	Client->set_reconnect_attempts(-1);
	Client->set_reconnect_delay(5000);
	Client->set_reconnect_delay_max(5000);
	Client->set_logs_verbose();
	Client->set_fail_listener(std::bind(&USocketIoClient::OnConnectionFailed, this));
	Client->set_reconnecting_listener(std::bind(&USocketIoClient::OnReconnecting, this));
	Client->set_close_listener(std::bind(&USocketIoClient::OnConnectionClosed, this));
	Socket = Client->socket(TCHAR_TO_ANSI(*Namespace));

	for (auto Kvp : OnMessageMap)
	{
		Socket->on(TCHAR_TO_ANSI(*Kvp.Key), std::bind(&USocketIoClient::OnMessage, this, std::placeholders::_1));
	}

	Client->connect(TCHAR_TO_ANSI(*Url));
	Client->set_socket_open_listener([Namespace, this](std::string const& nsp) {
		if (FString(TEXT("/")) + Namespace == ANSI_TO_TCHAR(nsp.c_str()))
		{
			AsyncTask(ENamedThreads::GameThread, [this]() {
				Client->socket()->close();
				isConnected = true;
				OnConnected.Broadcast();
			});
		}
	});
}

FOnSocketIoMessageDelegate& USocketIoClient::GetOnMessage(const FString& Message)
{
	if (Socket && !OnMessageMap.Contains(Message))
	{
		Socket->on(TCHAR_TO_ANSI(*Message), std::bind(&USocketIoClient::OnMessage, this, std::placeholders::_1));
	}

	return OnMessageMap.FindOrAdd(Message);
}

static sio::message::ptr JsonToMessage(const TSharedPtr<FJsonValue>& Value)
{
	if (!Value)
		return sio::null_message::create();

	switch (Value->Type)
	{
	case EJson::String:
		return sio::string_message::create(TCHAR_TO_ANSI(*Value->AsString()));
	case EJson::Number:
		return sio::double_message::create(Value->AsNumber());
	case EJson::Boolean:
		return sio::bool_message::create(Value->AsBool());
	case EJson::Array:
	{
		auto& Array = Value->AsArray();
		auto msg = sio::array_message::create();
		auto arrmsg = (sio::array_message*)msg.get();

		for (auto& ArrVal : Array)
		{
			arrmsg->push(JsonToMessage(ArrVal));
		}

		return msg;
	}
	case EJson::Object:
	{
		auto& Obj = Value->AsObject();
		auto msg = sio::object_message::create();
		auto objmsg = (sio::object_message*)msg.get();

		for (auto& ObjVal : Obj->Values)
		{
			objmsg->insert(TCHAR_TO_ANSI(*ObjVal.Key), JsonToMessage(ObjVal.Value));
		}

		return msg;
	}
	default:
		return sio::null_message::create();
	}
}

void USocketIoClient::Emit(const FString& EventName, const TSharedPtr<FJsonValue>& Contents)
{
	sio::message::list msgs;
	msgs.push(JsonToMessage(Contents));
	Socket->emit(TCHAR_TO_ANSI(*EventName), msgs);
}

static TSharedPtr<FJsonValue> MessageToJson(sio::message::ptr Msg, FString& OutError)
{
	TSharedPtr<FJsonValue> Value;

	switch (Msg->get_flag())
	{
	case sio::message::flag_integer:
		Value = MakeShared<FJsonValueNumber>(Msg->get_int());
		break;
	case sio::message::flag_double:
		Value = MakeShared<FJsonValueNumber>(Msg->get_double());
		break;
	case sio::message::flag_string:
		Value = MakeShared<FJsonValueString>(ANSI_TO_TCHAR(Msg->get_string().c_str()));
		break;
	case sio::message::flag_binary:
		OutError = TEXT("Received binary socket io message, which is not supported.");
		break;
	case sio::message::flag_array:
	{
		TArray<TSharedPtr<FJsonValue>> ValArr;

		auto& MsgArr = Msg->get_vector();
		for (auto ArrEntry : MsgArr)
		{
			ValArr.Push(MessageToJson(ArrEntry, OutError));
			if (!OutError.IsEmpty())
				return Value;
		}

		Value = MakeShared<FJsonValueArray>(ValArr);
	}
		break;
	case sio::message::flag_object:
	{
		TSharedPtr<FJsonObject> ValObj = MakeShared<FJsonObject>();
		auto& MsgMap = Msg->get_map();
		for (auto Kvp : MsgMap)
		{
			ValObj->SetField(ANSI_TO_TCHAR(Kvp.first.c_str()), MessageToJson(Kvp.second, OutError));
			if (!OutError.IsEmpty())
				return Value;
		}

		Value = MakeShared<FJsonValueObject>(ValObj);
	}
		break;
	case sio::message::flag_boolean:
		Value = MakeShared<FJsonValueBoolean>(Msg->get_bool());
		break;
	case sio::message::flag_null:
		Value = MakeShared<FJsonValueNull>();
		break;
	default:
		OutError = TEXT("Unrecognized socket io message type received");
	}

	return Value;
}

void USocketIoClient::OnConnectionFailed()
{
	AsyncTask(ENamedThreads::GameThread, [this]() {
		UE_LOG(LogPorta, Warning, TEXT("Socket IO connection failed"));
	});
}

void USocketIoClient::OnConnectionClosed()
{
}

void USocketIoClient::OnReconnecting()
{
	AsyncTask(ENamedThreads::GameThread, [this]() {
		UE_LOG(LogPorta, Warning, TEXT("Socket IO connection failed"));
	});
}

void USocketIoClient::OnMessage(sio::event& event)
{
	auto msg = event.get_message();
	std::string msgType = event.get_name();
	FString error;
	TSharedPtr<FJsonValue> Value = MessageToJson(msg, error);

	AsyncTask(ENamedThreads::GameThread, [this, Value, msgType, error]() {
		auto Delegate = OnMessageMap.Find(ANSI_TO_TCHAR(msgType.c_str()));
		if (!error.IsEmpty())
			UE_LOG(LogPorta, Error, TEXT("%s"), *error);

		if (!Delegate || !Value.IsValid())
			return;

		Delegate->Broadcast(Value);
	});
}
