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


#include "MainMenuPlayerController.h"

#include "CharacterAppearanceComponent.h"
#include "CharacterCustomizationWidget.h"
#include "CharacterPreviewActor.h"
#include "ConfirmDialogWidget.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "MainMenuWidget.h"
#include "MenuCameraSpot.h"
#include "ScreenFadeComponent.h"
#include "SessionSubsystem.h"
#include "SettingsMenuWidget.h"
#include "SurvivalGameInstance.h"
#include "SurvivalUserSettings.h"

#define LOCTEXT_NAMESPACE "MainMenu"

namespace
{
	// Le menu reste sous le voile de fondu, qui est en 1000.
	constexpr int32 ZOrderMainMenu = 0;

	// L'ecran de reglages recouvre le menu, la boite de confirmation passe
	// devant les deux, et le fondu reste au-dessus de tout a 1000.
	constexpr int32 ZOrderMainMenuSettings = 50;
	constexpr int32 ZOrderQuitDialog = 100;
}

AMainMenuPlayerController::AMainMenuPlayerController()
{
	// Meme composant que dans le jeu : un seul fondu a maintenir.
	ScreenFade = CreateDefaultSubobject<UScreenFadeComponent>(TEXT("ScreenFade"));

	bShowMouseCursor = true;

	QuitDialogMessage = LOCTEXT("QuitConfirm", "Quitter le jeu ?");
}

void AMainMenuPlayerController::BeginPlay()
{
	Super::BeginPlay();

	// L'ecran part du noir : sans ca on verrait une frame de la map, cadree
	// n'importe comment, avant que le fondu ne commence.
	ScreenFade->SnapToBlack();

	if (AMenuCameraSpot* Spot = PickCameraSpot())
	{
		// Sans blend : on arrive directement sur l'angle tire au sort.
		SetViewTarget(Spot);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : aucune AMenuCameraSpot active dans la map, le menu s'affichera sur la vue par defaut."));
	}

	// APRES SetViewTarget, jamais avant : les images de chauffe doivent etre
	// rendues depuis l'angle qu'on va reellement montrer, sinon elles raffinent
	// un decor qu'on ne verra pas. Le voile est deja noir, donc rien ne fuit.
	USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
	if (SurvivalGameInstance)
	{
		SurvivalGameInstance->ResumeWorldRendering();
	}

	if (MainMenuClass)
	{
		MainMenu = CreateWidget<UMainMenuWidget>(this, MainMenuClass);
		if (MainMenu)
		{
			MainMenu->AddToViewport(ZOrderMainMenu);
		}
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : MainMenuClass n'est pas assignee, aucun menu ne s'affichera."));
	}

	// GameAndUI et non UIOnly : UIOnly avale aussi la console et les raccourcis
	// du moteur, ce qui rend le debug penible. Il n'y a de toute facon aucun
	// input de jeu dans cette map, rien ne peut passer a travers.
	FInputModeGameAndUI Mode;
	Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	Mode.SetHideCursorDuringCapture(false);
	SetInputMode(Mode);
	SetShowMouseCursor(true);

	ScreenFade->OnFadeFinished.AddDynamic(this, &AMainMenuPlayerController::HandleFadeFinished);

	// Meme raison qu'en jeu : on laisse le decor rendre quelques images derriere
	// le voile noir avant de l'eclaircir, le temps que Nanite et les textures
	// virtuelles se raffinent. Sans ca on voit le terrain passer de sa version
	// grossiere a la vraie, et les modeles apparaitre un a un.
	if (SurvivalGameInstance)
	{
		SurvivalGameInstance->BeginSceneWarmup(
			FSimpleDelegate::CreateUObject(this, &AMainMenuPlayerController::StartOpeningFade));
	}
	else
	{
		StartOpeningFade();
	}
}

void AMainMenuPlayerController::StartOpeningFade()
{
	ScreenFade->FadeIn(OpeningFadeDuration);

	if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
	{
		Sessions->OnHostFailed.AddDynamic(this, &AMainMenuPlayerController::HandleHostFailed);
		Sessions->OnJoinFailed.AddDynamic(this, &AMainMenuPlayerController::HandleJoinFailed);
		Sessions->OnTravelPending.AddDynamic(this, &AMainMenuPlayerController::HandleTravelPending);

		// Revenir au menu depuis une partie laisse la session ouverte : elle
		// resterait annoncee a tes amis alors que plus personne n'est dedans.
		Sessions->LeaveSession();

		UE_LOG(LogTemp, Log, TEXT("AMainMenuPlayerController : service en ligne [%s]."), *Sessions->GetOnlineServiceName());
	}

	// On est arrive : le filet de secours reseau redevient actif pour la
	// prochaine partie. Sans cette remise a zero, un hote qui relance apres
	// avoir quitte ne serait plus jamais ramene au menu en cas de coupure.
	if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
	{
		SurvivalGameInstance->ClearReturnToMainMenu();
	}
}

