// Fill out your copyright notice in the Description page of Project Settings.

#include "LovenseToyEventsWebSocketHandler.h"

// EXTERNAL INCLUDES
#include <IWebSocket.h>
#include <WebSocketsModule.h>
#include <Dom/JsonObject.h>
#include <Serialization/JsonReader.h>
#include <Serialization/JsonSerializer.h>
#include <Misc/App.h>
#include <Misc/ConfigCacheIni.h>
#include <Misc/Paths.h>

// INTERNAL INCLUDES
#include "LovenseIntegration.h"
#include "LovenseManager.h"


LovenseToyEventsWebSocketHandler::LovenseToyEventsWebSocketHandler() {
	this->webSocket = nullptr;
	this->pingTimerHandle = FTimerHandle();
	this->pingTimestamp = 0.0;
}

bool LovenseToyEventsWebSocketHandler::Initialize(const FString& ip, int32 port, FOnEventCode& resultCodeCallback, FOnGetLovenseEventToys& toysCallback) {
#if WITH_LOVENSE
	this->getToysCallback = toysCallback;
	this->codeResponse = resultCodeCallback;
	
	FLovenseIntegrationModule::Get().StartHeartbeat();
	this->socketIp = ip;
	//this->socketPort = port;
	
	if (!this->webSocket) {
		// Fetch IP and Port override configs.
		//FString configFilePath = FString::Printf(TEXT("%s%s.ini"), *FPaths::GeneratedConfigDir(), *LovenseIntegration::configFileName);
		//bool bResult = false;
		//FString deviceIpOverride = TEXT("");
		//bResult = GConfig->GetString(*LovenseIntegration::configSectionName, *LovenseIntegration::configDeviceIpOverrideKeyName, deviceIpOverride, configFilePath);
		if (!FLovenseManager::Get()->IsIPStringValid(ip)) {
			return false;
		}
		if (!FLovenseManager::Get()->IsPortIntValid(port)) {
			this->socketPort = 20010;
		}
		else {
			this->socketPort = port;
		}

		//TSharedRef<LovenseToyEventsWebSocketHandler> sharedThis = SharedThis(this);

		const FString serverUrl = FString::Printf(TEXT("ws://%s:%d/v1"), *(this->socketIp), this->socketPort);
		//const FString serverUrl = "";

		this->webSocket = FWebSocketsModule::Get().CreateWebSocket(serverUrl);
		if (!this->webSocket.IsValid()) return false;

		TSharedPtr<LovenseToyEventsWebSocketHandler> handler2 = SharedThis(this);
		this->webSocket->OnConnected().AddLambda([handler2]() {
			const TCHAR* ProjectName = FApp::GetProjectName();
			FString AppName = FString::Printf(TEXT("%s"), ProjectName);
			handler2->GetSocket()->Send(
				FString::Printf(
					TEXT("{type:\"access\",data:{appName:\"%s\"}}"),
					*AppName
				)
			);
			//handler2.Get()->Ping();
		});
		//this->webSocket->OnConnected().AddSP(handler2.Get(), &LovenseToyEventsWebSocketHandler::OnConnected);
		this->webSocket->OnConnectionError().AddSP(handler2.Get(), &LovenseToyEventsWebSocketHandler::OnConnectionError);
		this->webSocket->OnClosed().AddSP(handler2.Get(), &LovenseToyEventsWebSocketHandler::OnConnectionClosed);
		this->webSocket->OnMessage().AddSP(handler2.Get(), &LovenseToyEventsWebSocketHandler::OnMessage);
		this->webSocket->OnRawMessage().AddSP(handler2.Get(), &LovenseToyEventsWebSocketHandler::OnRawMessage);
		this->webSocket->OnMessageSent().AddSP(handler2.Get(), &LovenseToyEventsWebSocketHandler::OnMessageSent);
		/*this->webSocket->OnConnectionError().AddSP(this, &LovenseToyEventsWebSocketHandler::OnConnectionError);
		this->webSocket->OnClosed().AddSP(this, &LovenseToyEventsWebSocketHandler::OnConnectionClosed);
		this->webSocket->OnMessage().AddSP(this, &LovenseToyEventsWebSocketHandler::OnMessage);
		this->webSocket->OnRawMessage().AddSP(this, &LovenseToyEventsWebSocketHandler::OnRawMessage);
		this->webSocket->OnMessageSent().AddSP(this, &LovenseToyEventsWebSocketHandler::OnMessageSent);*/
	}

	this->webSocket->Connect();
	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::init]"));
	if (!this->pingTimerHandle.IsValid()) {
		TSharedPtr<LovenseToyEventsWebSocketHandler> handler2 = SharedThis(this);
		//FLovenseManager::Get()->StartTimerByEventsAPI(pingTimerHandle, FTimerDelegate::CreateSP(handler2.Get(), &LovenseToyEventsWebSocketHandler::Ping), 4.0f, true);
		FLovenseManager::Get()->StartTimerByEventsAPI(this->pingTimerHandle, FTimerDelegate::CreateLambda([handler2]() {
			handler2.Get()->Ping(); 
			}), 4.0f, true);
	}
	return true;
