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


#include "FpsPlayerController.h"

#include "Blueprint/UserWidget.h"
#include "ConfirmDialogWidget.h"
#include "EnhancedInputComponent.h"
#include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h"
#include "SurvivalStatsComponent.h"
#include "HotbarWidget.h"
#include "InputActionValue.h"
#include "InputCoreTypes.h"
#include "InventoryComponent.h"
#include "InteractionPromptWidget.h"
#include "InventoryWidget.h"
#include "Kismet/GameplayStatics.h"
#include "PauseMenuWidget.h"
#include "ScreenFadeComponent.h"
#include "SettingsMenuWidget.h"
#include "SurvivalStatsWidget.h"

#define LOCTEXT_NAMESPACE "PauseMenu"

namespace
{
	// Ordre d'empilement des widgets plein ecran, du fond vers l'avant.
	//
	// La barre rapide passe AU-DESSUS de l'inventaire, et pas l'inverse : le
	// Background Blur de l'inventaire floute tout ce qui est rendu en dessous
	// de lui. Avec un ZOrder inferieur, la barre se retrouverait floutee et
	// inutilisable des l'ouverture de l'inventaire.
	//
	// Bonus : etant devant, elle reste une cible de depot valide -- on peut
	// glisser un objet de la grille directement dans la barre.
	//
	// Pas de 10 pour laisser de la place aux widgets a venir.
	constexpr int32 ZOrderCrosshair = 0;
	constexpr int32 ZOrderSurvivalStats = 5;
	constexpr int32 ZOrderInteractionPrompt = 10;
	constexpr int32 ZOrderInventory = 20;
	constexpr int32 ZOrderHotbar = 30;

	// Le menu de pause passe devant tout le HUD, et la boite de confirmation
	// devant lui. Meme echelle que dans le menu principal.
	//
	// Les noms doivent rester uniques dans TOUT le module : le build unity
	// concatene les .cpp, et un namespace anonyme n'isole plus rien. D'ou le
	// prefixe InGame, qui evite la collision avec le menu principal.
	constexpr int32 ZOrderPauseMenu = 100;
	constexpr int32 ZOrderInGameSettingsMenu = 150;
	constexpr int32 ZOrderQuitToMenuDialog = 200;
}

AFpsPlayerController::AFpsPlayerController()
{
	// Le fondu est un systeme a part entiere, donc un composant -- et le menu
	// principal reutilise exactement le meme.
	//
	// Rappel Unreal : ce constructeur tourne aussi dans l'editeur, sur le CDO.
	// Il ne doit contenir que de la construction, jamais de logique de jeu.
	ScreenFade = CreateDefaultSubobject<UScreenFadeComponent>(TEXT("ScreenFade"));

	QuitToMenuMessage = LOCTEXT("QuitToMenuConfirm", "Retourner au menu principal ?\nTa progression sera perdue.");
}

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

	// Le mode d'input vit sur le VIEWPORT, pas sur le controller. OpenLevel
	// detruit le controller du menu principal mais pas la fenetre : son
	// GameAndUI et son curseur visible restent en place, et la camera ne
	// repondrait qu'au clic droit maintenu. On ne suppose donc jamais l'etat
	// laisse par la map precedente, on le repose.
	if (IsLocalController())
	{
		SetInputMode(FInputModeGameOnly());
		SetShowMouseCursor(false);
	}

	// Le delegue se declenche a la fin de CHAQUE fondu : c'est
	// bQuittingToMainMenu qui distingue le fondu de sortie du reste.
	ScreenFade->OnFadeFinished.AddDynamic(this, &AFpsPlayerController::HandleFadeFinished);

	if (bFadeInOnStart)
	{
		ScreenFade->FadeIn(StartupFadeDuration);
	}
}

