// Copyright Epic Games, Inc. All Rights Reserved.

#include "AgrarianGameCharacter.h"
#include "AgrarianBuildingPlacementComponent.h"
#include "AgrarianCraftingComponent.h"
#include "AgrarianInteractable.h"
#include "AgrarianInventoryComponent.h"
#include "AgrarianSurvivalComponent.h"
#include "Engine/LocalPlayer.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "AgrarianGame.h"
#include "Net/UnrealNetwork.h"

AAgrarianGameCharacter::AAgrarianGameCharacter()
{
	PrimaryActorTick.bCanEverTick = true;
	bReplicates = true;
	SetReplicateMovement(true);
	SetNetUpdateFrequency(30.0f);
	SetMinNetUpdateFrequency(10.0f);

	// Set size for collision capsule
	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
		
	// Don't rotate when the controller rotates. Let that just affect the camera.
	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	// Configure character movement
	GetCharacterMovement()->NavAgentProps.bCanCrouch = true;
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);

	// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
	// instead of recompiling to adjust them
	GetCharacterMovement()->JumpZVelocity = 500.f;
	GetCharacterMovement()->AirControl = 0.35f;
	GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
	GetCharacterMovement()->MaxWalkSpeedCrouched = WalkSpeed * CrouchSpeedMultiplier;
	GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
	GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
	GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;

	// Create a camera boom (pulls in towards the player if there is a collision)
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);
	CameraBoom->TargetArmLength = ThirdPersonCameraDistance;
	CameraBoom->bUsePawnControlRotation = true;

	// Create a follow camera
	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
	FollowCamera->bUsePawnControlRotation = false;

	SurvivalComponent = CreateDefaultSubobject<UAgrarianSurvivalComponent>(TEXT("SurvivalComponent"));
	InventoryComponent = CreateDefaultSubobject<UAgrarianInventoryComponent>(TEXT("InventoryComponent"));
	CraftingComponent = CreateDefaultSubobject<UAgrarianCraftingComponent>(TEXT("CraftingComponent"));
	BuildingPlacementComponent = CreateDefaultSubobject<UAgrarianBuildingPlacementComponent>(TEXT("BuildingPlacementComponent"));

	// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character) 
	// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)
}

void AAgrarianGameCharacter::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	if (IsLocallyControlled())
	{
		UpdateInteractionPrompt();
	}

	ApplyMovementSpeed();

	if (!HasAuthority() || !bWantsToSprint)
	{
		return;
	}

	if (!CanSprint())
	{
		SetWantsToSprint(false);
		return;
	}

	if (GetVelocity().SizeSquared2D() > KINDA_SMALL_NUMBER && SurvivalComponent)
	{
		const float EffectiveEndurance = FMath::Max(0.25f, EnduranceMultiplier);
		SurvivalComponent->SpendStamina((SprintStaminaCostPerSecond / EffectiveEndurance) * DeltaSeconds);
		if (!CanSprint())
		{
			SetWantsToSprint(false);
		}
	}
}

void AAgrarianGameCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(AAgrarianGameCharacter, bWantsToSprint);
	DOREPLIFETIME(AAgrarianGameCharacter, bIsProne);
	DOREPLIFETIME(AAgrarianGameCharacter, AgeYears);
	DOREPLIFETIME(AAgrarianGameCharacter, PhysicalConditionMultiplier);
	DOREPLIFETIME(AAgrarianGameCharacter, StrengthMultiplier);
	DOREPLIFETIME(AAgrarianGameCharacter, EnduranceMultiplier);
	DOREPLIFETIME(AAgrarianGameCharacter, TerrainMovementMultiplier);
}

void AAgrarianGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	// Set up action bindings
	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
		
		// Jumping
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);

		// Moving
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AAgrarianGameCharacter::Move);
		EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AAgrarianGameCharacter::Look);

		// Looking
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AAgrarianGameCharacter::Look);

		if (InteractAction)
		{
			EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::Interact);
		}

		if (SprintAction)
		{
			EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::StartSprint);
			EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AAgrarianGameCharacter::StopSprint);
			EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Canceled, this, &AAgrarianGameCharacter::StopSprint);
		}

		if (CrouchAction)
		{
			EnhancedInputComponent->BindAction(CrouchAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::ToggleCrouchStance);
		}

		if (ProneAction)
		{
			EnhancedInputComponent->BindAction(ProneAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::ToggleProneStance);
		}

		if (ToggleCameraAction)
		{
			EnhancedInputComponent->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::ToggleCameraPerspective);
		}
	}
	else
	{
		UE_LOG(LogAgrarianGame, Error, TEXT("'%s' Failed to find an Enhanced Input component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this));
	}
}

void AAgrarianGameCharacter::Move(const FInputActionValue& Value)
{
	// input is a Vector2D
	FVector2D MovementVector = Value.Get<FVector2D>();

	// route the input
	DoMove(MovementVector.X, MovementVector.Y);
}

void AAgrarianGameCharacter::Look(const FInputActionValue& Value)
{
	// input is a Vector2D
	FVector2D LookAxisVector = Value.Get<FVector2D>();

	// route the input
	DoLook(LookAxisVector.X, LookAxisVector.Y);
}

void AAgrarianGameCharacter::Interact()
{
	TryInteract();
}

void AAgrarianGameCharacter::StartSprint()
{
	if (bIsProne || bIsCrouched)
	{
		return;
	}

	SetWantsToSprint(true);
}

void AAgrarianGameCharacter::StopSprint()
{
	SetWantsToSprint(false);
}

void AAgrarianGameCharacter::ToggleCrouchStance()
{
	if (bIsProne)
	{
		SetProne(false);
		return;
	}

	SetWantsToSprint(false);
	if (bIsCrouched)
	{
		UnCrouch();
	}
	else
	{
		Crouch();
	}

	ApplyMovementSpeed();
}

void AAgrarianGameCharacter::ToggleProneStance()
{
	SetProne(!bIsProne);
}

void AAgrarianGameCharacter::ToggleCameraPerspective()
{
	SetFirstPersonCamera(!bFirstPersonCamera);
}

void AAgrarianGameCharacter::SetFirstPersonCamera(bool bEnableFirstPerson)
{
	bFirstPersonCamera = bEnableFirstPerson;

	if (!CameraBoom || !FollowCamera)
	{
		return;
	}

	if (bFirstPersonCamera)
	{
		CameraBoom->TargetArmLength = 0.0f;
		CameraBoom->SocketOffset = FirstPersonCameraOffset;
		CameraBoom->bDoCollisionTest = false;
		FollowCamera->bUsePawnControlRotation = false;

		if (GetMesh())
		{
			GetMesh()->SetOwnerNoSee(true);
		}
	}
	else
	{
		CameraBoom->TargetArmLength = ThirdPersonCameraDistance;
		CameraBoom->SocketOffset = FVector::ZeroVector;
		CameraBoom->bDoCollisionTest = true;
		FollowCamera->bUsePawnControlRotation = false;

		if (GetMesh())
		{
			GetMesh()->SetOwnerNoSee(false);
		}
	}
}

void AAgrarianGameCharacter::SetWantsToSprint(bool bNewWantsToSprint)
{
	const bool bAllowedSprintIntent = bNewWantsToSprint && CanSprint();
	if (bWantsToSprint == bAllowedSprintIntent)
	{
		return;
	}

	bWantsToSprint = bAllowedSprintIntent;
	ApplyMovementSpeed();

	if (!HasAuthority())
	{
		ServerSetWantsToSprint(bAllowedSprintIntent);
	}
}

bool AAgrarianGameCharacter::CanSprint() const
{
	return SurvivalComponent
		&& SurvivalComponent->IsAlive()
		&& !bIsProne
		&& !bIsCrouched
		&& SurvivalComponent->Survival.Exhaustion < 85.0f
		&& SurvivalComponent->Survival.Stamina > MinSprintStamina;
}

bool AAgrarianGameCharacter::IsSprinting() const
{
	return bWantsToSprint && CanSprint();
}

float AAgrarianGameCharacter::GetCurrentCarryWeight() const
{
	return InventoryComponent ? InventoryComponent->GetTotalWeight() : 0.0f;
}

float AAgrarianGameCharacter::GetCurrentMovementSpeedMultiplier() const
{
	return CalculateMovementSpeedMultiplier();
}

void AAgrarianGameCharacter::SetProne(bool bNewProne)
{
	if (bIsProne == bNewProne)
	{
		return;
	}

	bIsProne = bNewProne;

	if (bIsProne)
	{
		SetWantsToSprint(false);
		UnCrouch();
	}

	ApplyMovementSpeed();

	if (!HasAuthority())
	{
		ServerSetProne(bIsProne);
	}
}

void AAgrarianGameCharacter::SetTerrainMovementMultiplier(float NewTerrainMovementMultiplier)
{
	const float ClampedTerrainMovementMultiplier = FMath::Clamp(NewTerrainMovementMultiplier, 0.25f, 1.25f);
	if (!HasAuthority())
	{
		ServerSetTerrainMovementMultiplier(ClampedTerrainMovementMultiplier);
		return;
	}

	if (FMath::IsNearlyEqual(TerrainMovementMultiplier, ClampedTerrainMovementMultiplier))
	{
		return;
	}

	TerrainMovementMultiplier = ClampedTerrainMovementMultiplier;
	ApplyMovementSpeed();
}

void AAgrarianGameCharacter::ApplyMovementSpeed()
{
	if (UCharacterMovementComponent* MovementComponent = GetCharacterMovement())
	{
		const float BaseSpeed = IsSprinting() ? SprintSpeed : WalkSpeed;
		const float FinalSpeed = FMath::Max(0.0f, BaseSpeed * CalculateMovementSpeedMultiplier());
		MovementComponent->MaxWalkSpeed = FinalSpeed;
		MovementComponent->MaxWalkSpeedCrouched = FinalSpeed;
	}
}

float AAgrarianGameCharacter::CalculateMovementSpeedMultiplier() const
{
	const float TraitMultiplier = FMath::Clamp(PhysicalConditionMultiplier, 0.25f, 1.25f)
		* FMath::Clamp(EnduranceMultiplier, 0.25f, 2.0f);

	return FMath::Clamp(
		CalculateAgeMovementMultiplier()
		* TraitMultiplier
		* CalculateSurvivalMovementMultiplier()
		* CalculateCarryWeightMovementMultiplier()
		* CalculateStanceMovementMultiplier()
		* FMath::Clamp(TerrainMovementMultiplier, 0.25f, 1.25f),
		0.15f,
		1.35f);
}

float AAgrarianGameCharacter::CalculateAgeMovementMultiplier() const
{
	if (AgeYears < 13.0f)
	{
		return 0.65f;
	}

	if (AgeYears < 18.0f)
	{
		return FMath::GetMappedRangeValueClamped(FVector2D(13.0f, 18.0f), FVector2D(0.8f, 1.0f), AgeYears);
	}

	if (AgeYears <= 50.0f)
	{
		return 1.0f;
	}

	if (AgeYears <= 70.0f)
	{
		return FMath::GetMappedRangeValueClamped(FVector2D(50.0f, 70.0f), FVector2D(1.0f, 0.75f), AgeYears);
	}

	return 0.65f;
}

float AAgrarianGameCharacter::CalculateSurvivalMovementMultiplier() const
{
	if (!SurvivalComponent || !SurvivalComponent->IsAlive())
	{
		return 0.0f;
	}

	const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival;
	const float HungerMultiplier = Survival.Hunger >= 50.0f
		? 1.0f
		: FMath::GetMappedRangeValueClamped(FVector2D(0.0f, 50.0f), FVector2D(0.85f, 1.0f), Survival.Hunger);
	const float ThirstMultiplier = Survival.Thirst >= 50.0f
		? 1.0f
		: FMath::GetMappedRangeValueClamped(FVector2D(0.0f, 50.0f), FVector2D(0.75f, 1.0f), Survival.Thirst);
	const float InjuryMultiplier = FMath::GetMappedRangeValueClamped(
		FVector2D(0.0f, 100.0f),
		FVector2D(1.0f, 0.5f),
		Survival.InjurySeverity);
	const float SprainMultiplier = FMath::GetMappedRangeValueClamped(
		FVector2D(0.0f, 100.0f),
		FVector2D(1.0f, 0.45f),
		Survival.SprainSeverity);
	const float SicknessMultiplier = FMath::GetMappedRangeValueClamped(
		FVector2D(0.0f, 100.0f),
		FVector2D(1.0f, 0.7f),
		Survival.SicknessSeverity);
	const float ExhaustionMultiplier = FMath::GetMappedRangeValueClamped(
		FVector2D(0.0f, 100.0f),
		FVector2D(1.0f, 0.55f),
		Survival.Exhaustion);

	return HungerMultiplier * ThirstMultiplier * InjuryMultiplier * SprainMultiplier * SicknessMultiplier * ExhaustionMultiplier;
}

float AAgrarianGameCharacter::CalculateCarryWeightMovementMultiplier() const
{
	const float Strength = FMath::Clamp(StrengthMultiplier, 0.25f, 2.0f);
	const float EffectiveComfortWeight = ComfortableCarryWeight * Strength;
	const float EffectiveHeavyWeight = HeavyCarryWeight * Strength;
	const float CurrentCarryWeight = GetCurrentCarryWeight();

	if (CurrentCarryWeight <= EffectiveComfortWeight || EffectiveHeavyWeight <= EffectiveComfortWeight)
	{
		return 1.0f;
	}

	if (CurrentCarryWeight >= EffectiveHeavyWeight)
	{
		return 0.45f;
	}

	return FMath::GetMappedRangeValueClamped(
		FVector2D(EffectiveComfortWeight, EffectiveHeavyWeight),
		FVector2D(1.0f, 0.65f),
		CurrentCarryWeight);
}

float AAgrarianGameCharacter::CalculateStanceMovementMultiplier() const
{
	if (bIsProne)
	{
		return FMath::Clamp(ProneSpeedMultiplier, 0.05f, 1.0f);
	}

	if (bIsCrouched)
	{
		return FMath::Clamp(CrouchSpeedMultiplier, 0.1f, 1.0f);
	}

	return 1.0f;
}

void AAgrarianGameCharacter::OnRep_SprintState()
{
	ApplyMovementSpeed();
}

void AAgrarianGameCharacter::OnRep_ProneState()
{
	if (bIsProne)
	{
		UnCrouch();
	}

	ApplyMovementSpeed();
}

void AAgrarianGameCharacter::OnRep_MovementModifierState()
{
	ApplyMovementSpeed();
}

void AAgrarianGameCharacter::DoMove(float Right, float Forward)
{
	if (GetController() != nullptr)
	{
		// find out which way is forward
		const FRotator Rotation = GetController()->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);

		// get forward vector
		const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);

		// get right vector 
		const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

		// add movement 
		AddMovementInput(ForwardDirection, Forward);
		AddMovementInput(RightDirection, Right);
	}
}