#endif
}

void LovenseToyEventsWebSocketHandler::SetConnectEventCallback( FOnLovenseConnectStatusEvent connectCallback) {
	this->connectCallBack = connectCallback;
	
}
void LovenseToyEventsWebSocketHandler::SetBatteryEventCallback( FOnLovenseBatteryEvent batteryCallback) {
	this->batteryCallBack = batteryCallback;
	
}
void LovenseToyEventsWebSocketHandler::SetFunctionsValueEventCallback( FOnFunctionsEvent functionsValueCallback) {
	this->valueCallBack = functionsValueCallback;
	
}
void LovenseToyEventsWebSocketHandler::SetEveryShakeEventCallback( FOnEveryShakeEvent everyShakeCallback) {
	this->everyShakeCallBack = everyShakeCallback;
	
}
void LovenseToyEventsWebSocketHandler::SetButtonEventCallback(FOnButtonEvent buttonCallback) {
	this->buttonCallBack = buttonCallback;
}

void LovenseToyEventsWebSocketHandler::SetMotionChangedEventCallback(FOnMotionChangedEvent motionChangedCallback1) {
	this->motionChangedCallback = motionChangedCallback1;
}

TSharedPtr<class IWebSocket> LovenseToyEventsWebSocketHandler::GetSocket() {
	return this->webSocket;
}


void LovenseToyEventsWebSocketHandler::Shutdown() {
#if WITH_LOVENSE
	if (this->pingTimerHandle.IsValid())
		FLovenseManager::Get()->StopTimer(this->pingTimerHandle);
	this->pingTimerHandle.Invalidate();

	if (this->webSocket) {
		this->webSocket->Close(1000, TEXT("Shutdown"));
	}
	this->webSocket.Reset();
#endif
}

bool LovenseToyEventsWebSocketHandler::SendJsonData(const FString& data) {
	this->webSocket->Send(data);
	return true;
}

void LovenseToyEventsWebSocketHandler::OnConnected() {
//#if WITH_LOVENSE
	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnConnected]"));
	const TCHAR* ProjectName = FApp::GetProjectName();
	FString AppName = FString::Printf(TEXT("%s"), ProjectName);
	this->webSocket->Send(
		FString::Printf(
			TEXT("{type:\"access\",data:{appName:\"%s\"}}"),
			*AppName
		)
	);
//#endif
	//UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnConnected]"));
}

void LovenseToyEventsWebSocketHandler::OnConnectionError(const FString& error) {
	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnConnectionError] [Error: %s]"), *error);
	codeResponse.ExecuteIfBound(-1);
}

