#include "ZMMOPlayerProxy.h"

#include "Animation/AnimInstance.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/World.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "UObject/ConstructorHelpers.h"
#include "ZMMO.h"
#include "ZMMOProxyMovementComponent.h"

AZMMOPlayerProxy::AZMMOPlayerProxy(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<UZMMOProxyMovementComponent>(ACharacter::CharacterMovementComponentName))
{
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.bStartWithTickEnabled = true;

	UCapsuleComponent* Capsule = GetCapsuleComponent();
	if (Capsule)
	{
		Capsule->InitCapsuleSize(42.0f, 96.0f);
		Capsule->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	}

	UCharacterMovementComponent* CMC = GetCharacterMovement();
	if (CMC)
	{
		// MOVE_Walking (em vez de MOVE_None) e necessario para o AnimBP do Quinn
		// transitar entre Idle/Run — a state machine consulta `IsFalling()` /
		// `MovementMode` no CMC mesmo quando este nao integra fisica.
		CMC->bOrientRotationToMovement = false;
		CMC->RotationRate = FRotator::ZeroRotator;
		CMC->bRunPhysicsWithNoController = false;
		CMC->MaxWalkSpeed = 600.0f;
		CMC->SetMovementMode(MOVE_Walking);

		// Sem input local, o CMC zerava `Velocity` por braking/friction antes do
		// `NativeUpdateAnimation` lê-la, deixando o AnimBP em Idle. Zerar friction
		// preserva a Velocity escrita por `RefreshAnimationDrivers` entre snapshots.
		CMC->BrakingDecelerationWalking = 0.0f;
		CMC->GroundFriction = 0.0f;
		CMC->BrakingFrictionFactor = 0.0f;
	}

	// Defaults visuais (mesh + AnimBP) em paridade com `AZMMOPlayerCharacter`.
	if (USkeletalMeshComponent* MeshComponent = GetMesh())
	{
		static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh(
			TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple"));
		static ConstructorHelpers::FClassFinder<UAnimInstance> QuinnAnimBp(
			TEXT("/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed"));

		if (QuinnMesh.Succeeded())
		{
			MeshComponent->SetSkeletalMesh(QuinnMesh.Object);
			MeshComponent->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
			MeshComponent->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
		}
		else
		{
			UE_LOG(LogZMMO, Warning,
				TEXT("AZMMOPlayerProxy: SKM_Quinn_Simple nao encontrado em /Game/Characters/Mannequins/Meshes."));
		}

		if (QuinnAnimBp.Succeeded())
		{
			MeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
			MeshComponent->SetAnimInstanceClass(QuinnAnimBp.Class);
		}
		else
		{
			UE_LOG(LogZMMO, Warning,
				TEXT("AZMMOPlayerProxy: ABP_Unarmed nao encontrado em /Game/Characters/Mannequins/Anims/Unarmed."));
		}
	}

	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;
}

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

	// Tick do proxy precisa rodar **antes** do tick do mesh para que
	// `RefreshAnimationDrivers` populate `Velocity`+`Acceleration` no CMC custom
	// antes que `NativeUpdateAnimation` do AnimBP os leia. Sem isto, AnimBP
	// recalcula `ShouldMove` com Acceleration=0 → state machine presa em Idle.
	if (USkeletalMeshComponent* MeshComponent = GetMesh())
	{
		MeshComponent->AddTickPrerequisiteActor(this);
	}
}

