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


#include "StorageContainer.h"

#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/CollisionProfile.h"
#include "Engine/StaticMesh.h"
#include "FpsPlayerController.h"
#include "GameFramework/Pawn.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "Kismet/GameplayStatics.h"
#include "Net/UnrealNetwork.h"

#define LOCTEXT_NAMESPACE "StorageContainer"

AStorageContainer::AStorageContainer()
{
	// Le tick n'existe que pour animer le couvercle : il demarre eteint et ne
	// s'allume que le temps du mouvement.
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.bStartWithTickEnabled = false;

	// Acteur du monde : contenu et couvercle appartiennent au serveur.
	// Le UInventoryComponent, lui, se replique deja tout seul -- c'est la
	// classe entiere qui le fait, coffre comme sac.
	bReplicates = true;
	SetReplicateMovement(false);

	BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
	SetRootComponent(BaseMesh);

	// Les meshes ne portent AUCUNE collision : elle vit entierement dans la
	// boite. Un mesh de pack sans collision simple rendrait le coffre muet sans
	// qu'aucune erreur ne le signale, et celle du couvercle partirait avec lui
	// des l'ouverture.
	BaseMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);

	InteractionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("InteractionBox"));
	InteractionBox->SetupAttachment(BaseMesh);

	// Explicite plutot que de compter sur le profil par defaut : ce composant a
	// deux roles indissociables, empecher le joueur de traverser le coffre et
	// bloquer le canal Visibility pour que le trace d'interaction le trouve.
	// Un profil qui laisserait passer Visibility rendrait le coffre inutilisable
	// sans qu'aucune erreur ne le signale.
	InteractionBox->SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);

	// Valeur de depart pour un coffre qui n'aurait pas encore de mesh :
	// FitInteractionBox() l'ecrase des qu'il y en a un.
	InteractionBox->SetBoxExtent(FVector(40.f, 30.f, 30.f));

	// Enfant direct de la caisse : c'est le mesh lui-meme qu'on tourne, donc la
	// charniere est le pivot que le pack a donne a SM_Chest_Top.
	LidMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LidMesh"));
	LidMesh->SetupAttachment(BaseMesh);
	LidMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);

	Storage = CreateDefaultSubobject<UInventoryComponent>(TEXT("Storage"));

	DisplayName = LOCTEXT("ChestName", "Chest");
	InteractionPrompt = LOCTEXT("OpenChest", "Ouvrir le coffre");
}

void AStorageContainer::PreInitializeComponents()
{
	// Avant InitializeComponents(), donc avant que le composant fige sa taille
	// par defaut de 30 slots avec une barre rapide.
	if (Storage)
	{
		Storage->ConfigureAsContainer(SlotCount);
	}

	Super::PreInitializeComponents();
}

void AStorageContainer::BeginPlay()
{
	Super::BeginPlay();

	// Le couvercle repose a zero, quelle que soit la rotation laissee dans le
	// Blueprint : le repos EST la reference de l'animation.
	LidAlpha = 0.f;
	ApplyLidRotation();

	if (!Storage)
	{
		return;
	}

	for (const FStorageStartingItem& Entry : StartingItems)
	{
		if (!Entry.Item || Entry.Quantity <= 0)
		{
			continue;
		}

		const int32 Left = Storage->AddItem(Entry.Item, Entry.Quantity);
		if (Left > 0)
		{
			UE_LOG(LogTemp, Warning, TEXT("%s : %d x %s n'ont pas tenu dans le coffre (%d cases)."),
				*GetName(), Left, *GetNameSafe(Entry.Item), SlotCount);
		}
	}
}

void AStorageContainer::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);

	// Tourne aussi dans l'editeur : la boite se recale a l'instant ou tu
	// changes de mesh, sans avoir a lancer le jeu pour t'en assurer.
	if (bAutoFitInteractionBox)
	{
		FitInteractionBox();
	}
}

