/*
* 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 "PlayerTankPawn.h"
#include <Kismet/GameplayStatics.h>
#include "PhysicsEngine/PhysicsConstraintComponent.h"
#include "Components/ArrowComponent.h"
#include "Components/AudioComponent.h"
#include "../NeuralTanksGameInstance.h"
#include "../Utils/NeuralTanksUtils.h"
#include "../NeuralTanksCheatManager.h"

/// <summary>
/// Default constructor.
/// </summary>
APlayerTankPawn::APlayerTankPawn() : Super()
{
	myIsUsingSimpleControls = true;

	myIsCursorDisplayed = false;
	myIsLevelCameraInUse = false;
	myMoneyPerTankAmmunition = 1;
	myLookTurnRate = 10.0f;
	myLookUpRate = 10.0f;
	myPlayerController = NULL;
}

/// <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 APlayerTankPawn::OnConstruction(const FTransform& transform)
{
	UWorld* theWorld = GetWorld();

	Super::OnConstruction(transform);

	//Initialize the boombox for playing music. Simply use the CDO since the data is static. 
	myBoombox = NewObject<UBoombox>(this, myBoomboxBlueprint);
	myBoombox->Initialize();

	//Ensure world is valid before attempting to get the Neural Tanks game instance.
	if (theWorld != NULL)
	{
		myGameInstance = Cast<UNeuralTanksGameInstance>(UGameplayStatics::GetGameInstance(theWorld));
		if (myGameInstance != NULL)
		{
			//Need to do the equipment loading here since we are attaching/changing static meshes on components with physics enabled.
			//Also applies accessories after applying equipment and its stat modifiers.
			myGameInstance->EquipLoadout(this);
		}
	}
}

/// <summary>
/// Called when this Player Tank first spawns into the world/level. Loads the save game,
/// binds some events, and sets defaults for the tank if equipment was changed. Also
/// applies equipment stats.
/// </summary>
void APlayerTankPawn::BeginPlay()
{
	UWorld* theWorld = GetWorld();
	
	myPlayerController = UGameplayStatics::GetPlayerController(GetWorld(), 0);
	
	myPlayerProgression = myGameInstance->GetPlayerProgressionSaveGame();

	myGameMode = Cast<ANeuralTanksGameMode>(UGameplayStatics::GetGameMode(theWorld));
	myGameMode->OnRoundEnded_Event.AddUObject(this, &APlayerTankPawn::OnRoundEnded);

	FCoreDelegates::OnControllerConnectionChange.AddUObject(this, &APlayerTankPawn::OnControllerConnectionChanged);

	//Set the defaults in-case equipment was changed.
	myLookTurnRate = 10.0f;
	myLookUpRate = 10.0f;
	myMaximumHealthPoints = 100.0f;
	myMaximumArmorPoints = 100.0f;
	myArmorRegenerationAmountPerSecond = 0.25f;
	myCannonReloadTime = 3.0f;

	//Set values based on enabled cheats.
	myIsGodModeEnabled = myGameInstance->GetCheatManager()->IsCheatEnabled(ECheatName::VE_GODMODE);
	if (myGameInstance->GetCheatManager()->IsCheatEnabled(ECheatName::VE_INSTANTRELOAD) == true)
	{
		myCannonReloadTime = 0.0f;
	}

	//Apply the equipment stats here since they are blueprint implemented and not available until BeginPlay().
	myGameInstance->ApplyEquipmentStats(this);

	//Start the tank.
	Super::BeginPlay();
}

/// <summary>
/// Registers the inputs (axis and actions) used by the player.
/// </summary>
/// <param name="playerInputComponent">The input component used to register bindings.</param>
void APlayerTankPawn::SetupPlayerInputComponent(class UInputComponent* playerInputComponent)
{
	Super::SetupPlayerInputComponent(playerInputComponent);

	//Set up gameplay key bindings
	check(playerInputComponent);

	if (myIsUsingSimpleControls == true)
	{
		playerInputComponent->BindAxis("MoveForward", this, &APlayerTankPawn::MoveForward);
		playerInputComponent->BindAxis("MoveLeftRight", this, &APlayerTankPawn::MoveLeftRight);
	}
	else
	{
		playerInputComponent->BindAxis("MoveLeftTrackForward", this, &APlayerTankPawn::MoveLeftTrackForward);
		playerInputComponent->BindAxis("MoveRightTrackForward", this, &APlayerTankPawn::MoveRightTrackForward);
	}

	playerInputComponent->BindAxis("TurnTurret", this, &APlayerTankPawn::TurnTurret);
	playerInputComponent->BindAxis("PitchCannon", this, &APlayerTankPawn::PitchCannon);

	playerInputComponent->BindAction("QuickSaveGame", IE_Pressed, this, &APlayerTankPawn::QuickSaveGame);

	playerInputComponent->BindAction("FireCannon", IE_Pressed, this, &APlayerTankPawn::OnPlayerFireCannon);

	playerInputComponent->BindAction("ToggleCursor", IE_Pressed, this, &APlayerTankPawn::OnTogglePlayerCursor);
	playerInputComponent->BindAction("ToggleCamera", IE_Pressed, this, &APlayerTankPawn::OnTogglePlayerCamera);

	playerInputComponent->BindAction("ToggleBoombox", IE_Pressed, this, &APlayerTankPawn::OnToggleBoombox);
	playerInputComponent->BindAction("IncreaseBoomboxVolume", IE_Pressed, this, &APlayerTankPawn::OnIncreaseBoomboxVolume);
	playerInputComponent->BindAction("DecreaseBoomboxVolume", IE_Pressed, this, &APlayerTankPawn::OnDecreaseBoomboxVolume);
	playerInputComponent->BindAction("NextBoomboxStation", IE_Pressed, this, &APlayerTankPawn::OnNextBoomboxStation);
	playerInputComponent->BindAction("PreviousBoomboxStation", IE_Pressed, this, &APlayerTankPawn::OnPreviousBoomboxStation);
}

/// <summary>
/// Called once per frame to repair the tank over time. This tank
/// has it's armor replenished once every myTotalTime (in seconds)
/// has passed.
/// </summary>
/// <param name="deltaTime">Time in milliseconds between frames.</param>
void APlayerTankPawn::Tick(float deltaTime)
{
	Super::Tick(deltaTime);
	
	//Repair the tank over time.
	myTotalTime += deltaTime;
	if (myTotalTime >= 1.0f)
	{
		RepairTank(myArmorRegenerationAmountPerSecond);
		myTotalTime = 0.0f;
	}
}

/// <summary>
/// Plays vibration(s) on controller depending on the provided effect.
/// </summary>
/// <param name="theForceFeedbackEffect">The details of the vibration such as intensity and length.</param>
void APlayerTankPawn::PlayForceFeedback(UForceFeedbackEffect* theForceFeedbackEffect)
{
	if (theForceFeedbackEffect != NULL)
	{
		myPlayerController->ClientPlayForceFeedback(theForceFeedbackEffect);
	}
	else
	{
		UNeuralTanksUtils::WriteToLog("APlayerTankPawn::PlayForceFeedback() Error: theForceFeedbackEffect is NULL!");
	}
}

/// <summary>
/// Called if/when a controller is connected or disconnected. We use this to
/// pause the game if a controller is disconnected.
/// </summary>
/// <param name="isConnected">True if the controller was just connected, false if it was just disconnected.</param>
/// <param name="userID">The unique identifier for the platform user associated with the device. This can be used to track specific users across multiple controllers.</param>
/// <param name="userIndex">The unique identifier for the specific hardware input device. You can use this to distinguish between different controllers connected to the same machine (usually 0).</param>
void APlayerTankPawn::OnControllerConnectionChanged(bool isConnected, FPlatformUserId userID, int32 userIndex)
{
	//Let the blueprint side know the controller was disconnected (pausing/pause is handled in blueprints).
	OnControllerConnectionChangedBlueprint(isConnected, userID, userIndex);
}

/// <summary>
/// Called when the level has been completed or the player has died.
/// </summary>
/// <param name="hasPlayerWon">true if the player has won the round, false if they died.</param>
void APlayerTankPawn::OnRoundEnded(bool hasPlayerWon)
{
	if (hasPlayerWon == true)
	{
		myPlayerProgression->OnRoundWon(GetWorld());

		APhysicsTankPawn::StopTank();

		//Play sound effect for when the player wins the round.
		UGameplayStatics::PlaySoundAtLocation(this, myPlayerWonRoundSoundEffect, GetActorLocation());
	}
	else
	{
		myPlayerProgression->OnRoundLost(GetWorld());

		APhysicsTankPawn::DestroyTank();
	}
}

/// <summary>
/// Called when this tank collides/runs over an item on the ground.
/// </summary>
/// <param name="theItem">The item this tank collided with.</param>
void APlayerTankPawn::OnTankOverlappedItem(ATankItem* theItem)
{
	Super::OnTankOverlappedItem(theItem);

	if (theItem->GetItemType() == EItemType::VE_AMMO)
	{
		myPlayerProgression->IncrementNumberOfTankAmmunitionObtained();
		myPlayerProgression->AddMoney(theItem->GetItemCost());
	}
	else
	{
		myPlayerProgression->AddItemToInventory(theItem);

		//Move the item off level since we don't want to destroy it.
		theItem->SetActorLocation(FVector(0.0f, 0.0f, -10000.0));
	}

	//Play sound effect for player picking up an item.
	UGameplayStatics::PlaySoundAtLocation(this, myTankPickUpItemSoundEffect, GetActorLocation());
}

/// <summary>
/// Called when this player presses the controller/keyboard button for firing the cannon.
/// It spawns and fires the projectile and also plays vibration/force feedback if using
/// a controller.
/// </summary>
void APlayerTankPawn::OnPlayerFireCannon()
{
	if (FireCannon(myCannonReloadTime) == true)
	{
		PlayForceFeedback(myFireCannonForceFeedbackEffect);
	}
}

/// <summary>
/// Shows/Hides the mouse cursor in the game. The mouse cursor can be used for interacting
/// with the boombox (if you are using mouse and keyboard).
/// </summary>
void APlayerTankPawn::OnTogglePlayerCursor()
{
	if (myIsCursorDisplayed == false)
	{
		//Show the cursor.
		myIsCursorDisplayed = true;
		myPlayerController->SetShowMouseCursor(true);

		//Set focus just for the UI so the player isn't controlling the tank at the same time.
		myPlayerController->SetInputMode(FInputModeGameAndUI());
	}
	else
	{
		//Hide the cursor.
		myIsCursorDisplayed = false;
		myPlayerController->SetShowMouseCursor(false);

		//Set focus back to the game so the player doesn't have to click to re-gain focus.
		myPlayerController->SetInputMode(FInputModeGameOnly());
	}
}

/// <summary>
/// Switches between the regular player camera, and the overhead
/// drone camera that shows the whole map.
/// </summary>
void APlayerTankPawn::OnTogglePlayerCamera()
{
	if (myLevelCamera != NULL)
	{
		if (myIsLevelCameraInUse == false)
		{
			myPlayerController->SetViewTarget(myLevelCamera);
			myIsLevelCameraInUse = true;
		}
		else
		{
			myPlayerController->SetViewTarget(this);
			myIsLevelCameraInUse = false;
		}
	}
}

/// <summary>
/// Shows/Hides the boombox in the player HUD.
/// </summary>
void APlayerTankPawn::OnToggleBoombox()
{
	if (myBoombox != NULL)
	{
		//Minimize/Maximize the Boombox UI.
		myBoombox->ToggleBoomboxUI();

		//Turn On/Off the Boombox UI.
		myBoombox->ToggleOnOff();
		
		//Update the UI to reflect the changes.
		myBoombox->UpdateBoomboxUI();
	}
}

/// <summary>
/// Increases the volume on the boombox/radio.
/// </summary>
void APlayerTankPawn::OnIncreaseBoomboxVolume()
{
	if (myBoombox != NULL)
	{
		myBoombox->IncreaseVolume();

		//Update the UI to reflect the changes.
		myBoombox->UpdateBoomboxUI();
	}
}

/// <summary>
/// Decreases the volume on the boombox/radio.
/// </summary>
void APlayerTankPawn::OnDecreaseBoomboxVolume()
{
	if (myBoombox != NULL)
	{
		myBoombox->DecreaseVolume();

		//Update the UI to reflect the changes.
		myBoombox->UpdateBoomboxUI();
	}
}

/// <summary>
/// Changes the boombox/radio to the next station (and starts playing that station).
/// </summary>
void APlayerTankPawn::OnNextBoomboxStation()
{
	if (myBoombox != NULL)
	{
		myBoombox->TuneToNextStation();

		//Update the UI to reflect the changes.
		myBoombox->UpdateBoomboxUI();
	}
}

/// <summary>
/// Changes the boombox/radio to the previous station (and starts playing that station).
/// </summary>
void APlayerTankPawn::OnPreviousBoomboxStation()
{
	if (myBoombox != NULL)
	{
		myBoombox->TuneToPreviousStation();

		//Update the UI to reflect the changes.
		myBoombox->UpdateBoomboxUI();
	}
}

/// <summary>
/// Called when this player has successfully hit an enemy tank.
/// </summary>
/// <param name="theHitTankPawn">The tank that the player hit.</param>
void APlayerTankPawn::OnSuccessfullyHitTank(ATankPawn* theHitTankPawn)
{
	myPlayerProgression->IncrementNumberOfTanksDestroyed();
}

/// <summary>
/// Called when this tank has been hit with a projectile.
/// </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 APlayerTankPawn::OnTankHit(ATankPawn* theAttackingPawn, float damage)
{
	Super::OnTankHit(theAttackingPawn, damage);

	DamageTank(damage);
}

/// <summary>
/// Rotates the tank turret (and cannon) left and right based on the player input.
/// </summary>
/// <param name="rate">The turn rate to use for moving the turret (and cannon).</param>
void APlayerTankPawn::TurnTurret(float rate)
{
	float turnAmount = rate * myLookTurnRate * GetWorld()->GetDeltaSeconds();

	//Rotate the turret and prevent overflow by using modulus division. 
	myTurretY += turnAmount;
	myTurretY = FMath::Fmod(myTurretY, 360.0f);
}

/// <summary>
/// Moves the tank cannon up and down based on the player input.
/// </summary>
/// <param name="rate">The rate to use for moving the cannon.</param>
void APlayerTankPawn::PitchCannon(float rate)
{
	float pitchAmount = rate * myLookUpRate * GetWorld()->GetDeltaSeconds();
	float angularLimit = myCenterTurretCannonConstraintComponent->ConstraintInstance.GetAngularSwing2Limit();

	myCannonPitch -= pitchAmount;

	//Ensure user does not move cannon above/below constraint bounds (default bounds between -40 and 40).
	myCannonPitch = FMath::Clamp(myCannonPitch, -angularLimit, angularLimit);
}

/// <summary>
/// Creates a new save and saves the current game progress without having to go to the save menu.
/// </summary>
void APlayerTankPawn::QuickSaveGame()
{
	if (myGameInstance->SaveGame(myGameInstance->GetPlayerProgressionSaveGame(), false) == false)
	{
		UNeuralTanksUtils::WriteToLog("APlayerTankPawn::QuickSaveGame() Failed to save game.");
	}
}

/// <summary>
/// Returns the current amount of health (HP) the player tank has.
/// </summary>
/// <returns>The current amount of health (HP) the player tank has.</returns>
float APlayerTankPawn::GetCurrentHealth()
{
	return myCurrentHealthPoints;
}