package net.woodyfolsom.msproj.ann2;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;

import net.woodyfolsom.msproj.ann2.math.ActivationFunction;
import net.woodyfolsom.msproj.ann2.math.Linear;
import net.woodyfolsom.msproj.ann2.math.Sigmoid;

public abstract class FeedforwardNetwork {
	private ActivationFunction activationFunction;
	private boolean biased;
	private List<Connection> connections;
	private List<Neuron> neurons;
	private String name;

	private transient int biasNeuronId;
	private transient Map<Integer, List<Connection>> connectionsFrom;
	private transient Map<Integer, List<Connection>> connectionsTo;
	
	public FeedforwardNetwork() {
		this(false);
	}
	
	public FeedforwardNetwork(boolean biased) {
		//No-arg constructor for JAXB
		this.activationFunction = Sigmoid.function;
		this.connections = new ArrayList<Connection>();
		this.connectionsFrom = new HashMap<Integer,List<Connection>>();
		this.connectionsTo = new HashMap<Integer,List<Connection>>();
		this.neurons = new ArrayList<Neuron>();
		this.name = "UNDEFINED";
		this.biasNeuronId = -1;
		setBiased(biased);
	}

	public void addConnection(Connection connection) {	
		connections.add(connection);
		
		int src = connection.getSrc();
		int dest = connection.getDest();
		
		if (!connectionsFrom.containsKey(src)) {
			connectionsFrom.put(src, new ArrayList<Connection>());
		}
		
		if (!connectionsTo.containsKey(dest)) {
			connectionsTo.put(dest, new ArrayList<Connection>());
		}
		
		connectionsFrom.get(src).add(connection);
		connectionsTo.get(dest).add(connection);
	}
	
	public NNData compute(NNDataPair nnDataPair) {
		NNData actual = new NNData(nnDataPair.getIdeal().getFields(),
				compute(nnDataPair.getInput().getValues()));
		return actual;
	}

	public double[] compute(double[] input) {
		zeroInputs();
		setInput(input);
		feedforward();
		return getOutput();
	}

	void createBiasConnection(int neuronId, double weight) {
		if (!biased) {
			throw new UnsupportedOperationException("Not a biased network");
		}
		addConnection(new Connection(biasNeuronId, neuronId, weight));
	}
	
	/**
	 * Adds a new neuron with a unique id to this FeedforwardNetwork.
	 * @return
	 */
	Neuron createNeuron(boolean input) {
		Neuron neuron;
		if (input) {
			neuron = new Neuron(Linear.function, neurons.size());
		} else {
			neuron = new Neuron(activationFunction, neurons.size());
		}
		neurons.add(neuron);
		return neuron;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		FeedforwardNetwork other = (FeedforwardNetwork) obj;
		if (activationFunction == null) {
			if (other.activationFunction != null)
				return false;
		} else if (!activationFunction.equals(other.activationFunction))
			return false;
		if (connections == null) {
			if (other.connections != null)
				return false;
		} else if (!connections.equals(other.connections))
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (neurons == null) {
			if (other.neurons != null)
				return false;
		} else if (!neurons.equals(other.neurons))
			return false;
		return true;
	}
	
	protected void feedforward() {
		for (int i = 0; i < neurons.size(); i++) {
			Neuron src = neurons.get(i);
			for (Connection connection : getConnectionsFrom(src.getId())) {
				Neuron dest = getNeuron(connection.getDest());
				dest.addInput(src.getOutput() * connection.getWeight());
			}
		}
	}

	@XmlElement(type = Sigmoid.class)
	public ActivationFunction getActivationFunction() {
		return activationFunction;
	}
	
	protected abstract double[] getOutput();
	protected abstract Neuron[] getOutputNeurons();
	
	@XmlAttribute
	public String getName() {
		return name;
	}

	protected Neuron getNeuron(int id) {
		return neurons.get(id);
	}

	@XmlElement
	protected Connection[] getConnections() {
		return connections.toArray(new Connection[connections.size()]);
	}

	protected Connection[] getConnectionsFrom(int neuronId) {
		List<Connection> connList = connectionsFrom.get(neuronId);
		
		if (connList == null) {
			return new Connection[0];
		} else {
			return connList.toArray(new Connection[connList.size()]);
		}
	}
	
	protected Connection[] getConnectionsTo(int neuronId) {
		List<Connection> connList = connectionsTo.get(neuronId);
		
		if (connList == null) {
			return new Connection[0];
		} else {
			return connList.toArray(new Connection[connList.size()]);
		}
	}
	
	@XmlAttribute
	public boolean isBiased() {
		return biased;
	}

	@XmlElement
	protected Neuron[] getNeurons() {
		return neurons.toArray(new Neuron[neurons.size()]);
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime
				* result
				+ ((activationFunction == null) ? 0 : activationFunction
						.hashCode());
		result = prime * result
				+ ((connections == null) ? 0 : connections.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + ((neurons == null) ? 0 : neurons.hashCode());
		return result;
	}
	
	public void initWeights() {
		for (Connection connection : connections) {
			connection.setWeight(1.0-Math.random()*2.0);
		}
	}

	public abstract boolean load(InputStream is);

	public abstract boolean save(OutputStream os);

	public void setActivationFunction(ActivationFunction activationFunction) {
		this.activationFunction = activationFunction;
	}
	
	public void setBiased(boolean biased) {
		
		if (this.biased == biased) {
			return;
		}
		
		this.biased = biased;
		
		if (biased) {
			Neuron biasNeuron = createNeuron(true);
			biasNeuron.setInput(1.0);
			biasNeuronId = biasNeuron.getId();
		} else {
			//This is an inefficient but concise way to remove all connections involving the bias Neuron
			//from the global 
			
			//Remove all connections from biasId from this index
			List<Connection> connectionsFromBias = connectionsFrom.remove(biasNeuronId);
			
			//Remove all connections to all nodes from biasId from this index
			for (Map.Entry<Integer,List<Connection>> mapEntry : connectionsTo.entrySet()) {
				mapEntry.getValue().removeAll(connectionsFromBias);
			}
			
			//Finally, remove from the (serialized) list of non-indexed connections
			connections.remove(connectionsFromBias);
			
			biasNeuronId = -1;
		}
	}
	
	protected void setConnections(Connection[] connections) {
		this.connections.clear();
		this.connectionsFrom.clear();
		this.connectionsTo.clear();
		for (Connection connection : connections) {
			addConnection(connection);
		}
	}
	
	protected abstract void setInput(double[] input);

	public void setName(String name) {
		this.name = name;
	}

	protected void setNeurons(Neuron[] neurons) {
		this.neurons.clear();
		for (Neuron neuron : neurons) {
			this.neurons.add(neuron);
		}
	}
	
	public void setWeights(double[] weights) {
		if (weights.length != connections.size()) {
			throw new IllegalArgumentException("# of weights must == # of connections");
		}
		
		for (int i = 0; i < connections.size(); i++) {
			connections.get(i).setWeight(weights[i]);
		}
	}
	
	protected void zeroInputs() {
		for (Neuron neuron : neurons) {
			if (neuron.getId() != biasNeuronId){
				neuron.setInput(0.0);
			}
		}
	}

}