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


#include "SurvivalGameInstance.h"

#include "ContentStreaming.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/Engine.h"
#include "Engine/GameViewportClient.h"
#include "Engine/LocalPlayer.h"
#include "GameMapsSettings.h"
#include "InputMappingContext.h"
#include "KeyboardLayout.h"
#include "Kismet/GameplayStatics.h"
#include "SessionSubsystem.h"
#include "SurvivalUserSettings.h"
#include "UserSettings/EnhancedInputUserSettings.h"

void USurvivalGameInstance::Init()
{
	Super::Init();

	// Pas d'enregistrement des IMC ici : a ce stade il n'y a ni joueur local ni
	// PlayerController, donc pas encore de UserSettings a qui parler.
	// RegisterMappableContexts() est appelee par l'ecran de reglages.

	// Les reglages sont deja LUS depuis le .ini par le moteur, mais rien ne les
	// a encore APPLIQUES : sans cet appel, la resolution et la qualite
	// choisies au dernier lancement resteraient lettre morte jusqu'au premier
	// passage dans le menu.
	//
	// C'est le bon endroit : le GameInstance est le seul objet qui naisse avant
	// la premiere map et survive a tous les changements de niveau.
	if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
	{
		Settings->ApplySettings(/*bCheckForCommandLineOverrides=*/false);
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, aucun reglage ne sera pris en compte."));
	}

	// Filet de securite reseau. On s'abonne ici, une fois pour toutes : ces
	// evenements arrivent au pire moment, typiquement quand le monde est en
	// train d'etre detruit, et un abonnement porte par un acteur mourrait
	// justement avec lui.
	if (GEngine)
	{
		NetworkFailureHandle = GEngine->OnNetworkFailure().AddUObject(this, &USurvivalGameInstance::HandleNetworkFailure);
		TravelFailureHandle = GEngine->OnTravelFailure().AddUObject(this, &USurvivalGameInstance::HandleTravelFailure);
	}

	PreLoadMapHandle = FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &USurvivalGameInstance::HandlePreLoadMap);
}

void USurvivalGameInstance::HandlePreLoadMap(const FString& MapName)
{
	// On coupe AVANT que quoi que ce soit du nouveau monde n'existe. C'est le
	// premier controller pret qui rallumera, une fois son voile noir en place --
	// et il naitra forcement apres nous, ce qui garantit le bon ordre.
	SetWorldRenderingSuspended(true);
	UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : chargement de [%s], rendu du monde suspendu."), *MapName);

	// Le timer du GameInstance et non celui du monde : celui-ci est justement en
	// train d'etre detruit.
	GetTimerManager().SetTimer(
		RenderingResumeTimeout, this, &USurvivalGameInstance::ResumeWorldRendering,
		20.f, /*bLoop=*/false);
}

void USurvivalGameInstance::BeginSceneWarmup(FSimpleDelegate OnComplete)
{
	SceneWarmupCompleted = OnComplete;
	RemainingWarmupFrames = FMath::Max(SceneWarmupFrames, 0);
	WarmupStartTime = FPlatformTime::Seconds();

	if (RemainingWarmupFrames <= 0)
	{
		SceneWarmupCompleted.ExecuteIfBound();
		SceneWarmupCompleted.Unbind();
		return;
	}

	TickSceneWarmup();
}

void USurvivalGameInstance::TickSceneWarmup()
{
	const double Elapsed = FPlatformTime::Seconds() - WarmupStartTime;
	const bool bCapReached = Elapsed >= SceneWarmupMaxSeconds;

	if (RemainingWarmupFrames > 0 && !bCapReached)
	{
		--RemainingWarmupFrames;

		// SetTimerForNextTick et non un timer en secondes : une image rendue,
		// un tick. C'est l'unite dans laquelle Nanite et Lumen progressent.
		GetTimerManager().SetTimerForNextTick(this, &USurvivalGameInstance::TickSceneWarmup);
		return;
	}

	UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : chauffe terminee -- %d images rendues en %.0f ms%s."),
		SceneWarmupFrames - RemainingWarmupFrames, Elapsed * 1000.0,
		bCapReached ? TEXT(" (plafond atteint)") : TEXT(""));

	// Copie avant execution : le rappel declenche un fondu, qui pourrait
	// theoriquement relancer une chauffe et ecraser le delegue en pleine main.
	FSimpleDelegate Completed = SceneWarmupCompleted;
	SceneWarmupCompleted.Unbind();
	Completed.ExecuteIfBound();
}

void USurvivalGameInstance::SetWorldRenderingSuspended(bool bSuspended)
{
	if (UGameViewportClient* Viewport = GetGameViewportClient())
	{
		Viewport->bDisableWorldRendering = bSuspended;
	}
}

