/*
* Neural Tanks is a game created by Eddie O'Hagan that leverages Neural Networking
* to use for AI. This was created in Unreal Engine 4 which is developed by Epic Games.
* (Copyright Epic Games, Inc. All Rights Reserved.)
*
* Smart Tanks was originally developed by Mat Buckland in 2002 as an example
* on how Neural Networking can be used to train a set of minesweepers
* to collect mines. I (Eddie O'Hagan) updated this project in 2024 to use more
* Object Oriented Programming and modern practices. To see the original tutorial;
* visit <see href="http://www.ai-junkie.com/ann/evolved/nnt1.html">Neural Networking Tutorial</see>
* and <see href="http://www.ai-junkie.com/ga/intro/gat1.html">Genetic Algorithm Tutorial</see>
*/
#include "TankPawn.h"
#include "../Gameplay/TankProjectile.h"
#include "../Utils/TrackPos.h"
#include "../Utils/TrackPoint.h"
#include "Camera/CameraComponent.h"
#include "Components/AudioComponent.h"
#include "Components/ArrowComponent.h"
#include "Components/SplineComponent.h"
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
#include "Components/LineBatchComponent.h"
#include "Particles/ParticleSystemComponent.h"
#include "PhysicsEngine/PhysicsConstraintComponent.h"
#include <Kismet/GameplayStatics.h>
#include <SceneManagement.h>

/// <summary>
/// Default constructor.
/// </summary>
ATankPawn::ATankPawn() : Super()
{
	myIsDestroyed = false;
	myHasStarted = false;

	myRecentlySpawnedProjectile = NULL;

	PrimaryActorTick.bCanEverTick = true;

	myIsGodModeEnabled = false;
	myMaximumHealthPoints = 100.0f;
	myMaximumArmorPoints = 100.0f;
	myArmorRegenerationAmountPerSecond = 0.25f;
	myCannonReloadTime = 3.0f;
	myCurrentHealthPoints = myMaximumHealthPoints;
	myCurrentArmorPoints = myMaximumArmorPoints;
}

/// <summary>
/// Called when this blueprint is first opened or initialized. This is equivalent to a 
/// constructor but for Unreal Engine. 
/// </summary>
/// <param name="transform">The transform the actor was constructed at.</param>
void ATankPawn::OnConstruction(const FTransform& transform)
{
	TArray<UActorComponent*> components;

	GetComponents(components);

	for (int index = 0; index < components.Num(); index++)
	{
		myComponents.Add(components[index]->GetFName(), components[index]);
	}

	FindComponents();
}

/// <summary>
/// Finds this tank's blueprint components and initializes them.
/// </summary>
void ATankPawn::FindComponents()
{
	AActor* landscapeActor;

	myDestroyedTankParticles = Cast<UParticleSystemComponent>(GetComponentByName("FX_DestroyedTank"));
	
	landscapeActor = UGameplayStatics::GetActorOfClass(GetWorld(), ALandscape::StaticClass());
	if (landscapeActor != NULL)
	{
		myLandscape = Cast<ALandscape>(landscapeActor);
	}
}

/// <summary>
/// Called when this tank first spawns in the world/level.
/// </summary>
void ATankPawn::BeginPlay()
{
	Super::BeginPlay();

	//The spawn point for the projectile fired by the tank.
	myMuzzleArrow = Cast<UArrowComponent>(GetComponentByName("MuzzleArrow"));

	myTankCannon = Cast<UStaticMeshComponent>(GetComponentByName("Tank_Cannon"));

	myDefaultProjectileObject = Cast<ATankProjectile>(myCannonProjectile->GetDefaultObject());
	myCannonProjectileInitialSpeed = myDefaultProjectileObject->GetProjectileMovementComponent()->InitialSpeed;

	myTankCannonReloadAudio = Cast<UAudioComponent>(GetComponentByName("AudioReload"));
}

