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

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "SurvivalGameInstance.generated.h"

class UInputMappingContext;
class ULocalPlayer;

/**
 * Le seul objet qui survit aux changements de map.
 *
 * C'est l'equivalent le plus proche d'un singleton DontDestroyOnLoad d'Unity :
 * il naît au lancement du jeu et meurt a sa fermeture. Tout le reste --
 * GameMode, GameState, PlayerController, pawns, widgets -- est detruit et
 * recree a chaque OpenLevel.
 *
 * Y mettre : les reglages, la sauvegarde en cours, et plus tard l'hebergement
 * et le rejoindre-une-partie. Surtout pas d'etat de gameplay, qui appartient au
 * monde et doit disparaitre avec lui.
 */
UCLASS()
class EMBERWILD_API USurvivalGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	virtual void Init() override;
	virtual void Shutdown() override;

	/** Nom de la derniere camera de menu utilisee. NAME_None au premier lancement. */
	UFUNCTION(BlueprintPure, Category = "Menu")
	FName GetLastMenuCameraName() const { return LastMenuCameraName; }

	UFUNCTION(BlueprintCallable, Category = "Menu")
	void SetLastMenuCameraName(FName SpotName) { LastMenuCameraName = SpotName; }

	/**
	 * Declare les IMC remappables aupres des reglages d'input du joueur.
	 *
	 * Appelee A LA DEMANDE, par l'ecran de reglages avant de lire les touches, et
	 * non depuis un evenement de demarrage. Raison : UserSettings n'est cree que
	 * dans UEnhancedInputLocalPlayerSubsystem::PlayerControllerChanged, et
	 * ULocalPlayer diffuse ses evenements AVANT de prevenir ses sous-systemes --
	 * tout hook precoce arrive donc trop tot, quel qu'il soit.
	 *
	 * Idempotente : le moteur ignore un contexte deja connu.
	 */
	UFUNCTION(BlueprintCallable, Category = "Input")
	void RegisterMappableContexts();

	/**
	 * Traduit les touches par defaut vers la disposition clavier du joueur.
	 *
	 * Les IMC sont ecrits en QWERTY US -- EKeys::W pour avancer -- et Windows livre
	 * au moteur une touche DEJA traduite par la disposition : en AZERTY, la touche
	 * du haut du bloc de deplacement arrive comme EKeys::Z et le mapping "W" ne se
	 * declenche jamais. On repose donc chaque touche a la position equivalente.
	 * Le detail du mecanisme est dans KeyboardLayout.h.
	 *
	 * Ne touche QUE les mappings que le joueur n'a pas remappes lui-meme : un
	 * choix explicite l'emporte toujours sur une deduction. Et sur un clavier
	 * QWERTY la conversion rend la touche d'origine, donc rien n'est ecrit du tout
	 * et le profil reste vierge.
	 *
	 * Declenchee UNE FOIS par RegisterMappableContexts(), au premier enregistrement
	 * des contextes ; ensuite seul "Reinitialiser" la redemande. Elle appartient a
	 * l'initialisation, pas au rafraichissement d'ecran -- l'onglet Touches se
	 * rafraichit apres chaque remappage, et repasser dessus reprendrait sa touche
	 * au joueur qui aurait justement choisi celle de l'IMC (la lettre W sur un
	 * AZERTY, par exemple : identique au defaut, donc indiscernable d'un mapping
	 * jamais touche).
	 */
	UFUNCTION(BlueprintCallable, Category = "Input")
	void ApplyKeyboardLayoutToDefaultKeys();

	/**
	 * Un depart VOULU vers le menu commence : le filet de secours reseau doit
	 * se taire.
	 *
	 * Sans ce drapeau, la connexion qui tombe pendant le fondu de sortie
	 * declencherait HandleNetworkFailure, qui chargerait le menu sur-le-champ
	 * et couperait le fondu en plein milieu -- exactement le saut d'image qu'on
	 * cherchait a eviter en fondant.
	 */
	void BeginReturnToMainMenu() { bReturningToMainMenu = true; }

	/** On est arrive au menu : le filet redevient actif pour la partie suivante. */
	void ClearReturnToMainMenu() { bReturningToMainMenu = false; }

	/**
	 * Rend la main au rendu du monde, coupe depuis le chargement de la map.
	 *
	 * A n'appeler QUE lorsque le voile de fondu est deja noir : le monde
	 * reapparait alors derriere lui, invisible.
	 *
	 * Pourquoi couper le rendu plutot que poser un widget. Mesure sur une vraie
	 * connexion : 2,4 secondes s'ecoulent entre `LoadMap` chez le client et la
	 * naissance de son PlayerController, que le serveur lui envoie. Pendant ce
	 * temps la map est chargee et le moteur la rend depuis une vue par defaut --
	 * sous le terrain. Il n'existe alors AUCUN acteur a qui accrocher un voile,
	 * et le composant de fondu vit justement sur ce controller absent. Le
	 * viewport, lui, survit aux changements de map et n'appartient a personne.
	 *
	 * Attend aussi la fin du streaming avant de rendre la main : c'est le seul
	 * instant du chargement ou l'on sait que rien ne sera montre tant qu'on n'a
	 * pas fini. Sans ca, le joueur voit le terrain passer de sa version
	 * grossiere a la vraie, et les modeles apparaitre un a un.
	 */
	UFUNCTION(BlueprintCallable, Category = "Chargement")
	void ResumeWorldRendering();

	/**
	 * Laisse le monde rendre SceneWarmupFrames images derriere le voile noir,
	 * puis rappelle OnComplete.
	 *
	 * Compte des IMAGES et non des secondes, et c'est tout l'interet. Ce qui
	 * reste a se raffiner apres le flush de streaming -- Nanite, les ombres
	 * virtuelles, la convergence de Lumen -- progresse a chaque image RENDUE,
	 * pas avec le temps qui passe. Une duree fixe donnerait quarante images sur
	 * une machine rapide et cinq sur une machine lente : exactement celle qui en
	 * aurait eu le plus besoin.
	 */
	void BeginSceneWarmup(FSimpleDelegate OnComplete);

