/*
Copyright (C) 2011  Diego Darriba, David Posada

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package es.uvigo.darwin.jmodeltest.tree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.List;

import pal.tree.Tree;
import es.uvigo.darwin.jmodeltest.io.TextOutputStream;
import es.uvigo.darwin.jmodeltest.model.Model;
import es.uvigo.darwin.jmodeltest.utilities.Utilities;

public class TreeSummary {

	private int AIC_INDEX = 0;
	private int AICC_INDEX = 1;
	private int BIC_INDEX = 2;
	private int DT_INDEX = 3;
	// number of Information Criteria
	private int IC_COUNT = 4;

	private Tree bestTree[] = new Tree[IC_COUNT];
	private TreeDistancesCache rfDistances = TreeRFDistancesCache.getInstance();
	private TreeDistancesCache euclideanDistances = TreeEuclideanDistancesCache
			.getInstance();
	private Hashtable<Tree, SummaryRow> summary;
	// sorted topologies by criterion
	private List<Tree> topologiesAIC = null, topologiesAICc = null,
			topologiesBIC = null, topologiesDT = null, sortedTopologies = null;

	public TreeSummary(Tree bestAIC, Tree bestAICc, Tree bestBIC, Tree bestDT,
			Model[] models) {
		if (bestAIC != null) {
			this.bestTree[AIC_INDEX] = bestAIC;
			this.topologiesAIC = new ArrayList<Tree>();
		}
		if (bestAICc != null) {
			this.bestTree[AICC_INDEX] = bestAICc;
			this.topologiesAICc = new ArrayList<Tree>();
		}
		if (bestBIC != null) {
			this.bestTree[BIC_INDEX] = bestBIC;
			this.topologiesBIC = new ArrayList<Tree>();
		}
		if (bestDT != null) {
			this.bestTree[DT_INDEX] = bestDT;
			this.topologiesDT = new ArrayList<Tree>();
		}
		this.summary = new Hashtable<Tree, SummaryRow>();

		for (Model model : models) {
			boolean done = false;
			for (Tree tree : summary.keySet()) {
				if (sameTopology(tree, model.getTree())) {
					summary.get(tree).addModel(model);
					done = true;
					break;
				}
			}
			if (!done) {
				// new topology
				SummaryRow newRow = new SummaryRow(model.getTree());
				newRow.addModel(model);
				summary.put(model.getTree(), newRow);
				if (topologiesAIC != null)
					topologiesAIC.add(model.getTree());
				if (topologiesAICc != null)
					topologiesAICc.add(model.getTree());
				if (topologiesBIC != null)
					topologiesBIC.add(model.getTree());
				if (topologiesDT != null)
					topologiesDT.add(model.getTree());
			}
		}

		for (Tree tree : summary.keySet()) {
			summary.get(tree).computeEuclideanDistances();
		}
		// sort by criteria
		if (topologiesAIC != null)
			Collections.sort(topologiesAIC, new AicComparator());
		if (topologiesAICc != null)
			Collections.sort(topologiesAICc, new AiccComparator());
		if (topologiesBIC != null)
			Collections.sort(topologiesBIC, new BicComparator());
		if (topologiesDT != null)
			Collections.sort(topologiesDT, new DtComparator());
		
		sortedTopologies = topologiesAIC != null ? topologiesAIC
				: topologiesBIC != null ? topologiesBIC
						: topologiesAICc != null ? topologiesAICc
								: topologiesDT != null ? topologiesDT
									: new ArrayList<Tree>(summary.keySet());
	}

	public int getNumberOfTopologies() {
		return summary.size();
	}

	public Tree getTopology (int index) {
		return sortedTopologies.get(index);
	}
	
	public List<Model> getModelsByTopology (int index) {
		return summary.get(getTopology(index)).models;
	}
	
	public List<Model> getAICModels(int index) {
		Tree key = topologiesAIC.get(index);
		return summary.get(key).models;
	}

	public List<Model> getBICModels(int index) {
		Tree key = topologiesBIC.get(index);
		return summary.get(key).models;
	}

	public List<Model> getAICcModels(int index) {
		Tree key = topologiesAICc.get(index);
		return summary.get(key).models;
	}

	public List<Model> getDTModels(int index) {
		Tree key = topologiesDT.get(index);
		return summary.get(key).models;
	}

	public int aicIndexOf(Tree tree) {
		return topologiesAIC.indexOf(tree);
	}

	public long aicRfOf(Tree tree) {
		return summary.get(tree).rfDistance[AIC_INDEX];
	}
	
	public double aicAvgDistance(Tree tree) {
		return summary.get(tree).avgEuclideanDistance[AIC_INDEX];
	}
	
	public double aicVarDistance(Tree tree) {
		return summary.get(tree).varEuclideanDistance[AIC_INDEX];
	}
	
	public double aiccWeight(Tree tree) {
		return summary.get(tree).support[AICC_INDEX];
	}
	
	public int aiccIndexOf(Tree tree) {
		return topologiesAICc.indexOf(tree);
	}

	public long aiccRfOf(Tree tree) {
		return summary.get(tree).rfDistance[AICC_INDEX];
	}
	
	public double aiccAvgDistance(Tree tree) {
		return summary.get(tree).avgEuclideanDistance[AICC_INDEX];
	}
	
	public double aiccVarDistance(Tree tree) {
		return summary.get(tree).varEuclideanDistance[AICC_INDEX];
	}
	
	public double aicWeight(Tree tree) {
		return summary.get(tree).support[AIC_INDEX];
	}
	
	public int bicIndexOf(Tree tree) {
		return topologiesBIC.indexOf(tree);
	}

	public long bicRfOf(Tree tree) {
		return summary.get(tree).rfDistance[BIC_INDEX];
	}
	
	public double bicAvgDistance(Tree tree) {
		return summary.get(tree).avgEuclideanDistance[BIC_INDEX];
	}
	
	public double bicVarDistance(Tree tree) {
		return summary.get(tree).varEuclideanDistance[BIC_INDEX];
	}
	
	public double bicWeight(Tree tree) {
		return summary.get(tree).support[BIC_INDEX];
	}
	
	public int dtIndexOf(Tree tree) {
		return topologiesDT.indexOf(tree);
	}

	public long dtRfOf(Tree tree) {
		return summary.get(tree).rfDistance[DT_INDEX];
	}
	
	public double dtAvgDistance(Tree tree) {
		return summary.get(tree).avgEuclideanDistance[DT_INDEX];
	}
	
	public double dtVarDistance(Tree tree) {
		return summary.get(tree).varEuclideanDistance[DT_INDEX];
	}
	
	public double dtWeight(Tree tree) {
		return summary.get(tree).support[DT_INDEX];
	}
	
	private boolean sameTopology(Tree t1, Tree t2) {
		return (rfDistances.getDistance(t1, t2) == 0);
	}

	private long rfDistance(Tree t1, Tree t2) {
		if (t1 == null || t2 == null) {
			return -1;
		} else {
			return Math.round(rfDistances.getDistance(t1, t2));
		}
	}

	class SummaryRow {
		List<Model> models;
		Tree commonTopology;
		long rfDistance[] = new long[] { 0l, 0l, 0l, 0l };
		double avgEuclideanDistance[] = new double[] { 0.0d, 0.0d, 0.0d, 0.0d };
		double varEuclideanDistance[] = new double[] { 0.0d, 0.0d, 0.0d, 0.0d };
		double support[] = new double[] { 0.0d, 0.0d, 0.0d, 0.0d };

		SummaryRow(Tree commonTopology) {
			this.commonTopology = commonTopology;
			for (int i = 0; i < IC_COUNT; i++) {
				this.rfDistance[i] = rfDistance(commonTopology, bestTree[i]);
			}
			this.models = new ArrayList<Model>();
		}

		void addModel(Model model) {
			// check same topology
			if (checkTopology(model)) {
				models.add(model);
				support[AIC_INDEX] += model.getAICw();
				support[AICC_INDEX] += model.getAICcw();
				support[BIC_INDEX] += model.getBICw();
				support[DT_INDEX] += model.getDTw();
			}
		}

		void computeEuclideanDistances() {
			// set variables to zero
			for (int i = 0; i < IC_COUNT; i++) {
				if (bestTree[i] != null) {
					avgEuclideanDistance[i] = 0.0d;
					varEuclideanDistance[i] = 0.0d;
					for (Model model : models) {
						double distance = euclideanDistances.getDistance(
								model.getTree(), bestTree[i]);
						avgEuclideanDistance[i] += distance;
						varEuclideanDistance[i] += distance * distance;
					}
					avgEuclideanDistance[i] /= models.size();
					varEuclideanDistance[i] /= models.size();
					varEuclideanDistance[i] -= avgEuclideanDistance[i]
							* avgEuclideanDistance[i];
				}
			}
		}

		boolean checkTopology(Model model) {
			return (rfDistances.getDistance(commonTopology, model.getTree()) == 0);
		}
	}

	class AicComparator implements Comparator<Tree> {

		@Override
		public int compare(Tree o1, Tree o2) {
			long d1 = rfDistance(o1, bestTree[AIC_INDEX]);
			long d2 = rfDistance(o2, bestTree[AIC_INDEX]);
			return (int) (d1 - d2);
		}

	}

	class AiccComparator implements Comparator<Tree> {

		@Override
		public int compare(Tree o1, Tree o2) {
			long d1 = rfDistance(o1, bestTree[AICC_INDEX]);
			long d2 = rfDistance(o2, bestTree[AICC_INDEX]);
			return (int) (d1 - d2);
		}

	}

	class BicComparator implements Comparator<Tree> {

		@Override
		public int compare(Tree o1, Tree o2) {
			long d1 = rfDistance(o1, bestTree[BIC_INDEX]);
			long d2 = rfDistance(o2, bestTree[BIC_INDEX]);
			return (int) (d1 - d2);
		}

	}

	class DtComparator implements Comparator<Tree> {

		@Override
		public int compare(Tree o1, Tree o2) {
			long d1 = rfDistance(o1, bestTree[DT_INDEX]);
			long d2 = rfDistance(o2, bestTree[DT_INDEX]);
			return (int) (d1 - d2);
		}

	}

	public void print(TextOutputStream stream) {
		int MAX_LEN = 80;
		if (topologiesAIC != null || topologiesBIC != null
				|| topologiesAICc != null || topologiesDT != null) {
			List<Tree> sortedTopologies = topologiesAIC != null ? topologiesAIC
					: topologiesBIC != null ? topologiesBIC
							: topologiesAICc != null ? topologiesAICc
									: topologiesDT;
			stream.println("::Optimized Topologies Summary::");
			stream.println("");
			stream.println("There are " + getNumberOfTopologies()
					+ " different topologies.");
			stream.println("");
			for (int i = 0; i < getNumberOfTopologies(); i++) {
				int index = i + 1;
				Tree currentTree = sortedTopologies.get(i);

				SummaryRow summaryRow = summary.get(currentTree);

				int aicIndex = topologiesAIC != null ? aicIndexOf(currentTree) + 1
						: -1;
				int aiccIndex = topologiesAICc != null ? aiccIndexOf(currentTree) + 1
						: -1;
				int bicIndex = topologiesBIC != null ? bicIndexOf(currentTree) + 1
						: -1;
				int dtIndex = topologiesDT != null ? dtIndexOf(currentTree) + 1
						: -1;
				stream.println("Topology Id: " + index);
				stream.println("\tRank\tWeight\t\t RF\tAvgEucl\t\tVarEucl");
				if (topologiesAIC != null) {
						stream.println(getIcRow("AIC", aicIndex, summaryRow, AIC_INDEX));
				}
				if (topologiesBIC != null) {
					stream.println(getIcRow("BIC", bicIndex, summaryRow, BIC_INDEX));
				}
				if (topologiesAICc != null) {
					stream.println(getIcRow("AICc", aiccIndex, summaryRow, AICC_INDEX));
				}
				if (topologiesDT != null) {
					stream.println(getIcRow("DT", dtIndex, summaryRow, DT_INDEX));
				}
				stream.println("Models supporting:   "
						+ summaryRow.models.size());

				stream.print("                     ");
				int chars = 0;
				for (Model model : summaryRow.models) {
					if (chars >= MAX_LEN) {
						stream.println("");
						stream.print("                     ");
						chars = 0;
					}
					stream.print(model.getName() + " ");
					chars += model.getName().length() + 1;
				}
				stream.println("");
				stream.println("");
			}
		}
	}
	
	private String getIcRow(String name, int rankIndex, SummaryRow summaryRow, int criterionIndex) {
		return name + "\t"
				+ Utilities.format(rankIndex,3,0,false)
				+ "\t"
				+ Utilities
						.asPercent(summaryRow.support[criterionIndex] * 100)
				+ "\t\t" + Utilities.format(summaryRow.rfDistance[criterionIndex],3,0,false)
				+ "\t" + Utilities.format(summaryRow.avgEuclideanDistance[criterionIndex],8,2,true)
				+ "\t" + Utilities.format(summaryRow.varEuclideanDistance[criterionIndex],8,2,true);
	}
}