/// <summary>
/// Called once per frame to repair the tank over time. 
/// Keeps track of the current cannon reload time (the 
/// time between shots in seconds).
/// </summary>
/// <param name="deltaTime">Time in milliseconds between frames.</param>
void ATankPawn::Tick(float deltaTime)
{
	//Accumulate the current time and prevent overflow.
	myCannonCurrentReloadTime = FMath::Clamp(myCannonCurrentReloadTime + deltaTime, 0.0f, (float)MAX_flt);
}

/// <summary>
/// Called when the tank overlaps with a <c>TankAmmunition</c> in the game world.
/// Depending on the tank, this has different outcomes. If it's an <c>AAITank</c>, 
/// the fitness of that Neural Networked tank gets increased. If it's a <c>APlayerTank</c>,
/// the player earns some money.
/// </summary>
void ATankPawn::OnTankOverlappedItem(ATankItem* theItem)
{
	//Typically implemented in children.
}

/// <summary>
/// Called when this tank has been hit with a projectile.
/// Typically implemented in children.
/// </summary>
/// <param name="theAttackingPawn">The tank that hit this tank/player.</param>
/// <param name="damage">The amount of damage caused by the hit.</param>
void ATankPawn::OnTankHit(ATankPawn* theAttackingPawn, float damage)
{
	//Typically implemented in children.

	OnTankHitBlueprint(theAttackingPawn, damage);
}

/// <summary>
/// Called when this player has successfully hit an enemy tank.
/// Typically implemented in children.
/// </summary>
/// <param name="theHitTankPawn">The tank that the player hit.</param>
void ATankPawn::OnSuccessfullyHitTank(ATankPawn* theHitTankPawn)
{
	//Typically implemented in children.
}

/// <summary>
/// Spawns and fires this tank's projectile. Also plays sounds and
/// spawns particle effects. Impulse is also applied to the tank cannon.
/// </summary>
/// <param name="cannonReloadTime">The amount of time in seconds before this Tank can fire again.</param>
/// <returns>true if a projectile was spawned and fired, false otherwise.</returns>
bool ATankPawn::FireCannon(const float cannonReloadTime)
{
	FTransform spawnTransform;

	if (myIsDestroyed == false && myHasStarted == true)
	{
		//Ensure we waited for reload.
		if (myCannonCurrentReloadTime >= cannonReloadTime)
		{
			//Get the world transform of the muzzle's arrow component.
			spawnTransform = myMuzzleArrow->GetComponentTransform();
			spawnTransform.SetScale3D(GetActorScale3D());

			//Play ballistics sound effect.
			UGameplayStatics::PlaySoundAtLocation(this, myTankCannonFireAudio, GetActorLocation());

			//Spawn smoke particle effects at the fire(spawn) location.
			UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), myMuzzleSmokeParticles, spawnTransform);

			//Spawn fire effects at the fire(spawn) location.
			UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), myMuzzleFlashParticles, spawnTransform);

			//If cannon component if provided, apply impulse when fired.
			if (myTankCannon != NULL)
			{
				//Apply impulse to simulate recoil.
				myTankCannon->AddImpulse(spawnTransform.GetRotation().GetForwardVector() * myDefaultProjectileObject->GetImpulseToApply(), NAME_None, true);
			}

			FActorSpawnParameters spawnParams;
			spawnParams.Owner = this;
			spawnParams.Instigator = this->GetInstigator();
			spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

			//Spawn the ballistic.
			myRecentlySpawnedProjectile = GetWorld()->SpawnActor<ATankProjectile>(myCannonProjectile, spawnTransform, spawnParams);

			//Reset the timer.
			myCannonCurrentReloadTime = 0;

			if (myTankCannonReloadAudio != NULL)
			{
				myTankCannonReloadAudio->PitchMultiplier = CalculateReloadAudioSpeedBasedOnReloadTime(cannonReloadTime);
				myTankCannonReloadAudio->Play();
			}

			return true;
		}
	}

	return false;
}

