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


#include "SettingsDisplayWidget.h"

#include "Kismet/KismetSystemLibrary.h"
#include "SettingRowWidget.h"
#include "SettingsMenuWidget.h"
#include "SurvivalUserSettings.h"

#define LOCTEXT_NAMESPACE "Reglages"

namespace SettingsDisplayTab
{
	// Ordre des modes dans la ligne. Il doit correspondre a MakeWindowMode et a
	// FindDisplayModeIndex : trois listes a garder alignees, d'ou leur voisinage.
	constexpr int32 ModeFullscreen = 0;
	constexpr int32 ModeBorderless = 1;
	constexpr int32 ModeWindowed = 2;

	EWindowMode::Type MakeWindowMode(int32 Index)
	{
		switch (Index)
		{
		case ModeBorderless: return EWindowMode::WindowedFullscreen;
		case ModeWindowed:   return EWindowMode::Windowed;
		default:             return EWindowMode::Fullscreen;
		}
	}

	int32 FindDisplayModeIndex(EWindowMode::Type Mode)
	{
		switch (Mode)
		{
		case EWindowMode::WindowedFullscreen: return ModeBorderless;
		case EWindowMode::Windowed:           return ModeWindowed;
		default:                              return ModeFullscreen;
		}
	}

	/**
	 * Limites de FPS proposees. 0 = illimitee, c'est la convention de
	 * SetFrameRateLimit et non une valeur sentinelle a nous.
	 */
	const TArray<float>& FrameRateLimits()
	{
		static const TArray<float> Limits = { 30.f, 60.f, 90.f, 120.f, 144.f, 165.f, 240.f, 0.f };
		return Limits;
	}

	int32 FindFrameRateIndex(float Limit)
	{
		const TArray<float>& Limits = FrameRateLimits();

		// Comparaison a la tolerance pres : la valeur relue du .ini a pu passer
		// par une conversion et ne plus etre strictement egale.
		for (int32 Index = 0; Index < Limits.Num(); ++Index)
		{
			if (FMath::IsNearlyEqual(Limits[Index], Limit, 0.5f))
			{
				return Index;
			}
		}

		// Valeur exotique posee a la main dans le .ini : on retombe sur
		// "Illimitee" plutot que d'afficher n'importe quoi.
		return Limits.Num() - 1;
	}
}

USettingsDisplayWidget::USettingsDisplayWidget(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// Dans le constructeur, pour la meme raison que ConflictMessage : ecrit dans
	// NativeConstruct, ce texte ecraserait a chaque ouverture celui regle dans le
	// Blueprint.
	VideoConfirmMessage = LOCTEXT("VideoConfirm",
		"Garder ce mode d'affichage ?\nRetour automatique dans {0} s.");
}

void USettingsDisplayWidget::NativeConstruct()
{
	Super::NativeConstruct();

	using namespace SurvivalSettingsRange;

	if (DisplayModeRow)
	{
		DisplayModeRow->SetupAsChoice(LOCTEXT("ModeAffichage", "Mode d'affichage"),
			{ LOCTEXT("PleinEcran", "Plein écran"),
			  LOCTEXT("SansBordure", "Sans bordure"),
			  LOCTEXT("Fenetre", "Fenêtré") }, 0);
		DisplayModeRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleDisplayModeChanged);
	}

	if (ResolutionRow)
	{
		ResolutionRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleResolutionChanged);
	}

	if (FrameRateLimitRow)
	{
		TArray<FText> Labels;
		Labels.Reserve(SettingsDisplayTab::FrameRateLimits().Num());
		for (const float Limit : SettingsDisplayTab::FrameRateLimits())
		{
			Labels.Add(FMath::IsNearlyZero(Limit)
				? LOCTEXT("Illimitee", "Illimitée")
				: FText::AsNumber(FMath::RoundToInt(Limit)));
		}

		FrameRateLimitRow->SetupAsChoice(LOCTEXT("LimiteFPS", "Limite de FPS"), Labels, 0);
		FrameRateLimitRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleFrameRateLimitChanged);
	}

	if (VSyncRow)
	{
		VSyncRow->SetupAsChoice(LOCTEXT("VSync", "Synchronisation verticale"),
			{ LOCTEXT("Non", "Non"), LOCTEXT("Oui", "Oui") }, 0);
		VSyncRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleVSyncChanged);
	}

	if (ResolutionScaleRow)
	{
		ResolutionScaleRow->SetupAsNumber(LOCTEXT("EchelleResolution", "Échelle de résolution"),
			ResolutionScaleMin, ResolutionScaleMax, ResolutionScaleStep, ResolutionScaleArrowStep,
			ResolutionScaleMax, LOCTEXT("Pourcent", " %"));
		ResolutionScaleRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleResolutionScaleChanged);
	}

	if (BrightnessRow)
	{
		BrightnessRow->SetupAsNumber(LOCTEXT("Luminosite", "Luminosité"),
			BrightnessMin, BrightnessMax, BrightnessStep, BrightnessArrowStep,
			BrightnessMin, LOCTEXT("Pourcent", " %"));
		BrightnessRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleBrightnessChanged);
	}

	RebuildResolutionList();
	RefreshFromSettings();
}

