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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "PoseSearch/PoseSearchTrajectoryPredictor.h"
#include "Animation/TrajectoryTypes.h"
#include "Net/UnrealNetwork.h"
#include "SpringMovementComponent.generated.h"

USTRUCT(BlueprintType)
struct FSpringTrajectorySample
{
	GENERATED_BODY()

	UPROPERTY(BlueprintReadWrite, Category = "Trajectory")
	float Time;

	UPROPERTY(BlueprintReadWrite, Category = "Trajectory")
	FVector Position;

	UPROPERTY(BlueprintReadWrite, Category = "Trajectory")
	FVector Velocity;

	UPROPERTY(BlueprintReadWrite, Category = "Trajectory")
	FVector Acceleration;

	UPROPERTY(BlueprintReadWrite, Category = "Trajectory")
	FRotator Rotation;

	UPROPERTY(BlueprintReadWrite, Category = "Trajectory")
	FVector MeshPosition;

	UPROPERTY(BlueprintReadWrite, Category = "Trajectory")
	FRotator MeshRotation;
};

static float HalflifeToDamping(float halflife, float eps = 1e-5f)
{
	return (4.0f * 0.69314718056f) / (halflife + eps);
}

static float FastNegExp(float x)
{
	return 1.0f / (1.0f + x + 0.48f * x * x + 0.235f * x * x * x);
}

static void simple_spring_damper_exact(
	double &x,
	double &v,
	float x_goal,
	float halflife,
	float dt)
{
	float y = HalflifeToDamping(halflife) / 2.0f;
	double j0 = x - x_goal;
	double j1 = v + j0 * y;
	float eydt = FastNegExp(y * dt);

	x = eydt * (j0 + j1 * dt) + x_goal;
	v = eydt * (v - j1 * y * dt);
}

/**
 *
 */
UCLASS()
class SPRINGMOVEMENT_API USpringMovementComponent : public UCharacterMovementComponent, public IPoseSearchTrajectoryPredictorInterface
{
	GENERATED_BODY()

public:
	USpringMovementComponent();

	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const override;

	// Begin UActorComponent Interface
	virtual void BeginPlay() override;
	virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;

	// Begin IPoseSearchTrajectoryPredictorInterface Interface

	// TODO: Implement arc prediction & update InOutDesiredControllerYawLastUpdate
	UFUNCTION(BlueprintCallable, Category = "Animation|PoseSearch|Experimental")
	virtual void Predict(FTransformTrajectory &InOutTrajectory, int32 NumPredictionSamples, float SecondsPerPredictionSample, int32 NumHistorySamples) override;
	virtual void GetGravity(FVector &OutGravityAccel) override;
	virtual void GetCurrentState(FVector &OutPosition, FQuat &OutFacing, FVector &OutVelocity) override;
	virtual void GetVelocity(FVector &OutVelocity) override;
	// End IPoseSearchTrajectoryPredictorInterface Interface

	UPROPERTY(BlueprintReadOnly, Category = "Spring Movement")
	FVector TargetVelocity = FVector(0.0f);

	UPROPERTY(BlueprintReadOnly, Category = "Spring Movement")
	FRotator AngVel = FRotator(0.0f);

	UPROPERTY(BlueprintReadOnly, Category = "Spring Movement|Trajectory")
	TArray<FSpringTrajectorySample> HistorySamples;

	UPROPERTY(BlueprintReadOnly, Category = "Spring Movement|Trajectory")
	TArray<FSpringTrajectorySample> FutureSamples;

	// Begin MovementComponent Interface
	virtual float GetMaxSpeed() const override;
	virtual float GetMinAnalogSpeed() const override;
	// End MovementComponent Interface

