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

/// <summary>
/// Default constructor.
/// </summary>
UNeuronLayer::UNeuronLayer()
{
	myTotalNumberOfInputs = 0;
	mySigmoidActivationResponse = 1;
}

/// <summary>
/// Initializes this Neuron Layer with the provided number of neurons (and inputs per nueron).
/// </summary>
/// <param name="numberOfNeurons">The number of neurons to generate for this NeuronLayer.</param>
/// <param name="numberOfInputsPerNeruon">The number of inputs per neuron for each NeuronLayer.</param>
void UNeuronLayer::Initialize(const int numberOfNeurons, const int numberOfInputsPerNeruon)
{
	myTotalNumberOfInputs = 0;
	mySigmoidActivationResponse = 1;
	InitializeLayerWithRandomWeights(numberOfNeurons, numberOfInputsPerNeruon);
}

/// <summary>
/// Adds the provided Neuron to this layers Neuron collection.
/// </summary>
/// <param name="neuronToAdd">The neuron to add to the collection.</param>
void UNeuronLayer::AddNeuron(UNeuron* neuronToAdd)
{
	myLayer.Add(neuronToAdd);
	myTotalNumberOfInputs += neuronToAdd->GetNumberOfInputs();
}

/// <summary>
/// Adds the provided Neurons to the Neuron collection.
/// </summary>
/// <param name="neuronInputValues">The input values to add to the collection.</param>
/// <param name="neuronInputWeights">The corresponding weight values to add to the collection.</param>
void UNeuronLayer::AddNeuron(const TArray<float>& neuronInputValues, const TArray<float>& neuronInputWeights)
{
	int numberOfInputValues = neuronInputValues.Num();
	int numberOfInputWeights = neuronInputWeights.Num();

	if (numberOfInputValues > 0)
	{
		if (numberOfInputValues == numberOfInputWeights)
		{
			UNeuron* neuronToAdd = NewObject<UNeuron>();

			for (int index = 0U; index < numberOfInputValues; index++)
			{
				neuronToAdd->AddInput(neuronInputValues[index], neuronInputWeights[index]);
			}

			AddNeuron(neuronToAdd);
		}
		else
		{
			throw new std::exception("NeuronLayer::AddNeuron(const TArray<float>, const TArray<float>) failed; number of input values does not match number of corresponding weights.");
		}
	}
}

/// <summary>
/// Calculates the activation values for each Neuron in the collection and returns
/// those calculated values as a collection of doubles. 
/// </summary>
/// <returns>The calculated activation values for each Neuron in the collection.</returns>
TArray<float> UNeuronLayer::CalculateOutputs()
{
	float currNeuronOutput;
	TArray<float> outputs;

	for (int index = 0U; index < myLayer.Num(); index++)
	{
		//Use the sigmoid function as the step function to calculate the output.
		UNeuron* currNeuron = myLayer[index];  
		currNeuronOutput = Sigmoid(currNeuron->CalculateActivation(), mySigmoidActivationResponse);
		outputs.Add(currNeuronOutput);
	}

	return outputs;
}

/// <summary>
/// Creates a specified number of Neurons (and corresponding number of inputs) for this layer
/// (Number of inputs in a single Neuron * number of neurons in this layer).
/// </summary>
/// <param name="numberOfNeurons">The number of Neurons to generate/add to this layer.</param>
/// <param name="numberOfInputsPerNeruon">The number of inputs for each Neurons in the collection.</param>
void UNeuronLayer::InitializeLayerWithRandomWeights(const int numberOfNeurons, const int numberOfInputsPerNeruon)
{
	for (int count = 1U; count <= numberOfNeurons; count++)
	{
		UNeuron* neuronToAdd = NewObject<UNeuron>();
		neuronToAdd->Initialize(numberOfInputsPerNeruon);

		AddNeuron(neuronToAdd);
	}
}

/// <summary>
/// Determines if a Neuron's activation value will trigger the Neuron to be on/off. 
/// If the activation value is less than 1 the sigmoid function returns zero. 
/// </summary>
/// <param name="netinput">The activation value of the neuron.</param>
/// <param name="response">Used for adjusting the Sigmoid function, typically 1.</param>
/// <returns>Close to 1 or close to zero depending on the activation value (netinput).</returns>
float UNeuronLayer::Sigmoid(float netinput, float response)
{
	return (1 / (1 + exp(-netinput / response)));
}

/// <summary>
/// Returns the number of Neurons in this NeuronLayer.
/// </summary>
/// <returns>The number of Neurons in this NeuronLayer.</returns>
int UNeuronLayer::GetNumberOfNeurons() const
{
	return myLayer.Num();
}

