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

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HeadBobComponent.generated.h"

class ACharacter;
class UCharacterMovementComponent;

/**
 * Les reglages d'un etat de deplacement : immobile, marche, sprint, accroupi, en l'air.
 *
 * Regroupes dans une struct plutot qu'a plat sur le composant pour deux raisons.
 * D'abord le Details panel : cinq etats x cinq valeurs a plat feraient vingt-cinq
 * lignes melangees, alors que la struct donne un repli par etat. Ensuite le code :
 * passer d'un etat a l'autre est une interpolation champ a champ, et une struct
 * permet de l'ecrire une seule fois au lieu d'une par valeur.
 */
USTRUCT(BlueprintType)
struct FHeadBobProfile
{
	GENERATED_BODY()

	FHeadBobProfile() = default;

	FHeadBobProfile(float InStrideFrequency, float InVerticalAmount, float InLateralAmount, float InRollAmount, float InPitchAmount)
		: StrideFrequency(InStrideFrequency)
		, VerticalAmount(InVerticalAmount)
		, LateralAmount(InLateralAmount)
		, RollAmount(InRollAmount)
		, PitchAmount(InPitchAmount)
	{
	}

	/**
	 * Foulees par seconde. Une foulee compte DEUX pas : l'oscillation verticale
	 * bat donc au double de cette valeur, une bosse par pied qui touche le sol.
	 * A 1.1, les pas tombent a 2.2 Hz, ce qui correspond a une marche normale.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "4"))
	float StrideFrequency = 1.1f;

	/** Amplitude verticale en cm, mesuree du repos au sommet (le debattement total vaut le double). */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
	float VerticalAmount = 1.6f;

	/** Amplitude laterale en cm : le balancement gauche-droite du buste. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
	float LateralAmount = 1.2f;

	/** Roulis en degres au pic du balancement lateral. La tete penche dans le sens du pas. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
	float RollAmount = 0.4f;

	/**
	 * Tangage en degres au pic vertical. A 0 par defaut, et ce n'est pas un oubli :
	 * le tangage deplace le reticule, donc il degrade la visee. A n'ouvrir que si
	 * l'on assume un jeu ou l'on ne vise pas au pixel.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
	float PitchAmount = 0.f;
};

/**
 * Balancement de tete procedural, sans la moindre animation authoree.
 *
 * Le principe tient en une phase unique, avancee chaque frame par la frequence de
 * l'etat courant, et une amplitude qui glisse d'un etat a l'autre. C'est ce qui
 * rend le passage marche -> sprint continu : ce n'est pas une animation qu'on
 * coupe pour en lancer une autre, ce sont deux jeux de coefficients entre lesquels
 * on interpole pendant que la phase, elle, ne s'interrompt jamais. Remettre la
 * phase a zero a chaque changement d'etat produirait exactement le "clac" qu'on
 * cherche a eviter.
 *
 * Le composant ne touche PAS a la camera. Il expose un decalage de position et un
 * decalage de rotation, que AFpsPlayer::ApplyCameraTransform additionne aux siens.
 * C'est la condition pour que cette fonction reste le seul endroit du projet qui
 * ecrit la transform de la camera -- elle est rappelee depuis CalcCamera, et deux
 * ecrivains se marcheraient dessus une frame sur deux.
 *
 * Rien n'est replique et rien n'a a l'etre : le balancement n'existe que dans les
 * yeux du joueur local. Les bras suivent gratuitement, puisqu'ils sont attaches a
 * la camera.
 */
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UHeadBobComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	UHeadBobComponent();

	/**
	 * Fait avancer la phase et recalcule les decalages.
	 *
	 * Appelee par le pawn depuis son Tick, et surtout PAS depuis un TickComponent a
	 * lui : l'ordre de tick entre un acteur et ses composants n'est pas garanti sans
	 * dependance explicite, et le resultat serait un decalage lu une frame apres
	 * avoir ete calcule -- invisible a 120 fps, franchement mou a 30.
	 */
	void UpdateBob(float DeltaTime);

	/** Decalage a ajouter a la position RELATIVE de la camera (X avant, Y droite, Z haut), en cm. */
	UFUNCTION(BlueprintPure, Category = "Head Bob")
	FVector GetLocationOffset() const { return LocationOffset; }

	/** Decalage a ajouter a la rotation de la camera, en degres. */
	UFUNCTION(BlueprintPure, Category = "Head Bob")
	FRotator GetRotationOffset() const { return RotationOffset; }

