#include "UnrealProject.h"
#include "SessionManager.h"
#include "DefaultGameInstance.h"
#include "MatchMaking.h"

#if PLATFORM_SPECIFIC_WIN == 0 
// Steam includes + disable warnings for unsafe functions
#pragma warning(push)
#pragma warning(disable:4996)
#include "steam/steam_api.h"
#include "steam/isteamutils.h"
#include "steam/isteamfriends.h"

#pragma warning(pop)
#endif

// For avatar to UTexture2D
#include "ImageUtils.h"

#include <string>

#define POST_DESTROY_CREATE 1
#define POST_DESTROY_JOIN 2
#define POST_DESTROY_FIND 3


namespace
{
	uint64 NetIDToInt(const FUniqueNetId& id)
	{
		std::wstring str = id.ToString().GetCharArray().GetData();
		uint64 asInt = 0;
		str >> asInt;
		return asInt;
	}
}


static UTexture2D* defaultAvatar;
USessionManager::USessionManager()
{
	onCreateSessionCompleteDelegate = FOnCreateSessionCompleteDelegate::CreateUObject(this, &USessionManager::m_OnCreateSessionComplete);
	onDestroySessionCompleteDelegate = FOnDestroySessionCompleteDelegate::CreateUObject(this, &USessionManager::m_OnDestroySessionComplete);
	onStartSessionCompleteDelegate = FOnStartSessionCompleteDelegate::CreateUObject(this, &USessionManager::m_OnStartOnlineGameComplete);
	onFindSessionsCompleteDelegate = FOnFindSessionsCompleteDelegate::CreateUObject(this, &USessionManager::m_OnFindSessionsComplete);
	onJoinSessionCompleteDelegate = FOnJoinSessionCompleteDelegate::CreateUObject(this, &USessionManager::m_OnJoinSessionComplete);
	onPingSearchResultsCompleteDelegate = FOnPingSearchResultsCompleteDelegate::CreateUObject(this, &USessionManager::m_OnPingSearchResultsComplete);

	// Default avatar in case some doesn't have an avatar or the online subsystem does not provide one
	defaultAvatar = ConstructorHelpers::FObjectFinder<UTexture2D>(TEXT("/Game/Assets/GUI/564e176e1a279230")).Object;

	m_postDestroyAction = 0;

	sessionSearch = MakeShareable(new FOnlineSessionSearch());
	sessionSearch->bIsLanQuery = false;
	sessionSearch->MaxSearchResults = 100;
	sessionSearch->PingBucketSize = 50;
	sessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);

	// Cached online settings
	m_onlineSystem = IOnlineSubsystem::Get();
	check(m_onlineSystem);
	m_session = m_onlineSystem->GetSessionInterface();
	check(m_session.IsValid());

	GPRINT("Session manager created with subsystem: " + m_onlineSystem->GetInstanceName());

	uint32 sessionCount = m_session->GetNumSessions();
	GPRINT(sessionCount + " initial sessions found.");

	// Set default session settings
	sessionSettings.bIsLANMatch = false;
	sessionSettings.bUsesPresence = true;
	sessionSettings.NumPublicConnections = 4;
	sessionSettings.NumPrivateConnections = 0;
	sessionSettings.bAllowInvites = true;
	sessionSettings.bAllowJoinInProgress = true;
	sessionSettings.bShouldAdvertise = true;
	sessionSettings.bAllowJoinViaPresence = true;
	sessionSettings.bAllowJoinViaPresenceFriendsOnly = false;

	sessionSettings.Set(SETTING_MAPNAME, FString("No Map"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);

	// Register ping callback
	onPingSearchResultsCompleteDelegateHandle = m_session->AddOnPingSearchResultsCompleteDelegate_Handle(onPingSearchResultsCompleteDelegate);
}
void USessionManager::Shutdown()
{
	m_session->ClearOnPingSearchResultsCompleteDelegate_Handle(onPingSearchResultsCompleteDelegateHandle);

	// Clean up sessions
	uint32 sessionCount = m_session->GetNumSessions();
	if(sessionCount > 0)
	{
		DestroySession();
	}

	// Close net connections
	CloseNetConnections();
}