/// <summary>
/// Returns the total number of neuron inputs in each Neuron in this NeuronLayer.
/// </summary>
/// <returns>The total number of neuron inputs in each Neuron in this NeuronLayer.</returns>
int UNeuronLayer::GetTotalNumberOfNeuronInputs() const
{
	return myTotalNumberOfInputs;
}

/// <summary>
/// Returns the Neuron at the specified index in this NeuronLayer.
/// </summary>
/// <param name="index">The index of the Neuron in this layer.</param>
/// <returns>The Neuron reference at the specified index.</returns>
UNeuron* UNeuronLayer::GetNeuronAtIndex(const int index)
{
	if (index < GetNumberOfNeurons())
	{
		return myLayer[index];
	}
	else
	{
		throw new std::exception("NeuronLayer::GetNeuronAtIndex() failed to get neuron, index out of bounds.");
	}

	return NULL;
}

/// <summary>
/// Returns the Neuron value at the specific index in the neuron 
/// and the index in the NeuronLayer's collection.
/// </summary>
/// <param name="neuronIndex">The index of the Neuron in this NeuronLayer.</param>
/// <param name="inputIndex">The specific index of the value in the Neuron.</param>
/// <returns>The value of a specific NeuronInput in a Neuron at the specified location in the collection.</returns>
float UNeuronLayer::GetValueAtIndex(const int neuronIndex, const int inputIndex)
{
	UNeuron* theNeuron = GetNeuronAtIndex(neuronIndex);
	return theNeuron->GetInputValueAtIndex(inputIndex);
}

/// <summary>
/// Returns the Neuron weight at the specific index in the neuron 
/// and the index in the NeuronLayer's collection.
/// </summary>
/// <param name="neuronIndex">The index of the Neuron in this NeuronLayer.</param>
/// <param name="inputIndex">The specific index of the weight in the Neuron.</param>
/// <returns>The weight of a specific NeuronInput in a Neuron at the specified location in the collection.</returns>
float UNeuronLayer::GetWeightAtIndex(const int neuronIndex, const int inputIndex)
{
	UNeuron* theNeuron = GetNeuronAtIndex(neuronIndex);
	return theNeuron->GetInputWeightAtIndex(inputIndex);
}

/// <summary>
/// Sets the input values of the Neurons in this NeuronLayer to the provided values.
/// </summary>
/// <param name="inputs">The new input values to use.</param>
void UNeuronLayer::SetInputs(TArray<float>& inputs)
{
	for (int neuronIndex = 0U; neuronIndex < myLayer.Num(); neuronIndex++)
	{
		myLayer[neuronIndex]->SetInputValues(inputs);
	}
}

/// <summary>
/// Sets the input weights of the Neurons in this NeuronLayer to the provided values.
/// The weights are assumed to be all in one collection and then added here, thats why
/// the weightIndex is needed.
/// </summary>
/// <param name="weightIndex">The current index in the provided weight collection.</param>
/// <param name="weights">The new weight values to use.</param>
void UNeuronLayer::SetWeights(int& weightIndex, const TArray<float>& weights)
{
	for (int neuronIndex = 0U; neuronIndex < GetNumberOfNeurons(); neuronIndex++)
	{
		UNeuron* currNeuron = myLayer[neuronIndex];

		for (int inputIndex = 0; inputIndex < currNeuron->GetNumberOfInputs(); inputIndex++)
		{
			currNeuron->SetInputWeightAtIndex(inputIndex, weights[weightIndex++]);
		}
	}
}

/// <summary>
/// Sets a single Neuron value for a particular Neuron in this NeuronLayer.
/// </summary>
/// <param name="neuronIndex">The index of the Neuron in this NeuronLayer.</param>
/// <param name="inputIndex">The index of the specific NeuronInput in the Neuron</param>
/// <param name="value">The value to set for the specific NeuronInput.</param>
void UNeuronLayer::SetValueAtIndex(const int neuronIndex, const int inputIndex, const float value)
{
	UNeuron* theNeuron = GetNeuronAtIndex(neuronIndex);
	theNeuron->SetInputValueAtIndex(inputIndex, value);
}

/// <summary>
/// Sets a single Neuron value for a particular Neuron in this NeuronLayer.
/// </summary>
/// <param name="neuronIndex">The index of the Neuron in this NeuronLayer.</param>
/// <param name="inputIndex">The index of the specific NeuronInput in the Neuron</param>
/// <param name="weight">The value to set for the specific NeuronInput.</param>
void UNeuronLayer::SetWeightAtIndex(const int neuronIndex, const int inputIndex, const float weight)
{
	UNeuron* theNeuron = GetNeuronAtIndex(neuronIndex);
	theNeuron->SetInputWeightAtIndex(inputIndex, weight);
}