	/**
	 * Updates Velocity and Acceleration based on the current state, applying the effects of friction and acceleration or deceleration. Does not apply gravity.
	 * This is used internally during movement updates. Normally you don't need to call this from outside code, but you might want to use it for custom movement modes.
	 *
	 * @param	DeltaTime						time elapsed since last frame.
	 * @param	Friction						coefficient of friction when not accelerating, or in the direction opposite acceleration.
	 * @param	bFluid							true if moving through a fluid, causing Friction to always be applied regardless of acceleration.
	 * @param	BrakingDeceleration				deceleration applied when not accelerating, or when exceeding max velocity.
	 */
	virtual void CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration) override;

	virtual void PhysicsRotation(float DeltaTime) override;

	virtual FRotator ComputeOrientToMovementRotation(const FRotator &CurrentRotation, float DeltaTime, FRotator &DeltaRotation) const override;

	/** Returns how far to rotate character during the time interval DeltaTime. */
	virtual FRotator GetDeltaRotation(float DeltaTime) const override;

	/** Computes the analog input modifier based on current input vector and/or acceleration. */
	virtual float ComputeAnalogInputModifier() const override;

	virtual void ControlledCharacterMove(const FVector &InputVector, float DeltaSeconds) override;
	virtual void SimulateMovement(float DeltaSeconds) override;

	virtual void PredictTrajectory(TArray<float> &SampleTimes, TArray<FSpringTrajectorySample> &Samples);

	virtual void SpringVelocityUpdate(FVector &Pos, FVector &Vel, FVector &Accel, const FVector &VelGoal, float Halflife, float DeltaTime);
	virtual void SpringRotationUpdate(FRotator &RotGoal, float Halflife, float DeltaTime);

	virtual float GetDirectionalSpeedScalingFactor() const;
	virtual float GetArcDampingFactor() const;

	/** Enforce constraints on input given current state. For instance, don't move upwards if walking and looking up. */
	FVector ConstrainTargetVelocity(const FVector &InputVelocity) const;

	float GetCurrentVelocityHalflife(const FVector &InputVector) const
	{
		return InputVector.Size() > 0 ? VelocityHalflife : VelocityHalflife * BrakingHalflifeScalar;
	}

	// Debug
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Debug")
	bool bDrawDebugTrajectory = false;

	// --- Replication ---

	/** Server RPC: autonomous proxy sends its intent to the server each tick. Unreliable because we send every tick and can afford drops. */
	UFUNCTION(Server, Unreliable)
	void ServerUpdateIntent(FVector_NetQuantize InInputVector, FRotator InControlRotation, float InSmoothedYawRate);

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Spring Movement")
	float VelocityHalflife = 0.100f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Spring Movement")
	float RotationHalflife = 0.100f;

	UPROPERTY(BlueprintReadOnly, Category = "Spring Movement")
	FRotator LastDesiredRotation = FRotator(0.0f);

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Spring Movement")
	float BrakingHalflifeScalar = 1.25f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement")
	float TurnInPlaceAngleThreshold = 30.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Speed Scaling")
	bool bUseSpeedScaling = false;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Speed Scaling", meta = (EditCondition = "bUseSpeedScaling"))
	float StrafeSpeedScale = 0.75f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Speed Scaling", meta = (EditCondition = "bUseSpeedScaling"))
	float BackwardsSpeedScale = 0.65f;

	// The threshold at which to start scaling speed for strafing movement.
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Speed Scaling", meta = (EditCondition = "bUseSpeedScaling"))
	float StrafeSpeedScaleThreshold = 80.0f;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Speed Scaling", meta = (EditCondition = "bUseSpeedScaling"))
	float BackwardsSpeedScaleThreshold = 130.0f;

	// Whether to apply damping to the movement speed based on controller yaw input.
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Arc Speed Damping")
	bool bUseArcDamping = false;

	// Any controller yaw inputs below the minimum will not be damped, and any above the maximum will be fully damped (Degrees).
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Arc Speed Damping", meta = (EditCondition = "bUseArcDamping"))
	FFloatRange ArcDampingRange = FFloatRange(20.0f, 150.0f);

	// The damping scalar to apply to speed when at maximum damping.
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Arc Speed Damping", meta = (EditCondition = "bUseArcDamping"))
	float MaxArcDampingScalar = 0.8f;

	/** When bOrientRotationToMovement is enabled, this setting will make the character rotate towards their last input direction even whilst stationary */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Arc Speed Damping")
	bool bOrientToMovementWhenStationary = false;

	/** Whether to smooth the yaw rate is smoothed for trajectory predictions and arc damping */
	UPROPERTY(EditAnywhere, Category = "Spring Movement|Yaw Rate")
	bool bSmoothYawRate = true;

	/** The speed at which the yaw rate is smoothed for trajectory predictions and arc damping (using a weighted moving average) */
	UPROPERTY(EditAnywhere, Category = "Spring Movement|Yaw Rate", meta = (EditCondition = "bSmoothYawRate"))
	float YawRateSmoothingSpeed = 15.0f;

	/** The speed at which the yaw rate is smoothed WHEN DECREASING TO ZERO, for trajectory predictions and arc damping (using a weighted moving average) */
	UPROPERTY(EditAnywhere, Category = "Spring Movement|Yaw Rate", meta = (EditCondition = "bSmoothYawRate"))
	float YawRateSmoothingZeroSpeed = 25.0f;

	/** Whether to use the yaw rate for trajectory predictions. This will make arcing movements more accurate */
	UPROPERTY(EditAnywhere, Category = "Spring Movement|Trajectory|Yaw Rate")
	bool bUseYawRateForPrediction = true;

	/** The max yaw rate (absolute value) that can be applied to trajectory predictions */
	UPROPERTY(EditAnywhere, Category = "Spring Movement|Yaw Rate", meta = (EditCondition = "bSmoothYawRate"))
	float MaxYawRateForPrediction = 150.0f;

	/** Whether to enable yaw rate decay when running trajectory predictions. This is a feature because typically, players are not keeping a consistent yaw rate input for very long.
	 *  If using keyboard and mouse input, this should be enabled.
	 *  If using controller input, this can be disabled. But it should be kept on if your control system is a high-sensitivity 'twitchy' style that prefers quick changes in yaw rate.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spring Movement|Trajectory|Yaw Rate", meta = (EditCondition = "bUseYawRateForPrediction"))
	bool bEnableYawRateDecay = true;

	/** The time it takes for the yaw rate to decay to zero when running trajectory predictions */
	UPROPERTY(EditAnywhere, Category = "Spring Movement|Trajectory|Yaw Rate", meta = (EditCondition = "bUseYawRateForPrediction"))
	float YawRateDecayTime = 1.0f;

	// TODO: Make these private (?)
	UPROPERTY(BlueprintReadOnly, Category = "Spring Movement|Yaw Rate")
	float CurrentYawRate = 0.0f;

	UPROPERTY(BlueprintReadOnly, Category = "Spring Movement|Yaw Rate")
	float SmoothedYawRate = 0.0f;

