package weka.clusterers;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import weka.clusterers.forMetisMQI.GraphAlgorithms;
import weka.clusterers.forMetisMQI.graph.Node;
import weka.clusterers.forMetisMQI.graph.UndirectedGraph;
import weka.clusterers.forMetisMQI.util.Configuration;
import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.Utils;
import weka.core.Capabilities.Capability;

public class MetisMQIClusterer extends AbstractClusterer implements
		OptionHandler {

	/**
	 * It maps each cluster with an integer id.
	 */
	private Map<Set<Node>, Integer> clustersMap = null;

	/**
	 * Holds the cluster membership for each node.
	 */
	private Map<Node, Integer> nodeMap = null;
	

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	@Override
	public void buildClusterer(Instances data) throws Exception {
		getCapabilities().testWithFail(data);
		UndirectedGraph g = new UndirectedGraph();
		g.loadFromInstance(data);
		Set<Set<Node>> clusters = GraphAlgorithms.metisMqi(g);
		setNumClusters(clusters.size());
		int i = 0;
		Iterator<Set<Node>> clusterIterator = clusters.iterator();
		clustersMap = new HashMap<Set<Node>, Integer>();
		nodeMap = new HashMap<Node, Integer>();
		while (clusterIterator.hasNext()) {
			Set<Node> cluster = clusterIterator.next();
			clustersMap.put(cluster, i);
			Iterator<Node> nodeIterator = cluster.iterator();
			while (nodeIterator.hasNext()) {
				Node n = nodeIterator.next();
				if (nodeMap.get(n) == null) {
					nodeMap.put(n, i);
				}
			}
			i++;
		}
	}

	@Override
	public int clusterInstance(Instance instance) throws Exception {
		Attribute from = instance.dataset().attribute("from");
		Attribute to = instance.dataset().attribute("to");
		Instance edge = instance;
		Node node1 = new Node(Integer.toString(((int) Math.round(edge
				.value(from)))));
		Node node2 = new Node(Integer.toString(((int) Math
				.round(edge.value(to)))));
		if (nodeMap.get(node1) == nodeMap.get(node2))
			return nodeMap.get(node1);
		else
			throw new Exception();
	}

	/**
	 * Parses a given list of options.
	 * <p/>
	 * 
	 * <!-- options-start --> Valid options are:
	 * <p/>
	 * 
	 * <pre>
	 * -N &lt;num&gt;
	 *  number of clusters.
	 *  (default 2).
	 * </pre>
	 * 
	 * <pre>
	 * -S
	 *  Maximum size of the finer graph during the coarsening phase.
	 * </pre>
	 * 
	 * <!-- options-end -->
	 * 
	 * @param options
	 *            the list of options as an array of strings
	 * @throws Exception
	 *             if an option is not supported
	 */
	@Override
	public void setOptions(String[] options) throws Exception {
		String optionString = Utils.getOption('N', options);
		if (optionString.length() != 0) {
			setNumClusters(Integer.parseInt(optionString));
		}
		optionString = Utils.getOption('S', options);
		if (optionString.length() != 0) {
			setSizeFinerGraph(Integer.parseInt(optionString));
		}
		optionString = Utils.getOption('R', options);
		setRandomBisection(Boolean.parseBoolean(optionString));
		optionString = Utils.getOption('V', options);
		if (optionString.length() != 0) {
			setVerboseLevel(Integer.parseInt(optionString));
		}
		optionString = Utils.getOption('o', options);
		if (optionString.length() != 0) {
			setOutputFile(optionString);
		}
	}

	private void setOutputFile(String outputFile) {
		Configuration.instance().setOutputFile(outputFile);
	}
	
	private String getOutputFile() {
		return Configuration.instance().getOutputFile();
	}

	private void setVerboseLevel(int verboseLevel) {
		Configuration.instance().setVerboseLevel(verboseLevel);
	}

	private void setRandomBisection(boolean b) {
		Configuration.instance().setRandomBisection(b);
	}

	/**
	 * Gets the current settings of MetisMQIClusterer
	 * 
	 * @return an array of strings suitable for passing to setOptions()
	 */
	@SuppressWarnings("unchecked")
	@Override
	public String[] getOptions() {
		Vector result;
		result = new Vector();
		result.add("-N");
		result.add("" + getNumClusters());
		result.add("-S");
		result.add("" + getSizeFinerGraph());
		result.add("-R");
		result.add("" + getRandomBisection());
		result.add("-V");
		result.add("" + getVerboseLevel());
		result.add("-o");
		result.add("" + getOutputFile());
		return (String[]) result.toArray(new String[result.size()]);
	}

	private boolean getRandomBisection() {
		return Configuration.instance().isRandomBisection();
	}

	private int getVerboseLevel() {
		return Configuration.instance().getVerboseLevel();
	}

	private int getSizeFinerGraph() {
		return Configuration.instance().getSizeFinerGraph();
	}

	private int getNumClusters() {
		return Configuration.instance().getNumberOfClusters();
	}

	/**
	 * Returns an enumeration describing the available options.
	 * 
	 * @return an enumeration of all the available options.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Enumeration listOptions() {
		Vector result = new Vector();
		result.addElement(new Option("\tnumber of clusters.\n"
				+ "\t(default 2).", "N", 1, "-N <num>"));
		result.addElement(new Option("\tsize of finer graph.\n"
				+ "\t(default 10).", "S", 1, "-S <num>"));
		result.addElement(new Option("\trandom bisection.\n"
				+ "\t(default false).", "R", 1, "-V <boolean>"));
		result.addElement(new Option("\tverbosity of graphical output.\n"
				+ "\t(default 1).", "V", 1, "-V <num>"));
		result.addElement(new Option("\tpath of the output file.\n"
				, "o", 1, "-o <path>"));
		return result.elements();
	}

	private void setSizeFinerGraph(int size) {
		Configuration.instance().setSizeFinerGraph(size);
	}

	private void setNumClusters(int n) {
		Configuration.instance().setNumberOfClusters(n);
	}

	@Override
	public double[] distributionForInstance(Instance instance) throws Exception {
		double[] d = new double[numberOfClusters()];
		d[clusterInstance(instance)] = 1.0;
		return d;
	}

	@Override
	public Capabilities getCapabilities() {
		Capabilities result = super.getCapabilities();
		result.enable(Capability.NUMERIC_ATTRIBUTES);
		result.enable(Capability.NO_CLASS);
		return result;
	}

	@Override
	public int numberOfClusters() throws Exception {
		return Configuration.instance().getNumberOfClusters();
	}

	/**
	 * Main method for executing this clusterer.
	 * 
	 * @param args
	 *            the options, use "-h" to display options
	 */
	public static void main(String[] args) {
		AbstractClusterer.runClusterer(new MetisMQIClusterer(), args);
	}

}