void AFpsPlayerController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	// On cree les widgets ici et pas dans BeginPlay : a ce stade le pawn est
	// garanti possede, et le prompt comme l'inventaire ont besoin de lui pour
	// trouver leurs composants des leur NativeConstruct.
	CreateHudWidgets();

	// Apres un respawn les widgets existent deja, mais pointent vers les
	// composants du cadavre. On les rebranche sur le nouveau pawn.
	if (Crosshair) { /* pas d'abonnement */ }
	if (InteractionPrompt) { InteractionPrompt->BindToOwningPawn(); }
	if (Hotbar) { Hotbar->BindToOwningPawn(); }
	if (InventoryWidget) { InventoryWidget->BindToOwningPawn(); }
	if (SurvivalStatsWidget) { SurvivalStatsWidget->BindToOwningPawn(); }

	if (AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(InPawn))
	{
		if (USurvivalStatsComponent* Stats = PlayerPawn->GetSurvivalStats())
		{
			Stats->OnDied.AddDynamic(this, &AFpsPlayerController::HandlePawnDied);
		}
	}
}

void AFpsPlayerController::HandlePawnDied()
{
	// L'inventaire ouvert au moment de la mort resterait affiche par-dessus
	// l'ecran noir, curseur compris.
	if (bInventoryOpen)
	{
		ToggleInventory();
	}

	ScreenFade->FadeOut(DeathFadeDuration);

	GetWorldTimerManager().SetTimer(
		RespawnTimerHandle, this, &AFpsPlayerController::RespawnPlayer,
		FMath::Max(RespawnDelay, DeathFadeDuration), /*bLoop=*/false);
}

void AFpsPlayerController::RespawnPlayer()
{
	// On depossede AVANT de detruire : detruire un pawn encore possede laisse
	// le controller dans un etat incoherent le temps d'une frame.
	APawn* PreviousPawn = GetPawn();
	UnPossess();
	if (IsValid(PreviousPawn))
	{
		PreviousPawn->Destroy();
	}

	// RestartPlayer fait apparaitre un nouveau pawn sur un PlayerStart et le
	// possede. Le nouveau USurvivalStatsComponent repart plein tout seul dans
	// son BeginPlay : rien a reinitialiser a la main.
	if (AGameModeBase* GameMode = GetWorld()->GetAuthGameMode())
	{
		GameMode->RestartPlayer(this);
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : aucun GameMode, impossible de reapparaitre."));
	}

	ScreenFade->FadeIn(StartupFadeDuration);
}

void AFpsPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(InputComponent);
	if (!Input)
	{
		return;
	}

	if (ToggleInventoryAction)
	{
		Input->BindAction(ToggleInventoryAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleInventory);
	}

	if (DropItemAction)
	{
		Input->BindAction(DropItemAction, ETriggerEvent::Started, this, &AFpsPlayerController::DropHoveredItem);
	}

	// La barre rapide est bindee sur le controller et non sur le pawn : ainsi
	// changer d'objet actif reste possible meme quand l'inventaire est ouvert
	// et que les inputs du pawn sont coupes.
	if (HotbarSelectAction)
	{
		Input->BindAction(HotbarSelectAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandleHotbarSelect);
	}

	if (HotbarScrollAction)
	{
		Input->BindAction(HotbarScrollAction, ETriggerEvent::Triggered, this, &AFpsPlayerController::HandleHotbarScroll);
	}

	if (PauseAction)
	{
		Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandlePauseInput);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseAction n'est pas assignee, le menu de pause sera inaccessible."));
	}
}

UInventoryComponent* AFpsPlayerController::GetPawnInventory() const
{
	AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
	return PlayerPawn ? PlayerPawn->GetInventoryComponent() : nullptr;
}

void AFpsPlayerController::HandleHotbarSelect(const FInputActionValue& Value)
{
	UInventoryComponent* Inventory = GetPawnInventory();
	if (!Inventory)
	{
		return;
	}

	// La valeur vient du modificateur Scalar de la touche : 1 pour la touche 1,
	// 2 pour la touche 2, etc. On repasse en index a partir de zero.
	const int32 Number = FMath::RoundToInt(Value.Get<float>());
	Inventory->SelectHotbarSlot(Number - 1);
}

void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value)
{
	const float Axis = Value.Get<float>();
	if (FMath::IsNearlyZero(Axis))
	{
		return;
	}

	if (UInventoryComponent* Inventory = GetPawnInventory())
	{
		// Molette vers le haut = slot precedent, comme dans la plupart des FPS.
		Inventory->CycleHotbarSelection(Axis > 0.f ? -1 : 1);
	}
}

