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

#pragma once

#include "CoreMinimal.h"
#include "Interfaces/OnlineSessionInterface.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "SessionSubsystem.generated.h"

/**
 * Raison d'un echec, deja redigee pour etre affichee telle quelle.
 *
 * Il n'existe volontairement PAS de delegue de succes symetrique : quand
 * l'hebergement ou la connexion reussit, la map change et tout ce qui ecoutait
 * est detruit dans la seconde. Un delegue de succes serait donc soit jamais lu,
 * soit lu par un widget qui n'existe deja plus.
 */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionError, FText, Reason);

/** Fin d'une recherche. SessionCount vaut 0 si personne n'heberge. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSessionSearchFinished, bool, bWasSuccessful, int32, SessionCount);

/**
 * La connexion a abouti, le voyage part dans un instant.
 *
 * Existe uniquement pour laisser le temps d'un fondu : le sous-systeme sait ou
 * aller, mais le voile appartient au PlayerController -- et une invitation peut
 * etre acceptee depuis le menu comme en pleine partie, donc depuis deux
 * controllers differents. Chacun s'abonne et fond a sa maniere.
 */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSessionTravelPending);

/**
 * Hebergement et connexion : creer une partie, la trouver, la rejoindre.
 *
 * GameInstanceSubsystem et pas un composant : une session doit survivre au
 * changement de map, puisque c'est justement elle qui le provoque. Le critere
 * habituel du projet -- "si le pawn meurt, est-ce que ca doit survivre ?" --
 * la place au meme endroit que les reglages.
 *
 * Le C++ ici ne parle JAMAIS de Steam. Tout passe par IOnlineSubsystem, dont
 * l'implementation est choisie par DefaultEngine.ini. Consequence directe : le
 * meme code marche en LAN via OnlineSubsystemNULL, ce qui permet de tester a
 * deux instances sur la meme machine sans client Steam.
 */
UCLASS()
class EMBERWILD_API USessionSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:
	/** Coop a quatre : la valeur de reference du projet. */
	static constexpr int32 DefaultMaxPlayers = 4;

	virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	virtual void Deinitialize() override;

	/**
	 * Cree la session puis ouvre la map en serveur d'ecoute.
	 *
	 * C'est le sous-systeme qui charge la map et non l'appelant, parce que la
	 * creation est ASYNCHRONE : entre le clic et la reponse de Steam il peut
	 * s'ecouler une seconde, et ouvrir la map avant d'avoir la session donnerait
	 * une partie que personne ne peut rejoindre.
	 *
	 * En cas d'echec, rien ne bouge et OnHostFailed est diffuse.
	 *
	 * MaxPlayers n'a volontairement PAS de valeur par defaut : UHT ne sait pas
	 * resoudre une constante qualifiee dans une signature UFUNCTION, et ecrire
	 * "= 4" ici recreerait le second nombre a tenir en phase avec le GameMode.
	 * Les appelants passent USessionSubsystem::DefaultMaxPlayers.
	 */
	UFUNCTION(BlueprintCallable, Category = "Session")
	void HostSession(const TSoftObjectPtr<UWorld>& Level, int32 MaxPlayers);

	/** Cherche les parties joignables. Le resultat arrive par OnSearchFinished. */
	UFUNCTION(BlueprintCallable, Category = "Session")
	void FindSessions(int32 MaxResults = 20);

	/**
	 * Rejoint un resultat de la derniere recherche, puis voyage vers l'hote.
	 * Un index hors bornes diffuse OnJoinFailed plutot que de ne rien faire.
	 */
	UFUNCTION(BlueprintCallable, Category = "Session")
	void JoinFoundSession(int32 ResultIndex = 0);

	/**
	 * Cherche puis rejoint la premiere partie trouvee, en une commande console.
	 *
	 * Aucun bouton ne mene ici : on ne rejoint une partie QUE par invitation
	 * Steam. Cette commande n'existe que pour pouvoir tester a deux instances
	 * sur une seule machine, ou aucune invitation n'est possible. La taper dans
	 * la console (touche ~) sur la seconde fenetre.
	 */
	UFUNCTION(Exec, Category = "Session")
	void EmberJoin();

	/**
	 * Ouvre le panneau d'invitation de l'overlay Steam.
	 *
	 * @return false si l'overlay est indisponible -- pas de service en ligne,
	 *         pas de session, ou overlay desactive cote client Steam.
	 */
	UFUNCTION(BlueprintCallable, Category = "Session")
	bool ShowInviteUI();

	/**
	 * Peut-on inviter maintenant. Sert a griser le bouton plutot qu'a le laisser
	 * cliquer dans le vide : une partie lancee alors que Steam etait absent
	 * tourne tres bien, mais n'a personne a qui s'annoncer.
	 */
	UFUNCTION(BlueprintPure, Category = "Session")
	bool CanInvite() const;

	/**
	 * Quitte la session courante. A appeler en revenant au menu principal :
	 * une session laissee derriere soi empeche d'en creer une nouvelle, et
	 * laisse surtout ta partie annoncee a tes amis alors qu'elle n'existe plus.
	 */
	UFUNCTION(BlueprintCallable, Category = "Session")
	void LeaveSession();

	/** Vrai si une session est ouverte, qu'on l'heberge ou qu'on l'ait rejointe. */
	UFUNCTION(BlueprintPure, Category = "Session")
	bool IsSessionActive() const;

	/**
	 * Nom du service en ligne reellement charge : "Steam", ou "NULL" en LAN.
	 * Sert a l'UI et surtout au diagnostic -- voir "NULL" alors qu'on attendait
	 * Steam est le symptome le plus frequent, et il se lit en une seconde.
	 */
	UFUNCTION(BlueprintPure, Category = "Session")
	FString GetOnlineServiceName() const;

	/** Combien de resultats la derniere recherche a rapportes. */
	UFUNCTION(BlueprintPure, Category = "Session")
	int32 GetFoundSessionCount() const;

	UPROPERTY(BlueprintAssignable, Category = "Session")
	FOnSessionError OnHostFailed;

	UPROPERTY(BlueprintAssignable, Category = "Session")
	FOnSessionError OnJoinFailed;

	UPROPERTY(BlueprintAssignable, Category = "Session")
	FOnSessionSearchFinished OnSearchFinished;

	UPROPERTY(BlueprintAssignable, Category = "Session")
	FOnSessionTravelPending OnTravelPending;

	/**
	 * Duree laissee au fondu avant que le voyage parte.
	 *
	 * C'est LE sous-systeme qui la porte et non chaque controller : les deux
	 * doivent fondre exactement le temps qu'il attend, sinon l'ecran voyage
	 * avant d'etre noir. Une seule valeur, lue par tous.
	 *
	 * Pas de UPROPERTY editable : un GameInstanceSubsystem n'a aucun asset ou
	 * la regler. Si elle doit bouger un jour, ce sera ici.
	 */
	UFUNCTION(BlueprintPure, Category = "Session")
	float GetTravelFadeDuration() const { return TravelFadeDuration; }

