/*
* 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 "Neuron.h"

/// <summary>
/// Default Constructor.
/// </summary>
UNeuron::UNeuron()
{
	myActivatonThesholdBias = -1;
}

/// <summary>
/// Adds a specificed number of random weights to the neuron.
/// </summary>
/// <param name="numberOfInputs">The number of values (default 0) and weights (random float) to add to this Neuron.</param>
void UNeuron::Initialize(const int numberOfInputs)
{
	myActivatonThesholdBias = -1;

	//Initialize the neuron with random weights.
	for (int count = 1U; count <= numberOfInputs + 1; count++)
	{
		AddInput(0.0, FMath::FRandRange(-1.0f, 1.0f));
	}
}

/// <summary>
/// Updates the inputs and weights to the provided ones.
/// </summary>
/// <param name="inputValues">The values to add to this Neuron.</param>
/// <param name="inputWeights">The corresponding weights to add to this Neuron.</param>
void UNeuron::Initialize(const TArray<float>& inputValues, const TArray<float>& inputWeights)
{
	myActivatonThesholdBias = -1;

	int numberOfInputValues = inputValues.Num();
	int numberOfWeightValues = inputWeights.Num();

	if (inputValues.Num() != 0)
	{
		if (numberOfInputValues == numberOfWeightValues)
		{
			for (int inputIndex = 0; inputIndex < numberOfInputValues; inputIndex++)
			{
				AddInput(inputValues[inputIndex], inputWeights[inputIndex]);
			}
		}
		else
		{
			throw new std::exception("Neuron::Neuron(const TArray<float>&, const TArray<float>&) Failed to initialize; the number of inputValues does not match the number of weightValues.");
		}
	}
	else
	{
		throw new std::exception("Neuron::Neuron(const TArray<float>&, const TArray<float>&) Failed to initialize; inputValues and/or weightValues are empty.");
	}
}

/// <summary>
/// Adds the provided NeuronInput to the collection of neuron inputs in this Neuron.
/// </summary>
/// <param name="neuronInput">The NeuronInput to add.</param>
void UNeuron::AddInput(const FNeuronInput& neuronInput)
{
	myNeuronInputs.Add(neuronInput);
}

/// <summary>
/// Adds the provided value and corresponding weight to this Neuron.
/// </summary>
/// <param name="neuronInputValue">The input value to add to this Neuron.</param>
/// <param name="neuronInputWeight">The corresponding weight to add to this Neuron.</param>
void UNeuron::AddInput(const float neuronInputValue, const float neuronInputWeight)
{
	AddInput(FNeuronInput(neuronInputWeight, neuronInputWeight));
}

/// <summary>
/// Calculates the total Activation value of this Neuron by going though
/// each NeuronInput, multiply its value and weight, accumulate those products,
/// and then add the threshold to it. Depending on the if this activation value is
/// above a particular threshold, the value will 0 or 1 (ignore or use).
/// </summary>
/// <returns>The activation value of this neuron based on the collection of NeuronInputs.</returns>
float UNeuron::CalculateActivation()
{
	const int numInputs = myNeuronInputs.Num();
	float activation = 0.0;

	if (numInputs > 0)
	{
		for (int index = 0U; index < numInputs - 1; index++)
		{
			activation += myNeuronInputs[index].CalculateActivation();
		}

		//Incorporate the threshold/bias.
		activation += myActivatonThesholdBias * myNeuronInputs[numInputs - 1].Weight;
	}

	return activation;
}

/// <summary>
/// Returns the total number of NeuronInputs in this Neuron's collection.
/// </summary>
/// <returns>The total number of NeuronInputs in this Neuron.</returns>
int UNeuron::GetNumberOfInputs() const
{
	return myNeuronInputs.Num();
}

/// <summary>
/// Returns the input value at the provided index.
/// </summary>
/// <param name="index">The index of the NeuronInput's value in the collection.</param>
/// <returns>The value portion of the specified NeuronInput based on the index.</returns>
float UNeuron::GetInputValueAtIndex(const int index) const
{
	if (index < GetNumberOfInputs())
	{
		return myNeuronInputs[index].Value;
	}
	else
	{
		throw new std::exception("Neuron::GetInputValueAtIndex() failed to get neuron input, index out of bounds");
	}

	return 0.0;
}

/// <summary>
/// Returns the input weight at the provided index.
/// </summary>
/// <param name="index">The index of the NeuronInput's weight in the collection.</param>
/// <returns>The weight portion of the specified NeuronInput based on the index.</returns>
float UNeuron::GetInputWeightAtIndex(const int index) const
{
	if (index < GetNumberOfInputs())
	{
		return myNeuronInputs[index].Weight;
	}
	else
	{
		throw new std::exception("Neuron::GetInputWeightAtIndex() failed to get neuron input, index out of bounds.");
	}

	return 0.0;
}

/// <summary>
/// Assigns the provided input values to this Neurons collection of NeuronInputs.
/// </summary>
/// <param name="inputValues">The input values to use for this Neuron.</param>
void UNeuron::SetInputValues(const TArray<float>& inputValues)
{
	for (int inputValueIndex = 0U; inputValueIndex < inputValues.Num(); inputValueIndex++)
	{
		myNeuronInputs[inputValueIndex].Value = inputValues[inputValueIndex];
	}
}

/// <summary>
/// Assigns the provided weight values to this Neurons collection of NeuronInputs.
/// </summary>
/// <param name="inputValues">The weight values to use for this Neuron.</param>
void UNeuron::SetInputWeights(const TArray<float>& inputWeights)
{
	for (int inputWeightIndex = 0U; inputWeightIndex < inputWeights.Num(); inputWeightIndex++)
	{
		myNeuronInputs[inputWeightIndex].Weight = inputWeights[inputWeightIndex];
	}
}

/// <summary>
/// Assigns a single value in the NeuronInput collection based on the provided index.
/// </summary>
/// <param name="index">The index in the NeuronInput collection.</param>
/// <param name="neuronInputValue">The value to assign to the provided neuron input.</param>
void UNeuron::SetInputValueAtIndex(const int index, const float neuronInputValue)
{
	if (index < GetNumberOfInputs())
	{
		myNeuronInputs[index].Value = neuronInputValue;
	}
	else
	{
		throw new std::exception("Neuron::SetInputValueAtIndex() failed to get neuron input, index out of bounds.");
	}
}

/// <summary>
/// Assigns a single weight in the NeuronInput collection based on the provided index.
/// </summary>
/// <param name="index">The index in the NeuronInput collection.</param>
/// <param name="neuronInputWeight">The value to assign to the provided neuron input.</param>
void UNeuron::SetInputWeightAtIndex(const int index, const float neuronInputWeight)
{
	if (index < GetNumberOfInputs())
	{
		myNeuronInputs[index].Weight = neuronInputWeight;
	}
	else
	{
		throw new std::exception("Neuron::SetInputWeightAtIndex() failed to get neuron input, index out of bounds.");
	}
}