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


#include "SettingRowWidget.h"

#include "Components/Button.h"
#include "Components/ProgressBar.h"
#include "Components/Slider.h"
#include "Components/TextBlock.h"

void USettingRowWidget::NativeOnInitialized()
{
	Super::NativeOnInitialized();

	// NativeOnInitialized et non NativeConstruct : l'onglet parent appelle
	// SetupAs... depuis SON NativeConstruct, et rien ne garantit l'ordre entre
	// le construct du parent et celui de ses enfants. Initialize, lui, tourne
	// une seule fois a la creation de l'arbre, avant tout le monde.
	if (LeftArrowButton)
	{
		LeftArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleLeftClicked);
	}

	if (RightArrowButton)
	{
		RightArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleRightClicked);
	}

	if (ValueSlider)
	{
		ValueSlider->OnValueChanged.AddDynamic(this, &USettingRowWidget::HandleSliderValueChanged);
	}

	// La valeur est POSEE SUR le curseur : si elle restait cliquable, elle
	// avalerait le clic au milieu de la course et le glissement ne partirait
	// jamais. Force ici plutot que dans le WBP : c'est le genre de case qu'on
	// oublie et dont le symptome (une zone morte au centre) est illisible.
	if (ValueText)
	{
		ValueText->SetVisibility(ESlateVisibility::HitTestInvisible);
	}
}

void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray<FText>& Options, int32 Index)
{
	bNumeric = false;
	ChoiceLabels = Options;
	StepCount = FMath::Max(Options.Num(), 1);
	ArrowIndexDelta = 1;
	MaxSelectableIndex = INDEX_NONE;
	CurrentIndex = FMath::Clamp(Index, 0, StepCount - 1);

	if (LabelText)
	{
		LabelText->SetText(Label);
	}

	// Une jauge sur "Maintien / Bascule" ne veut rien dire : deux positions et
	// rien a doser. On replie l'empilement plutot que d'imposer un second WBP.
	if (ValueBar)
	{
		ValueBar->SetVisibility(ESlateVisibility::Collapsed);
	}

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

	Refresh();
}

void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max, float Step, float ArrowStep, float Value, const FText& Suffix)
{
	bNumeric = true;
	ChoiceLabels.Reset();
	NumericMin = Min;
	NumericStep = FMath::Max(Step, KINDA_SMALL_NUMBER);
	NumericSuffix = Suffix;

	// +1 : les deux extremites comptent. 70 a 120 par 1 fait 51 paliers, pas 50.
	StepCount = FMath::Max(FMath::FloorToInt((Max - Min) / NumericStep) + 1, 1);

	// Le bond des fleches ne vit qu'en nombre de paliers fins : tout le reste du
	// widget raisonne en index, y compris le curseur.
	ArrowIndexDelta = FMath::Max(FMath::RoundToInt(ArrowStep / NumericStep), 1);
	MaxSelectableIndex = INDEX_NONE;

	if (LabelText)
	{
		LabelText->SetText(Label);
	}

	// HitTestInvisible : le remplissage est un decor pose SOUS le curseur, il ne
	// doit surtout pas intercepter le clic qui devrait lancer le glissement.
	if (ValueBar)
	{
		ValueBar->SetVisibility(ESlateVisibility::HitTestInvisible);
	}

	if (ValueSlider)
	{
		ValueSlider->SetVisibility(ESlateVisibility::Visible);

		// La course du curseur est graduee en INDEX, pas dans l'unite du
		// reglage : 0 a 10 pour un FOV de 70 a 120 par pas de 5. Le glissement
		// ne peut donc produire qu'un palier valide, et le meme curseur sert a
		// n'importe quelle plage sans conversion.
		ValueSlider->SetMinValue(0.f);
		ValueSlider->SetMaxValue(static_cast<float>(FMath::Max(StepCount - 1, 1)));
		ValueSlider->SetStepSize(1.f);
	}

	SetNumericValueSilently(Value);
}

float USettingRowWidget::GetNumericValue() const
{
	return NumericMin + CurrentIndex * NumericStep;
}

void USettingRowWidget::SetIndexSilently(int32 NewIndex)
{
	// Borne sur StepCount et non sur MaxSelectable : le code a le droit
	// d'afficher un etat que le joueur ne peut pas choisir.
	CurrentIndex = FMath::Clamp(NewIndex, 0, StepCount - 1);
	Refresh();
}

void USettingRowWidget::SetMaxSelectableIndex(int32 InMaxSelectableIndex)
{
	MaxSelectableIndex = InMaxSelectableIndex;
	Refresh();
}

