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


#include "CharacterCustomizationWidget.h"

#include "CharacterPartsDataAsset.h"
#include "Components/Button.h"
#include "Components/PanelWidget.h"
#include "CustomizationCategoryWidget.h"
#include "CustomizationOptionWidget.h"

#define LOCTEXT_NAMESPACE "CharacterCustomization"

namespace CharacterCustomizationLabels
{
	/**
	 * Le libelle de chaque categorie, dans l'ordre de l'enum.
	 *
	 * Ici et pas en UPROPERTY : ce sont des textes d'interface, pas des
	 * reglages. Les exposer inviterait a les vider par erreur, et une colonne
	 * aux rangees anonymes est inutilisable.
	 *
	 * Nom de namespace unique dans TOUT le module : le build unity concatene
	 * les .cpp, et un namespace anonyme n'isolerait rien.
	 */
	static FText Get(ECustomizationCategory Category)
	{
		switch (Category)
		{
		case ECustomizationCategory::Body:           return LOCTEXT("CatBody", "Silhouette");
		case ECustomizationCategory::Head:           return LOCTEXT("CatHead", "Visage");
		case ECustomizationCategory::Hair:           return LOCTEXT("CatHair", "Cheveux");
		case ECustomizationCategory::Eyebrows:       return LOCTEXT("CatEyebrows", "Sourcils");
		case ECustomizationCategory::Beard:          return LOCTEXT("CatBeard", "Barbe");
		case ECustomizationCategory::Mustache:       return LOCTEXT("CatMustache", "Moustache");
		case ECustomizationCategory::Eyes:           return LOCTEXT("CatEyes", "Yeux");
		case ECustomizationCategory::SkinColor:      return LOCTEXT("CatSkinColor", "Teint");
		case ECustomizationCategory::HairColor:      return LOCTEXT("CatHairColor", "Couleur des cheveux");
		case ECustomizationCategory::EyeColor:       return LOCTEXT("CatEyeColor", "Couleur des yeux");
		case ECustomizationCategory::UnderwearColor: return LOCTEXT("CatUnderwear", "Sous-vetement");
		default:                                     return FText::GetEmpty();
		}
	}

	/** Une categorie de couleur remplit la grille de pastilles, pas de vignettes. */
	static bool IsColor(ECustomizationCategory Category)
	{
		return Category >= ECustomizationCategory::SkinColor;
	}

	/**
	 * Le cadrage a montrer. Seules la silhouette et le sous-vetement demandent
	 * de reculer : tout le reste se juge sur le visage.
	 */
	static ECharacterPreviewFraming GetFraming(ECustomizationCategory Category)
	{
		const bool bWholeBody = (Category == ECustomizationCategory::Body)
			|| (Category == ECustomizationCategory::SkinColor)
			|| (Category == ECustomizationCategory::UnderwearColor);

		return bWholeBody ? ECharacterPreviewFraming::FullBody : ECharacterPreviewFraming::Face;
	}

	/** La categorie de pieces correspondante, pour interroger le catalogue. */
	static ECharacterPartCategory ToPartCategory(ECustomizationCategory Category)
	{
		switch (Category)
		{
		case ECustomizationCategory::Head:     return ECharacterPartCategory::Head;
		case ECustomizationCategory::Hair:     return ECharacterPartCategory::Hair;
		case ECustomizationCategory::Eyebrows: return ECharacterPartCategory::Eyebrows;
		case ECustomizationCategory::Beard:    return ECharacterPartCategory::Beard;
		default:                               return ECharacterPartCategory::Mustache;
		}
	}
}

UCharacterCustomizationWidget::UCharacterCustomizationWidget(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// Sans ce drapeau, NativeOnKeyDown ne serait jamais appele -- et le
	// SetWidgetToFocus() pose par AMainMenuPlayerController::OpenCustomization
	// serait lui aussi sans effet : Slate REFUSE de focaliser un widget qui ne
	// declare pas supporter le focus clavier.
	SetIsFocusable(true);
}

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

	// Sans ca, le clic-glisser qui fait tourner le personnage ne repond jamais :
	// un UUserWidget ne recoit NativeOnMouseButtonDown que si sa Visibility vaut
	// Visible, et le defaut d'un widget cree est SelfHitTestInvisible -- il
	// laisse passer les clics au lieu de les capter. Impose ici plutot que
	// laisse a la Designer, ou l'oubli serait silencieux.
	//
	// Les boutons ne sont pas prives de leurs clics pour autant : Slate teste
	// les enfants AVANT le parent, donc seul ce qui est lache dans le vide
	// arrive jusqu'ici -- c'est-a-dire sur le personnage.
	SetVisibility(ESlateVisibility::Visible);

	if (PlayButton)
	{
		PlayButton->OnClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandlePlayClicked);
	}

	if (BackButton)
	{
		BackButton->OnClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandleBackClicked);
	}
}

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

	BuildCategories();
	BuildOptions();
	RefreshPreview();

	// Le focus est repris ici et pas seulement par le SetWidgetToFocus du
	// controller : les deux chemins existent, mais celui-ci est le seul qui vaut
	// encore apres une reconstruction du widget.
	SetKeyboardFocus();
}

