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


#include "SessionSubsystem.h"

#include "GameFramework/PlayerController.h"
#include "Interfaces/OnlineExternalUIInterface.h"
#include "Kismet/GameplayStatics.h"
#include "Online/OnlineSessionNames.h"
#include "OnlineSessionSettings.h"
#include "OnlineSubsystem.h"

#define LOCTEXT_NAMESPACE "Session"

// Namespace NOMME et non anonyme : le build unity concatene les .cpp du module,
// et deux constantes homonymes dans deux fichiers deviennent une redefinition
// qui ne sort qu'au premier rebuild complet.
namespace SessionSubsystemConstants
{
	/**
	 * Cle plantee dans les reglages de session et redemandee a la recherche.
	 * Sans elle on tomberait sur les parties de n'importe quel autre jeu partageant
	 * l'AppId de test 480 (Spacewar), qui sont legion.
	 */
	const FName GameKeyName(TEXT("EMBERWILD"));
	const TCHAR* GameKeyValue = TEXT("Coop");

	/** Le service charge quand Steam n'est pas disponible. */
	const FName NullServiceName(TEXT("NULL"));
}

void USessionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);

	IOnlineSubsystem* Online = IOnlineSubsystem::Get();
	if (!Online)
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : aucun OnlineSubsystem charge, la coop sera indisponible."));
		return;
	}

	SessionInterface = Online->GetSessionInterface();
	if (!SessionInterface.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : le service [%s] n'expose pas d'interface de session."), *Online->GetSubsystemName().ToString());
		return;
	}

	InviteAcceptedHandle = SessionInterface->AddOnSessionUserInviteAcceptedDelegate_Handle(
		FOnSessionUserInviteAcceptedDelegate::CreateUObject(this, &USessionSubsystem::HandleInviteAccepted));

	UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : service en ligne [%s], mode LAN [%d]."),
		*Online->GetSubsystemName().ToString(), IsLanMode() ? 1 : 0);
}

void USessionSubsystem::Deinitialize()
{
	if (SessionInterface.IsValid())
	{
		ClearPendingDelegates();
		SessionInterface->ClearOnSessionUserInviteAcceptedDelegate_Handle(InviteAcceptedHandle);
	}

	SearchSettings.Reset();
	SessionInterface.Reset();

	Super::Deinitialize();
}

bool USessionSubsystem::IsLanMode() const
{
	const IOnlineSubsystem* Online = IOnlineSubsystem::Get();
	return !Online || Online->GetSubsystemName() == SessionSubsystemConstants::NullServiceName;
}

FString USessionSubsystem::GetOnlineServiceName() const
{
	const IOnlineSubsystem* Online = IOnlineSubsystem::Get();
	return Online ? Online->GetSubsystemName().ToString() : TEXT("None");
}

bool USessionSubsystem::IsSessionActive() const
{
	return SessionInterface.IsValid() && SessionInterface->GetNamedSession(NAME_GameSession) != nullptr;
}

int32 USessionSubsystem::GetFoundSessionCount() const
{
	return SearchSettings.IsValid() ? SearchSettings->SearchResults.Num() : 0;
}

void USessionSubsystem::ClearPendingDelegates()
{
	if (!SessionInterface.IsValid())
	{
		return;
	}

	SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteHandle);
	SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteHandle);
	SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteHandle);
	SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteHandle);
}

// ----------------------------------------------------------------------------
// Hebergement
// ----------------------------------------------------------------------------

void USessionSubsystem::HostSession(const TSoftObjectPtr<UWorld>& Level, int32 MaxPlayers)
{
	if (!SessionInterface.IsValid())
	{
		OnHostFailed.Broadcast(LOCTEXT("HostNoService", "No online service available."));
		return;
	}

	if (Level.IsNull())
	{
		UE_LOG(LogTemp, Error, TEXT("USessionSubsystem::HostSession : aucune map fournie."));
		OnHostFailed.Broadcast(LOCTEXT("HostNoLevel", "No level configured for the game."));
		return;
	}

	PendingHostLevel = Level;
	PendingMaxPlayers = FMath::Clamp(MaxPlayers, 1, 16);

	// Une session survit a un retour au menu si personne ne l'a detruite. Le
	// moteur en refuse alors une seconde du meme nom, et l'echec est muet.
	if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
	{
		bCreateAfterDestroy = true;
		DestroySessionCompleteHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(
			FOnDestroySessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleDestroySessionComplete));

		SessionInterface->DestroySession(NAME_GameSession);
		return;
	}

	CreateSessionNow();
}