void AZMMOPlayerProxy::Tick(const float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	if (SnapshotBuffer.Num() == 0)
	{
		return;
	}

	// Tempo do servidor estimado: relogio local em ms - offset estabelecido na
	// primeira amostra. RenderMs = tempo "atrasado" usado para procurar o par
	// de snapshots a interpolar (snapshot interpolation a la Source/Quake).
	const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
	const int64 ServerNowMs = LocalNowMs - ServerClockOffsetMs;
	const int64 RenderMs = ServerNowMs - static_cast<int64>(InterpolationDelayMs);

	// Procura o snapshot B tal que A.t <= RenderMs <= B.t (A = B-1).
	int32 IdxB = INDEX_NONE;
	for (int32 i = 1; i < SnapshotBuffer.Num(); ++i)
	{
		if (SnapshotBuffer[i].ServerTimeMs >= RenderMs)
		{
			IdxB = i;
			break;
		}
	}

	FVector NewPos;
	FVector NewVel;
	float InterpAlpha = 0.0f;
	bool bExtrapolating = false;

	if (IdxB == INDEX_NONE)
	{
		// Sem snapshot futuro: extrapolacao curta com a ultima velocidade
		// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
		// o proxy fugir se a rede ficar muda.
		const FZMMOProxySnapshot& Last = SnapshotBuffer.Last();
		const float ExtrapSec = FMath::Clamp(
			static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f,
			0.0f, MaxExtrapolationSeconds);
		NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
		NewVel = Last.VelCmS;
		bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER;
	}
	else
	{
		const FZMMOProxySnapshot& A = SnapshotBuffer[IdxB - 1];
		const FZMMOProxySnapshot& B = SnapshotBuffer[IdxB];
		const double Span = FMath::Max<double>(1.0, static_cast<double>(B.ServerTimeMs - A.ServerTimeMs));
		InterpAlpha = static_cast<float>(FMath::Clamp(
			static_cast<double>(RenderMs - A.ServerTimeMs) / Span, 0.0, 1.0));
		NewPos = FMath::Lerp(A.PosCm, B.PosCm, InterpAlpha);
		NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha);
	}

	// Yaw deriva da velocidade interpolada: locomotion mantem face na direcao
	// do movimento; em idle preserva o yaw atual para nao "snappar".
	FRotator NewRot = GetActorRotation();
	if (FVector(NewVel.X, NewVel.Y, 0.0f).SizeSquared() > 1.0f)
	{
		NewRot.Yaw = FMath::RadiansToDegrees(FMath::Atan2(NewVel.Y, NewVel.X));
	}
	NewRot.Pitch = 0.0f;
	NewRot.Roll  = 0.0f;

	// Aceleracao derivada da curva interpolada (mais estavel que a derivada
	// snapshot-a-snapshot que tinhamos antes; AnimBP do Quinn precisa de
	// `Acceleration != 0` para sair de Idle).
	FVector DerivedAccel = FVector::ZeroVector;
	if (bHasPreviousSnapshot && DeltaSeconds > KINDA_SMALL_NUMBER)
	{
		DerivedAccel = (NewVel - PreviousVisualVelocity) / DeltaSeconds;
		DerivedAccel = DerivedAccel.GetClampedToMaxSize(8000.0f);
	}
	constexpr float AccelSmoothingAlpha = 0.35f;
	LastDerivedAccelerationCmS2 = bHasPreviousSnapshot
		? FMath::Lerp(LastDerivedAccelerationCmS2, DerivedAccel, AccelSmoothingAlpha)
		: DerivedAccel;

	// Fallback sintetico: locomotion a velocidade constante => derivada ~ 0.
	// Sem este nudge o AnimBP voltaria para Idle.
	const FVector2D Vel2D(NewVel.X, NewVel.Y);
	const float Speed2D = Vel2D.Size();
	const FVector2D Accel2D(LastDerivedAccelerationCmS2.X, LastDerivedAccelerationCmS2.Y);
	constexpr float MinSpeedForFallbackCmS = 50.0f;
	constexpr float MinAccelFallbackCmS2 = 200.0f;
	const bool bGroundedNow = SnapshotBuffer.Last().bGrounded;
	if (bGroundedNow && Speed2D >= MinSpeedForFallbackCmS && Accel2D.Size() < MinAccelFallbackCmS2)
	{
		const FVector2D VelDir = Vel2D.GetSafeNormal();
		const FVector2D FallbackAccel2D = VelDir * MinAccelFallbackCmS2;
		LastDerivedAccelerationCmS2.X = FallbackAccel2D.X;
		LastDerivedAccelerationCmS2.Y = FallbackAccel2D.Y;
		LastDerivedAccelerationCmS2.Z = 0.0f;
	}

	PreviousVisualVelocity = NewVel;
	bHasPreviousSnapshot = true;

	InterpolatedPosCm = NewPos;
	VisualVelocity = NewVel;
	SetActorLocationAndRotation(NewPos, NewRot);

	RefreshAnimationDrivers();

	// Diagnostico 1x/s: render lag, buffer, alpha, extrapolating?
	const double NowSec = FPlatformTime::Seconds();
	if (NowSec - LastDiagLogSec >= 1.0)
	{
		LastDiagLogSec = NowSec;
		const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
		const int64 RenderLagMs = ServerNowMs - NewestMs;
		UE_LOG(LogZMMO, Verbose,
			TEXT("ZMMOPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f"),
			EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha,
			bExtrapolating ? 1 : 0, InterpolationDelayMs);
	}
}