void AAgrarianGameCharacter::DoLook(float Yaw, float Pitch)
{
	if (GetController() != nullptr)
	{
		// add yaw and pitch input to controller
		AddControllerYawInput(Yaw);
		AddControllerPitchInput(Pitch);
	}
}

void AAgrarianGameCharacter::DoJumpStart()
{
	// signal the character to jump
	Jump();
}

void AAgrarianGameCharacter::DoJumpEnd()
{
	// signal the character to stop jumping
	StopJumping();
}

void AAgrarianGameCharacter::TryInteract()
{
	AActor* HitActor = FindFocusedInteractable();
	if (!HitActor)
	{
		return;
	}

	if (HasAuthority())
	{
		IAgrarianInteractable::Execute_Interact(HitActor, this);
	}
	else
	{
		ServerInteract(HitActor);
	}
}

bool AAgrarianGameCharacter::HasInteractionPrompt() const
{
	return FocusedInteractableActor != nullptr && !InteractionPromptText.IsEmpty();
}

void AAgrarianGameCharacter::UpdateInteractionPrompt()
{
	FText NewPromptText;
	FocusedInteractableActor = FindFocusedInteractable(&NewPromptText);
	InteractionPromptText = FocusedInteractableActor ? NewPromptText : FText::GetEmpty();
}

