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

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/EngineTypes.h"
#include "FootstepSoundsDataAsset.h"
#include "FootstepComponent.generated.h"

class ACharacter;
class UCharacterMovementComponent;
class USoundAttenuation;
class USoundBase;
class USoundConcurrency;

/**
 * Les reglages d'une allure : sa foulee et son volume.
 *
 * Groupes dans une struct pour la meme raison que FHeadBobProfile : trois
 * allures a plat feraient neuf lignes melangees dans le Details panel, alors
 * que la struct donne un repli par allure.
 */
USTRUCT(BlueprintType)
struct FFootstepGaitSettings
{
	GENERATED_BODY()

	FFootstepGaitSettings() = default;

	FFootstepGaitSettings(float InStrideLength, float InVolume)
		: StrideLength(InStrideLength)
		, Volume(InVolume)
	{
	}

	/**
	 * Distance parcourue entre deux pas, en cm.
	 *
	 * C'est le reglage central du systeme, et il se calcule plutot qu'il ne se
	 * devine : pour que les pas tombent en rythme avec le balancement de tete,
	 * il faut StrideLength = Vitesse / (2 x StrideFrequency du profil de
	 * UHeadBobComponent) -- le "2" parce qu'une foulee compte deux pas. Les
	 * valeurs par defaut sortent de ce calcul avec les vitesses du pawn.
	 *
	 * Corollaire a retenir : changer WalkSpeed ou SprintSpeed sans retoucher ici
	 * desynchronise le son de l'image, et ca s'entend avant de se voir.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "10", UIMax = "300"))
	float StrideLength = 180.f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "2"))
	float Volume = 1.f;

	/**
	 * Variation aleatoire de hauteur, en plus ou en moins autour de 1.
	 *
	 * Le pack ne livre qu'un ou deux echantillons par surface et par allure :
	 * sans ce grain, une course en ligne droite repete exactement le meme son
	 * trois fois par seconde et l'oreille l'entend comme une boucle. Quelques
	 * pourcents suffisent, au-dela le personnage change de poids a chaque pas.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "0.4"))
	float PitchVariation = 0.06f;
};

/**
 * Bruits de pas : mesure la distance parcourue, cherche la matiere sous les
 * pieds, joue le son qui va avec.
 *
 * ---------------------------------------------------------------------------
 * Pourquoi la distance et pas un AnimNotify
 * ---------------------------------------------------------------------------
 * C'est le choix structurant. Trois raisons de ne pas accrocher le son aux
 * animations :
 *
 *  - Le corps 3e personne du joueur local est en bOwnerNoSee. Faire dependre
 *    son propre son d'un mesh qu'il ne voit pas est fragile, et le devient
 *    d'autant plus le jour ou l'on optimisera son tick d'animation.
 *  - Les animations du pack n'ont aucun notify, et il faudrait en poser dans le
 *    BlendSpace, ou ils se declenchent selon l'echantillon dominant : le
 *    passage marche -> course y saute ou double un pas.
 *  - Les bras FPS sont sur BasePose_Skeleton, qui n'a rien a voir avec
 *    SK_BodyA. Aucun notify ne peut servir aux deux.
 *
 * La distance, elle, donne un espacement regulier, se regle en PIE sans
 * recompiler, et se comporte identiquement sur le pawn local et sur ceux des
 * amis.
 *
 * ---------------------------------------------------------------------------
 * Reseau : rien n'est replique, et rien n'a a l'etre
 * ---------------------------------------------------------------------------
 * Exactement le raisonnement de UPlayerBodyAnimInstance : tout ce que ce
 * composant lit DERIVE d'un etat deja synchronise -- la vitesse par le
 * CharacterMovement, l'accroupissement par ACharacter, le mode de deplacement
 * par la replication du mouvement. Chaque machine calcule donc les pas de tous
 * les pawns qu'elle connait. Un Multicast par pas, a trois pas par seconde et
 * par joueur, serait du trafic pur pour un resultat identique.
 *
 * Le son du joueur local part en 2D, celui des autres en 3D a leurs pieds :
 * un pas local joue en 3D depuis les chevilles est attenue et sonne creux.
 */
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UFootstepComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	UFootstepComponent();

	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

	/**
	 * Coupe ou rallume les pas. Pour la mort, une cinematique, un mode fantome.
	 * Coupe, le compteur de foulee est remis a zero : rallumer ne doit pas
	 * declencher le pas reste en attente depuis l'autre bout de la carte.
	 */
	UFUNCTION(BlueprintCallable, Category = "Footsteps")
	void SetFootstepsEnabled(bool bEnabled);

	/** Remet le cycle de foulee a zero. Pour une teleportation ou un respawn. */
	UFUNCTION(BlueprintCallable, Category = "Footsteps")
	void ResetStride();