	/**
	 * Remet le balancement au repos sans transition.
	 * Pour une teleportation ou une prise de controle : on ne veut pas que la tete
	 * finisse le pas d'avant a l'autre bout de la carte.
	 */
	UFUNCTION(BlueprintCallable, Category = "Head Bob")
	void ResetBob();

	/**
	 * Facteur global pousse par les reglages joueur (curseur "secousses de camera").
	 * A 0 la tete ne bouge plus du tout : c'est un confort d'accessibilite, pas une
	 * option cosmetique, et il doit valoir pour le balancement comme pour les chocs.
	 */
	UFUNCTION(BlueprintCallable, Category = "Head Bob")
	void SetIntensityScale(float NewScale);

protected:
	virtual void BeginPlay() override;

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

	/** Coupe entierement l'effet, sans avoir a remettre les cinq profils a zero. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob")
	bool bEnableHeadBob = true;

	/**
	 * Multiplicateur d'amplitude commun aux cinq profils.
	 * C'est le curseur a bouger pour un reglage d'ensemble ("un peu moins de tout")
	 * sans avoir a retoucher vingt valeurs une par une.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "3"))
	float GlobalIntensity = 1.f;

	/**
	 * Vitesse de fondu d'un profil vers un autre (facteur d'interpolation
	 * exponentielle, par seconde). Plus haut = le sprint prend son amplitude plus
	 * vite. Trop haut, le changement d'etat redevient une cassure.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0.1", UIMax = "20"))
	float ProfileBlendSpeed = 6.f;

	// ------------------------------------------------------------------
	// Seuils de choix d'etat
	// ------------------------------------------------------------------

	/**
	 * En dessous de cette vitesse horizontale (cm/s), on est considere immobile.
	 * Le seuil se lit sur la vitesse REELLE et non sur la touche enfoncee : marcher
	 * dans un mur laisse l'input actif alors que le corps ne bouge plus, et sans ce
	 * detail la tete continuerait de gigoter face a la cloison.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
	float IdleSpeedThreshold = 10.f;

	/**
	 * Au-dessus de cette vitesse horizontale (cm/s), on bascule sur le profil de
	 * sprint. A regler entre WalkSpeed et SprintSpeed du pawn -- 400 et 650 par
	 * defaut, d'ou ces 520 : assez haut pour que la phase d'acceleration ne compte
	 * pas comme du sprint, assez bas pour que le sprint reel soit toujours detecte.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
	float SprintSpeedThreshold = 520.f;

	// ------------------------------------------------------------------
	// Profils, un par etat. Valeurs par defaut dans le constructeur.
	// ------------------------------------------------------------------

	/** Immobile : une respiration lente, quasi purement verticale. C'est ce qui evite la statue. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
	FHeadBobProfile IdleProfile;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
	FHeadBobProfile WalkProfile;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
	FHeadBobProfile SprintProfile;

	/** Accroupi : plus lent et plus discret que la marche, le corps est ramasse. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
	FHeadBobProfile CrouchProfile;

	/**
	 * En l'air : tout a zero, y compris la frequence -- la phase se fige donc en vol
	 * et repart d'ou elle en etait a la reception, ce qui evite que le premier pas
	 * apres un saut tombe sur un pied arbitraire. Le profil reste editable malgre
	 * tout, au cas ou l'on veuille un flottement pendant la chute.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
	FHeadBobProfile AirProfile;

private:
	/** Choisit le profil vise a partir de l'etat reel du CharacterMovement. */
	const FHeadBobProfile& SelectTargetProfile() const;

	/** Rapproche BlendedProfile de Target, champ par champ. */
	void BlendTowards(const FHeadBobProfile& Target, float Step);

	/**
	 * Position dans le cycle de foulee, en radians, ramenee dans [0, 2*PI[.
	 * Le repli n'est pas cosmetique : sans lui la valeur grossit indefiniment et la
	 * precision du float se degrade, jusqu'a un balancement qui devient saccade au
	 * bout d'une longue partie.
	 */
	float StridePhase = 0.f;

	/** Profil courant, resultat du fondu entre les profils d'etat. */
	FHeadBobProfile BlendedProfile;

	FVector LocationOffset = FVector::ZeroVector;

	FRotator RotationOffset = FRotator::ZeroRotator;

	/** Pousse par les reglages joueur, se multiplie a GlobalIntensity. */
	float IntensityScale = 1.f;

	/** Caches : ces deux pointeurs sont lus a chaque frame, pas question d'y caster. */
	TObjectPtr<ACharacter> OwnerCharacter;

	TObjectPtr<UCharacterMovementComponent> OwnerMovement;
};