AActor* AAgrarianGameCharacter::FindFocusedInteractable(FText* OutPromptText) const
{
	FVector TraceStart;
	FRotator TraceRotation;

	if (Controller)
	{
		Controller->GetPlayerViewPoint(TraceStart, TraceRotation);
	}
	else
	{
		TraceStart = FollowCamera ? FollowCamera->GetComponentLocation() : GetActorLocation();
		TraceRotation = FollowCamera ? FollowCamera->GetComponentRotation() : GetActorRotation();
	}

	const FVector TraceEnd = TraceStart + TraceRotation.Vector() * InteractionDistance;
	FHitResult Hit;
	FCollisionQueryParams Params(SCENE_QUERY_STAT(AgrarianInteractTrace), false, this);

	if (!GetWorld() || !GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_Visibility, Params))
	{
		return nullptr;
	}

	AActor* HitActor = Hit.GetActor();
	if (!HitActor || !HitActor->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()))
	{
		return nullptr;
	}

	if (!IAgrarianInteractable::Execute_CanInteract(HitActor, this))
	{
		return nullptr;
	}

	if (OutPromptText)
	{
		*OutPromptText = IAgrarianInteractable::Execute_GetInteractionText(HitActor, this);
	}

	return HitActor;
}

void AAgrarianGameCharacter::ServerSetWantsToSprint_Implementation(bool bNewWantsToSprint)
{
	SetWantsToSprint(bNewWantsToSprint);
}

void AAgrarianGameCharacter::ServerSetProne_Implementation(bool bNewProne)
{
	SetProne(bNewProne);
}

void AAgrarianGameCharacter::ServerSetTerrainMovementMultiplier_Implementation(float NewTerrainMovementMultiplier)
{
	SetTerrainMovementMultiplier(NewTerrainMovementMultiplier);
}

void AAgrarianGameCharacter::ServerInteract_Implementation(AActor* TargetActor)
{
	if (!TargetActor || !TargetActor->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()))
	{
		return;
	}

	if (FVector::DistSquared(TargetActor->GetActorLocation(), GetActorLocation()) > FMath::Square(InteractionDistance + 100.0f))
	{
		return;
	}

	if (IAgrarianInteractable::Execute_CanInteract(TargetActor, this))
	{
		IAgrarianInteractable::Execute_Interact(TargetActor, this);
	}
}
