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


#include "FootstepComponent.h"

#include "Components/CapsuleComponent.h"
#include "Engine/World.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/GameplayStatics.h"

UFootstepComponent::UFootstepComponent()
{
	PrimaryComponentTick.bCanEverTick = true;

	// 20 Hz, et pas la frequence d'affichage. Le composant n'accumule qu'une
	// DISTANCE, mesuree entre deux positions exactes : allonger l'intervalle ne
	// degrade donc rien, la somme des deplacements reste la meme. A 120 fps
	// c'est six fois moins de reveils pour un resultat identique a l'oreille.
	//
	// Un TickComponent a nous et pas un appel depuis le Tick du pawn, contrairement
	// a UHeadBobComponent : lui doit tourner avant que la camera soit composee, ici
	// une frame de retard sur un son ne s'entend pas -- et surtout ce composant doit
	// rester posable sur n'importe quel ACharacter, y compris ceux qu'on n'ecrit pas.
	PrimaryComponentTick.TickInterval = 0.05f;

	// Foulees calees sur les frequences de UHeadBobComponent, avec les vitesses
	// du pawn : StrideLength = Vitesse / (2 x StrideFrequency).
	//   accroupi : 200 / (2 x 0.70) = 143
	//   marche   : 400 / (2 x 1.10) = 182
	//   sprint   : 650 / (2 x 1.55) = 210
	// Elles sont ici et non en initialiseurs de membre pour que le constructeur
	// reste le seul endroit a lire quand on cherche les valeurs par defaut.
	CrouchGait = FFootstepGaitSettings(143.f, 0.35f);
	WalkGait = FFootstepGaitSettings(182.f, 0.80f);
	SprintGait = FFootstepGaitSettings(210.f, 1.00f);
}

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

	OwnerCharacter = Cast<ACharacter>(GetOwner());
	if (!OwnerCharacter)
	{
		UE_LOG(LogTemp, Warning, TEXT("UFootstepComponent : le proprietaire n'est pas un ACharacter, les bruits de pas sont desactives."));
		SetComponentTickEnabled(false);
		return;
	}

	OwnerMovement = OwnerCharacter->GetCharacterMovement();

	// Le seul signal de decollage et de reception qui existe sur les quatre
	// machines. Voir le commentaire de HandleMovementModeChanged.
	OwnerCharacter->MovementModeChangedDelegate.AddDynamic(this, &UFootstepComponent::HandleMovementModeChanged);

	if (!SoundBank)
	{
		UE_LOG(LogTemp, Warning, TEXT("UFootstepComponent : aucune banque de sons assignee sur %s, aucun bruit de pas ne sera joue."),
			*GetNameSafe(GetOwner()));
	}
}

void UFootstepComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	if (OwnerCharacter)
	{
		OwnerCharacter->MovementModeChangedDelegate.RemoveDynamic(this, &UFootstepComponent::HandleMovementModeChanged);
	}

	Super::EndPlay(EndPlayReason);
}

void UFootstepComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (!bEnableFootsteps || !OwnerCharacter || !OwnerMovement || !SoundBank || !ShouldPlaySounds())
	{
		return;
	}

	const FVector Location = OwnerCharacter->GetActorLocation();

	// Premier tick valide : on se contente de memoriser la position. Sans ce
	// garde-fou, le premier deplacement mesure partirait de l'origine du monde
	// et declencherait une rafale de pas au spawn.
	if (!bHasLastLocation)
	{
		LastLocation = Location;
		bHasLastLocation = true;
		return;
	}

	const float Travelled = (Location - LastLocation).Size2D();
	LastLocation = Location;

	// En l'air on ne compte pas de foulee : le vol est traite par les deux cues
	// de HandleMovementModeChanged. On y profite du passage pour echantillonner
	// la vitesse de chute, introuvable au moment de l'impact.
	if (OwnerMovement->IsFalling())
	{
		MaxFallSpeed = FMath::Max(MaxFallSpeed, -OwnerMovement->Velocity.Z);
		return;
	}

	const float Speed = OwnerMovement->Velocity.Size2D();
	const bool bIsMoving = Speed > IdleSpeedThreshold;

	if (bIsMoving && !bWasMoving)
	{
		// Entree en mouvement : on amorce le compteur pour que le premier pas
		// tombe tout de suite plutot qu'apres deux metres de silence.
		DistanceSinceLastStep = GetGaitSettings(SelectGait(Speed)).StrideLength * FirstStepFraction;
		StepsSinceMoveStart = 0;
	}
	else if (!bIsMoving && bWasMoving)
	{
		// Immobilisation : le pas final. Conditionne a un vrai pas deja joue,
		// sinon un simple a-coup de collision ferait claquer un arret alors que
		// le personnage n'a pas avance d'un centimetre.
		if (bPlayStopSounds && StepsSinceMoveStart > 0)
		{
			FVector StopLocation;
			if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(StopLocation))
			{
				const FFootstepGaitSettings& StopSettings = GetGaitSettings(LastGait);
				PlayCue(Sounds->GetStopSound(LastGait), StopSettings.Volume, StopSettings.PitchVariation, StopLocation);
			}
		}

		DistanceSinceLastStep = 0.f;
		StepsSinceMoveStart = 0;
	}

	bWasMoving = bIsMoving;

	if (!bIsMoving)
	{
		return;
	}

	DistanceSinceLastStep += Travelled;

	const EFootstepGait Gait = SelectGait(Speed);
	const float StrideLength = GetGaitSettings(Gait).StrideLength;

	if (DistanceSinceLastStep >= StrideLength)
	{
		// Soustraction et pas remise a zero : le reliquat de distance doit
		// survivre au pas, sinon le rythme derive a chaque hoquet de framerate.
		DistanceSinceLastStep -= StrideLength;

		// Mais borne quand meme : apres une teleportation ou un long gel, le
		// reliquat vaudrait plusieurs foulees et partirait en rafale.
		DistanceSinceLastStep = FMath::Min(DistanceSinceLastStep, StrideLength);

		++StepsSinceMoveStart;
		PlayFootstepNow();
	}
}