void USessionSubsystem::CreateSessionNow()
{
	FOnlineSessionSettings Settings;

	Settings.NumPublicConnections = PendingMaxPlayers;
	Settings.NumPrivateConnections = 0;
	Settings.bIsLANMatch = IsLanMode();
	Settings.bShouldAdvertise = true;
	Settings.bAllowJoinInProgress = true;
	Settings.bAllowJoinViaPresence = true;
	Settings.bAllowInvites = true;
	Settings.bUsesPresence = true;

	// LE drapeau a ne pas oublier. Steam ne route le trafic P2P que par un LOBBY :
	// sans lui la session est bien creee, elle apparait meme dans une recherche,
	// mais aucune connexion n'aboutit en dehors du LAN.
	Settings.bUseLobbiesIfAvailable = true;
	Settings.bUseLobbiesVoiceChatIfAvailable = false;

	Settings.bUsesStats = false;
	Settings.bAntiCheatProtected = false;

	Settings.Set(SETTING_MAPNAME, PendingHostLevel.GetAssetName(), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);
	Settings.Set(SessionSubsystemConstants::GameKeyName, FString(SessionSubsystemConstants::GameKeyValue), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing);

	CreateSessionCompleteHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle(
		FOnCreateSessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleCreateSessionComplete));

	// CreateSession rend false quand l'appel n'a meme pas pu partir : dans ce cas
	// le callback ne viendra jamais, il faut donc debrancher et repondre ici.
	if (!SessionInterface->CreateSession(0, NAME_GameSession, Settings))
	{
		SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteHandle);
		UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : CreateSession a ete refusee immediatement."));
		OnHostFailed.Broadcast(LOCTEXT("HostRefused", "Could not create the session."));
	}
}

void USessionSubsystem::HandleCreateSessionComplete(FName SessionName, bool bWasSuccessful)
{
	SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteHandle);

	if (!bWasSuccessful)
	{
		UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : creation de la session [%s] echouee."), *SessionName.ToString());
		OnHostFailed.Broadcast(LOCTEXT("HostFailed", "Could not create the session."));
		return;
	}

	UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : session [%s] creee pour %d joueurs."), *SessionName.ToString(), PendingMaxPlayers);

	// "listen" fait de cette instance un serveur d'ecoute : l'hote joue ET
	// heberge dans le meme process, ce qui est exactement le modele voulu pour
	// une coop a quatre. Sans cette option la map s'ouvre en solo et les clients
	// se heurtent a un port ferme.
	UGameplayStatics::OpenLevelBySoftObjectPtr(GetGameInstance(), PendingHostLevel, /*bAbsolute=*/true, TEXT("listen"));
}

// ----------------------------------------------------------------------------
// Recherche
// ----------------------------------------------------------------------------

void USessionSubsystem::FindSessions(int32 MaxResults)
{
	if (!SessionInterface.IsValid())
	{
		OnSearchFinished.Broadcast(false, 0);
		return;
	}

	SearchSettings = MakeShared<FOnlineSessionSearch>();
	SearchSettings->MaxSearchResults = FMath::Max(1, MaxResults);
	SearchSettings->bIsLanQuery = IsLanMode();

	// Cote Steam les parties sont des lobbies, pas des serveurs annonces : sans
	// ce filtre la recherche part interroger le service de serveurs dedies et
	// ne rend jamais rien. En LAN la notion n'existe pas, on l'omet.
	if (!SearchSettings->bIsLanQuery)
	{
		SearchSettings->QuerySettings.Set(SEARCH_LOBBIES, true, EOnlineComparisonOp::Equals);
	}

	SearchSettings->QuerySettings.Set(SessionSubsystemConstants::GameKeyName, FString(SessionSubsystemConstants::GameKeyValue), EOnlineComparisonOp::Equals);

	FindSessionsCompleteHandle = SessionInterface->AddOnFindSessionsCompleteDelegate_Handle(
		FOnFindSessionsCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleFindSessionsComplete));

	if (!SessionInterface->FindSessions(0, SearchSettings.ToSharedRef()))
	{
		SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteHandle);
		OnSearchFinished.Broadcast(false, 0);
	}
}