	/**
	 * Joue un pas immediatement, a l'allure courante. Sert aux montages et aux
	 * AnimNotify si l'on veut un jour caler un geste precis sur un son.
	 */
	UFUNCTION(BlueprintCallable, Category = "Footsteps")
	void PlayFootstepNow();

protected:
	virtual void BeginPlay() override;
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

	/**
	 * Saut et reception, sur TOUTES les machines.
	 *
	 * Et surtout pas ACharacter::Landed / OnJumped, qui semblent pourtant faits
	 * pour ca : Landed vient de ProcessLanded, qui n'est appele que la ou le
	 * mouvement est reellement simule, et OnJumped que la ou l'input est lu. Ni
	 * l'un ni l'autre n'existe sur le pawn d'un ami vu depuis notre machine, ou
	 * le mouvement arrive deja fait. Le mode de deplacement, lui, EST replique,
	 * donc ce delegue est diffusé partout -- c'est le seul signal qui couvre les
	 * quatre joueurs.
	 */
	UFUNCTION()
	void HandleMovementModeChanged(ACharacter* Character, EMovementMode PrevMovementMode, uint8 PreviousCustomMode);

	// ------------------------------------------------------------------
	// Assets
	// ------------------------------------------------------------------

	/** La table surface -> sons. Sans elle le composant ne fait rien. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
	TObjectPtr<UFootstepSoundsDataAsset> SoundBank;

	/**
	 * Attenuation appliquee aux pas des AUTRES joueurs (le pas local est en 2D).
	 *
	 * A remplir : le pack ne fournit aucune attenuation, donc sans cet asset les
	 * cues sont jouees en 3D sans courbe de distance et s'entendent d'un bout a
	 * l'autre de la carte au meme volume.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
	TObjectPtr<USoundAttenuation> Attenuation;

	/**
	 * Concurrence, facultative. A quatre joueurs qui courent, huit a douze pas
	 * par seconde peuvent se superposer ; une regle a 4 ou 5 voix evite que les
	 * pas mangent le reste du mixage.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
	TObjectPtr<USoundConcurrency> Concurrency;

	// ------------------------------------------------------------------
	// Reglages generaux
	// ------------------------------------------------------------------

	/** Coupe entierement l'effet sans avoir a vider la banque de sons. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
	bool bEnableFootsteps = true;

	/**
	 * Joue le pas du joueur local en 2D plutot qu'a la position de ses pieds.
	 * Decoche pour l'entendre spatialise comme celui des autres -- utile pour
	 * regler l'attenuation sans avoir besoin d'un second joueur.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
	bool bPlayLocalStepsIn2D = true;

	/**
	 * Multiplicateur applique au seul pas local. Sous 1 par defaut : un son 2D
	 * arrive a pleine puissance dans les deux oreilles, la ou celui des autres
	 * passe par l'attenuation.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps", meta = (ClampMin = "0", UIMax = "2"))
	float LocalVolumeScale = 0.7f;

	/** Joue le pas final quand le personnage s'immobilise. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps")
	bool bPlayStopSounds = true;

	// ------------------------------------------------------------------
	// Allures
	// ------------------------------------------------------------------

	/** Accroupi. Valeurs par defaut dans le constructeur. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
	FFootstepGaitSettings CrouchGait;

	/** Marche normale. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
	FFootstepGaitSettings WalkGait;

	/** Sprint. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures")
	FFootstepGaitSettings SprintGait;

	/**
	 * En dessous de cette vitesse horizontale (cm/s), on considere le
	 * personnage immobile. Meme seuil et meme raison que UHeadBobComponent :
	 * marcher dans un mur laisse l'input actif alors que le corps n'avance plus.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0"))
	float IdleSpeedThreshold = 10.f;

	/**
	 * Au-dessus de cette vitesse horizontale (cm/s), on passe sur l'allure de
	 * sprint. A caler entre WalkSpeed et SprintSpeed du pawn, comme le seuil
	 * homonyme du balancement de tete -- les deux doivent basculer ensemble,
	 * sinon la tete accelere avant les pieds.
	 *
	 * La vitesse REELLE et pas AFpsPlayer::IsSprinting() : le composant reste
	 * ainsi utilisable par un PNJ ou un animal, qui n'ont pas de touche Maj.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0"))
	float SprintSpeedThreshold = 520.f;

	/**
	 * Fraction de foulee deja "parcourue" au demarrage du mouvement.
	 *
	 * A 0, le premier pas ne tombe qu'apres une foulee entiere : le personnage
	 * part en silence sur presque deux metres, ce qui se lit comme un temps de
	 * reaction du jeu. A 1 il claque avant que le corps ait bouge. La moitie
	 * place le premier pas juste apres l'entree en mouvement.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Allures", meta = (ClampMin = "0", ClampMax = "1"))
	float FirstStepFraction = 0.5f;

	// ------------------------------------------------------------------
	// Saut et reception
	// ------------------------------------------------------------------

	/**
	 * Vitesse de chute (cm/s) en dessous de laquelle la reception est muette.
	 * Descendre une marche d'escalier passe techniquement par un instant de
	 * chute ; sans ce seuil, la moindre bosse du terrain claque.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Air", meta = (ClampMin = "0"))
	float LandMinFallSpeed = 150.f;

	/** Volume du saut et de la reception. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Air", meta = (ClampMin = "0", UIMax = "2"))
	float AirVolume = 1.f;

	// ------------------------------------------------------------------
	// Trace de sol
	// ------------------------------------------------------------------

	/**
	 * Profondeur sondee sous les pieds, en cm. Quelques dizaines suffisent : le
	 * trace part du centre de la capsule et on ne cherche le sol que lorsqu'on
	 * marche dessus.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace", meta = (ClampMin = "1"))
	float TraceDepth = 60.f;

	/**
	 * Trace en collision COMPLEXE, et ce n'est pas negociable pour ce systeme.
	 *
	 * Un PhysicalMaterial pose sur un MATERIAU n'est rendu que par un trace
	 * per-poly : le trace simple remonte celui du BodySetup du mesh, qui est
	 * presque toujours vide. Le symptome d'un decochage est immediat et trompeur
	 * -- tout le monde marche sur DefaultSurface, comme si la table n'etait pas
	 * remplie.
	 *
	 * Le cout est nul a l'echelle du jeu : deux a trois traces par seconde et
	 * par personnage, jamais un par frame.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace")
	bool bTraceComplex = true;

	/** Canal du trace de sol. Visibility couvre tout le decor du projet. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Footsteps|Trace")
	TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;

private:
	/** L'allure correspondant a l'etat reel du CharacterMovement. */
	EFootstepGait SelectGait(float HorizontalSpeed) const;

