/**
 * Copyright (c) 2012-2013, Daniele Codecasa <codecasa.job@gmail.com>,
 * Models and Algorithms for Data & Text Mining (MAD) laboratory of
 * Milano-Bicocca University, and all the CTBNCToolkit contributors
 * that will follow.
 * All rights reserved.
 *
 * @author Daniele Codecasa and all the CTBNCToolkit contributors that will follow.
 * @copyright 2012-2013 Daniele Codecasa, MAD laboratory, and all the CTBNCToolkit contributors that will follow
 */
package CTBNCToolkit.tests;

import java.util.*;


import CTBNCToolkit.*;

/**
 * @author Daniele Codecasa <danielecdcs@gmail.com>
 *
 * Class used to generate CTBNC models easily.
 */
public class CTBNClassifierFactory implements IModelFactory<CTBNClassifier, Double, CTDiscreteNode> {
	
	private int nGeneratedModels = 0;
	private String name;
	private int[] nStates;
	private double[][] lambdaRanges;
	private double trajectoriesLength;
	private boolean[][] adjMatrix;
	private boolean[][] naiveBayesAdjMatrix;
	private NodeIndexing nodeIndexing;
	
	/**
	 * Constructor for a naive bayes.
	 * 
	 * @param modelsName name used for the model generated by the factory
	 * @param nStates number of state for each nodes (the first index is the class node)
	 * @param lambdaRanges matrix 2xN that indicate the lower and the upper lambda value for each node (the first index is the class node and it is ignored)
	 * @throws IllegalArgumentException in case of illegal arguments
	 */
	public CTBNClassifierFactory(String modelsName, int[] nStates, double[][] lambdaRanges) throws IllegalArgumentException {
		
		this.setParameters(modelsName, nStates, lambdaRanges);
		
		// Set the Naive Bayes structure
		this.adjMatrix = new boolean[nStates.length][nStates.length];
		for(int i = 0; i < this.adjMatrix.length; ++i)
			for(int j = 0; j < this.adjMatrix[0].length; ++j)
				this.adjMatrix[i][j] = false;
		for(int j = 1; j < this.adjMatrix[0].length; ++j)
			this.adjMatrix[0][j] = true;
	}
	