void USettingsDisplayWidget::RebuildResolutionList()
{
	AvailableResolutions.Reset();

	// La liste depend du mode : en fenetre on veut des tailles qui tiennent dans
	// l'ecran, pas les modes exclusifs de la carte graphique.
	const USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	const bool bWindowed = Settings && Settings->GetFullscreenMode() == EWindowMode::Windowed;

	if (bWindowed)
	{
		UKismetSystemLibrary::GetConvenientWindowedResolutions(AvailableResolutions);
	}
	else
	{
		UKismetSystemLibrary::GetSupportedFullscreenResolutions(AvailableResolutions);
	}

	if (AvailableResolutions.Num() == 0)
	{
		// Ecran non interroge (editeur, machine sans mode expose) : on propose au
		// moins la resolution courante pour ne pas afficher une ligne vide.
		UE_LOG(LogTemp, Warning, TEXT("USettingsDisplayWidget : aucune resolution retournee par le systeme, repli sur la resolution courante."));
		AvailableResolutions.Add(Settings ? Settings->GetScreenResolution() : FIntPoint(1920, 1080));
	}

	FilterResolutions(Settings ? Settings->GetScreenResolution() : FIntPoint(1920, 1080));

	if (!ResolutionRow)
	{
		return;
	}

	TArray<FText> Labels;
	Labels.Reserve(AvailableResolutions.Num());
	for (const FIntPoint& Resolution : AvailableResolutions)
	{
		// FString::Printf et non FText::AsNumber : ce dernier formate selon la
		// locale et insererait un separateur de milliers -- "1 920 x 1 080".
		// Une resolution s'ecrit "1920x1080", sans espace ni separateur : c'est
		// une reference technique, pas une quantite.
		Labels.Add(FText::FromString(FString::Printf(TEXT("%dx%d"), Resolution.X, Resolution.Y)));
	}

	ResolutionRow->SetupAsChoice(LOCTEXT("Resolution", "Résolution"),
		Labels, FindCurrentResolutionIndex(Settings));
}

void USettingsDisplayWidget::FilterResolutions(const FIntPoint& CurrentResolution)
{
	// Des PLAGES et non un rapport exact : les resolutions commerciales ne
	// tombent pas juste. En 16:9, 1366x768 vaut 1.779 ; en "21:9", 2560x1080
	// vaut 2.370, 3440x1440 vaut 2.389 et 3840x1600 vaut 2.400 -- trois valeurs
	// differentes pour un format qu'on annonce pourtant comme un seul.
	constexpr float WideMin = 1.74f;
	constexpr float WideMax = 1.81f;
	constexpr float UltrawideMin = 2.30f;
	constexpr float UltrawideMax = 2.42f;

	TArray<FIntPoint> Kept;
	Kept.Reserve(AvailableResolutions.Num());

	for (const FIntPoint& Resolution : AvailableResolutions)
	{
		if (Resolution.X < MinimumResolution.X || Resolution.Y < MinimumResolution.Y)
		{
			continue;
		}

		const float Ratio = static_cast<float>(Resolution.X) / static_cast<float>(Resolution.Y);

		const bool bIsWide = Ratio >= WideMin && Ratio <= WideMax;
		const bool bIsUltrawide = bAllowUltrawide && Ratio >= UltrawideMin && Ratio <= UltrawideMax;

		// Tout le reste degage : 4:3, 5:4, 16:10, et le 32:9 des ecrans doubles,
		// qui deformerait le HUD sans qu'on ait rien prevu pour lui.
		if (!bIsWide && !bIsUltrawide)
		{
			continue;
		}

		Kept.Add(Resolution);
	}

	// La resolution courante doit rester selectionnable meme si elle ne passe pas
	// le filtre : sinon un joueur en 16:10 verrait la ligne afficher autre chose
	// que ce qu'il utilise vraiment.
	if (!Kept.Contains(CurrentResolution) && CurrentResolution.X > 0 && CurrentResolution.Y > 0)
	{
		Kept.Add(CurrentResolution);
	}

	// Un ecran qui ne proposerait aucun 16:9 vaut mieux non filtre que vide.
	if (Kept.IsEmpty())
	{
		UE_LOG(LogTemp, Warning, TEXT("USettingsDisplayWidget : aucune resolution ne passe le filtre, la liste complete est conservee."));
		return;
	}

	// Tri croissant par surface : la plus petite en haut, la native en bas.
	Kept.Sort([](const FIntPoint& A, const FIntPoint& B)
	{
		return static_cast<int64>(A.X) * A.Y < static_cast<int64>(B.X) * B.Y;
	});

	AvailableResolutions = MoveTemp(Kept);
}