void USurvivalGameInstance::ResumeWorldRendering()
{
	GetTimerManager().ClearTimer(RenderingResumeTimeout);

	// Streaming vide AVANT de rallumer. UpdateResourceStreaming(0, true) traite
	// tout sans avoir besoin d'un point de vue -- ce qui tombe bien, le rendu
	// est encore coupe donc personne ne lui en a fourni -- puis on bloque le
	// temps que les requetes aboutissent.
	//
	// Ce blocage a lieu ECRAN NOIR : c'est du temps de chargement, pas une
	// saccade. C'est exactement ce que le moteur fait lui-meme a l'ouverture
	// d'une map, et c'est ce qui evite de voir le terrain se raffiner et les
	// modeles apparaitre un a un.
	const double StartTime = FPlatformTime::Seconds();
	const int32 PendingRequests = IStreamingManager::Get().StreamAllResources(FMath::Max(StreamingFlushTimeLimit, 0.f));
	const double ElapsedMs = (FPlatformTime::Seconds() - StartTime) * 1000.0;

	SetWorldRenderingSuspended(false);

	UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : rendu du monde repris apres %.0f ms de streaming (%d requete(s) restante(s))."),
		ElapsedMs, PendingRequests);
}

void USurvivalGameInstance::Shutdown()
{
	if (GEngine)
	{
		GEngine->OnNetworkFailure().Remove(NetworkFailureHandle);
		GEngine->OnTravelFailure().Remove(TravelFailureHandle);
	}

	FCoreUObjectDelegates::PreLoadMap.Remove(PreLoadMapHandle);

	Super::Shutdown();
}

void USurvivalGameInstance::HandleNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString)
{
	UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : echec reseau [%s] -- %s"),
		ENetworkFailure::ToString(FailureType), *ErrorString);

	// Depart deja en cours : c'est le cas NORMAL quand l'hote quitte, puisqu'il
	// a prevenu tout le monde avant de couper. On laisse les fondus finir.
	if (bReturningToMainMenu)
	{
		UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : depart deja en cours, echec ignore."));
		return;
	}

	// Ici en revanche personne n'a prevenu : hote qui plante, cable arrache,
	// relais Steam qui lache. C'est le filet de secours, sans fondu -- mieux
	// vaut un retour brutal au menu qu'un monde mort.
	ReturnToMainMenu();
}

void USurvivalGameInstance::HandleTravelFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ErrorString)
{
	UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : echec de voyage [%s] -- %s"),
		ETravelFailure::ToString(FailureType), *ErrorString);

	if (bReturningToMainMenu)
	{
		return;
	}

	ReturnToMainMenu();
}

void USurvivalGameInstance::ReturnToMainMenu()
{
	bReturningToMainMenu = true;

	// La session est morte avec la connexion : la detruire ici evite qu'elle
	// reste annoncee a tes amis, et surtout qu'elle bloque la creation de la
	// suivante.
	if (USessionSubsystem* Sessions = GetSubsystem<USessionSubsystem>())
	{
		Sessions->LeaveSession();
	}

	// GetGameDefaultMap et pas un chemin en dur : c'est deja MenuScene dans
	// DefaultEngine.ini, et une seconde copie du chemin finirait par diverger.
	const FString MenuMap = UGameMapsSettings::GetGameDefaultMap();
	if (MenuMap.IsEmpty())
	{
		UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : aucune GameDefaultMap, impossible de revenir au menu."));
		return;
	}

	UGameplayStatics::OpenLevel(this, FName(*MenuMap), /*bAbsolute=*/true);
}

void USurvivalGameInstance::RegisterMappableContexts()
{
	ULocalPlayer* LocalPlayer = GetFirstGamePlayer();
	if (!LocalPlayer)
	{
		UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : aucun joueur local, enregistrement des touches remis a plus tard."));
		return;
	}

	if (MappableContexts.IsEmpty())
	{
		UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : MappableContexts est vide, l'onglet Touches affichera 'Non assignee' partout. Ajoute IMC_Default dans le Blueprint de GameInstance."));
		return;
	}

	UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
	UEnhancedInputUserSettings* InputSettings = Subsystem ? Subsystem->GetUserSettings() : nullptr;

	if (!InputSettings)
	{
		UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : reglages d'input introuvables. Coche 'Enable User Settings' dans Project Settings > Enhanced Input."));
		return;
	}

	bool bRegisteredAnyContext = false;

	for (const TObjectPtr<UInputMappingContext>& Context : MappableContexts)
	{
		if (Context)
		{
			bRegisteredAnyContext = true;
			// Idempotent : le moteur ignore un contexte deja connu, on peut donc
			// rappeler cette fonction sans precaution.
			InputSettings->RegisterInputMappingContext(Context);
		}
	}

	// APRES l'enregistrement et jamais avant : le profil de touches ne connait un
	// mapping -- donc sa touche par defaut -- qu'une fois son IMC declare. Appeler
	// la conversion plus tot la ferait tourner sur un profil vide, sans erreur ni
	// effet, ce qui est le pire des cas a diagnostiquer.
	//
	// UNE SEULE FOIS par session, en revanche. Cette fonction est aussi le point
	// d'entree du rafraichissement de l'onglet Touches, appele apres CHAQUE
	// remappage : y laisser la conversion reprendrait sa touche au joueur qui a
	// choisi celle de l'IMC -- la lettre W sur un AZERTY est identique au defaut,
	// donc indiscernable d'un mapping auquel personne n'a touche.
	if (bRegisteredAnyContext && !bKeyboardLayoutApplied)
	{
		bKeyboardLayoutApplied = true;
		ApplyKeyboardLayoutToDefaultKeys();
	}
}

