package com.aelitis.azureus.core.neuronal;

public class NeuralNetworkLayer {
	
	int numberOfNodes;
	double[][] weights;
	double[][] weightChanges;
	double[] neuronValues;
	double[] desiredValues;
	double[] errors;
	double[] biasWeights;
	double[] biasValues;
	
	double learningRate;
	boolean linearOutput;
	boolean useMomentum;
	double momentumFactor;
	
	NeuralNetworkLayer parentLayer;
	NeuralNetworkLayer childLayer;

	ActivationFunction activationFunction;
	
	
	
	public NeuralNetworkLayer(int numberOfNodes) {
		
		this.numberOfNodes = numberOfNodes;
		
		linearOutput = false;
		useMomentum = false;
		momentumFactor = 0.9;
	}
	
	public void initialize(NeuralNetworkLayer parentLayer,NeuralNetworkLayer childLayer) {
		
		neuronValues = new double[numberOfNodes];
		desiredValues = new double[numberOfNodes];
		errors = new double[numberOfNodes];
		this.parentLayer = parentLayer;
		
		if(childLayer != null) {
			this.childLayer = childLayer;
			weights = new double[numberOfNodes][childLayer.getNumberOfNodes()];
			weightChanges = new double[numberOfNodes][childLayer.getNumberOfNodes()];
			biasValues = new double[childLayer.getNumberOfNodes()];
			biasWeights = new double[childLayer.getNumberOfNodes()];
			
			for(int j = 0 ; j < childLayer.getNumberOfNodes() ; j++) {
				biasValues[j] = -1;
				biasWeights[j] = 0;
			}
		}
	}
	
	public void randomizeWeights() {
		for(int i = 0 ; i < numberOfNodes ; i++) {
			for(int j = 0 ; j < childLayer.getNumberOfNodes() ; j++) {
				weights[i][j] = Math.random() * 2 - 1;
			}
		}
		
		for(int j = 0 ; j < childLayer.getNumberOfNodes() ; j++) {
			biasWeights[j] = Math.random() * 2 - 1;
		}
	}
	
	public void calculateNeuronValues() {
		if(parentLayer != null) {
			for(int j = 0 ; j < numberOfNodes ; j++) {
				double x = 0;
				
				for(int i = 0 ; i < parentLayer.getNumberOfNodes() ; i++) {
					x += parentLayer.neuronValues[i] * parentLayer.weights[i][j]; 
				}
				
				x+= parentLayer.biasValues[j] * parentLayer.biasWeights[j];
				
				if(childLayer == null && linearOutput) {
					neuronValues[j] = x;
				} else {
					neuronValues[j] = activationFunction.getValueFor(x);
				}
			}
		}
	}
	
	public void calculateErrors() {
		
		if(childLayer == null) {
			// output layer
			for(int i = 0 ; i < numberOfNodes ; i++) {
				errors[i] = (desiredValues[i] - neuronValues[i]) * activationFunction.getDerivedFunctionValueFor(neuronValues[i]);
			}
		} else if(parentLayer == null) {
			//input layer
			for(int i = 0 ; i < numberOfNodes ; i++) {
				errors[i] = 0.0;
			}
		} else {
			// hidden layer
			for(int i = 0 ; i < numberOfNodes ; i++) {
				double sum = 0;
				for(int j = 0 ; j < childLayer.getNumberOfNodes() ; j++) {
					sum += childLayer.errors[j] * weights[i][j];
				}
				errors[i] = sum * activationFunction.getDerivedFunctionValueFor(neuronValues[i]);
			}
		}
	
	}
	
	public void adjustWeights() {
		if(childLayer != null) {
			for(int i = 0 ; i < numberOfNodes ; i++) {
				for(int j = 0 ; j < childLayer.getNumberOfNodes() ; j++) {
					double dw = learningRate * childLayer.errors[j] * neuronValues[i];
					if(useMomentum) {
						weights[i][j] += dw + momentumFactor * weightChanges[i][j];
						weightChanges[i][j] = dw;
					} else {
						weights[i][j] += dw;
					}
				}
			}
			
			for(int j = 0 ; j < childLayer.getNumberOfNodes() ; j++) {
				biasWeights[j] += learningRate * childLayer.errors[j] * biasValues[j];
			}
		}
	}
	

	public int getNumberOfNodes() {
		return numberOfNodes;
	}

	public void setActivationFunction(ActivationFunction activationFunction) {
		this.activationFunction = activationFunction;
	}
	
	public void setMomentum(boolean useMomentum,double factor) {
		this.useMomentum = useMomentum;
		this.momentumFactor = factor;
	}
	
	public void setLearningRate(double rate) {
		this.learningRate = rate;
	}
	
	public String toString() {
		StringBuffer sb = new StringBuffer();
		if(childLayer != null) {
			for(int j = 0 ; j < childLayer.getNumberOfNodes() ; j++) {
				sb.append(j);
				sb.append("\t> ");
				for(int i = 0 ; i < numberOfNodes ; i++) {
					sb.append(i);
					sb.append(":");
					sb.append(weights[i][j]);
					sb.append("\t");
				}
				sb.append("\n");
			}
		}
		
		return sb.toString();
	}

}