int32 USettingsDisplayWidget::FindCurrentResolutionIndex(const USurvivalUserSettings* Settings) const
{
	if (!Settings)
	{
		return 0;
	}

	const FIntPoint Current = Settings->GetScreenResolution();
	const int32 Found = AvailableResolutions.IndexOfByKey(Current);

	// Resolution absente de la liste (fenetre redimensionnee a la main) : on
	// retombe sur la plus grande, en fin de liste.
	return Found != INDEX_NONE ? Found : AvailableResolutions.Num() - 1;
}

int32 USettingsDisplayWidget::FindLargestResolutionIndex() const
{
	int32 BestIndex = INDEX_NONE;
	int64 BestArea = -1;

	// On compare la SURFACE et non l'index : rien ne garantit que le systeme
	// renvoie ses modes triés, et un 2560x1080 ultralarge ne doit pas passer
	// devant un 1920x1200.
	for (int32 Index = 0; Index < AvailableResolutions.Num(); ++Index)
	{
		const int64 Area = static_cast<int64>(AvailableResolutions[Index].X) * AvailableResolutions[Index].Y;
		if (Area > BestArea)
		{
			BestArea = Area;
			BestIndex = Index;
		}
	}

	return BestIndex;
}

void USettingsDisplayWidget::RefreshFromSettings()
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings)
	{
		return;
	}

	if (DisplayModeRow)
	{
		DisplayModeRow->SetIndexSilently(SettingsDisplayTab::FindDisplayModeIndex(Settings->GetFullscreenMode()));
	}

	if (ResolutionRow)
	{
		ResolutionRow->SetIndexSilently(FindCurrentResolutionIndex(Settings));
	}

	if (FrameRateLimitRow)
	{
		FrameRateLimitRow->SetIndexSilently(SettingsDisplayTab::FindFrameRateIndex(Settings->GetFrameRateLimit()));
	}

	if (VSyncRow)
	{
		VSyncRow->SetIndexSilently(Settings->IsVSyncEnabled() ? 1 : 0);
	}

	if (ResolutionScaleRow)
	{
		// On lit la valeur BRUTE en pourcentage et non la normalisee : la plage
		// autorisee par le moteur ne commence pas a 50 %, une valeur normalisee
		// ne correspondrait donc pas a ce que la ligne affiche.
		float CurrentNormalized = 0.f;
		float CurrentValue = 100.f;
		float MinValue = 0.f;
		float MaxValue = 100.f;
		Settings->GetResolutionScaleInformationEx(CurrentNormalized, CurrentValue, MinValue, MaxValue);

		ResolutionScaleRow->SetNumericValueSilently(CurrentValue);
	}

	if (BrightnessRow)
	{
		BrightnessRow->SetNumericValueSilently(Settings->GetBrightness());
	}
}

void USettingsDisplayWidget::ResetToDefaults()
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings)
	{
		return;
	}

	// On ne touche PAS a la resolution ni au mode d'affichage : les remettre
	// "par defaut" pourrait basculer sur un mode que l'ecran ne gere pas, et le
	// joueur n'a pas demande ca en cliquant Reinitialiser.
	Settings->SetFrameRateLimit(0.f);
	Settings->SetVSyncEnabled(false);
	Settings->SetResolutionScaleNormalized(1.f);
	Settings->SetSurvivalDisplayToDefaults();

	Settings->ApplyNonResolutionSettings();
	RefreshFromSettings();
}