void USessionSubsystem::HandleFindSessionsComplete(bool bWasSuccessful)
{
	SessionInterface->ClearOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteHandle);

	const int32 Count = GetFoundSessionCount();
	UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : recherche terminee (succes %d), %d partie(s)."), bWasSuccessful ? 1 : 0, Count);

	OnSearchFinished.Broadcast(bWasSuccessful, Count);
}

// ----------------------------------------------------------------------------
// Connexion
// ----------------------------------------------------------------------------

void USessionSubsystem::JoinFoundSession(int32 ResultIndex)
{
	if (!SearchSettings.IsValid() || !SearchSettings->SearchResults.IsValidIndex(ResultIndex))
	{
		OnJoinFailed.Broadcast(LOCTEXT("JoinNoSession", "No game found to join."));
		return;
	}

	JoinSearchResult(SearchSettings->SearchResults[ResultIndex]);
}

void USessionSubsystem::EmberJoin()
{
	UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : EmberJoin, recherche d'une partie..."));

	// On se branche pour un seul tour : la commande enchaine recherche puis
	// connexion, ce que le menu ne fait plus depuis que l'on ne rejoint que
	// par invitation.
	OnSearchFinished.AddDynamic(this, &USessionSubsystem::HandleExecJoinSearchFinished);
	FindSessions();
}

void USessionSubsystem::HandleExecJoinSearchFinished(bool bWasSuccessful, int32 SessionCount)
{
	OnSearchFinished.RemoveDynamic(this, &USessionSubsystem::HandleExecJoinSearchFinished);

	if (!bWasSuccessful || SessionCount <= 0)
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : EmberJoin n'a trouve aucune partie."));
		return;
	}

	JoinFoundSession(0);
}

bool USessionSubsystem::CanInvite() const
{
	if (!IsSessionActive() || IsLanMode())
	{
		return false;
	}

	const IOnlineSubsystem* Online = IOnlineSubsystem::Get();
	return Online && Online->GetExternalUIInterface().IsValid();
}

bool USessionSubsystem::ShowInviteUI()
{
	IOnlineSubsystem* Online = IOnlineSubsystem::Get();
	if (!Online)
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : pas de service en ligne, invitation impossible."));
		return false;
	}

	const IOnlineExternalUIPtr ExternalUI = Online->GetExternalUIInterface();
	if (!ExternalUI.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : le service [%s] n'a pas d'overlay."), *Online->GetSubsystemName().ToString());
		return false;
	}

	// Sans session ouverte, l'overlay s'affiche mais l'ami invite arrive sur
	// une partie qui n'existe pas. Mieux vaut refuser franchement.
	if (!IsSessionActive())
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : aucune session ouverte, invitation impossible."));
		return false;
	}

	return ExternalUI->ShowInviteUI(0, NAME_GameSession);
}

void USessionSubsystem::JoinSearchResult(const FOnlineSessionSearchResult& Result)
{
	if (!SessionInterface.IsValid())
	{
		OnJoinFailed.Broadcast(LOCTEXT("JoinNoService", "No online service available."));
		return;
	}

	if (!Result.IsValid())
	{
		OnJoinFailed.Broadcast(LOCTEXT("JoinInvalid", "This game is no longer available."));
		return;
	}

	// Rejoindre alors qu'on heberge deja laisserait deux sessions ouvertes et
	// le voyage echouerait sans message.
	if (SessionInterface->GetNamedSession(NAME_GameSession) != nullptr)
	{
		SessionInterface->DestroySession(NAME_GameSession);
	}

	JoinSessionCompleteHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle(
		FOnJoinSessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleJoinSessionComplete));

	if (!SessionInterface->JoinSession(0, NAME_GameSession, Result))
	{
		SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteHandle);
		OnJoinFailed.Broadcast(LOCTEXT("JoinRefused", "Could not join this game."));
	}
}