protected:
	TArray<float> TrajectorySampleTimes = {0.2f, 0.4f, 0.6f};

private:
	FVector Position = FVector(0.0f);
	FVector LastNonzeroInputVector = FVector(0.0f);
	FRotator DesiredRotation = FRotator(0.0f);
	FRotator ControlRotation = FRotator(0.0f);
	FRotator LastControlRotation = FRotator(0.0f);
	float LastDeltaSeconds = 0.0f;
	void SmoothYawRate(float DeltaTime);

	float ComputeSpeedScalingFactor() const;

	// --- Replicated intent (server → simulated proxies) ---

	UPROPERTY(ReplicatedUsing = OnRep_ReplicatedIntent)
	FVector_NetQuantize ReplicatedInputVector = FVector::ZeroVector;

	UPROPERTY(Replicated)
	FRotator ReplicatedControlRotation = FRotator::ZeroRotator;

	UPROPERTY(Replicated)
	float ReplicatedSmoothedYawRate = 0.0f;

	UFUNCTION()
	void OnRep_ReplicatedIntent();

	/** True once the simulated proxy has received at least one intent update. Prevents running spring sim from default-zero intent. */
	bool bHasReceivedReplicatedIntent = false;

	// Helpers
	bool IsGroundedAndStationary() const;

	/** Sets TargetVelocity, LastNonzeroInputVector, and AnalogInputModifier from an input vector. Shared between local control and replicated-intent paths. */
	void ApplyMovementIntent(const FVector &InputVector);

	// Debug
	void DrawDebugTrajectory();
};