void USurvivalGameInstance::ApplyKeyboardLayoutToDefaultKeys()
{
	const ULocalPlayer* LocalPlayer = GetFirstGamePlayer();
	if (!LocalPlayer)
	{
		return;
	}

	UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
	UEnhancedInputUserSettings* InputSettings = Subsystem ? Subsystem->GetUserSettings() : nullptr;
	const UEnhancedPlayerMappableKeyProfile* Profile = InputSettings ? InputSettings->GetActiveKeyProfile() : nullptr;

	if (!Profile)
	{
		return;
	}

	// On COLLECTE avant d'appliquer, au lieu de mapper au fil de la lecture.
	// MapPlayerKey ecrit dans les memes lignes que celles qu'on parcourt : muter un
	// conteneur pendant son iteration invalide les iterateurs des que le stockage
	// est reorganise. Le bug qui en sort est intermittent et ne ressemble a rien.
	struct FPendingRemap
	{
		FName MappingName;
		EPlayerMappableKeySlot Slot;
		FKey LayoutKey;
	};

	TArray<FPendingRemap> PendingRemaps;

	for (const TPair<FName, FKeyMappingRow>& Row : Profile->GetPlayerMappingRows())
	{
		for (const FPlayerKeyMapping& Mapping : Row.Value.Mappings)
		{
			// Le joueur a deja choisi cette touche : son choix fait autorite, meme
			// s'il est incoherent avec sa disposition. C'est aussi ce qui rend la
			// fonction idempotente -- notre propre conversion compte comme un choix
			// des qu'elle est posee, donc on ne repasse jamais dessus.
			if (Mapping.IsCustomized())
			{
				continue;
			}

			const FKey DefaultKey = Mapping.GetDefaultKey();
			const FKey LayoutKey = KeyboardLayout::RemapKeyToActiveLayout(DefaultKey);

			// Clavier QWERTY, touche insensible a la disposition, ou manette : la
			// conversion rend la touche d'origine et il n'y a rien a ecrire. Sans ce
			// test on poserait une surcharge identique au defaut sur chaque mapping,
			// et "Reinitialiser" n'aurait plus rien a reinitialiser.
			if (LayoutKey == DefaultKey)
			{
				continue;
			}

			PendingRemaps.Add({ Row.Key, Mapping.GetSlot(), LayoutKey });
		}
	}

	// Journalise AVANT de sortir, et donc meme quand il n'y a rien a faire. C'est
	// la seule trace lisible quand un testeur signale des touches inattendues, et
	// le cas "0 touche" est justement celui qu'il faut pouvoir distinguer du
	// silence : sans cette ligne, un clavier QWERTY et un systeme qui n'a pas
	// tourne du tout produisent exactement le meme log, c'est-a-dire aucun.
	//
	// Un KLID se lit en deux temps : "0000040C" est un AZERTY francais, "00000409"
	// un QWERTY US. Un utilisateur francais sur clavier QWERTY rend bien 00000409,
	// parce que c'est la DISPOSITION qui est journalisee, pas la langue -- et c'est
	// exactement la distinction sur laquelle une detection par pays se tromperait.
	UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : disposition clavier [%s], %d touche(s) a traduire depuis le QWERTY de reference."),
		*KeyboardLayout::GetActiveLayoutId(), PendingRemaps.Num());

	if (PendingRemaps.IsEmpty())
	{
		return;
	}

	for (const FPendingRemap& Remap : PendingRemaps)
	{
		FMapPlayerKeyArgs Args;
		Args.MappingName = Remap.MappingName;
		Args.Slot = Remap.Slot;
		Args.NewKey = Remap.LayoutKey;

		FGameplayTagContainer FailureReason;
		InputSettings->MapPlayerKey(Args, FailureReason);

		if (!FailureReason.IsEmpty())
		{
			UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : conversion de '%s' vers '%s' refusee (%s)."),
				*Remap.MappingName.ToString(), *Remap.LayoutKey.ToString(), *FailureReason.ToString());
		}
	}

	// Pousse les surcharges dans les contextes deja actifs : le pawn n'a rien a
	// rebrancher, les touches sont bonnes des la premiere image.
	InputSettings->ApplySettings();
}