void UCharacterCustomizationWidget::SetupCustomization(ACharacterPreviewActor* InPreview, const FCharacterAppearance& InAppearance)
{
	Preview = InPreview;
	Appearance = InAppearance;

	// Systematique : un catalogue ampute entre deux versions du jeu laisserait
	// dans les reglages sauvegardes des index qui ne designent plus rien.
	if (Catalog)
	{
		Catalog->Sanitize(Appearance);
	}
}

void UCharacterCustomizationWidget::BuildCategories()
{
	if (!CategoryList || !CategoryWidgetClass)
	{
		return;
	}

	CategoryList->ClearChildren();
	CategoryWidgets.Reset();
	CategoryWidgets.Reserve(static_cast<int32>(ECustomizationCategory::Count));

	for (int32 i = 0; i < static_cast<int32>(ECustomizationCategory::Count); ++i)
	{
		const ECustomizationCategory Category = static_cast<ECustomizationCategory>(i);

		// Une categorie sans option n'a pas de rangee : c'est ce qui fait
		// disparaitre « Barbe » et « Moustache » quand on passe sur le corps B,
		// pour lequel le pack n'en livre aucune. Une rangee qui ouvre une grille
		// vide se lit comme un bug.
		if (GetOptionCountForCategory(Category) <= 0)
		{
			continue;
		}

		UCustomizationCategoryWidget* Row = CreateWidget<UCustomizationCategoryWidget>(this, CategoryWidgetClass);
		if (!Row)
		{
			continue;
		}

		Row->Setup(i, CharacterCustomizationLabels::Get(Category));
		Row->OnCategoryClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandleCategoryClicked);
		CategoryList->AddChild(Row);
		CategoryWidgets.Add(Row);
	}

	// La categorie active a pu disparaitre en changeant de silhouette : on
	// retombe sur la premiere rangee encore presente plutot que sur une grille
	// vide.
	if (GetOptionCountForCategory(ActiveCategory) <= 0 && CategoryWidgets.Num() > 0)
	{
		ActiveCategory = ECustomizationCategory::Body;
	}
}

void UCharacterCustomizationWidget::BuildOptions()
{
	if (!OptionGrid || !OptionWidgetClass || !Catalog)
	{
		return;
	}

	OptionGrid->ClearChildren();
	OptionWidgets.Reset();

	const int32 Count = GetOptionCountForCategory(ActiveCategory);
	OptionWidgets.Reserve(Count);

	for (int32 i = 0; i < Count; ++i)
	{
		UCustomizationOptionWidget* Option = CreateWidget<UCustomizationOptionWidget>(this, OptionWidgetClass);
		if (!Option)
		{
			continue;
		}

		if (CharacterCustomizationLabels::IsColor(ActiveCategory))
		{
			const TArray<FLinearColor>* Palette = nullptr;
			switch (ActiveCategory)
			{
			case ECustomizationCategory::SkinColor:      Palette = &Catalog->SkinColors; break;
			case ECustomizationCategory::HairColor:      Palette = &Catalog->HairColors; break;
			case ECustomizationCategory::EyeColor:       Palette = &Catalog->EyeColors; break;
			default:                                     Palette = &Catalog->UnderwearColors; break;
			}

			Option->SetupAsColor(i, (*Palette)[i]);
		}
		else if (ActiveCategory == ECustomizationCategory::Body)
		{
			Option->SetupAsPart(i, Catalog->BodySets.IsValidIndex(i) ? Catalog->BodySets[i].Icon : nullptr);
		}
		else if (ActiveCategory == ECustomizationCategory::Eyes)
		{
			// Les yeux n'ont pas de vignette : ce sont des materiaux, pas des
			// entrees de catalogue. La case retombe sur son numero.
			Option->SetupAsPart(i, nullptr);
		}
		else
		{
			const ECharacterPartCategory PartCategory = CharacterCustomizationLabels::ToPartCategory(ActiveCategory);
			const TArray<FCharacterPartEntry>& Entries = Catalog->GetParts(Appearance.BodyType, PartCategory);
			Option->SetupAsPart(i, Entries.IsValidIndex(i) ? Entries[i].Icon : nullptr);
		}

		Option->OnOptionClicked.AddDynamic(this, &UCharacterCustomizationWidget::HandleOptionClicked);
		OptionGrid->AddChild(Option);
		OptionWidgets.Add(Option);
	}

	RefreshSelection();
}