void UFootstepComponent::PlayFootstepNow()
{
	if (!OwnerMovement || !SoundBank || !ShouldPlaySounds())
	{
		return;
	}

	const EFootstepGait Gait = SelectGait(OwnerMovement->Velocity.Size2D());

	FVector StepLocation;
	const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(StepLocation);
	if (!Sounds)
	{
		return;
	}

	const FFootstepGaitSettings& Settings = GetGaitSettings(Gait);
	PlayCue(Sounds->GetStepSound(Gait), Settings.Volume, Settings.PitchVariation, StepLocation);

	// Memorise APRES coup : c'est cette allure-la que le son d'arret devra
	// reprendre, pas celle qu'aura le personnage une fois immobile.
	LastGait = Gait;
}

void UFootstepComponent::HandleMovementModeChanged(ACharacter* Character, EMovementMode PrevMovementMode, uint8 PreviousCustomMode)
{
	if (!bEnableFootsteps || !OwnerMovement || !SoundBank || !ShouldPlaySounds())
	{
		return;
	}

	const EMovementMode NewMode = OwnerMovement->MovementMode;

	// Decollage
	if (NewMode == MOVE_Falling && PrevMovementMode == MOVE_Walking)
	{
		MaxFallSpeed = 0.f;

		// Un saut, et pas une sortie de rebord : c'est le SIGNE de la vitesse
		// verticale qui tranche, exactement comme dans l'AnimBP du corps. Sans
		// ce test, marcher au bord d'une caisse ferait grogner le personnage.
		if (OwnerMovement->Velocity.Z > 0.f)
		{
			FVector JumpLocation;
			if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(JumpLocation))
			{
				const FFootstepGaitSettings& Settings = GetGaitSettings(SelectGait(OwnerMovement->Velocity.Size2D()));
				PlayCue(Sounds->Jump, AirVolume, Settings.PitchVariation, JumpLocation);
			}
		}

		// Le cycle de foulee repart de zero a la reception : bWasMoving remis a
		// faux fait passer le premier tick au sol par la branche "entree en
		// mouvement", donc par FirstStepFraction. Retomber en pleine course
		// redonne ainsi un pas tout de suite.
		DistanceSinceLastStep = 0.f;
		StepsSinceMoveStart = 0;
		bWasMoving = false;
		return;
	}

	// Reception
	if (PrevMovementMode == MOVE_Falling && NewMode == MOVE_Walking)
	{
		if (MaxFallSpeed > LandMinFallSpeed)
		{
			FVector LandLocation;
			if (const FFootstepSurfaceSounds* Sounds = QueryGroundSounds(LandLocation))
			{
				const FFootstepGaitSettings& Settings = GetGaitSettings(SelectGait(OwnerMovement->Velocity.Size2D()));
				PlayCue(Sounds->Land, AirVolume, Settings.PitchVariation, LandLocation);
			}
		}

		MaxFallSpeed = 0.f;
		DistanceSinceLastStep = 0.f;
		StepsSinceMoveStart = 0;
		bWasMoving = false;
	}
}

void UFootstepComponent::SetFootstepsEnabled(bool bEnabled)
{
	bEnableFootsteps = bEnabled;

	if (!bEnabled)
	{
		ResetStride();
	}
}