void USessionSubsystem::HandleJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
	SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteHandle);

	if (Result != EOnJoinSessionCompleteResult::Success)
	{
		UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : JoinSession a echoue (code %d)."), static_cast<int32>(Result));

		// On distingue le seul cas que le joueur peut corriger lui-meme du reste,
		// qui ne lui apprendrait rien.
		const FText Reason = (Result == EOnJoinSessionCompleteResult::SessionIsFull)
			? LOCTEXT("JoinFull", "This game is full.")
			: LOCTEXT("JoinFailed", "Could not join this game.");

		OnJoinFailed.Broadcast(Reason);
		return;
	}

	// L'adresse n'est connue qu'ici : c'est le service en ligne qui la fabrique,
	// et sous Steam ce n'est meme pas une IP mais un identifiant de relais.
	FString ConnectString;
	if (!SessionInterface->GetResolvedConnectString(SessionName, ConnectString))
	{
		UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : adresse de l'hote introuvable."));
		OnJoinFailed.Broadcast(LOCTEXT("JoinNoAddress", "Could not reach the host."));
		return;
	}

	// On ne voyage PAS tout de suite : on annonce, on laisse le controller
	// courant fondre au noir, puis on part. Sans ce delai, accepter une
	// invitation depuis le menu coupe l'image d'un coup -- le seul endroit du
	// jeu ou une transition n'etait pas fondue.
	PendingTravelURL = ConnectString;
	OnTravelPending.Broadcast();

	UGameInstance* OwningGameInstance = GetGameInstance();
	if (TravelFadeDuration <= 0.f || !OwningGameInstance)
	{
		CommitPendingTravel();
		return;
	}

	// Le TimerManager du GameInstance et non celui du monde : le monde courant
	// est justement celui qu'on s'apprete a quitter.
	OwningGameInstance->GetTimerManager().SetTimer(
		TravelTimer, this, &USessionSubsystem::CommitPendingTravel,
		TravelFadeDuration, /*bLoop=*/false);
}

void USessionSubsystem::CommitPendingTravel()
{
	if (PendingTravelURL.IsEmpty())
	{
		return;
	}

	const FString TravelURL = MoveTemp(PendingTravelURL);
	PendingTravelURL.Reset();

	APlayerController* Controller = GetGameInstance() ? GetGameInstance()->GetFirstLocalPlayerController() : nullptr;
	if (!Controller)
	{
		OnJoinFailed.Broadcast(LOCTEXT("JoinNoController", "Could not reach the host."));
		return;
	}

	UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : voyage vers [%s]."), *TravelURL);
	Controller->ClientTravel(TravelURL, TRAVEL_Absolute);
}

void USessionSubsystem::HandleInviteAccepted(const bool bWasSuccessful, const int32 ControllerId, FUniqueNetIdPtr UserId, const FOnlineSessionSearchResult& InviteResult)
{
	if (!bWasSuccessful)
	{
		UE_LOG(LogTemp, Warning, TEXT("USessionSubsystem : invitation acceptee mais illisible."));
		return;
	}

	UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : invitation acceptee, connexion en cours."));
	JoinSearchResult(InviteResult);
}

// ----------------------------------------------------------------------------
// Sortie
// ----------------------------------------------------------------------------

void USessionSubsystem::LeaveSession()
{
	if (!SessionInterface.IsValid() || SessionInterface->GetNamedSession(NAME_GameSession) == nullptr)
	{
		return;
	}

	// Pas de reprise apres coup ici : on quitte, point.
	bCreateAfterDestroy = false;

	DestroySessionCompleteHandle = SessionInterface->AddOnDestroySessionCompleteDelegate_Handle(
		FOnDestroySessionCompleteDelegate::CreateUObject(this, &USessionSubsystem::HandleDestroySessionComplete));

	SessionInterface->DestroySession(NAME_GameSession);
}

void USessionSubsystem::HandleDestroySessionComplete(FName SessionName, bool bWasSuccessful)
{
	SessionInterface->ClearOnDestroySessionCompleteDelegate_Handle(DestroySessionCompleteHandle);

	if (!bCreateAfterDestroy)
	{
		return;
	}

	bCreateAfterDestroy = false;

	if (!bWasSuccessful)
	{
		UE_LOG(LogTemp, Error, TEXT("USessionSubsystem : impossible de detruire la session [%s], hebergement annule."), *SessionName.ToString());
		OnHostFailed.Broadcast(LOCTEXT("HostStaleSession", "A previous game is still closing. Try again."));
		return;
	}

	CreateSessionNow();
}

#undef LOCTEXT_NAMESPACE