void AZMMOPlayerProxy::SetZMMOIdentity(const int64 InEntityId, const EZMMOEntityType InEntityType)
{
	EntityId = InEntityId;
	EntityType = InEntityType;
}

void AZMMOPlayerProxy::ApplyEntitySnapshot(const FZMMOEntitySnapshot& Snapshot)
{
	// Estabiliza o offset de relogio na primeira amostra. Subsequente nao
	// re-sincroniza para evitar saltos visuais; um esquema mais sofisticado
	// (ema do offset) cabe quando tivermos ping/RTT estabilizado por sessao.
	if (SnapshotBuffer.Num() == 0)
	{
		const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
		ServerClockOffsetMs = LocalNowMs - Snapshot.ServerTimeMs;
	}

	FZMMOProxySnapshot S;
	S.PosCm = Snapshot.PositionCm;
	S.VelCmS = Snapshot.VelocityCmS;
	S.bGrounded = Snapshot.bGrounded;
	S.ServerTimeMs = Snapshot.ServerTimeMs;

	// Insercao ordenada defensiva (UDP pode trocar a ordem de pacotes).
	int32 InsertAt = SnapshotBuffer.Num();
	for (int32 i = SnapshotBuffer.Num() - 1; i >= 0; --i)
	{
		if (SnapshotBuffer[i].ServerTimeMs < S.ServerTimeMs)
		{
			InsertAt = i + 1;
			break;
		}
		if (SnapshotBuffer[i].ServerTimeMs == S.ServerTimeMs)
		{
			// Dedupe: substitui pelo mais recente (este).
			SnapshotBuffer[i] = S;
			InsertAt = INDEX_NONE;
			break;
		}
		InsertAt = i;
	}
	if (InsertAt != INDEX_NONE)
	{
		SnapshotBuffer.Insert(S, InsertAt);
	}
	while (SnapshotBuffer.Num() > SnapshotBufferCapacity)
	{
		SnapshotBuffer.RemoveAt(0);
	}

	AuthoritativePosCm = Snapshot.PositionCm;
	LastSnapshotPosition = Snapshot.PositionCm;
	bHasFirstSnapshot = true;

	// `bGrounded` e evento — aplicar `MovementMode` imediatamente para que o
	// pulo nao espere `InterpolationDelayMs` para mudar a state machine do AnimBP.
	if (UCharacterMovementComponent* CMC = GetCharacterMovement())
	{
		const EMovementMode DesiredMode = Snapshot.bGrounded ? MOVE_Walking : MOVE_Falling;
		if (CMC->MovementMode != DesiredMode)
		{
			CMC->SetMovementMode(DesiredMode);
		}
	}

	ApplyCollisionPolicy();
}

void AZMMOPlayerProxy::SetEntityRelevant(const bool bRelevant)
{
	if (bIsZMMORelevant == bRelevant)
	{
		return;
	}

	bIsZMMORelevant = bRelevant;
	SetActorHiddenInGame(!bRelevant);
	SetActorTickEnabled(bRelevant);
	ApplyCollisionPolicy();
}

void AZMMOPlayerProxy::RefreshAnimationDrivers()
{
	UCharacterMovementComponent* CMC = GetCharacterMovement();
	if (!CMC)
	{
		return;
	}

	// Defesa: snapshots em walking não devem injetar Z residual (jitter de
	// terminal velocity / drift) no CMC, que o AnimBP veria como queda.
	FVector VelForCMC = VisualVelocity;
	if (CMC->MovementMode == MOVE_Walking)
	{
		VelForCMC.Z = 0.0f;
	}
	CMC->Velocity = VelForCMC;

	// Acceleration externa via CMC custom — alimenta `Get Current Acceleration`
	// que o AnimBP do Quinn usa em `ShouldMove` e nas transições de Jump_Start.
	if (UZMMOProxyMovementComponent* ProxyCMC = Cast<UZMMOProxyMovementComponent>(CMC))
	{
		ProxyCMC->SetZMMOExternalAcceleration(LastDerivedAccelerationCmS2);
	}
}

void AZMMOPlayerProxy::ApplyCollisionPolicy()
{
	UCapsuleComponent* Capsule = GetCapsuleComponent();
	if (!Capsule)
	{
		return;
	}

	const bool bShouldCollide = bIsZMMORelevant && bHasFirstSnapshot;
	Capsule->SetCollisionEnabled(bShouldCollide ? ECollisionEnabled::QueryOnly : ECollisionEnabled::NoCollision);
}