protected:
	/**
	 * Temps maximal accordé au streaming avant de montrer le monde, en secondes.
	 *
	 * Ce blocage se produit ECRAN NOIR, donc il ne se voit pas -- c'est du temps
	 * de chargement, pas une saccade. Le dépasser vaut mieux que de montrer un
	 * terrain qui se raffine sous les yeux du joueur, mais on le borne quand
	 * même : une machine lente ne doit pas rester bloquée indéfiniment.
	 *
	 * Réglable sur BP_SurvivalGameInstance.
	 */
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0", Units = "s"))
	float StreamingFlushTimeLimit = 5.f;

	/**
	 * Nombre d'images rendues derriere le voile noir avant de l'eclaircir.
	 * Voir BeginSceneWarmup(). Reglable sur BP_SurvivalGameInstance.
	 */
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0"))
	int32 SceneWarmupFrames = 45;

	/**
	 * Plafond de la chauffe, en secondes.
	 *
	 * Le compte d'images reste la mesure PRINCIPALE -- c'est la bonne unite.
	 * Ceci n'est qu'un garde-fou : sur une machine tres lente, 45 images
	 * pourraient prendre plusieurs secondes d'ecran noir, et mieux vaut un
	 * decor pas tout a fait converge qu'une attente qui inquiete.
	 */
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0.1", Units = "s"))
	float SceneWarmupMaxSeconds = 2.f;

	/**
	 * Les mapping contexts dont les touches sont remappables. Mets-y IMC_Default.
	 *
	 * Ici et PAS sur le pawn : le menu principal n'a aucun pawn, et l'ecran de
	 * reglages doit pourtant afficher les touches. C'est le critere habituel --
	 * "est-ce que ca doit survivre au changement de map ?" -- qui envoie cette
	 * liste sur le GameInstance.
	 */
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
	TArray<TObjectPtr<UInputMappingContext>> MappableContexts;

