
package fr.perceptron;

import java.io.Serializable;
import java.util.Random;

/**
 * The class Layer is one layer of the perceptron 
 */

public class Layer implements Serializable {
    
    public int numInputNeurons;
    public int numNeurons;

    public double[] weights;
    public double[] biases;
    public double[] rawLayerData;

    private transient Random random;

    public Layer(int numInputNeurons, int numNeurons, Random random) {

        if (numInputNeurons < 1 || numNeurons < 1) {
            throw new IllegalArgumentException("Invalid number of neurons");
        }
        
        this.random = random;

        this.weights = new double[numInputNeurons * numNeurons];
        this.biases = new double[numNeurons]; 
        this.rawLayerData = new double[numNeurons];
        this.numInputNeurons = numInputNeurons;
        this.numNeurons = numNeurons;

        this.initRandomWeights();
        // this.initRandomBiases();
    }

    public Layer(int numInputNeurons, int numNeurons) {
        this(numInputNeurons, numNeurons, new Random());
    }

    public void initRandomWeights() {

        for (int i = 0; i < this.weights.length; ++i) {
            this.weights[i] = random.nextDouble() * 2 - 1.0; // [-1.0; 1.0[ range
        }
    }

    public void initRandomBiases() {
        for (int i = 0; i < this.biases.length; ++i) {
            this.biases[i] = random.nextDouble() * 2 - 1.0; // [-1.0; 1.0[ range
        }
    }

    public double[] forward(double[] inputs) {
        double[] outputs = new double[this.numNeurons];

        for (int numOutput = 0; numOutput < this.numNeurons; ++numOutput) {

            outputs[numOutput] = this.biases[numOutput];
            for (int numInput = 0; numInput < this.numInputNeurons; ++numInput) {
                outputs[numOutput] += this.getWeight(numInput, numOutput) * inputs[numInput];
            }
            this.rawLayerData[numOutput] = outputs[numOutput];
        }

        return activationFunction(outputs);
    }

    public static double activationFunction(double x) {
        // Sigmoid function
        return 1.0d / (1.0d + Math.exp(-x));
    }

    public static double activationDerivativeFunction(double x){
        double temp = activationFunction(x);
        return temp * (1.0d - temp);
    }

    public static double[] activationFunction(double[] inputs) {
        double[] outputs = new double[inputs.length];
        for (int i = 0; i < inputs.length; ++i) {
            outputs[i] = activationFunction(inputs[i]);
        }
        return outputs;
    }

    public double getWeight(int neuronIn, int neuronOut) {
        return this.weights[neuronOut * this.numInputNeurons + neuronIn];
    }
    public void setWeight(int neuronIn, int neuronOut, double v) {
        this.weights[neuronOut * this.numInputNeurons + neuronIn] = v;
    }

    @Override
    public String toString() {
        return String.format("Layer[%d, %d]", this.numInputNeurons, this.numNeurons);
    }
}