void USessionManager::CreateSession(bool destroyOld)
{
	if(destroyOld && SessionExists())
	{
		m_postDestroyAction = POST_DESTROY_CREATE;
		DestroySession();
		return;
	}

	APlayerController* pc = m_instance->GetFirstLocalPlayerController();
	if(!pc)
	{
		GERROR("There is no player controller!!");
		onCreateSessionComplete.Broadcast(false);
		return;
	}

	// Register delegate
	onCreateSessionCompleteDelegateHandle = m_session->AddOnCreateSessionCompleteDelegate_Handle(onCreateSessionCompleteDelegate);

	const FUniqueNetId& netID = *pc->PlayerState->UniqueId;
	if(!m_session->CreateSession(netID, GameSessionName, sessionSettings))
	{
		GERROR("m_session->CreateSession call failed!");
		onCreateSessionComplete.Broadcast(false);
		m_session->ClearOnCreateSessionCompleteDelegate_Handle(onCreateSessionCompleteDelegateHandle);
	}
}
void USessionManager::FindSessions(bool destroyOld)
{
 	APlayerController* pc = m_instance->GetFirstLocalPlayerController();
	if(!pc)
	{
		GERROR("There is no player controller!!");
		onFindSessionsComplete.Broadcast(false); 
		return;
	}

	if(destroyOld && SessionExists())
	{
		m_postDestroyAction = POST_DESTROY_FIND;
		DestroySession();
		return;
	}

	// Register delegate
	onFindSessionsCompleteDelegateHandle = m_session->AddOnFindSessionsCompleteDelegate_Handle(onFindSessionsCompleteDelegate);

	const FUniqueNetId& netID = *pc->PlayerState->UniqueId;
	if(!m_session->FindSessions(netID, sessionSearch.ToSharedRef()))
	{
		GERROR("m_session->CreateSession call failed!");
		onCreateSessionComplete.Broadcast(false);
		m_session->ClearOnCreateSessionCompleteDelegate_Handle(onCreateSessionCompleteDelegateHandle);
	}
}
void USessionManager::JoinSession(const FOnlineSessionSearchResult& searchResult, bool destroyOld)
{
	joinSessionResult = searchResult;

	if(destroyOld && SessionExists())
	{
		m_postDestroyAction = POST_DESTROY_JOIN;
		DestroySession();
		return;
	}

	APlayerController* pc = m_instance->GetFirstLocalPlayerController();
	if(!pc)
	{
		GERROR("There is no player controller!!");
		onFindSessionsComplete.Broadcast(false);
		return;
	}

	// Register delegate
	onJoinSessionCompleteDelegateHandle = m_session->AddOnJoinSessionCompleteDelegate_Handle(onJoinSessionCompleteDelegate);

	const FUniqueNetId& netID = *pc->PlayerState->UniqueId;
	if(!m_session->JoinSession(netID, GameSessionName, joinSessionResult))
	{
		GERROR("m_session->CreateSession call failed!");
		onCreateSessionComplete.Broadcast(false);
		m_session->ClearOnJoinSessionCompleteDelegate_Handle(onJoinSessionCompleteDelegateHandle);
	}
}
void USessionManager::DestroySession()
{
	onDestroySessionCompleteDelegateHandle = m_session->AddOnDestroySessionCompleteDelegate_Handle(onDestroySessionCompleteDelegate);

	if(!m_session->DestroySession(GameSessionName))
	{
		GERROR("m_session->DestroySession call failed!");
		onDestroySessionComplete.Broadcast(false);
		m_session->ClearOnDestroySessionCompleteDelegate_Handle(onDestroySessionCompleteDelegateHandle);
	}
}
void USessionManager::PingResult(const FOnlineSessionSearchResult& result)
{
	if(!m_session->PingSearchResults(result))
	{
		onPingComplete.Broadcast(false);
	}
}

bool USessionManager::SessionExists() const
{
	return m_session->GetNumSessions() > 0;
}

void USessionManager::StartSession()
{
	m_session->StartSession(GameSessionName);
}
void USessionManager::EndSession()
{
	m_session->EndSession(GameSessionName);
}

bool USessionManager::HasNetConnection()
{
	// Server?
	UWorld* world = m_instance->GetWorld();
	if(world->GetNetDriver())
		return true;

	// Client?
	APlayerController* pc = m_instance->GetFirstLocalPlayerController();
	if(pc->GetNetConnection())
		return true;
	return false;
}