private:
	/**
	 * L'hote a quitte, ou la connexion est tombee.
	 *
	 * Sans ce filet, un client dont l'hote ferme le jeu reste planté dans un
	 * monde qui ne repond plus : le moteur coupe la connexion, mais rien ne le
	 * ramene nulle part. C'est ici et pas sur le PlayerController parce que
	 * celui-ci est justement detruit par l'echec -- le GameInstance est le seul
	 * objet dont on soit certain qu'il vive encore a cet instant.
	 */
	void HandleNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString);

	/** Meme filet pour un voyage qui echoue en route. */
	void HandleTravelFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ErrorString);

	/**
	 * Ferme la session en cours et rouvre le menu principal.
	 *
	 * La map du menu n'est PAS une propriete de plus : c'est deja la
	 * GameDefaultMap du projet. En dupliquer le chemin ici donnerait deux
	 * valeurs a tenir en phase, et celle-ci ne serait jamais testee.
	 */
	void ReturnToMainMenu();

	/**
	 * Une map va etre chargee : on coupe le rendu jusqu'a ce qu'un controller
	 * soit pret.
	 *
	 * PreLoadMap et surtout PAS PostLoadMapWithWorld : ce dernier est diffuse
	 * APRES le BeginPlay des acteurs. Sur une map chargee localement -- le menu,
	 * ou la partie chez l'hote -- le controller rendait donc la main au rendu,
	 * et on la lui reprenait juste apres. Ecran noir definitif, jusqu'au filet
	 * de securite. Chez un client ca passait par accident, son controller
	 * arrivant plus tard.
	 */
	void HandlePreLoadMap(const FString& MapName);

	void SetWorldRenderingSuspended(bool bSuspended);

	/** Une image de chauffe vient de passer : on decompte, ou on rappelle. */
	void TickSceneWarmup();

	/** Images restantes a rendre avant d'eclaircir. */
	int32 RemainingWarmupFrames = 0;

	/** Debut de la chauffe, pour appliquer le plafond en secondes. */
	double WarmupStartTime = 0.0;

	/** Ce qu'on fait une fois la chauffe finie -- eclaircir, cote appelant. */
	FSimpleDelegate SceneWarmupCompleted;

	FDelegateHandle NetworkFailureHandle;
	FDelegateHandle TravelFailureHandle;
	FDelegateHandle PreLoadMapHandle;

	/**
	 * Filet : si aucun controller ne rend la main -- connexion qui n'aboutit
	 * pas, spawn rate -- on rallume quand meme. Un ecran noir definitif se lit
	 * comme un plantage, et c'est pire que le defaut qu'on corrige.
	 */
	FTimerHandle RenderingResumeTimeout;

	/** Un retour au menu est deja en cours, voulu ou non. */
	bool bReturningToMainMenu = false;

	/**
	 * La disposition clavier a deja ete appliquee dans cette session.
	 *
	 * Pas de UPROPERTY(config) : ce n'est PAS "l'a-t-on deja fait un jour". Les
	 * surcharges posees, elles, sont persistees par Enhanced Input, et elles
	 * suffisent a rendre l'operation sans effet au lancement suivant. Ce drapeau
	 * ne protege que du re-declenchement en cours de session.
	 */
	bool bKeyboardLayoutApplied = false;

	/**
	 * Stocke un NOM et pas un index : les index changent des qu'on ajoute ou
	 * supprime une camera dans la map, et on retomberait sur autre chose.
	 *
	 * Ne sert qu'a eviter de retirer deux fois de suite le meme angle quand le
	 * joueur revient au menu -- d'ou le Transient, ca n'a pas a etre sauvegarde.
	 */
	UPROPERTY(Transient)
	FName LastMenuCameraName = NAME_None;
};