AMenuCameraSpot* AMainMenuPlayerController::PickCameraSpot()
{
	UWorld* World = GetWorld();
	if (!World)
	{
		return nullptr;
	}

	USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
	const FName LastUsed = SurvivalGameInstance ? SurvivalGameInstance->GetLastMenuCameraName() : NAME_None;

	// TActorIterator plutot que GetAllActorsOfClass : pas de tableau d'AActor*
	// intermediaire, pas de cast par element.
	TArray<AMenuCameraSpot*> AllSpots;
	TArray<AMenuCameraSpot*> Candidates;
	for (TActorIterator<AMenuCameraSpot> It(World); It; ++It)
	{
		AMenuCameraSpot* Spot = *It;
		if (!Spot->bEnabled)
		{
			continue;
		}

		AllSpots.Add(Spot);

		// On ecarte l'angle du dernier passage pour ne pas revoir deux fois de
		// suite le meme decor en revenant au menu.
		if (Spot->GetFName() != LastUsed)
		{
			Candidates.Add(Spot);
		}
	}

	// Une seule camera dans la map : on la reprend plutot que de n'afficher
	// aucune vue.
	if (Candidates.Num() == 0)
	{
		Candidates = MoveTemp(AllSpots);
	}

	if (Candidates.Num() == 0)
	{
		return nullptr;
	}

	AMenuCameraSpot* Chosen = Candidates[FMath::RandRange(0, Candidates.Num() - 1)];

	if (SurvivalGameInstance)
	{
		SurvivalGameInstance->SetLastMenuCameraName(Chosen->GetFName());
	}

	return Chosen;
}

void AMainMenuPlayerController::StartGame()
{
	// Le fondu est deja lance : on ignore le second clic plutot que d'empiler
	// deux chargements.
	if (PendingAction != EMenuPendingAction::None)
	{
		return;
	}

	// L'ecran de customisation s'intercale ICI, entre le clic et le fondu :
	// c'est le choix fait pour ce projet, on ne lance pas une partie sans etre
	// passe par son personnage. Sans classe assignee, on retombe sur l'ancien
	// comportement -- pratique pour tester le reste sans le traverser.
	if (CustomizationMenuClass && !CustomizationMenu)
	{
		PendingAction = EMenuPendingAction::OpenCustomization;

		if (MainMenu)
		{
			MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
		}

		// On fond au noir AVANT de bouger la camera : le mannequin est ailleurs
		// dans la meme map, et un travelling jusqu'a lui traverserait le decor.
		ScreenFade->FadeOut(CustomizationFadeDuration);
		return;
	}

	if (GameLevel.IsNull())
	{
		UE_LOG(LogTemp, Error, TEXT("AMainMenuPlayerController : GameLevel n'est pas assignee, impossible de lancer la partie."));
		return;
	}

	PendingAction = EMenuPendingAction::StartGame;

	// Le menu reste visible pendant le fondu mais cesse de repondre : cliquer
	// Quitter pendant le fondu vers la partie serait un piege.
	//
	// Le test de visibilite est indispensable : quand on arrive ici depuis
	// l'ecran de customisation, le menu a ete Collapsed a son ouverture, et
	// HitTestInvisible le RESSUSCITERAIT -- on verrait reapparaitre Jouer,
	// Reglages et Quitter par-dessus le personnage qu'on vient de valider.
	// HitTestInvisible rend non cliquable, il ne cache pas.
	if (MainMenu && MainMenu->GetVisibility() != ESlateVisibility::Collapsed)
	{
		MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
	}

	ScreenFade->FadeOut(LeaveFadeDuration);
}

// ----------------------------------------------------------------------
// Ecran de customisation
// ----------------------------------------------------------------------