void USessionManager::CloseNetConnections()
{
	if(!m_instance)
	{
		GERROR("INSTANCE NOT SET/INVALID??");
		return;
	}

	// Close the network connection
	UWorld* world = m_instance->GetWorld();
	if(!world)
	{
		GERROR("Can't close net connections, No world set");
		return;
	}
	ENetMode netMode = world->GetNetMode();
	GPRINT("Leaving client's net mode = " + netMode);

	UNetDriver* netDriver = world->GetNetDriver();

	APlayerController* pc = m_instance->GetFirstLocalPlayerController();
	UNetConnection* netConnection = pc ? pc->GetNetConnection() : nullptr;
	if(netConnection)
	{
		check(netMode == NM_Client);
		// Close the client connection
		netConnection->Close();
	}
	else if(netDriver)
	{
		check(netMode == NM_ListenServer);
		// Shutdown the net driver
		world->SetNetDriver(nullptr);
		GEngine->DestroyNamedNetDriver(world, NAME_GameNetDriver);
	}
}

void USessionManager::AcceptUserInvite(const bool bWasSuccess, const int32 ControllerId, TSharedPtr<const FUniqueNetId> Us, const FOnlineSessionSearchResult& res)
{
	onAcceptInvite.Broadcast(bWasSuccess, res);
}

FString USessionManager::GetPlayerName(const FUniqueNetId& netID)
{
	FString ret;
#if PLATFORM_SPECIFIC_WIN == 0
	ISteamFriends* friends = SteamFriends();
	if(friends)
	{
		FString steamIDString = netID.ToString();
		CSteamID steamID((uint64)_wtoi64(steamIDString.GetCharArray().GetData()));
		const char* name = SteamFriends()->GetFriendPersonaName(steamID);
		if(!name)
			ret = FString(TEXT("<unknown>"));
		else
			ret = FString(name);
	}
	else
#endif
	{
		IOnlineIdentityPtr identity = m_onlineSystem->GetIdentityInterface();
		ret = identity->GetPlayerNickname(netID);
	}
	return ret;
}

FString USessionManager::GetSteamID(const FUniqueNetId& netID)
{
	FString ret;
#if PLATFORM_SPECIFIC_WIN == 0
	ISteamFriends* friends = SteamFriends();
	if(friends)
	{
		FString steamIDString = netID.ToString();
		CSteamID steamID((uint64)_wtoi64(steamIDString.GetCharArray().GetData()));
		return steamIDString;
	}
#endif
	return ret;
}

UTexture2D* USessionManager::GetPlayerAvatar(const FUniqueNetId& netID)
{
#if PLATFORM_SPECIFIC_WIN == 0
	FString idString = netID.GetHexEncodedString();
	if(m_profilePictures.Contains(idString))
	{
		UTexture2D* ret = m_profilePictures[idString];
		if(ret)
		{
			return ret;
		}
		else
		{
			m_profilePictures.Remove(idString);
		}
	}

	FString steamIDString = netID.ToString();
	CSteamID steamID((uint64)_wtoi64(steamIDString.GetCharArray().GetData()));

	ISteamFriends* steamFriends = SteamFriends();
	if(!steamFriends)
		return defaultAvatar;
	int imgId = steamFriends->GetMediumFriendAvatar(steamID);
	if(imgId == -1)
		return defaultAvatar;

	uint32 w, h;
	if(!SteamUtils()->GetImageSize(imgId, &w, &h))
		return defaultAvatar;
	TArray<FColor> imagePixels;
	imagePixels.SetNumUninitialized(w * h);
	if(!SteamUtils()->GetImageRGBA(imgId, (uint8*)imagePixels.GetData(), w * h * 4))
		return defaultAvatar;

	for(uint32 i = 0; i < (w*h); i++)
	{
		imagePixels[i] = FColor(imagePixels[i].B, imagePixels[i].G, imagePixels[i].R, imagePixels[i].A);
	}

	FString imageName = FString("T_Avatar_") + netID.ToString();
	FCreateTexture2DParameters params;
	params.bDeferCompression = false;
	params.bSRGB = false;
	params.bUseAlpha = true;
	UTexture2D* tex = m_CreateTexture2D(w, h, imagePixels, true);
	//FImageUtils::CreateTexture2D(w, h, imagePixels, GetWorld(), imageName, EObjectFlags)0, params);
	check(tex);
	m_profilePictures.Add(idString, tex);
	return tex;
#else
	return defaultAvatar;
#endif
}