void UFootstepComponent::ResetStride()
{
	DistanceSinceLastStep = 0.f;
	StepsSinceMoveStart = 0;
	MaxFallSpeed = 0.f;
	bWasMoving = false;

	// La position memorisee aussi : un respawn deplace le pawn de plusieurs
	// centaines de metres, et ce deplacement ne doit pas compter comme un pas.
	bHasLastLocation = false;
}

EFootstepGait UFootstepComponent::SelectGait(float HorizontalSpeed) const
{
	if (OwnerMovement && OwnerMovement->IsCrouching())
	{
		return EFootstepGait::Walk;
	}

	// Le seuil se lit sur la vitesse REELLE et pas sur AFpsPlayer::IsSprinting() :
	// le composant reste ainsi posable sur un PNJ, qui n'a pas de touche Maj.
	return (HorizontalSpeed > SprintSpeedThreshold) ? EFootstepGait::Run : EFootstepGait::Jog;
}

const FFootstepGaitSettings& UFootstepComponent::GetGaitSettings(EFootstepGait Gait) const
{
	switch (Gait)
	{
	case EFootstepGait::Walk:	return CrouchGait;
	case EFootstepGait::Run:	return SprintGait;
	default:					return WalkGait;
	}
}

const FFootstepSurfaceSounds* UFootstepComponent::QueryGroundSounds(FVector& OutLocation) const
{
	if (!SoundBank || !OwnerCharacter)
	{
		return nullptr;
	}

	const UCapsuleComponent* Capsule = OwnerCharacter->GetCapsuleComponent();
	const float HalfHeight = Capsule ? Capsule->GetScaledCapsuleHalfHeight() : 0.f;

	const FVector Center = OwnerCharacter->GetActorLocation();
	const FVector Feet = Center - FVector(0.f, 0.f, HalfHeight);

	// Repli de position : si le trace ne touche rien, le son part quand meme,
	// depuis les pieds. Un silence serait plus difficile a diagnostiquer.
	OutLocation = Feet;

	const UWorld* World = GetWorld();
	if (!World)
	{
		return &SoundBank->DefaultSurface;
	}

	// bTraceComplex : voir le commentaire de la propriete. Sans lui on recupere
	// le PhysicalMaterial du BodySetup, presque toujours vide, et tout le decor
	// sonne sur DefaultSurface.
	FCollisionQueryParams Params(SCENE_QUERY_STAT(Footstep), bTraceComplex, OwnerCharacter);
	Params.bReturnPhysicalMaterial = true;

	FHitResult Hit;
	if (!World->LineTraceSingleByChannel(Hit, Center, Feet - FVector(0.f, 0.f, TraceDepth), TraceChannel, Params))
	{
		return &SoundBank->DefaultSurface;
	}

	OutLocation = Hit.ImpactPoint;
	return &SoundBank->GetSurfaceSounds(UGameplayStatics::GetSurfaceType(Hit));
}

void UFootstepComponent::PlayCue(USoundBase* Sound, float Volume, float PitchVariation, const FVector& Location) const
{
	if (!Sound || Volume <= 0.f)
	{
		return;
	}

	const float Pitch = (PitchVariation > 0.f)
		? FMath::FRandRange(1.f - PitchVariation, 1.f + PitchVariation)
		: 1.f;

	// Le pas du joueur local en 2D : joue en 3D depuis ses propres chevilles, il
	// passe par l'attenuation et sonne creux, alors qu'il devrait etre le son le
	// plus present du mixage. Les Cue n'ont pas de SoundClass, donc les deux
	// chemins tombent sur SC_SFX, la classe par defaut du projet -- le curseur
	// "Effets" les couvre sans qu'on ait rien a taguer.
	if (bPlayLocalStepsIn2D && OwnerCharacter && OwnerCharacter->IsLocallyControlled())
	{
		UGameplayStatics::PlaySound2D(this, Sound, Volume * LocalVolumeScale, Pitch, 0.f, Concurrency, GetOwner());
		return;
	}

	// PlaySoundAtLocation et pas SpawnSoundAttached : un pas est un one-shot de
	// quelques dixiemes de seconde, l'attacher creerait puis detruirait un
	// UAudioComponent trois fois par seconde et par joueur pour rien.
	UGameplayStatics::PlaySoundAtLocation(this, Sound, Location, FRotator::ZeroRotator,
		Volume, Pitch, 0.f, Attenuation, Concurrency, GetOwner());
}

bool UFootstepComponent::ShouldPlaySounds() const
{
	// Un serveur dedie n'a pas de sortie audio : inutile d'y tracer le sol pour
	// quatre personnages. Le projet tourne en serveur d'ecoute, mais la garde ne
	// coute qu'une comparaison et evite d'y repenser le jour ou ca change.
	const UWorld* World = GetWorld();
	return World && World->GetNetMode() != NM_DedicatedServer;
}