void LovenseToyEventsWebSocketHandler::OnConnectionClosed(int32 statusCode, const FString& reason, bool bWasClean) {
	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnConnectionClosed] [Status Code: %d][Reason: %s][Was Clean: %s]"), statusCode, *reason, bWasClean ? TEXT("True") : TEXT("False"));
	codeResponse.ExecuteIfBound(0);
}

void LovenseToyEventsWebSocketHandler::OnMessage(const FString& message) {
//#if WITH_LOVENSE
	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnMessage] [Message: %s]"), *message);

	// Parse response into a json object.
	TSharedPtr<FJsonObject> jsonObject = MakeShareable(new FJsonObject());
	TSharedRef<TJsonReader<TCHAR>> jsonReader = TJsonReaderFactory<TCHAR>::Create(message);
	FJsonSerializer::Deserialize(jsonReader, jsonObject);

	const FString messageType = jsonObject->GetStringField(TEXT("type"));

	if (messageType == TEXT("pong")) {
		double pingPongTime = FPlatformTime::Seconds() - this->pingTimestamp;
		//UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnMessage] [Ping Pong Time: %.2f]"), pingPongTime);
	} else if (messageType == TEXT("battery-changed")) {
		if (true) {
			FString id = jsonObject->GetStringField(TEXT("toyId"));
			const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
			if (!data.IsValid()) return;
			
			int value = data->GetIntegerField(TEXT("value"));
			batteryCallBack.ExecuteIfBound(id,value);
		}
	} else if (messageType == TEXT("toy-status")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		if (!data.IsValid()) return;
		bool connected = data->GetBoolField(TEXT("connected"));
		connectCallBack.ExecuteIfBound(id, connected);
	}
	else if (messageType == TEXT("shake-frequency-changed")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		ELovenseEventType type = ELovenseEventType::SHAKE;
		int value = data->GetIntegerField(TEXT("value"));
		valueCallBack.ExecuteIfBound(id, type, value, 0);
	}
	else if (messageType == TEXT("shake")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		everyShakeCallBack.ExecuteIfBound(id);
	}
	else if (messageType == TEXT("depth-changed")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		ELovenseEventType type = ELovenseEventType::DEPTH;
		int value = data->GetIntegerField(TEXT("value"));
		valueCallBack.ExecuteIfBound(id, type, value, 0);
	}
	else if (messageType == TEXT("function-strength-changed")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		if (!data.IsValid()) return;
		FString func = data->GetStringField(TEXT("function"));
		int value = data->GetIntegerField(TEXT("value"));
		int index = data->GetIntegerField(TEXT("index"));
		ELovenseEventType type = ELovenseEventType::VIBRATE;
		if (func == TEXT("vibration")) {
			type = ELovenseEventType::VIBRATE;
		}
		else if (func == TEXT("rotitaion")) {
			type = ELovenseEventType::ROTATE;
		}
		else if (func == TEXT("thrusting")) {
			type = ELovenseEventType::THRUST;
		}
		else if (func == TEXT("inflation")) {
			type = ELovenseEventType::INFLATION;
		}
		
		valueCallBack.ExecuteIfBound(id, type, value,index);
	}
	else if (messageType == TEXT("toy-list")) {
		const TArray<TSharedPtr<FJsonValue>>* toyList;
		TArray<UELovenseEventsToy*> myList;
		if (jsonObject->TryGetArrayField(TEXT("toyList"), toyList))
		{
			for (const TSharedPtr<FJsonValue>& toyValue : *toyList)
			{
				TSharedPtr<FJsonObject> toyObject = toyValue->AsObject();
				if (toyObject.IsValid() && toyObject->HasField(TEXT("id")) && toyObject->HasField(TEXT("name")))
				{
					FString id = toyObject->GetStringField(TEXT("id"));
					FString name = toyObject->GetStringField(TEXT("name"));
					FString type = toyObject->GetStringField(TEXT("type"));
					FString hVersion = toyObject->GetStringField(TEXT("hVersion"));
					int32 fVersion = toyObject->GetIntegerField(TEXT("fVersion"));
					//FString nickname = toyObject->GetStringField(TEXT("nickname"));
					int32 battery = toyObject->GetIntegerField(TEXT("battery"));
					bool connected = toyObject->GetBoolField(TEXT("connected"));
					UELovenseEventsToy* temp = NewObject<UELovenseEventsToy>();
					temp->SetId(id);
					temp->SetName(name);
					temp->SetType(type);
					temp->SetHVersion(hVersion);
					temp->SetFVersion(fVersion);
					//temp->SetNickName(nickname);
					temp->SetBattery(battery);
					temp->SetConnected(connected);
					myList.Add(temp);
				}
				getToysCallback.ExecuteIfBound(myList);
			}
		}
	} else if (messageType == TEXT("access-granted")) {
		codeResponse.ExecuteIfBound(200);
	} else if (messageType == TEXT("event-closed")) {
		codeResponse.ExecuteIfBound(0); 
	} else if (messageType == TEXT("button-pressed")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		ELovenseButtonEventType type = ELovenseButtonEventType::BUTTON_PRESSED;
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		int index = data->GetIntegerField(TEXT("index"));
		buttonCallBack.ExecuteIfBound(id, type, index);
	} else if (messageType == TEXT("button-up")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		ELovenseButtonEventType type = ELovenseButtonEventType::BUTTON_UP;
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		int index = data->GetIntegerField(TEXT("index"));
		buttonCallBack.ExecuteIfBound(id, type, index);
	} else if (messageType == TEXT("button-down")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		ELovenseButtonEventType type = ELovenseButtonEventType::BUTTON_DOWN;
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		int index = data->GetIntegerField(TEXT("index"));
		buttonCallBack.ExecuteIfBound(id, type, index);
	}
	else if (messageType == TEXT("motion-changed")) {
		FString id = jsonObject->GetStringField(TEXT("toyId"));
		const TSharedPtr<FJsonObject> data = jsonObject->GetObjectField(TEXT("data"));
		const TArray<TSharedPtr<FJsonValue>>& motionData = data->GetArrayField(TEXT("motionData"));
		for (const TSharedPtr<FJsonValue> motionDt : motionData) {
			int direction = motionDt->AsObject()->GetIntegerField(TEXT("direction"));
			int position = motionDt->AsObject()->GetIntegerField(TEXT("position"));
			int speed = motionDt->AsObject()->GetIntegerField(TEXT("speed"));
			motionChangedCallback.ExecuteIfBound(id, direction == 0, position, speed);
		}

		//

	}