/// <summary>
/// Determines how fast the reload sound effect (approximate) should play based on the amount
/// of time it takes to actually reload the cannon.
/// 
/// Since the multiplier is 1 when the reload time is 3 seconds (original playback time), and 
/// when the multiplier is 2, the reload time is 1.5 seconds. Using this info we can derive a 
/// linear formula using y=mx+b so we know the multiplier for almost any provided reload time.
/// </summary>
/// <param name="cannonReloadTime">The amount of time in seconds for the cannon to reload.</param>
float ATankPawn::CalculateReloadAudioSpeedBasedOnReloadTime(const float cannonReloadTime)
{
	return (cannonReloadTime - 4.5f) / -1.5f;
}

/// <summary>
/// Applies damage to this tank by subtracting the provided <c>damageAmount</c>
/// from first the armor points, and then the health points.
/// </summary>
/// <param name="damageAmount">The amount of damage to apply to the tank.</param>
void ATankPawn::DamageTank(const float damageAmount)
{
	if (myIsGodModeEnabled == false)
	{
		if (myCurrentArmorPoints >= damageAmount)
		{
			myCurrentArmorPoints = FMath::Clamp(myCurrentArmorPoints - damageAmount, 0.0f, myMaximumArmorPoints);
		}
		else if (myCurrentArmorPoints > 0.0f)
		{
			//Since the damage is more then the armor points, calculate the amount of damage passed on to the HP.
			float passedOnDamage = damageAmount - myCurrentArmorPoints;
			myCurrentArmorPoints = 0.0f;
			myCurrentHealthPoints = FMath::Clamp(myCurrentHealthPoints - passedOnDamage, 0.0f, myMaximumHealthPoints);
		}
		else
		{
			myCurrentHealthPoints = FMath::Clamp(myCurrentHealthPoints - damageAmount, 0.0f, myMaximumHealthPoints);
		}
	}
}

/// <summary>
/// Removes damage from this tank by adding the provided <c>healAmount</c> to the health points.
/// </summary>
/// <param name="healAmount">The amount of health points to add back.</param>
void ATankPawn::HealTank(const float healAmount)
{
	myCurrentHealthPoints = FMath::Clamp(myCurrentHealthPoints + healAmount, 0.0f, myMaximumHealthPoints);
}

/// <summary>
/// Removes damage from this tank by adding the provided <c>repairAmount</c> to the armor points.
/// </summary>
/// <param name="repairAmount">The amount of armor points to add back.</param>
void ATankPawn::RepairTank(const float repairAmount)
{
	myCurrentArmorPoints = FMath::Clamp(myCurrentArmorPoints + repairAmount, 0.0f, myMaximumArmorPoints);
}

/// <summary>
/// Returns true if this tank has been destroyed and is immobile,false if it is alive and moving.
/// </summary>
/// <returns>true if this tank has been destroyed and is immobile, false if it is alive and moving.</returns>
bool ATankPawn::GetIsTankDestroyed()
{
	return myIsDestroyed;
}

/// <summary>
/// If a landscape exists, returns the landscape height provided location.
/// If no landscape is used, then the original provided location's Z component is returned.
/// </summary>
/// <returns>The landscape height at the provided world/level location.</returns>
float ATankPawn::GetLandscapeHeightAtLocation(const FVector& location)
{
	float heightAtLocation = location.Z;
	TOptional<float> landscapeHeight;

	if (myLandscape != NULL)
	{
		landscapeHeight = myLandscape->GetHeightAtLocation(location);
		if (landscapeHeight.IsSet() == true)
		{
			return landscapeHeight.GetValue();
		}
	}

	return heightAtLocation;
}

/// <summary>
/// If a landscape exists, returns the landscape height at the tank's actor location.
/// If no landscape is used, then the original actor's location Z component is returned.
/// </summary>
/// <returns>The landscape height at this tank's world/level location.</returns>
float ATankPawn::GetLandscapeHeightAtTankLocation()
{
	return GetLandscapeHeightAtLocation(GetActorLocation());
}

/// <summary>
/// Returns the amount of armor (AP) that is regenerated every second for this tank.
/// </summary>
/// <returns>The amount of armor (AP) that is regenerated every second for this tank.</returns>
float ATankPawn::GetArmorRegenerationAmountPerSecond()
{
	return myArmorRegenerationAmountPerSecond;
}