void AFpsPlayerController::DropHoveredItem()
{
	if (!bInventoryOpen || !InventoryWidget)
	{
		return;
	}

	const int32 SlotIndex = InventoryWidget->GetHoveredSlotIndex();
	if (SlotIndex == INDEX_NONE)
	{
		return;
	}

	AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
	if (!PlayerPawn)
	{
		return;
	}

	// Ctrl enfonce : un seul exemplaire, comme pour le glisser. On lit l'etat
	// de la touche plutot que d'en faire une action separee : le geste est
	// modal, pas une commande distincte.
	const bool bSingle = IsInputKeyDown(EKeys::LeftControl) || IsInputKeyDown(EKeys::RightControl);

	// MAX_int32 pour "tout" : DropSlot borne de lui-meme a ce que contient le slot.
	PlayerPawn->DropSlot(SlotIndex, bSingle ? 1 : MAX_int32);
}

void AFpsPlayerController::CreateHudWidgets()
{
	// Un widget n'existe que sur la machine du joueur local : inutile et
	// incorrect d'en creer un sur un serveur dedie.
	if (!IsLocalController())
	{
		return;
	}

	if (!Crosshair && CrosshairClass)
	{
		Crosshair = CreateWidget<UUserWidget>(this, CrosshairClass);
		if (Crosshair)
		{
			Crosshair->AddToViewport(ZOrderCrosshair);
		}
	}

	if (!InteractionPrompt)
	{
		if (InteractionPromptClass)
		{
			InteractionPrompt = CreateWidget<UInteractionPromptWidget>(this, InteractionPromptClass);
			if (InteractionPrompt)
			{
				InteractionPrompt->AddToViewport(ZOrderInteractionPrompt);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InteractionPromptClass n'est pas assignee, aucun prompt ne s'affichera."));
		}
	}

	// Le voile de fondu n'est plus cree ici : UScreenFadeComponent s'en charge
	// dans son propre BeginPlay, et son ZOrder de 1000 le garde devant tout le
	// HUD quel que soit l'ordre d'ajout.

	if (!SurvivalStatsWidget && SurvivalStatsClass)
	{
		SurvivalStatsWidget = CreateWidget<USurvivalStatsWidget>(this, SurvivalStatsClass);
		if (SurvivalStatsWidget)
		{
			SurvivalStatsWidget->AddToViewport(ZOrderSurvivalStats);
		}
	}

	if (!Hotbar && HotbarClass)
	{
		Hotbar = CreateWidget<UHotbarWidget>(this, HotbarClass);
		if (Hotbar)
		{
			Hotbar->AddToViewport(ZOrderHotbar);
		}
	}

	if (!InventoryWidget)
	{
		if (InventoryClass)
		{
			InventoryWidget = CreateWidget<UInventoryWidget>(this, InventoryClass);
			if (InventoryWidget)
			{
				InventoryWidget->AddToViewport(ZOrderInventory);
				// Cree des le depart pour que son abonnement au delegue soit
				// actif, mais cache : ainsi il reste a jour meme ferme, et
				// l'ouverture est instantanee.
				InventoryWidget->SetVisibility(ESlateVisibility::Collapsed);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryClass n'est pas assignee, l'inventaire ne s'ouvrira pas."));
		}
	}

	if (!PauseMenu)
	{
		if (PauseMenuClass)
		{
			PauseMenu = CreateWidget<UPauseMenuWidget>(this, PauseMenuClass);
			if (PauseMenu)
			{
				PauseMenu->AddToViewport(ZOrderPauseMenu);
				// Cree une fois pour toutes et simplement cache, comme
				// l'inventaire : le menu de pause survit ainsi aux respawns.
				PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseMenuClass n'est pas assignee, le menu de pause ne s'ouvrira pas."));
		}
	}
}

void AFpsPlayerController::ToggleInventory()
{
	if (!InventoryWidget)
	{
		return;
	}

	bInventoryOpen = !bInventoryOpen;

	InventoryWidget->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);

	AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());

	if (bInventoryOpen)
	{
		// GameAndUI et non UIOnly : en UIOnly, plus aucune action Enhanced
		// Input ne passerait et on ne pourrait plus refermer avec Tab.
		FInputModeGameAndUI Mode;
		Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
		Mode.SetHideCursorDuringCapture(false);
		SetInputMode(Mode);
		SetShowMouseCursor(true);

		// Le mode GameAndUI laisse passer le jeu : sans ces deux appels, la
		// souris ferait tourner la camera pendant qu'on navigue dans la grille.
		SetIgnoreLookInput(true);
		SetIgnoreMoveInput(true);

		// Les deux appels ci-dessus ne couvrent que le deplacement et le regard.
		// Accroupi, sprint, saut et interaction resteraient actifs -- appuyer
		// sur Ctrl pour glisser un objet ferait s'accroupir le personnage.
		// DisableInput retire l'input component du PAWN de la pile ; celui du
		// controller reste, donc Tab continue de repondre.
		if (PlayerPawn)
		{
			PlayerPawn->ClearTransientInputStates();
			PlayerPawn->DisableInput(this);
		}
	}
	else
	{
		SetInputMode(FInputModeGameOnly());
		SetShowMouseCursor(false);

		// Reset et non SetIgnoreXxx(false) : ces fonctions gerent un compteur,
		// et un desequilibre finirait par bloquer definitivement les inputs.
		ResetIgnoreLookInput();
		ResetIgnoreMoveInput();

		if (PlayerPawn)
		{
			PlayerPawn->EnableInput(this);
		}
	}
}

void AFpsPlayerController::HandlePauseInput()
{
	// La boite a le focus et consomme deja Echap elle-meme. Si elle l'a perdu,
	// on l'ignore quand meme : repondre ici refermerait le menu en laissant la
	// question posee.
	if (bQuitToMenuDialogOpen)
	{
		return;
	}

	// L'ecran de reglages consomme Echap lui-meme tant qu'il a le focus. Comme
	// pour la boite, on ne repond ici que s'il l'a perdu.
	if (bSettingsOpen)
	{
		HandleSettingsCloseRequested();
		return;
	}

	// Echap sert d'abord a annuler ce qui est ouvert. Fermer l'inventaire est
	// ce que le joueur attend en premier.
	if (bInventoryOpen)
	{
		ToggleInventory();
		return;
	}

	if (bPauseMenuOpen)
	{
		ResumeGame();
		return;
	}

	OpenPauseMenu();
}

void AFpsPlayerController::OpenPauseMenu()
{
	if (!PauseMenu || bPauseMenuOpen)
	{
		return;
	}

	// Mettre en pause pendant l'ecran de mort figerait le timer de respawn : le
	// joueur resterait devant un ecran noir dont plus rien ne le sortirait.
	if (GetWorldTimerManager().IsTimerActive(RespawnTimerHandle))
	{
		return;
	}

	bPauseMenuOpen = true;
	PauseMenu->SetVisibility(ESlateVisibility::Visible);

	// GameAndUI et non UIOnly, meme raison que l'inventaire : en UIOnly plus
	// aucune action Enhanced Input ne passe, et Echap ne refermerait rien.
	FInputModeGameAndUI Mode;
	Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	Mode.SetHideCursorDuringCapture(false);
	SetInputMode(Mode);
	SetShowMouseCursor(true);

	// Pas de DisableInput sur le pawn ici, contrairement a l'inventaire : le
	// monde est reellement fige, et les actions du pawn n'ont pas
	// "Trigger When Paused" -- elles ne se declenchent donc plus d'elles-memes.
	SetPause(true);
}

void AFpsPlayerController::ResumeGame()
{
	if (!bPauseMenuOpen)
	{
		return;
	}

	// Reprendre alors qu'une boite ou les reglages sont encore ouverts les
	// laisserait affiches par-dessus la partie relancee.
	if (bQuitToMenuDialogOpen)
	{
		HandleQuitToMenuCancelled();
	}

	if (bSettingsOpen)
	{
		HandleSettingsCloseRequested();
	}

	bPauseMenuOpen = false;

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

	SetPause(false);

	SetInputMode(FInputModeGameOnly());
	SetShowMouseCursor(false);
}

void AFpsPlayerController::OpenSettings()
{
	if (bSettingsOpen)
	{
		return;
	}

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

	// Creee a la premiere ouverture seulement : un ecran de reglages qu'on ne
	// visite peut-etre jamais n'a pas a peser sur chaque demarrage de partie.
	if (!SettingsMenu)
	{
		SettingsMenu = CreateWidget<USettingsMenuWidget>(this, SettingsMenuClass);
		if (!SettingsMenu)
		{
			return;
		}

		SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested);
		SettingsMenu->AddToViewport(ZOrderInGameSettingsMenu);

		// L'ecran de reglages est partage avec le menu principal, dont
		// l'empilement n'a rien a voir avec celui du jeu : c'est a nous, qui
		// possedons la pile, de lui dire ou poser sa boite. +10 la place devant
		// lui sans empieter sur la boite d'abandon de partie a 200.
		SettingsMenu->SetDialogZOrder(ZOrderInGameSettingsMenu + 10);
	}

	bSettingsOpen = true;
	SettingsMenu->Open();

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

void AFpsPlayerController::HandleSettingsCloseRequested()
{
	bSettingsOpen = false;

	if (SettingsMenu)
	{
		SettingsMenu->Close();
	}

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

void AFpsPlayerController::RequestQuitToMainMenu()
{
	// Le fondu de sortie est deja lance : on ignore le second clic plutot que
	// d'empiler deux chargements.
	if (bQuittingToMainMenu)
	{
		return;
	}

	if (MainMenuLevel.IsNull())
	{
		UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : MainMenuLevel n'est pas assignee, impossible de retourner au menu."));
		return;
	}

	// Sans boite assignee on ne bloque pas le joueur dans sa partie : on part.
	if (!QuitToMenuDialogClass)
	{
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : QuitToMenuDialogClass n'est pas assignee, retour au menu sans confirmation."));
		HandleQuitToMenuConfirmed();
		return;
	}

	// Creee au premier clic seulement, puis reutilisee.
	if (!QuitToMenuDialog)
	{
		QuitToMenuDialog = CreateWidget<UConfirmDialogWidget>(this, QuitToMenuDialogClass);
		if (!QuitToMenuDialog)
		{
			return;
		}

		QuitToMenuDialog->OnConfirmed.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuConfirmed);
		QuitToMenuDialog->OnCancelled.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuCancelled);
		QuitToMenuDialog->AddToViewport(ZOrderQuitToMenuDialog);
	}

	bQuitToMenuDialogOpen = true;
	QuitToMenuDialog->Open(QuitToMenuMessage);

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

void AFpsPlayerController::HandleQuitToMenuConfirmed()
{
	bQuitToMenuDialogOpen = false;
	bQuittingToMainMenu = true;

	if (QuitToMenuDialog)
	{
		QuitToMenuDialog->Close();
	}

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

	// On reste en pause pendant le fondu : relancer le monde une seconde pour
	// l'abandonner ensuite laisserait le temps a la faim ou a une chute de tuer
	// un joueur qui a deja quitte. C'est UScreenFadeComponent qui continue de
	// ticker en pause, pas le monde.
	ScreenFade->FadeOut(QuitToMenuFadeDuration);
}

void AFpsPlayerController::HandleQuitToMenuCancelled()
{
	bQuitToMenuDialogOpen = false;

	if (QuitToMenuDialog)
	{
		QuitToMenuDialog->Close();
	}

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

void AFpsPlayerController::HandleFadeFinished()
{
	if (!bQuittingToMainMenu)
	{
		return;
	}

	// La pause vit sur les WorldSettings, que OpenLevel detruit avec le monde.
	// On la leve quand meme : un monde qu'on quitte en etat de pause est un
	// piege si le chargement echoue.
	SetPause(false);

	UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuLevel);
}

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

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

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

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

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

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

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

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

	// Le voile de fondu est nettoye par UScreenFadeComponent::EndPlay.

	Super::EndPlay(EndPlayReason);
}

#undef LOCTEXT_NAMESPACE