void AMainMenuPlayerController::OpenCustomization()
{
	// Cherche au moment de l'ouverture et pas au BeginPlay : c'est le seul
	// endroit ou l'on est sur que la map est entierement chargee, et un
	// mannequin absent doit rester rattrapable.
	if (!CharacterPreview)
	{
		for (TActorIterator<ACharacterPreviewActor> It(GetWorld()); It; ++It)
		{
			CharacterPreview = *It;
			break;
		}
	}

	if (!CharacterPreview)
	{
		UE_LOG(LogTemp, Warning,
			TEXT("AMainMenuPlayerController : aucun ACharacterPreviewActor dans la map, l'ecran de customisation est saute."));

		// On ne bloque pas le joueur pour un acteur manquant : meme logique
		// qu'un echec de session, il a clique Jouer, il veut jouer.
		CustomizationMenuClass = nullptr;
		StartGame();
		return;
	}

	CustomizationMenu = CreateWidget<UCharacterCustomizationWidget>(this, CustomizationMenuClass);
	if (!CustomizationMenu)
	{
		return;
	}

	// TOUJOURS l'apparence par defaut, jamais celle persistee : cet ecran est
	// une etape de CREATION, pas une reprise. Le jour ou un lobby verifiera que
	// les quatre joueurs ont valide leur personnage, il lui faudra un point de
	// depart identique pour tout le monde.
	//
	// La persistance ne devient pas inutile pour autant : elle sert au respawn,
	// et surtout a qui arrive par invitation Steam -- celui-la ne passe jamais
	// par ici et doit bien avoir un visage.
	//
	// « Par defaut » veut dire le DefaultAppearance du mannequin, pas les zeros
	// du C++ : le personnage de depart se regle ainsi dans BP_CharacterPreview,
	// a l'oeil et sans recompiler. C'est le meme reglage que celui pose au
	// BeginPlay, donc l'ouverture de l'ecran ne change rien a ce qu'on voyait.
	const UCharacterAppearanceComponent* PreviewAppearance = CharacterPreview->GetAppearanceComponent();
	CustomizationMenu->SetupCustomization(
		CharacterPreview,
		PreviewAppearance ? PreviewAppearance->DefaultAppearance : FCharacterAppearance());

	CustomizationMenu->OnConfirmed.AddDynamic(this, &AMainMenuPlayerController::HandleCustomizationConfirmed);
	CustomizationMenu->OnCancelled.AddDynamic(this, &AMainMenuPlayerController::HandleCustomizationCancelled);

	// Sans blend : l'ecran est noir a cet instant, personne ne voit la bascule.
	// C'est justement ce que le fondu achete -- un travelling depuis l'angle de
	// menu jusqu'au mannequin traverserait le decor de la map.
	SetViewTarget(CharacterPreview);

	if (MainMenu)
	{
		MainMenu->SetVisibility(ESlateVisibility::Collapsed);
	}

	CustomizationMenu->AddToViewport();

	// L'ecran a des boutons ET un clic-glisser : il lui faut la souris, et le
	// mode UI-only l'empeche de tomber dans le jeu.
	SetInputMode(FInputModeUIOnly().SetWidgetToFocus(CustomizationMenu->TakeWidget()));
	SetShowMouseCursor(true);

	ScreenFade->FadeIn(CustomizationFadeDuration);
}

void AMainMenuPlayerController::HandleCustomizationConfirmed()
{
	// Persiste ET annonce -- mais nous sommes dans le menu, aucun serveur
	// n'ecoute encore. Seule la persistance compte ici ; l'annonce partira du
	// AnnounceCharacterAppearance() d'AFpsPlayerController, une fois la partie
	// chargee et le PlayerState en place.
	if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
	{
		Settings->SetCharacterAppearance(CustomizationMenu->GetAppearance());
		Settings->SaveSettings();
	}

	CloseCustomization();

	// La classe est mise de cote le temps du lancement : sans ca, StartGame
	// rouvrirait l'ecran qu'on vient de fermer.
	CustomizationMenuClass = nullptr;
	StartGame();
}

void AMainMenuPlayerController::HandleCustomizationCancelled()
{
	// Le fondu de retour est deja lance : on ignore la demande plutot que de le
	// relancer depuis le debut. La garde compte plus depuis qu'Echap declenche
	// ce chemin -- une touche se repete bien plus facilement qu'un clic.
	if (PendingAction != EMenuPendingAction::None)
	{
		return;
	}

	// Meme mecanique qu'a l'aller : on fond, PUIS on bascule la vue. Le retour
	// se termine dans HandleFadeFinished.
	PendingAction = EMenuPendingAction::CloseCustomization;

	if (CustomizationMenu)
	{
		CustomizationMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
	}

	ScreenFade->FadeOut(CustomizationFadeDuration);
}