void UCharacterCustomizationWidget::RefreshPreview()
{
	if (Preview)
	{
		Preview->ApplyAppearance(Appearance);
		Preview->SetFraming(CharacterCustomizationLabels::GetFraming(ActiveCategory));
	}
}

void UCharacterCustomizationWidget::RefreshSelection()
{
	const int32 Current = GetCurrentIndexForCategory(ActiveCategory);

	for (int32 i = 0; i < OptionWidgets.Num(); ++i)
	{
		if (OptionWidgets[i])
		{
			OptionWidgets[i]->SetSelected(OptionWidgets[i]->GetOptionIndex() == Current);
		}
	}

	for (UCustomizationCategoryWidget* Row : CategoryWidgets)
	{
		if (Row)
		{
			Row->SetSelected(false);
		}
	}

	// La rangee active n'est pas au meme rang que la categorie dans l'enum :
	// les categories vides ont ete sautees. On la retrouve par son libelle
	// d'index, pose dans Setup.
	const int32 ActiveIndex = static_cast<int32>(ActiveCategory);
	for (UCustomizationCategoryWidget* Row : CategoryWidgets)
	{
		if (Row && Row->GetCategoryIndex() == ActiveIndex)
		{
			Row->SetSelected(true);
			break;
		}
	}
}

int32 UCharacterCustomizationWidget::GetCurrentIndexForCategory(ECustomizationCategory Category) const
{
	switch (Category)
	{
	case ECustomizationCategory::Body:           return static_cast<int32>(Appearance.BodyType);
	case ECustomizationCategory::Head:           return Appearance.HeadIndex;
	case ECustomizationCategory::Hair:           return Appearance.HairIndex;
	case ECustomizationCategory::Eyebrows:       return Appearance.EyebrowsIndex;
	case ECustomizationCategory::Beard:          return Appearance.BeardIndex;
	case ECustomizationCategory::Mustache:       return Appearance.MustacheIndex;
	case ECustomizationCategory::Eyes:           return Appearance.EyesIndex;
	case ECustomizationCategory::SkinColor:      return Appearance.SkinColorIndex;
	case ECustomizationCategory::HairColor:      return Appearance.HairColorIndex;
	case ECustomizationCategory::EyeColor:       return Appearance.EyeColorIndex;
	case ECustomizationCategory::UnderwearColor: return Appearance.UnderwearColorIndex;
	default:                                     return 0;
	}
}

void UCharacterCustomizationWidget::SetIndexForCategory(ECustomizationCategory Category, int32 NewIndex)
{
	const uint8 Value = static_cast<uint8>(FMath::Clamp(NewIndex, 0, 254));

	switch (Category)
	{
	case ECustomizationCategory::Body:           Appearance.BodyType = static_cast<ECharacterBodyType>(Value); break;
	case ECustomizationCategory::Head:           Appearance.HeadIndex = Value; break;
	case ECustomizationCategory::Hair:           Appearance.HairIndex = Value; break;
	case ECustomizationCategory::Eyebrows:       Appearance.EyebrowsIndex = Value; break;
	case ECustomizationCategory::Beard:          Appearance.BeardIndex = Value; break;
	case ECustomizationCategory::Mustache:       Appearance.MustacheIndex = Value; break;
	case ECustomizationCategory::Eyes:           Appearance.EyesIndex = Value; break;
	case ECustomizationCategory::SkinColor:      Appearance.SkinColorIndex = Value; break;
	case ECustomizationCategory::HairColor:      Appearance.HairColorIndex = Value; break;
	case ECustomizationCategory::EyeColor:       Appearance.EyeColorIndex = Value; break;
	case ECustomizationCategory::UnderwearColor: Appearance.UnderwearColorIndex = Value; break;
	default: break;
	}
}