	/** Les reglages de l'allure courante. */
	const FFootstepGaitSettings& GetGaitSettings(EFootstepGait Gait) const;

	/**
	 * Cherche la matiere sous les pieds et rend les sons correspondants.
	 * @param OutLocation ou jouer le son : le point d'impact, ou les pieds si
	 *        le trace n'a rien touche (le sol peut etre hors du canal trace).
	 */
	const FFootstepSurfaceSounds* QueryGroundSounds(FVector& OutLocation) const;

	/** Joue une cue, en 2D chez le proprietaire et en 3D partout ailleurs. */
	void PlayCue(USoundBase* Sound, float Volume, float PitchVariation, const FVector& Location) const;

	/** Vrai quand le son doit sortir des haut-parleurs de CETTE machine. */
	bool ShouldPlaySounds() const;

	// ------------------------------------------------------------------
	// Etat
	// ------------------------------------------------------------------

	/** Distance horizontale accumulee depuis le dernier pas, en cm. */
	float DistanceSinceLastStep = 0.f;

	/** Position de l'acteur au tick precedent, pour mesurer le deplacement reel. */
	FVector LastLocation = FVector::ZeroVector;

	/**
	 * Vrai des le premier tick valide. Sans lui, le premier deplacement mesure
	 * partirait de l'origine du monde et declencherait une salve de pas.
	 */
	bool bHasLastLocation = false;

	/** Etat du tick precedent, pour detecter le demarrage et l'arret. */
	bool bWasMoving = false;

	/** Allure du dernier pas joue : c'est elle qui choisit le son d'arret. */
	EFootstepGait LastGait = EFootstepGait::Jog;

	/**
	 * Nombre de pas depuis l'entree en mouvement. Le son d'arret n'est joue
	 * qu'a partir de un : un a-coup de collision suffit sinon a faire claquer
	 * un pas final alors que le personnage n'a pas bouge.
	 */
	int32 StepsSinceMoveStart = 0;

	/**
	 * Plus grande vitesse de chute observee, en cm/s et positive.
	 *
	 * Mesuree pendant la chute plutot que lue a l'impact : au moment ou le mode
	 * repasse a Walking, le moteur a deja remis Velocity.Z a zero. Un tick a
	 * 20 Hz l'echantillonne largement assez -- et le cas ou il n'a pas eu le
	 * temps de tourner du tout est precisement celui d'une chute negligeable,
	 * qu'on veut de toute facon silencieuse.
	 */
	float MaxFallSpeed = 0.f;

	/** Caches : lus a chaque tick, pas question d'y caster. */
	TObjectPtr<ACharacter> OwnerCharacter;

	TObjectPtr<UCharacterMovementComponent> OwnerMovement;
};