void AMainMenuPlayerController::CloseCustomization()
{
	if (CustomizationMenu)
	{
		CustomizationMenu->RemoveFromParent();
		CustomizationMenu = nullptr;
	}

	// Le menu principal reprend la main : son propre mode d'input est pose par
	// le Blueprint, on remet simplement le curseur dans un etat coherent.
	SetInputMode(FInputModeUIOnly());
	SetShowMouseCursor(true);
}

void AMainMenuPlayerController::HandleHostFailed(FText Reason)
{
	UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : session indisponible (%s), la partie se lance hors ligne."), *Reason.ToString());

	// Sans session on ouvre quand meme en serveur d'ecoute plutot qu'en solo
	// sec : l'ecart de cout est nul a un joueur, et ca evite d'avoir deux
	// facons differentes de demarrer une partie a maintenir.
	UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameLevel, /*bAbsolute=*/true, TEXT("listen"));
}

void AMainMenuPlayerController::HandleTravelPending()
{
	USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr;
	if (!Sessions)
	{
		return;
	}

	// Le menu reste lisible pendant le fondu mais cesse de repondre : cliquer
	// Jouer alors qu'on part deja rejoindre un ami serait un piege.
	if (MainMenu)
	{
		MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
	}

	// Exactement la duree que le sous-systeme attend : fondre plus lentement
	// laisserait voyager avant que l'ecran soit noir, plus vite laisserait un
	// ecran noir immobile.
	ScreenFade->FadeOut(Sessions->GetTravelFadeDuration());

	// PendingAction reste a None VOLONTAIREMENT : ce n'est pas nous qui
	// chargerons la map, c'est le sous-systeme. HandleFadeFinished doit donc ne
	// rien faire quand ce fondu-la se termine.
}

void AMainMenuPlayerController::HandleJoinFailed(FText Reason)
{
	UE_LOG(LogTemp, Error, TEXT("AMainMenuPlayerController : connexion impossible -- %s"), *Reason.ToString());

	// Ici pas de repli : le joueur voulait rejoindre un ami precis. On le rend
	// simplement a son menu, ecran remonte -- sans quoi un echec le laisserait
	// devant un noir definitif, ce qui se lit comme un plantage.
	PendingAction = EMenuPendingAction::None;

	if (MainMenu)
	{
		MainMenu->SetVisibility(ESlateVisibility::Visible);
	}

	ScreenFade->FadeIn(LeaveFadeDuration);
}

void AMainMenuPlayerController::QuitGame()
{
	// Une partie est deja en cours de chargement : la question n'a plus de sens.
	if (PendingAction != EMenuPendingAction::None)
	{
		return;
	}

	// Sans boite assignee on ne bloque pas le joueur dans son menu : on ferme.
	if (!QuitDialogClass)
	{
		UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : QuitDialogClass n'est pas assignee, fermeture sans confirmation."));
		CloseGame();
		return;
	}

	// Creee au premier clic seulement, puis reutilisee : une boite qu'on ouvre
	// et ferme plusieurs fois n'a aucune raison d'etre reconstruite.
	if (!QuitDialog)
	{
		QuitDialog = CreateWidget<UConfirmDialogWidget>(this, QuitDialogClass);
		if (!QuitDialog)
		{
			CloseGame();
			return;
		}

		QuitDialog->OnConfirmed.AddDynamic(this, &AMainMenuPlayerController::HandleQuitConfirmed);
		QuitDialog->OnCancelled.AddDynamic(this, &AMainMenuPlayerController::HandleQuitCancelled);
		QuitDialog->AddToViewport(ZOrderQuitDialog);
	}

	QuitDialog->Open(QuitDialogMessage);

	// Le menu reste lisible derriere mais cesse de repondre : sans ca, on
	// pourrait relancer une partie alors que la question est encore posee.
	if (MainMenu)
	{
		MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
	}
}