private:
	void HandleCreateSessionComplete(FName SessionName, bool bWasSuccessful);
	void HandleFindSessionsComplete(bool bWasSuccessful);
	void HandleJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
	void HandleDestroySessionComplete(FName SessionName, bool bWasSuccessful);

	/**
	 * L'ami a clique "Rejoindre la partie" dans l'overlay Steam.
	 *
	 * Ecoute des l'Initialize et jamais coupee : une invitation peut tomber a
	 * n'importe quel instant, y compris en pleine partie ou alors qu'aucun menu
	 * n'est affiche. Le sous-systeme est le seul objet dont on soit certain
	 * qu'il soit vivant a ce moment-la.
	 */
	void HandleInviteAccepted(const bool bWasSuccessful, const int32 ControllerId, FUniqueNetIdPtr UserId, const FOnlineSessionSearchResult& InviteResult);

	/**
	 * Second temps de la commande EmberJoin : la recherche a rendu, on rejoint.
	 * UFUNCTION obligatoire, c'est ce qui permet le AddDynamic sur OnSearchFinished.
	 */
	UFUNCTION()
	void HandleExecJoinSearchFinished(bool bWasSuccessful, int32 SessionCount);

	/** La creation proprement dite, une fois le terrain degage. */
	void CreateSessionNow();

	void JoinSearchResult(const FOnlineSessionSearchResult& Result);

	/**
	 * Vrai quand le service charge est OnlineSubsystemNULL, c'est-a-dire qu'on
	 * n'a pas Steam. Toutes les sessions basculent alors en LAN : c'est ce qui
	 * rend le test a deux instances possible sur une seule machine.
	 */
	bool IsLanMode() const;

	/** Coupe proprement un delegue encore branche. Idempotent. */
	void ClearPendingDelegates();

	/** Le fondu a eu son temps : on voyage pour de bon. */
	void CommitPendingTravel();

	/** Adresse resolue de l'hote, en attente du fondu. */
	FString PendingTravelURL;

	FTimerHandle TravelTimer;

	float TravelFadeDuration = 1.f;

	IOnlineSessionPtr SessionInterface;

	TSharedPtr<FOnlineSessionSearch> SearchSettings;

	FDelegateHandle CreateSessionCompleteHandle;
	FDelegateHandle FindSessionsCompleteHandle;
	FDelegateHandle JoinSessionCompleteHandle;
	FDelegateHandle DestroySessionCompleteHandle;
	FDelegateHandle InviteAcceptedHandle;

	/** Map a ouvrir une fois la session creee. */
	TSoftObjectPtr<UWorld> PendingHostLevel;

	int32 PendingMaxPlayers = DefaultMaxPlayers;

	/**
	 * Une session trainait encore : on la detruit d'abord et la creation reprend
	 * dans le callback de destruction. Sans ce drapeau, quitter une partie puis
	 * en relancer une echoue silencieusement -- le moteur refuse deux sessions
	 * du meme nom, et l'erreur ne remonte nulle part.
	 */
	bool bCreateAfterDestroy = false;
};