	/**
	 * Constructor for a generic structure.
	 * 
	 * @param modelsName name used for the model generated by the factory
	 * @param nStates number of state for each nodes (the first index is the class node)
	 * @param lambdaRanges matrix 2xN that indicate the lower and the upper lambda value for each node (the first index is the class node and it is ignored)
	 * @param adjMatrix adjacency matrix that define the model structure showing childhood relations (the first index is the class node)
	 * @throws IllegalArgumentException in case of illegal arguments
	 */
	public CTBNClassifierFactory(String modelsName, int[] nStates, double[][] lambdaRanges, boolean[][] adjMatrix) throws IllegalArgumentException {
		
		if(nStates.length != adjMatrix.length || nStates.length != adjMatrix[0].length)
			throw new IllegalArgumentException("Error: adjacency matrix dimensions must correspond with the nState length");
		for(int iNode = 0; iNode < adjMatrix.length; ++iNode)
			if( adjMatrix[iNode][0])
			throw new IllegalArgumentException("Error: class node can not have parents (index zero corresponde to the class node)");
		
		this.setParameters(modelsName, nStates, lambdaRanges);
		
		// Set the Naive Bayes structure
		this.adjMatrix = adjMatrix;
	}
	
	
	/**
	 * Set the main parameters of the factory.
	 * 
	 * @param modelsName name used for the model generated by the factory
	 * @param nStates number of state for each nodes (the first index is the class node)
	 * @param lambdaRanges matrix 2xN that indicate the lower and the upper lambda value for each node (the first index is the class node and it is ignored)
	 * @throws IllegalArgumentException in case of illegal arguments
	 */
	private void setParameters(String modelsName, int[] nStates, double[][] lambdaRanges) throws IllegalArgumentException {

		if( modelsName == null || modelsName.equals(""))
			throw new IllegalArgumentException("Error: invalid models name");
		if( lambdaRanges.length != 2)
			throw new IllegalArgumentException("Error: the lambdaRanges matrix must be a matrix 2xN where N is the number of nodes");
		if( nStates.length != lambdaRanges[0].length)
			throw new IllegalArgumentException("Error: the lenght of the nStates vector and the second dimension of the lambdaRanges matrix must correspond and be equal to the number of no class nodes");
		double minAvgLambda = Double.MAX_VALUE;
		int maxNStates = 0;
		//double avgLambdaSum = 0.0; //?OLD
		for( int iNode = 0; iNode < nStates.length; ++iNode) {
			if( nStates[iNode] < 2)
				throw new IllegalArgumentException("Error (node " + iNode + "): the number of states must be at least 2 for each node");
			
			if( iNode != 0) {
				if( lambdaRanges[0][iNode] > lambdaRanges[1][iNode])
					throw new IllegalArgumentException("Error (node " + iNode + "): the minimum lambda range must be lower then the maximum lambda range");
				if( lambdaRanges[0][iNode] < 0 || lambdaRanges[1][iNode] < 0)
					throw new IllegalArgumentException("Error (node " + iNode + "): lambda ranges must be equal or bigger then one");
				
				double avgLambda = (lambdaRanges[0][iNode] + lambdaRanges[1][iNode]) / 2.0;
				if( avgLambda < minAvgLambda)
					minAvgLambda = avgLambda;
				if( nStates[iNode] > maxNStates)
					maxNStates = nStates[iNode];
				//avgLambdaSum += ((lambdaRanges[0][iNode] + lambdaRanges[1][iNode]) / 2.0) * (nStates[iNode] - 1); //?OLD
			}
		}
			
		this.name = modelsName;
		this.nStates = nStates;
		this.lambdaRanges = lambdaRanges;
		// Suggested trajectory length which make sense: 10 times
		// the expected transition time supposing the lowest average
		// lambda multiplied by the biggest number of state.
		this.trajectoriesLength = 10*maxNStates*(1.0 / minAvgLambda);
		//this.trajectoriesLength = 40*(nStates.length - 1) / avgLambdaSum; //?OLD
		
		// Set the index to name mapping
		String names[] = new String[this.nStates.length];
		names[0] = "Class";
		for(int iNode = 1; iNode < this.nStates.length; ++iNode)
			if( iNode < 10 && this.nStates.length > 9)
				names[iNode] = "N0" + iNode;
			else
				names[iNode] = "N" + iNode;
		this.nodeIndexing = NodeIndexing.getNodeIndexing(this.name, names, names[0], null);
		
		// Set the adj matrix that represent a naive bayes
		this.naiveBayesAdjMatrix = new boolean[this.nodeIndexing.getNodesNumber()][this.nodeIndexing.getNodesNumber()];
		for(int i = 0; i < this.nodeIndexing.getNodesNumber(); ++i)
			for(int j = 0; j < this.nodeIndexing.getNodesNumber(); ++j)
				this.naiveBayesAdjMatrix[i][j] = (i == this.nodeIndexing.getClassIndex() && j != this.nodeIndexing.getClassIndex());
		
	}

	
	/* (non-Javadoc)
	 * @see test.IModelFactory#newInstance()
	 */
	@Override
	public CTBNClassifier newInstance() {
	
		++nGeneratedModels;
		return newInstance(name + "-" + nGeneratedModels);		
	}
	
	
	/* (non-Javadoc)
	 * @see test.IModelFactory#newInstance()
	 */
	@Override
	public CTBNClassifier newInstance(String name) {
	
		CTDiscreteNode node;
		
		// Node generation (note that the indexing follow the nStates order)
		Set<CTDiscreteNode> nodesSet = new TreeSet<CTDiscreteNode>();
		nodesSet.add(new CTDiscreteNode(this.nodeIndexing.getClassName(), generateStates(this.nStates[0]), true));			// class
		for( int iNode = 1; iNode < this.nStates.length; ++iNode) {
			node = new CTDiscreteNode(this.nodeIndexing.getName(iNode), generateStates(this.nStates[iNode]), false);		// dynamic nodes
			nodesSet.add(node);
		}

		// Model generation
		CTBNClassifier model = new CTBNClassifier(this.nodeIndexing, name, nodesSet);
		model.setStructure(this.adjMatrix);
		
		// CIM generation
		// class
		node = model.getClassNode();
		for(int sE = 0; sE < node.getStatesNumber(); ++sE)
			node.setCIMValue(0, 0, sE, 1.0/node.getStatesNumber());
		if( node.checkCIMs() != -1)
			throw new RuntimeException("Error in CPT generation for the class node");
		// dynamic nodes
		for( int iNode = 1; iNode < this.nodeIndexing.getNodesNumber(); ++iNode) {
			node = model.getNode(iNode);
			
			// CIM generation
			for(int pE = 0; pE < node.getNumberParentsEntries(); ++pE) {
				
				double[][] cim = new double[node.getStatesNumber()][node.getStatesNumber()];
				for(int i = 0; i < cim.length; ++i) {
					cim[i][i] = 0.0;
					
					for(int j = 0; j < cim.length; ++j) {
						if( i == j) 
							continue;
						
						cim[i][j] = Math.random()*(this.lambdaRanges[1][iNode] - this.lambdaRanges[0][iNode]) + this.lambdaRanges[0][iNode];
						cim[i][i] -= cim[i][j];
					}
				}
						
				node.setCIM(pE, cim);
				
				int errPE = node.checkCIMs();
				if( errPE != -1)
					throw new RuntimeException("Error in CIM generation for parent entry " + errPE);
			}
		}
		
		return model;		
	}