void AMainMenuPlayerController::OpenSettings()
{
	// Une partie se charge deja : ouvrir un ecran maintenant serait un piege.
	if (PendingAction != EMenuPendingAction::None)
	{
		return;
	}

	if (!SettingsMenuClass)
	{
		UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : SettingsMenuClass n'est pas assignee, les reglages ne s'ouvriront pas."));
		return;
	}

	// Creee a la premiere ouverture seulement, puis reutilisee.
	if (!SettingsMenu)
	{
		SettingsMenu = CreateWidget<USettingsMenuWidget>(this, SettingsMenuClass);
		if (!SettingsMenu)
		{
			return;
		}

		SettingsMenu->OnCloseRequested.AddDynamic(this, &AMainMenuPlayerController::HandleSettingsCloseRequested);
		SettingsMenu->AddToViewport(ZOrderMainMenuSettings);

		// Meme ecran qu'en jeu, mais notre pile est plus courte : sa boite se
		// pose a 60, devant les reglages et derriere celle du bouton Quitter.
		SettingsMenu->SetDialogZOrder(ZOrderMainMenuSettings + 10);
	}

	SettingsMenu->Open();

	// Le menu reste lisible derriere mais cesse de repondre : sans ca on
	// pourrait lancer une partie a travers l'ecran de reglages.
	if (MainMenu)
	{
		MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
	}
}

void AMainMenuPlayerController::HandleSettingsCloseRequested()
{
	if (SettingsMenu)
	{
		SettingsMenu->Close();
	}

	if (MainMenu)
	{
		MainMenu->SetVisibility(ESlateVisibility::Visible);
	}
}

void AMainMenuPlayerController::HandleQuitConfirmed()
{
	// Pas de fondu : le joueur vient de confirmer, le faire patienter une
	// seconde de plus ne serait qu'agacant.
	CloseGame();
}

void AMainMenuPlayerController::HandleQuitCancelled()
{
	if (QuitDialog)
	{
		QuitDialog->Close();
	}

	if (MainMenu)
	{
		MainMenu->SetVisibility(ESlateVisibility::Visible);
	}
}

void AMainMenuPlayerController::CloseGame()
{
	UKismetSystemLibrary::QuitGame(this, this, EQuitPreference::Quit, /*bIgnorePlatformRestrictions=*/false);
}

void AMainMenuPlayerController::HandleFadeFinished()
{
	// L'ecran est noir : c'est ICI qu'on bascule la vue sur le mannequin, et
	// pas au clic. PendingAction est remise a None tout de suite, sinon le
	// fondu d'eclaircissement qui suit repasserait par la meme branche.
	if (PendingAction == EMenuPendingAction::OpenCustomization)
	{
		PendingAction = EMenuPendingAction::None;
		OpenCustomization();
		return;
	}

	if (PendingAction == EMenuPendingAction::CloseCustomization)
	{
		PendingAction = EMenuPendingAction::None;

		CloseCustomization();

		if (MainMenu)
		{
			MainMenu->SetVisibility(ESlateVisibility::Visible);
		}

		// Un nouvel angle au retour plutot que celui d'avant : PickCameraSpot
		// evite deja le dernier utilise, donc le menu ne se rouvre jamais sur
		// exactement la meme image.
		if (AMenuCameraSpot* Spot = PickCameraSpot())
		{
			SetViewTarget(Spot);
		}

		ScreenFade->FadeIn(CustomizationFadeDuration);
		return;
	}

	// Le delegue se declenche aussi a la fin du fondu d'ouverture, ou il n'y a
	// rien a faire : c'est PendingAction qui distingue les cas.
	if (PendingAction != EMenuPendingAction::StartGame)
	{
		return;
	}

	// C'est le sous-systeme qui ouvrira la map, et seulement une fois la session
	// reellement creee : ouvrir avant donnerait une partie que personne ne peut
	// rejoindre. Le cas d'echec passe par HandleHostFailed, qui charge quand meme.
	if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
	{
		Sessions->HostSession(GameLevel, USessionSubsystem::DefaultMaxPlayers);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : USessionSubsystem introuvable, partie lancee hors ligne."));
		UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameLevel, /*bAbsolute=*/true, TEXT("listen"));
	}
}

void AMainMenuPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	if (SettingsMenu)
	{
		SettingsMenu->RemoveFromParent();
		SettingsMenu = nullptr;
	}

	if (QuitDialog)
	{
		QuitDialog->RemoveFromParent();
		QuitDialog = nullptr;
	}

	if (MainMenu)
	{
		MainMenu->RemoveFromParent();
		MainMenu = nullptr;
	}

	Super::EndPlay(EndPlayReason);
}

#undef LOCTEXT_NAMESPACE