/// <summary>
/// Returns the current amount of health points (HP) this tank has.
/// </summary>
/// <returns>The current amount of health points (HP) this tank has.</returns>
float ATankPawn::GetCurrentHealthPoints()
{
	return myCurrentHealthPoints;
}

/// <summary>
/// Returns the current amount of armor points (AP) this tank has.
/// </summary>
/// <returns>The current amount of armor points (AP) this tank has.</returns>
float ATankPawn::GetCurrentArmorPoints()
{
	return myCurrentArmorPoints;
}

/// <summary>
/// Returns the total amount of health points (HP) this tank can have.
/// </summary>
/// <returns>The total amount of health points (HP) this tank can have.</returns>
float ATankPawn::GetMaximumHealthPoints()
{
	return myMaximumHealthPoints;
}

/// <summary>
/// Returns the total amount of armor points (AP) this tank can have.
/// </summary>
/// <returns>The total amount of armor points (AP) this tank can have.</returns>
float ATankPawn::GetMaximumArmorPoints()
{
	return myMaximumArmorPoints;
}

/// <summary>
/// Returns the amount of time (in seconds) the cannon can fire another projectile.
/// </summary>
/// <returns>The amount of time (in seconds) the cannon can fire another projectile.</returns>
float ATankPawn::GetCannonReloadTime()
{
	return myCannonReloadTime;
}

/// <summary>
/// Returns true if the invincibility (god mode) cheat is enabled for this tank.
/// </summary>
/// <returns>true if the invincibility (god mode) cheat is enabled for this tank, false otherwise.</returns>
bool ATankPawn::GetIsGodModeEnabled()
{
	return myIsGodModeEnabled;
}

/// <summary>
/// Finds and returns the corresponding tank component based on its name.
/// </summary>
/// <param name="name">The component name.</param>
/// <returns>The corresponding tank component.</returns>
UActorComponent* ATankPawn::GetComponentByName(const FName& name)
{
	if (myComponents.Contains(name) == true)
	{
		return myComponents[name];
	}

	return NULL;
}

/// <summary>
/// Updates/Changes the amount of armor points (AP) given back every second to the tank.
/// </summary>
/// <param name="armorRegenerationAmount">The new amount of armor given back every second to the tank.</param>
void ATankPawn::SetArmorRegenerationAmountPerSecond(const float armorRegenerationAmount)
{
	myArmorRegenerationAmountPerSecond = armorRegenerationAmount;
}

/// <summary>
/// Updates/Changes the maximum amount of health points (HP) the tank has.
/// </summary>
/// <param name="maximumHealthPoints">The new amount of health points (HP) the tank has.</param>
void ATankPawn::SetMaximumHealthPoints(const float maximumHealthPoints)
{
	myMaximumHealthPoints = maximumHealthPoints;
}

/// <summary>
/// Updates/Changes the maximum amount of armor points (AP) the tank has.
/// </summary>
/// <param name="maximumArmorPoints">The new amount of armor points (AP) the tank has.</param>
void ATankPawn::SetMaximumArmorPoints(const float maximumArmorPoints)
{
	myMaximumArmorPoints = maximumArmorPoints;
}

/// <summary>
/// Updates/Changes the amount of time (in seconds) it takes the cannon
/// to reload and fire another projectile.
/// </summary>
/// <param name="cannonReloadTime">The new amount of time (in seconds) it takes for the cannon to reload.</param>
void ATankPawn::SetCannonReloadTime(const float cannonReloadTime)
{
	myCannonReloadTime = cannonReloadTime;
}

/// <summary>
/// Updates/Changes this tank to be invulnerable (god mode).
/// This is normally set to false unless the cheat is enabled.
/// </summary>
/// <param name="isGodModeEnabled">true to enable god mode, false to disable it.</param>
void ATankPawn::SetIsGodModeEnabled(const bool isGodModeEnabled)
{
	myIsGodModeEnabled = isGodModeEnabled;
}