void USettingsDisplayWidget::HandleDisplayModeChanged(int32 NewIndex)
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings)
	{
		return;
	}

	Settings->SetFullscreenMode(SettingsDisplayTab::MakeWindowMode(NewIndex));

	// Passer en fenetre change la liste des tailles proposables.
	RebuildResolutionList();

	// En plein ecran ou sans bordure, on bascule d'office sur la plus grande
	// resolution disponible -- c'est-a-dire celle de l'ecran.
	//
	// Sans ca, un jeu lance en Standalone dans une fenetre de 1280x720 garderait
	// cette resolution en plein ecran : l'image serait etiree sur tout l'ecran et
	// visiblement floue, alors que le joueur vient de demander le contraire. En
	// fenetre on ne touche a rien, la taille choisie est celle qu'il veut.
	if (NewIndex != SettingsDisplayTab::ModeWindowed)
	{
		const int32 LargestIndex = FindLargestResolutionIndex();
		if (AvailableResolutions.IsValidIndex(LargestIndex))
		{
			Settings->SetScreenResolution(AvailableResolutions[LargestIndex]);

			if (ResolutionRow)
			{
				ResolutionRow->SetIndexSilently(LargestIndex);
			}
		}
	}

	ApplyVideoModeAndConfirm();
}

void USettingsDisplayWidget::HandleResolutionChanged(int32 NewIndex)
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings || !AvailableResolutions.IsValidIndex(NewIndex))
	{
		return;
	}

	Settings->SetScreenResolution(AvailableResolutions[NewIndex]);
	ApplyVideoModeAndConfirm();
}

void USettingsDisplayWidget::ApplyVideoModeAndConfirm()
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings)
	{
		return;
	}

	// ApplyResolutionSettings et non ApplySettings : on ne veut surtout pas
	// repousser la qualite graphique au passage, ce serait une saccade a chaque
	// clic de fleche.
	Settings->ApplyResolutionSettings(/*bCheckForCommandLineOverrides=*/false);

	if (!OwningMenu.IsValid())
	{
		// Sans ecran parent, personne ne peut poser la question. On valide, au
		// prix du filet de securite -- mieux que de laisser un etat en suspens.
		Settings->ConfirmVideoMode();
		return;
	}

	FOnSettingsConfirmationResult Callback;
	Callback.BindDynamic(this, &USettingsDisplayWidget::HandleVideoConfirmAnswered);

	if (!OwningMenu->RequestConfirmation(VideoConfirmMessage, VideoConfirmSeconds, Callback))
	{
		Settings->ConfirmVideoMode();
	}
}

void USettingsDisplayWidget::HandleVideoConfirmAnswered(bool bConfirmed)
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings)
	{
		return;
	}

	if (bConfirmed)
	{
		// Ce mode devient la reference vers laquelle RevertVideoMode ramenera la
		// prochaine fois.
		Settings->ConfirmVideoMode();
	}
	else
	{
		Settings->RevertVideoMode();
		Settings->ApplyResolutionSettings(/*bCheckForCommandLineOverrides=*/false);
		RebuildResolutionList();
	}

	RefreshFromSettings();
}

void USettingsDisplayWidget::HandleFrameRateLimitChanged(int32 NewIndex)
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings || !SettingsDisplayTab::FrameRateLimits().IsValidIndex(NewIndex))
	{
		return;
	}

	Settings->SetFrameRateLimit(SettingsDisplayTab::FrameRateLimits()[NewIndex]);
	Settings->ApplyNonResolutionSettings();
}

void USettingsDisplayWidget::HandleVSyncChanged(int32 NewIndex)
{
	if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
	{
		Settings->SetVSyncEnabled(NewIndex == 1);
		Settings->ApplyNonResolutionSettings();
	}
}

void USettingsDisplayWidget::HandleResolutionScaleChanged(int32 NewIndex)
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings || !ResolutionScaleRow)
	{
		return;
	}

	Settings->SetResolutionScaleValueEx(ResolutionScaleRow->GetNumericValue());
	Settings->ApplyNonResolutionSettings();
}

void USettingsDisplayWidget::HandleBrightnessChanged(int32 NewIndex)
{
	USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
	if (!Settings || !BrightnessRow)
	{
		return;
	}

	Settings->SetBrightness(BrightnessRow->GetNumericValue());

	// ApplySurvivalSettings et non ApplyNonResolutionSettings : la luminosite
	// est a nous, elle ne passe pas par le volet video du moteur.
	Settings->ApplySurvivalSettings();
}

#undef LOCTEXT_NAMESPACE