void AStorageContainer::FitInteractionBox()
{
	if (!InteractionBox || !BaseMesh)
	{
		return;
	}

	// Volume combine des deux meshes, exprime dans le repere de la caisse --
	// celui de la boite, qui en est fille.
	FBox Combined(ForceInit);

	const UStaticMeshComponent* Meshes[] = { BaseMesh, LidMesh };
	for (const UStaticMeshComponent* Component : Meshes)
	{
		const UStaticMesh* Mesh = Component ? Component->GetStaticMesh() : nullptr;
		if (!Mesh)
		{
			continue;
		}

		// Transform RELATIVE a la racine et non absolue : la boite doit rester
		// juste quel que soit l'endroit ou l'acteur est pose dans le niveau.
		const FTransform Relative = Component->GetComponentTransform().GetRelativeTransform(BaseMesh->GetComponentTransform());
		Combined += Mesh->GetBoundingBox().TransformBy(Relative);
	}

	if (!Combined.IsValid)
	{
		return;
	}

	InteractionBox->SetRelativeLocation(Combined.GetCenter());
	InteractionBox->SetBoxExtent(Combined.GetExtent() + FVector(InteractionBoxPadding));
}

FText AStorageContainer::GetInteractionPrompt_Implementation(AActor* Interactor) const
{
	return InteractionPrompt;
}

void AStorageContainer::Interact_Implementation(AActor* Interactor)
{
	// L'ecran vit sur le controller, pas sur le pawn : c'est lui qu'on cherche.
	const APawn* InteractingPawn = Cast<APawn>(Interactor);
	AFpsPlayerController* PlayerController = InteractingPawn ? Cast<AFpsPlayerController>(InteractingPawn->GetController()) : nullptr;

	if (!PlayerController)
	{
		return;
	}

	PlayerController->OpenStorage(this);
}

void AStorageContainer::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(AStorageContainer, bOpen);
}

void AStorageContainer::SetOpen(bool bInOpen)
{
	// Le couvercle appartient au monde : seul le serveur en decide, et la
	// replication l'annonce aux autres. Un client qui forcerait bOpen verrait
	// son coffre s'ouvrir seul, et le prochain OnRep le refermerait.
	if (!HasAuthority() || bOpen == bInOpen)
	{
		return;
	}

	bOpen = bInOpen;
	ApplyOpenState();
}

void AStorageContainer::OnRep_Open()
{
	ApplyOpenState();
}

void AStorageContainer::ApplyOpenState()
{
	if (USoundBase* Sound = bOpen ? OpenSound : CloseSound)
	{
		UGameplayStatics::PlaySoundAtLocation(this, Sound, GetActorLocation());
	}

	// Le tick s'eteint tout seul en fin de course. Le rallumer ici suffit, y
	// compris si l'animation precedente n'etait pas terminee : l'alpha repart
	// de sa valeur courante, donc refermer un coffre a moitie ouvert ne saute
	// pas.
	SetActorTickEnabled(true);
}

void AStorageContainer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	const float Target = bOpen ? 1.f : 0.f;
	const float Speed = 1.f / FMath::Max(0.05f, LidDuration);

	LidAlpha = FMath::FInterpConstantTo(LidAlpha, Target, DeltaTime, Speed);
	ApplyLidRotation();

	if (FMath::IsNearlyEqual(LidAlpha, Target))
	{
		// On force la valeur exacte avant de se rendormir : sans ca le couvercle
		// resterait a un poil de sa position finale pour le reste de la partie.
		LidAlpha = Target;
		ApplyLidRotation();
		SetActorTickEnabled(false);
	}
}

void AStorageContainer::ApplyLidRotation()
{
	if (!LidMesh)
	{
		return;
	}

	// Lissage aux extremites : un couvercle a vitesse constante demarre et
	// s'arrete d'un coup, ce qui se lit comme une saccade sur un objet lourd.
	const float Eased = FMath::SmoothStep(0.f, 1.f, LidAlpha);

	LidMesh->SetRelativeRotation(OpenRotation * Eased);
}

#undef LOCTEXT_NAMESPACE