int32 UCharacterCustomizationWidget::GetOptionCountForCategory(ECustomizationCategory Category) const
{
	if (!Catalog)
	{
		return 0;
	}

	switch (Category)
	{
	case ECustomizationCategory::Body:           return Catalog->BodySets.Num();
	case ECustomizationCategory::Eyes:           return Catalog->EyeMaterials.Num();
	case ECustomizationCategory::SkinColor:      return Catalog->SkinColors.Num();
	case ECustomizationCategory::HairColor:      return Catalog->HairColors.Num();
	case ECustomizationCategory::EyeColor:       return Catalog->EyeColors.Num();
	case ECustomizationCategory::UnderwearColor: return Catalog->UnderwearColors.Num();
	default:
		return Catalog->GetPartCount(Appearance.BodyType, CharacterCustomizationLabels::ToPartCategory(Category));
	}
}

void UCharacterCustomizationWidget::HandleCategoryClicked(int32 CategoryIndex)
{
	ActiveCategory = static_cast<ECustomizationCategory>(CategoryIndex);

	BuildOptions();
	RefreshPreview();
}

void UCharacterCustomizationWidget::HandleOptionClicked(int32 OptionIndex)
{
	SetIndexForCategory(ActiveCategory, OptionIndex);

	// Changer de silhouette change TOUT le reste : les listes de tetes, de
	// coiffures et de sourcils appartiennent au corps, et le corps B n'a ni
	// barbe ni moustache. On rebatit donc la colonne entiere, apres avoir
	// laisse Sanitize ramener les index devenus hors bornes.
	if (ActiveCategory == ECustomizationCategory::Body)
	{
		if (Catalog)
		{
			Catalog->Sanitize(Appearance);
		}

		BuildCategories();
		BuildOptions();
	}
	else
	{
		RefreshSelection();
	}

	RefreshPreview();
}

void UCharacterCustomizationWidget::HandlePlayClicked()
{
	OnConfirmed.Broadcast();
}

void UCharacterCustomizationWidget::HandleBackClicked()
{
	OnCancelled.Broadcast();
}

FReply UCharacterCustomizationWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
	const FKey Key = InKeyEvent.GetKey();

	// GetVirtualKey() et non la constante, depreciee en 5.8 : la resolution se
	// fait a l'appel, donc selon la manette reellement branchee.
	if (Key == EKeys::Escape || Key == EKeys::Virtual_Gamepad_Back.GetVirtualKey())
	{
		// Exactement le bouton Retour, jamais un second chemin vers le menu : le
		// controller a un fondu et une bascule de camera a jouer au retour, et
		// c'est OnCancelled qui les declenche.
		HandleBackClicked();

		// Handled : sans ca la touche continue de remonter et serait consommee
		// une seconde fois par ce qui se trouve derriere.
		return FReply::Handled();
	}

	return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}

// ----------------------------------------------------------------------
// Rotation du personnage
//
// Sur le widget plein ecran et pas sur une zone dediee : les boutons de la
// colonne consomment deja leurs propres clics, donc tout ce qui arrive ici a
// forcement ete lache dans le vide -- c'est-a-dire sur le personnage.
// ----------------------------------------------------------------------

FReply UCharacterCustomizationWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
	{
		bDragging = true;

		// CaptureMouse : sans elle, sortir du widget en glissant relacherait le
		// suivi et laisserait bDragging a vrai pour toujours.
		return FReply::Handled().CaptureMouse(TakeWidget());
	}

	return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
}

FReply UCharacterCustomizationWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && bDragging)
	{
		bDragging = false;
		return FReply::Handled().ReleaseMouseCapture();
	}

	return Super::NativeOnMouseButtonUp(InGeometry, InMouseEvent);
}

FReply UCharacterCustomizationWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	if (bDragging && Preview)
	{
		// Signe inverse : glisser vers la droite doit tourner le personnage
		// vers la droite, donc le faire pivoter dans l'autre sens.
		Preview->AddPreviewYaw(-InMouseEvent.GetCursorDelta().X * Preview->GetYawPerPixel());
		return FReply::Handled();
	}

	return Super::NativeOnMouseMove(InGeometry, InMouseEvent);
}

#undef LOCTEXT_NAMESPACE
