/*
 * Encog(tm) Core v3.0 - Java Version
 * http://www.heatonresearch.com/encog/
 * http://code.google.com/p/encog-java/
 
 * Copyright 2008-2011 Heaton Research, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *   
 * For more information on Heaton Research copyrights, licenses 
 * and trademarks visit:
 * http://www.heatonresearch.com/copyright
 */
package org.encog.neural.networks;

import org.encog.Encog;
import org.encog.engine.network.activation.ActivationFunction;
import org.encog.mathutil.randomize.NguyenWidrowRandomizer;
import org.encog.mathutil.randomize.RangeRandomizer;
import org.encog.ml.BasicML;
import org.encog.ml.MLClassification;
import org.encog.ml.MLContext;
import org.encog.ml.MLEncodable;
import org.encog.ml.MLError;
import org.encog.ml.MLRegression;
import org.encog.ml.MLResettable;
import org.encog.ml.data.MLData;
import org.encog.ml.data.MLDataSet;
import org.encog.ml.data.basic.BasicMLData;
import org.encog.neural.NeuralNetworkError;
import org.encog.neural.flat.FlatNetwork;
import org.encog.neural.networks.layers.Layer;
import org.encog.neural.networks.structure.NetworkCODEC;
import org.encog.neural.networks.structure.NeuralStructure;
import org.encog.util.EngineArray;
import org.encog.util.csv.CSVFormat;
import org.encog.util.csv.NumberList;
import org.encog.util.obj.ObjectCloner;
import org.encog.util.simple.EncogUtility;

/**
 * This class implements a neural network. This class works in conjunction the
 * Layer classes. Layers are added to the BasicNetwork to specify the structure
 * of the neural network.
 * 
 * The first layer added is the input layer, the final layer added is the output
 * layer. Any layers added between these two layers are the hidden layers.
 * 
 * The network structure is stored in the structure member. It is important to
 * call:
 * 
 * network.getStructure().finalizeStructure();
 * 
 * Once the neural network has been completely constructed.
 * 
 */