// Solution to ConstructTexture2D not being supported on consoles.
// From: https://answers.unrealengine.com/questions/33571/fimageutilscreatetexture2d-causes-brief-hang-is-th.html
UTexture2D* USessionManager::m_CreateTexture2D(const int32 srcWidth, const int32 srcHeight, const TArray<FColor>& srcColor, const bool useAlpha)
{
	UTexture2D* texture = UTexture2D::CreateTransient(srcWidth, srcHeight, PF_B8G8R8A8);

	uint8* MipData = static_cast<uint8*>(texture->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE));

	// Create base mip.
	uint8* DestPtr = NULL;
	const FColor* SrcPtr = NULL;
	for(int32 y = 0; y < srcHeight; y++)
	{
		DestPtr = &MipData[(srcHeight - 1 - y) * srcWidth * sizeof(FColor)];
		SrcPtr = const_cast<FColor*>(&srcColor[(srcHeight - 1 - y) * srcWidth]);
		for(int32 x = 0; x < srcWidth; x++)
		{
			*DestPtr++ = SrcPtr->B;
			*DestPtr++ = SrcPtr->G;
			*DestPtr++ = SrcPtr->R;
			if(useAlpha)
			{
				*DestPtr++ = SrcPtr->A;
			}
			else
			{
				*DestPtr++ = 0xFF;
			}
			SrcPtr++;
		}
	}

	// Unlock the texture
	texture->PlatformData->Mips[0].BulkData.Unlock();
	texture->UpdateResource();

	return texture;
}

void USessionManager::m_OnCreateSessionComplete(FName sessionName, bool successful)
{
	GWARNING("OnCreateSessionComplete " + sessionName.ToString().GetCharArray().GetData() + ", " + successful);

	m_session->ClearOnCreateSessionCompleteDelegate_Handle(onCreateSessionCompleteDelegateHandle);

	// Start listen server
	UWorld* world = m_instance->GetWorld();
	if(world->Listen(m_listenURL))
	{
		GPRINT("Started listen server on " + m_listenURL.ToString());
	}
	else
	{
		successful = false;
		GERROR("Failed to start listen server!");
	}

	onCreateSessionComplete.Broadcast(successful);
}
void USessionManager::m_OnStartOnlineGameComplete(FName sessionName, bool successful)
{
	GWARNING("OnStartOnlineGameComplete " + sessionName.ToString().GetCharArray().GetData() + ", " + successful);
	m_session->ClearOnStartSessionCompleteDelegate_Handle(onStartSessionCompleteDelegateHandle);
}
void USessionManager::m_OnFindSessionsComplete(bool successful)
{
	GWARNING("OnFindSessionsComplete " + successful + 
		" with " + sessionSearch->SearchResults.Num() + " sessions");

	m_session->ClearOnFindSessionsCompleteDelegate_Handle(onFindSessionsCompleteDelegateHandle);
	onFindSessionsComplete.Broadcast(true);
}
void USessionManager::m_OnJoinSessionComplete(FName sessionName, EOnJoinSessionCompleteResult::Type result)
{
	GWARNING("OnJoinSessionComplete " + sessionName.ToString().GetCharArray().GetData() + ", " + int32(result));

	m_session->ClearOnJoinSessionCompleteDelegate_Handle(onJoinSessionCompleteDelegateHandle);
	if(result == EOnJoinSessionCompleteResult::Success)
	{
		if(!m_session->GetResolvedConnectString(joinSessionResult, GamePort, joinConnectionString))
			result = EOnJoinSessionCompleteResult::CouldNotRetrieveAddress;
		else if(joinConnectionString.IsEmpty())
			result = EOnJoinSessionCompleteResult::CouldNotRetrieveAddress;
		else
			GPRINT("Resolved joined session address to: " + joinConnectionString);
	}
	onJoinSessionComplete.Broadcast(result);
}
void USessionManager::m_OnDestroySessionComplete(FName sessionName, bool successful)
{
	GWARNING("OnDestroySessionComplete " + sessionName.ToString().GetCharArray().GetData() + ", " + successful);

	m_session->ClearOnDestroySessionCompleteDelegate_Handle(onDestroySessionCompleteDelegateHandle);

	if(m_postDestroyAction != 0)
	{
		if(m_postDestroyAction == POST_DESTROY_CREATE)
		{
			CreateSession(false);
		}
		else if(m_postDestroyAction == POST_DESTROY_JOIN)
		{
			JoinSession(joinSessionResult, false);
		}
		else if(m_postDestroyAction == POST_DESTROY_FIND)
		{
			FindSessions(false);
		}
		m_postDestroyAction = 0;
		return;
	}

	onDestroySessionComplete.Broadcast(successful);
}
void USessionManager::m_OnPingSearchResultsComplete(bool successful)
{
	onPingComplete.Broadcast(successful);
}