int32 USettingRowWidget::MaxSelectable() const
{
	return MaxSelectableIndex == INDEX_NONE
		? StepCount - 1
		: FMath::Clamp(MaxSelectableIndex, 0, StepCount - 1);
}

void USettingRowWidget::SetNumericValueSilently(float Value)
{
	SetIndexSilently(FMath::RoundToInt((Value - NumericMin) / NumericStep));
}

void USettingRowWidget::HandleLeftClicked()
{
	StepIndex(ArrowTargetIndex(-1) - CurrentIndex);
}

void USettingRowWidget::HandleRightClicked()
{
	StepIndex(ArrowTargetIndex(1) - CurrentIndex);
}

int32 USettingRowWidget::ArrowTargetIndex(int32 Direction) const
{
	// Pas de bond a arrondir : une ligne de choix, ou un reglage dont les deux
	// pas sont identiques.
	if (ArrowIndexDelta <= 1)
	{
		return CurrentIndex + Direction;
	}

	// On vise le multiple du bond suivant, pas CurrentIndex + bond : arrive a 37
	// au curseur, la fleche droite doit donner 40 et la gauche 35. Sinon les
	// fleches perdent leur reperage des qu'on a glisse une fois.
	if (Direction > 0)
	{
		return ((CurrentIndex / ArrowIndexDelta) + 1) * ArrowIndexDelta;
	}

	// Division entiere arrondie au superieur, pour que 35 redescende bien a 30
	// et pas a 35 (ce qui n'aurait rien fait).
	return ((CurrentIndex + ArrowIndexDelta - 1) / ArrowIndexDelta - 1) * ArrowIndexDelta;
}

void USettingRowWidget::HandleSliderValueChanged(float NewValue)
{
	if (bRefreshingSlider)
	{
		return;
	}

	// On passe par StepIndex plutot que d'ecrire CurrentIndex : le glissement
	// et les fleches doivent produire exactement le meme effet, y compris le
	// grisage des fleches et la diffusion du delegue.
	StepIndex(FMath::RoundToInt(NewValue) - CurrentIndex);

	// Puis on recale la poignee sur le cran, meme si l'index n'a pas bouge :
	// StepIndex sort sans rien faire dans ce cas, et la poignee resterait
	// plantee entre deux paliers.
	Refresh();
}

void USettingRowWidget::StepIndex(int32 Delta)
{
	const int32 NewIndex = FMath::Clamp(CurrentIndex + Delta, 0, MaxSelectable());

	// Pas de bouclage, volontairement : passer de 100 a 5 de sensibilite d'un
	// clic de trop rendrait le jeu injouable sans qu'on comprenne pourquoi.
	if (NewIndex == CurrentIndex)
	{
		return;
	}

	CurrentIndex = NewIndex;
	Refresh();

	OnIndexChanged.Broadcast(CurrentIndex);
}

void USettingRowWidget::Refresh()
{
	if (ValueText)
	{
		if (bNumeric)
		{
			FNumberFormattingOptions Format;
			Format.MaximumFractionalDigits = 0;

			const FText Number = FText::AsNumber(GetNumericValue(), &Format);

			// FormatOrdered plutot qu'une concatenation : selon la langue, l'unite
			// ne se colle pas forcement du meme cote ni avec la meme espace.
			ValueText->SetText(NumericSuffix.IsEmpty()
				? Number
				: FText::FormatOrdered(NSLOCTEXT("Reglages", "ValeurAvecUnite", "{0}{1}"), Number, NumericSuffix));
		}
		else if (ChoiceLabels.IsValidIndex(CurrentIndex))
		{
			ValueText->SetText(ChoiceLabels[CurrentIndex]);
		}
	}

	if (ValueBar)
	{
		// Un seul palier : la jauge n'a aucun sens, on la remplit entierement
		// plutot que de diviser par zero.
		ValueBar->SetPercent(StepCount > 1
			? static_cast<float>(CurrentIndex) / static_cast<float>(StepCount - 1)
			: 1.f);
	}

	if (ValueSlider && bNumeric)
	{
		// Le drapeau evite qu'un repositionnement venant du code soit relu comme
		// un geste du joueur.
		bRefreshingSlider = true;
		ValueSlider->SetValue(static_cast<float>(CurrentIndex));
		bRefreshingSlider = false;
	}

	// Les fleches se grisent aux extremites : le joueur voit qu'il est au bout
	// au lieu de cliquer dans le vide.
	if (LeftArrowButton)
	{
		LeftArrowButton->SetIsEnabled(CurrentIndex > 0);
	}

	if (RightArrowButton)
	{
		RightArrowButton->SetIsEnabled(CurrentIndex < MaxSelectable());
	}
}
