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

/// <summary>
/// <b>NeuronLayer:</b> A Single layer in the <b>NeuralNetwork</b>. This class contains a 
/// collection of <b>Neuron</b>'s along with functions to add neurons, set inputs/weights, 
/// and do activation calculations for it's collection of Neurons.
/// </summary>
UCLASS(config = Game)
class UNeuronLayer : public UObject
{
	GENERATED_BODY()

protected:

	/// <summary>
	/// A multiplier used to adjust the curve of the sigmoid function. 
	/// </summary>
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General")
	int mySigmoidActivationResponse;

	/// <summary>
	/// The total number of inputs in this NeuronLayer (Number of inputs in a single Neuron * number of neurons in this layer). 
	/// </summary>
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General")
	int myTotalNumberOfInputs;

	/// <summary>
	/// The collection of Neurons in this layer.
	/// </summary>
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "General")
	TArray<UNeuron*> myLayer; 

public:
	/// <summary>
	/// Default constructor.
	/// </summary>
	UNeuronLayer();

	/// <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 Initialize(const int numberOfNeurons, const int numberOfInputsPerNeruon);

	/// <summary>
	/// Adds the provided Neuron to this layers Neuron collection.
	/// </summary>
	/// <param name="neuronToAdd">The neuron to add to the collection.</param>
	void AddNeuron(UNeuron* neuronToAdd);

	/// <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 AddNeuron(const TArray<float>& neuronInputValues, const TArray<float>& neuronInputWeights);

	/// <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> CalculateOutputs();

	/// <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 InitializeLayerWithRandomWeights(const int numberOfNeurons, const int numberOfInputsPerNeruon);

	/// <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 Sigmoid(float activation, float response);

	/// <summary>
	/// Returns the number of Neurons in this NeuronLayer.
	/// </summary>
	/// <returns>The number of Neurons in this NeuronLayer.</returns>
	int GetNumberOfNeurons() const;

	/// <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 GetTotalNumberOfNeuronInputs() const;

	/// <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* GetNeuronAtIndex(const int index);

	/// <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 GetValueAtIndex(const int neuronIndex, const int 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 GetWeightAtIndex(const int neuronIndex, const int 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 SetInputs(TArray<float>& 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 SetValueAtIndex(const int neuronIndex, const int inputIndex, const float 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="value">The value to set for the specific NeuronInput.</param>
	void SetWeightAtIndex(const int neuronIndex, const int inputIndex, const float weight);

	/// <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 SetWeights(int& weightIndex, const TArray<float>& weights);
};