	/* (non-Javadoc)
	 * @see test.ICTBNCFactory#setTrajectoryLength(double)
	 */
	@Override
	public void setTrajectoryLength(double trjLength) throws IllegalArgumentException {

		if( trjLength < 0)
			throw new IllegalArgumentException("Error: the suggested trajectory length must be positive");
		
		this.trajectoriesLength = trjLength;
	}
	

	/* (non-Javadoc)
	 * @see test.ICTBNCFactory#getTrajectoryLength()
	 */
	@Override
	public double getTrajectoryLength() {

		return this.trajectoriesLength;
	}
	
	
	/**
	 * Generate a set of N states called
	 * s1, ..., sN.
	 * 
	 * @param N number of states to generate
	 * @return set of states
	 */
	static private Set<String> generateStates(int N) {
		
		Set<String> states = new TreeSet<String>();
		for(int iState = 1; iState <= N; ++iState)
			states.add("s" + iState);
		
		return states;
			
	}

	/* (non-Javadoc)
	 * @see test.IModelFactory#getModelsName()
	 */
	@Override
	public String getModelsName() {
		
		return this.name;
	}

	/**
	 * Get the adjacency matrix of a Naive Bayes.
	 * 
	 * @return adjacency matrix of a Naive Bayes
	 */
	public boolean[][] getNaiveBayesAdjMatrix() {
		
		return this.naiveBayesAdjMatrix;
	}
	
	
	/**
	 * Give the mapping between node name and
	 * the set of states generate a Naive Bayes
	 * model.
	 * 
	 * @param nodeIndexing global node indexing to use
	 * @param statesForNode mapping between nodes names and theirs states
	 * @return a naive bayes model
	 * @throws IllegalArgumentException in case of illegal arguments
	 */
	public static CTBNClassifier getNaiveBayesFromDataset(NodeIndexing nodeIndexing, Map<String,Set<String>> statesForNode) throws IllegalArgumentException {
		
		if( nodeIndexing == null)
			throw new IllegalArgumentException("Null node indexing value");
		if( statesForNode == null)
			throw new IllegalArgumentException("Node name - node states mapping null value");
		
		// Generate the model
		Set<CTDiscreteNode> nodesSet = new TreeSet<CTDiscreteNode>();
		
		CTDiscreteNode classNode = new CTDiscreteNode(nodeIndexing.getClassName(), statesForNode.get(nodeIndexing.getClassName()), true); // Class Node
		nodesSet.add(classNode);

		Iterator<String> keys = statesForNode.keySet().iterator();
		while(keys.hasNext()) {
			String nodeName = keys.next();
			if( nodeName.equals(nodeIndexing.getClassName()))
				continue;
			
			CTDiscreteNode node = new CTDiscreteNode(nodeName, statesForNode.get(nodeName), false);
			node.addParent(classNode);
			nodesSet.add(node);
		}
		
		CTBNClassifier bnModel = new CTBNClassifier(nodeIndexing, "NaiveBayes", nodesSet);
		
		return bnModel;
	}

}