//#endif
//	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnMessage] [Message: %s]"), *message);

}

void LovenseToyEventsWebSocketHandler::OnRawMessage(const void* data, SIZE_T size, SIZE_T bytesRemaining) {
//#if WITH_LOVENSE
	TArray<ANSICHAR> dataArray;
	dataArray.SetNumUninitialized(size + 1);
	FMemory::Memcpy(dataArray.GetData(), data, size);
	dataArray.Add(TEXT('\0'));
	FString dataString = FString(dataArray.GetData());
	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnRawMessage] [Data: %s][Size: %llu][Bytes Remaining: %llu]"), *dataString, size, bytesRemaining);
//#endif

}

void LovenseToyEventsWebSocketHandler::OnMessageSent(const FString& messageString) {
//#if WITH_LOVENSE
	UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnMessageSent] [Message String: %s]"), *messageString);
//#endif
	//UE_LOG(LogLovenseIntegration, Log, TEXT("[LovenseToyEventsWebSocketHandler::OnMessageSent] [Message String: %s]"), *messageString);
}

void LovenseToyEventsWebSocketHandler::Ping() {
	if (this->webSocket != NULL && !this->webSocket->IsConnected()) {
		this->webSocket->Connect();
		return;
	}
	if (this->webSocket != NULL) {
		this->pingTimestamp = FPlatformTime::Seconds();
		this->webSocket->Send(TEXT("{type:\"ping\"}"));
	}
}