public class BasicNetwork extends BasicML implements ContainsFlat, MLContext,
		MLRegression, MLEncodable, MLResettable, MLClassification, MLError {

	/**
	 * Tag used for the connection limit.
	 */
	public static final String TAG_LIMIT = "CONNECTION_LIMIT";

	/**
	 * The default connection limit.
	 */
	public static final double DEFAULT_CONNECTION_LIMIT = 0.0000000001;

	/**
	 * Serial id for this class.
	 */
	private static final long serialVersionUID = -136440631687066461L;

	/**
	 * The property for connection limit.
	 */
	public static final String TAG_CONNECTION_LIMIT = "connectionLimit";
	
	/**
	 * The property for begin training.
	 */
	public static final String TAG_BEGIN_TRAINING = "beginTraining";
	
	/**
	 * The property for context target offset.
	 */
	public static final String TAG_CONTEXT_TARGET_OFFSET = "contextTargetOffset";
	
	/**
	 * The property for context target size.
	 */
	public static final String TAG_CONTEXT_TARGET_SIZE = "contextTargetSize";
	
	/**
	 * The property for end training.
	 */
	public static final String TAG_END_TRAINING = "endTraining";
	
	/**
	 * The property for has context.
	 */
	public static final String TAG_HAS_CONTEXT = "hasContext";
	
	/**
	 * The property for layer counts.
	 */
	public static final String TAG_LAYER_COUNTS = "layerCounts";
	
	/**
	 * The property for layer feed counts.
	 */
	public static final String TAG_LAYER_FEED_COUNTS = "layerFeedCounts";
	
	/**
	 * The property for layer index.
	 */
	public static final String TAG_LAYER_INDEX = "layerIndex";
	
	/**
	 * The property for weight index.
	 */
	public static final String TAG_WEIGHT_INDEX = "weightIndex";
	
	/**
	 * The property for bias activation.
	 */
	public static final String TAG_BIAS_ACTIVATION = "biasActivation";

	/**
	 * The property for layer context count.
	 */
	public static final String TAG_LAYER_CONTEXT_COUNT = "layerContextCount";

	/**
	 * Holds the structure of the network. This keeps the network from having to
	 * constantly lookup layers and synapses.
	 */
	private final NeuralStructure structure;

	/**
	 * Construct an empty neural network.
	 */
	public BasicNetwork() {
		this.structure = new NeuralStructure(this);
	}

	/**
	 * Add a layer to the neural network. If there are no layers added this
	 * layer will become the input layer. This function automatically updates
	 * both the input and output layer references.
	 * 
	 * @param layer
	 *            The layer to be added to the network.
	 */
	public final void addLayer(final Layer layer) {
		layer.setNetwork(this);
		this.structure.getLayers().add(layer);
	}

	/**
	 * Add to a weight.
	 * @param fromLayer The from layer.
	 * @param fromNeuron The from neuron.
	 * @param toNeuron The to neuron.
	 * @param value The value to add.
	 */
	public final void addWeight(final int fromLayer, 
			final int fromNeuron,
			final int toNeuron, final double value) {
		final double old = getWeight(fromLayer, fromNeuron, toNeuron);
		setWeight(fromLayer, fromNeuron, toNeuron, old + value);
	}

	/**
	 * Calculate the error for this neural network.
	 * 
	 * @param data
	 *            The training set.
	 * @return The error percentage.
	 */
	@Override
	public final double calculateError(final MLDataSet data) {
		return EncogUtility.calculateRegressionError(this, data);
	}

	/**
	 * Calculate the total number of neurons in the network across all layers.
	 * 
	 * @return The neuron count.
	 */
	public final int calculateNeuronCount() {
		int result = 0;
		for (final Layer layer : this.structure.getLayers()) {
			result += layer.getNeuronCount();
		}
		return result;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final int classify(final MLData input) {
		return winner(input);
	}

	/**
	 * Clear any data from any context layers.
	 */
	@Override
	public final void clearContext() {

		if (this.structure.getFlat() != null) {
			this.structure.getFlat().clearContext();
		}
	}

	/**
	 * Return a clone of this neural network. Including structure, weights and
	 * bias values. This is a deep copy.
	 * 
	 * @return A cloned copy of the neural network.
	 */
	@Override
	public final Object clone() {
		final BasicNetwork result = (BasicNetwork) ObjectCloner.deepCopy(this);
		return result;
	}

	/**
	 * Compute the output for this network.
	 * 
	 * @param input
	 *            The input.
	 * @param output
	 *            The output.
	 */
	public final void compute(final double[] input, final double[] output) {
		final BasicMLData input2 = new BasicMLData(input);
		final MLData output2 = this.compute(input2);
		EngineArray.arrayCopy(output2.getData(), output);
	}

	/**
	 * Compute the output for a given input to the neural network.
	 * 
	 * @param input
	 *            The input to the neural network.
	 * @return The output from the neural network.
	 */
	@Override
	public final MLData compute(final MLData input) {
		try {
			final MLData result = new BasicMLData(this.structure.getFlat()
					.getOutputCount());
			this.structure.getFlat().compute(input.getData(), result.getData());
			return result;
		} catch (final ArrayIndexOutOfBoundsException ex) {
			throw new NeuralNetworkError(
					"Index exception: there was likely a mismatch between layer sizes, or the size of the input presented to the network.",
					ex);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final void decodeFromArray(final double[] encoded) {
		this.structure.requireFlat();
		final double[] weights = this.structure.getFlat().getWeights();
		if (weights.length != encoded.length) {
			throw new NeuralNetworkError(
					"Size mismatch, encoded array should be of length "
							+ weights.length);
		}

		EngineArray.arrayCopy(encoded, weights);
	}

	/**
	 * @return The weights as a comma separated list.
	 */
	public final String dumpWeights() {

		final StringBuilder result = new StringBuilder();
		NumberList.toList(CSVFormat.EG_FORMAT, result, this.structure.getFlat()
				.getWeights());
		return result.toString();
	}

	/**
	 * Enable, or disable, a connection.
	 * 
	 * @param fromLayer
	 *            The layer that contains the from neuron.
	 * @param fromNeuron
	 *            The source neuron.
	 * @param toNeuron
	 *            The target connection.
	 * @param enable
	 *            True to enable, false to disable.
	 */
	public final void enableConnection(final int fromLayer, 
			final int fromNeuron,
			final int toNeuron, final boolean enable) {

		final double value = getWeight(fromLayer, fromNeuron, toNeuron);

		if (enable) {
			if (!this.structure.isConnectionLimited()) {
				return;
			}

			if (Math.abs(value) < this.structure.getConnectionLimit()) {
				setWeight(fromLayer, fromNeuron, toNeuron,
						RangeRandomizer.randomize(-1, 1));
			}
		} else {
			if (!this.structure.isConnectionLimited()) {
				this.setProperty(BasicNetwork.TAG_LIMIT,
						BasicNetwork.DEFAULT_CONNECTION_LIMIT);
				this.structure.updateProperties();

			}
			setWeight(fromLayer, fromNeuron, toNeuron, 0);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final int encodedArrayLength() {
		this.structure.requireFlat();
		return this.structure.getFlat().getEncodeLength();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final void encodeToArray(final double[] encoded) {
		this.structure.requireFlat();
		final double[] weights = this.structure.getFlat().getWeights();
		if (weights.length != encoded.length) {
			throw new NeuralNetworkError(
					"Size mismatch, encoded array should be of length "
							+ weights.length);
		}

		EngineArray.arrayCopy(weights, encoded);
	}

	/**
	 * Compare the two neural networks. For them to be equal they must be of the
	 * same structure, and have the same matrix values.
	 * 
	 * @param other
	 *            The other neural network.
	 * @return True if the two networks are equal.
	 */
	public final boolean equals(final BasicNetwork other) {
		return equals(other, Encog.DEFAULT_PRECISION);
	}

	/**
	 * Determine if this neural network is equal to another. Equal neural
	 * networks have the same weight matrix and bias values, within a specified
	 * precision.
	 * 
	 * @param other
	 *            The other neural network.
	 * @param precision
	 *            The number of decimal places to compare to.
	 * @return True if the two neural networks are equal.
	 */
	public final boolean equals(final BasicNetwork other, final int precision) {
		return NetworkCODEC.equals(this, other, precision);
	}

	/**
	 * Get the activation function for the specified layer.
	 * @param layer The layer.
	 * @return The activation function.
	 */
	public final ActivationFunction getActivation(final int layer) {
		this.structure.requireFlat();
		final int layerNumber = getLayerCount() - layer - 1;
		return this.structure.getFlat().getActivationFunctions()[layerNumber];
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final FlatNetwork getFlat() {
		return getStructure().getFlat();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final int getInputCount() {
		this.structure.requireFlat();
		return getStructure().getFlat().getInputCount();
	}

	/**
	 * Get the bias activation for the specified layer.
	 * @param l The layer.
	 * @return The bias activation.
	 */
	public final double getLayerBiasActivation(final int l) {
		if (!isLayerBiased(l)) {
			throw new NeuralNetworkError(
					"Error, the specified layer does not have a bias: " + l);
		}

		this.structure.requireFlat();
		final int layerNumber = getLayerCount() - l - 1;

		final int layerOutputIndex 
			= this.structure.getFlat().getLayerIndex()[layerNumber];
		final int count 
			= this.structure.getFlat().getLayerCounts()[layerNumber];
		return this.structure.getFlat().getLayerOutput()[layerOutputIndex
				+ count - 1];
	}

	/**
	 * @return The layer count.
	 */
	public final int getLayerCount() {
		this.structure.requireFlat();
		return this.structure.getFlat().getLayerCounts().length;
	}

	/**
	 * Get the neuron count.
	 * @param l The layer.
	 * @return The neuron count.
	 */
	public final int getLayerNeuronCount(final int l) {
		this.structure.requireFlat();
		final int layerNumber = getLayerCount() - l - 1;
		return this.structure.getFlat().getLayerFeedCounts()[layerNumber];
	}

	/**
	 * Get the layer output for the specified neuron.
	 * @param layer The layer.
	 * @param neuronNumber The neuron number.
	 * @return The output from the last call to compute.
	 */
	public final double getLayerOutput(final int layer, 
				final int neuronNumber) {
		this.structure.requireFlat();
		final int layerNumber = getLayerCount() - layer - 1;
		final int index = this.structure.getFlat().getLayerIndex()[layerNumber]
				+ neuronNumber;
		final double[] output = this.structure.getFlat().getLayerOutput();
		if (index >= output.length) {
			throw new NeuralNetworkError("The layer index: " + index
					+ " specifies an output index larger than the network has.");
		}
		return output[index];
	}

	/**
	 * Get the total (including bias and context) neuron cont for a layer.
	 * @param l The layer.
	 * @return The count.
	 */
	public final int getLayerTotalNeuronCount(final int l) {
		this.structure.requireFlat();
		final int layerNumber = getLayerCount() - l - 1;
		return this.structure.getFlat().getLayerCounts()[layerNumber];
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final int getOutputCount() {
		this.structure.requireFlat();
		return getStructure().getFlat().getOutputCount();
	}

	/**
	 * @return Get the structure of the neural network. The structure allows you
	 *         to quickly obtain synapses and layers without traversing the
	 *         network.
	 */
	public final NeuralStructure getStructure() {
		return this.structure;
	}

	/**
	 * Get the weight between the two layers.
	 * @param fromLayer The from layer.
	 * @param fromNeuron The from neuron.
	 * @param toNeuron The to neuron.
	 * @return The weight value.
	 */
	public final double getWeight(final int fromLayer, 
			final int fromNeuron,
			final int toNeuron) {
		this.structure.requireFlat();
		validateNeuron(fromLayer, fromNeuron);
		validateNeuron(fromLayer + 1, toNeuron);
		final int fromLayerNumber = getLayerCount() - fromLayer - 1;
		final int toLayerNumber = fromLayerNumber - 1;

		if (toLayerNumber < 0) {
			throw new NeuralNetworkError(
					"The specified layer is not connected to another layer: "
							+ fromLayer);
		}

		final int weightBaseIndex 
			= this.structure.getFlat().getWeightIndex()[toLayerNumber];
		final int count 
			= this.structure.getFlat().getLayerCounts()[fromLayerNumber];
		final int weightIndex = weightBaseIndex + fromNeuron
				+ (toNeuron * count);

		return this.structure.getFlat().getWeights()[weightIndex];
	}

	/**
	 * Generate a hash code.
	 * 
	 * @return THe hash code.
	 */
	@Override
	public final int hashCode() {
		return super.hashCode();
	}

	/**
	 * Determine if the specified connection is enabled.
	 * 
	 * @param layer
	 *            The layer to check.
	 * @param fromNeuron
	 *            The source neuron.
	 * @param toNeuron
	 *            THe target neuron.
	 * @return True, if the connection is enabled, false otherwise.
	 */
	public final boolean isConnected(final int layer, final int fromNeuron,
			final int toNeuron) {
		/*
		 * if (!this.structure.isConnectionLimited()) { return true; } final
		 * double value = synapse.getMatrix().get(fromNeuron, toNeuron);
		 * 
		 * return (Math.abs(value) > this.structure.getConnectionLimit());
		 */
		return false;
	}

	/**
	 * Determine if the specified layer is biased.
	 * @param l The layer number.
	 * @return True, if the layer is biased.
	 */
	public final boolean isLayerBiased(final int l) {
		this.structure.requireFlat();
		final int layerNumber = getLayerCount() - l - 1;
		return this.structure.getFlat().getLayerCounts()[layerNumber] 
		                                                 != this.structure
				.getFlat().getLayerFeedCounts()[layerNumber];
	}

	/**
	 * Reset the weight matrix and the bias values. This will use a
	 * Nguyen-Widrow randomizer with a range between -1 and 1. If the network
	 * does not have an input, output or hidden layers, then Nguyen-Widrow
	 * cannot be used and a simple range randomize between -1 and 1 will be
	 * used.
	 * 
	 */
	@Override
	public final void reset() {

		if (getLayerCount() < 3) {
			(new RangeRandomizer(-1, 1)).randomize(this);
		} else {
			(new NguyenWidrowRandomizer(-1, 1)).randomize(this);
		}
	}

	/**
	 * Reset the weight matrix and the bias values. This will use a
	 * Nguyen-Widrow randomizer with a range between -1 and 1. If the network
	 * does not have an input, output or hidden layers, then Nguyen-Widrow
	 * cannot be used and a simple range randomize between -1 and 1 will be
	 * used.
	 * 
	 * Use the specified seed.
	 * 
	 */
	@Override
	public final void reset(final int seed) {
		reset();
	}

	/**
	 * Sets the bias activation for every layer that supports bias. Make sure
	 * that the network structure has been finalized before calling this method.
	 * 
	 * @param activation
	 *            THe new activation.
	 */
	public final void setBiasActivation(final double activation) {
		// first, see what mode we are on. If the network has not been
		// finalized, set the layers
		if (this.structure.getFlat() == null) {
			for (final Layer layer : this.structure.getLayers()) {
				if (layer.hasBias()) {
					layer.setBiasActivation(activation);
				}
			}
		} else {
			for (int i = 0; i < getLayerCount(); i++) {
				if (isLayerBiased(i)) {
					setLayerBiasActivation(i, activation);
				}
			}
		}
	}

	/**
	 * Set the bias activation for the specified layer.
	 * @param l The layer to use.
	 * @param value The bias activation.
	 */
	public final void setLayerBiasActivation(final int l, 
				final double value) {
		if (!isLayerBiased(l)) {
			throw new NeuralNetworkError(
					"Error, the specified layer does not have a bias: " + l);
		}

		this.structure.requireFlat();
		final int layerNumber = getLayerCount() - l - 1;

		final int layerOutputIndex 
			= this.structure.getFlat().getLayerIndex()[layerNumber];
		final int count 
			= this.structure.getFlat().getLayerCounts()[layerNumber];
		this.structure.getFlat().getLayerOutput()[layerOutputIndex + count - 1] 
		                                          = value;
	}

	/**
	 * Set the weight between the two specified neurons.
	 * @param fromLayer The from layer.
	 * @param fromNeuron The from neuron.
	 * @param toNeuron The to neuron.
	 * @param value The to value.
	 */
	public final void setWeight(final int fromLayer, final int fromNeuron,
			final int toNeuron, final double value) {
		this.structure.requireFlat();
		final int fromLayerNumber = getLayerCount() - fromLayer - 1;
		final int toLayerNumber = fromLayerNumber - 1;

		if (toLayerNumber < 0) {
			throw new NeuralNetworkError(
					"The specified layer is not connected to another layer: "
							+ fromLayer);
		}

		final int weightBaseIndex 
			= this.structure.getFlat().getWeightIndex()[toLayerNumber];
		final int count 
			= this.structure.getFlat().getLayerCounts()[fromLayerNumber];
		final int weightIndex = weightBaseIndex + fromNeuron
				+ (toNeuron * count);

		this.structure.getFlat().getWeights()[weightIndex] = value;
	}


	/**
	 * {@inheritDoc}
	 */
	@Override
	public final String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append("[BasicNetwork: Layers=");
		final int layers = this.structure.getLayers().size();
		builder.append(layers);
		builder.append("]");
		return builder.toString();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final void updateProperties() {
		this.structure.updateProperties();

	}

	/**
	 * Validate the the specified targetLayer and neuron are valid.
	 * @param targetLayer The target layer.
	 * @param neuron The target neuron.
	 */
	public final void validateNeuron(final int targetLayer, final int neuron) {
		if ((targetLayer < 0) || (targetLayer >= getLayerCount())) {
			throw new NeuralNetworkError("Invalid layer count: " + targetLayer);
		}

		if ((neuron < 0) || (neuron >= getLayerTotalNeuronCount(targetLayer))) {
			throw new NeuralNetworkError("Invalid neuron number: " + neuron);
		}
	}

	/**
	 * Determine the winner for the specified input. This is the number of the
	 * winning neuron.
	 * 
	 * @param input
	 *            The input patter to present to the neural network.
	 * @return The winning neuron.
	 */
	public final int winner(final MLData input) {
		final MLData output = compute(input);
		return EngineArray.maxIndex(output.getData());
	}
}
