/*
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.exe;

import java.io.PrintWriter;
import java.io.PushbackReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.Vector;

import pal.misc.Identifier;
import pal.tree.ReadTree;
import pal.tree.Tree;
import pal.tree.TreeParseException;
import es.uvigo.darwin.jmodeltest.ModelTest;
import es.uvigo.darwin.jmodeltest.gui.XManager;
import es.uvigo.darwin.jmodeltest.io.TextOutputStream;
import es.uvigo.darwin.jmodeltest.model.Model;
import es.uvigo.darwin.jmodeltest.selection.InformationCriterion;
import es.uvigo.darwin.jmodeltest.tree.Consensus;
import es.uvigo.darwin.jmodeltest.tree.TreeUtilities;
import es.uvigo.darwin.jmodeltest.tree.WeightedTree;
import es.uvigo.darwin.jmodeltest.utilities.FixedBitSet;
import es.uvigo.darwin.jmodeltest.utilities.Utilities;

public class RunConsense {
	
	private TextOutputStream stream;
	private Consensus consensus;
	
//	private String criterion;
	private String consensusType;
	private InformationCriterion criterion;
	private int numModels;
	private Model[] model;
	private int[] order;
	private double confidenceInterval;
	private Vector<Model> confidenceModels;

	private double[] w;
	private double[] cumw;
	private boolean[] isInInterval;

	public static es.uvigo.darwin.jmodeltest.threads.SwingWorker workerConsense;

	// constructor
	public RunConsense(InformationCriterion tcriterion, String tconsensusType,
			double minterval) {

		criterion = tcriterion;
		consensusType = tconsensusType;
		numModels = tcriterion.getNumModels();
		model = ModelTest.getCandidateModels();
		order = new int[numModels];
		confidenceInterval = minterval;
		confidenceModels = new Vector<Model>();

		w = new double[numModels];
		cumw = new double[numModels];
		isInInterval = new boolean[numModels];

		stream = ModelTest.getMainConsole();

		/**
		 * This action listener, called by the "Start" button, effectively forks
		 * the thread that does the work.
		 * 
		 * /* Invoking start() on the SwingWorker causes a new Thread to be
		 * creaconsense that will call construct(), and then finished(). Note
		 * that finished() is called even if the worker is interrupconsense
		 * because we catch the InterrupconsenseException in doConsense().
		 */
		if (ModelTest.buildGUI)
			System.out.println("\nComputing model averaged phylogeny ...");

		buildConfidenceInterval();
		consensus = doConsense();
		printConsensus();

		if (ModelTest.buildGUI) {
			XManager.getInstance().getPane().setCaretPosition(
					XManager.getInstance().getPane().getDocument()
					.getLength());
			System.out.println("OK");
		}

	}

	/**
	 * This method represents the application code that we'd like to run on a
	 * separate thread.
	 */
	// Object doConsense()
	private Consensus doConsense() {

		List<WeightedTree> treeList = new ArrayList<WeightedTree>();

		for (Model m : confidenceModels) {

			try {
				// parse tree
				String tree = m.getTreeString();
				StringReader sr = new StringReader(tree);
				Tree t = new ReadTree(new PushbackReader(sr));

				double weight;
				// set criterion
				switch (criterion.getType()) {
				case InformationCriterion.IC_AIC:
					weight = m.getAICw();
					break;
				case InformationCriterion.IC_AICc:
					weight = m.getAICcw();
					break;
				case InformationCriterion.IC_BIC:
					weight = m.getBICw();
					break;
				case InformationCriterion.IC_DT:
					weight = m.getDTw();
					break;
				default:
					weight = 0.0d;
				}

				treeList.add(new WeightedTree(t, weight));

			} catch (TreeParseException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}

		double consensusThreshold;
		if (consensusType.equals("strict")) {
			consensusThreshold = 0.99999999d;
		} else {
			consensusThreshold = 0.5d;
		}
		;
		Consensus consensus = new Consensus(treeList, consensusThreshold,
				Consensus.BRANCH_LENGTHS_MEDIAN);

		return consensus;
	} // doConsense

	/**************
	 * buildConfidenceInterval ************************
	 * 
	 * Builds the confidence interval of selected models and their cumulative
	 * weight
	 * 
	 * The model that just passed the confidence will be or not in the interval
	 * by chance (see below)
	 ****************************************************************/

	private void buildConfidenceInterval() {
		int i;
		Model tmodel;
		tmodel = model[0];

		order = criterion.order;

		// set alias
		for (i = 0; i < numModels; i++) {
			tmodel = model[order[i]];

			w[i] = criterion.getWeight(tmodel);
			cumw[i] = criterion.getCumWeight(tmodel);
			
		}

		// construct the confidence interval for models
		if (confidenceInterval == 1.0) {
			for (i = 0; i < numModels; i++) {
				tmodel = model[order[i]];
				isInInterval[i] = true;
				confidenceModels.add(tmodel);
			}
		} else {
			for (i = 0; i < numModels; i++) {
				tmodel = model[order[i]];

				// System.out.print("name=" + tmodel.name + " w=" + w[i] +
				// " cumw=" + cumw[i]);

				if (cumw[i] <= confidenceInterval) {
					isInInterval[i] = true;
					confidenceModels.add(tmodel);
				} else
					break;
			}

			// lets decide whether the model that just passed the confidence
			// interval should be included (suggested by John Huelsenbeck)
			double probOut = (tmodel.getCumAICw() - confidenceInterval)
					/ tmodel.getAICw();
			double probIn = 1.0 - probOut;
			Random generator = new Random();
			double randomNumber = generator.nextDouble();
			if (randomNumber <= probIn) {
				isInInterval[i] = true;
				confidenceModels.add(tmodel);
			} else
				isInInterval[i] = false;
		}
	}

	public double getConfidenceInterval() {
		return confidenceInterval;
	}
	
	public String getConsensusType() {
		return consensusType;
	}
	
	public List<Model> getConfidenceModels() {
		return confidenceModels;
	}
	
	public Tree getConsensus() {
		return consensus.getConsensusTree();
	}
	
	private void printConsensus() {
		
		double consensusThreshold = consensusType.equals("50% majority rule")?0.5:1.0;
		
		// print results for best AIC model
		stream.println(" ");stream.println(" ");stream.println(" ");
		stream.println("---------------------------------------------------------------");
		stream.println("*                                                             *");
		stream.println("*                    MODEL AVERAGED PHYLOGENY                 *");
		stream.println("*                                                             *");
		stream.println("---------------------------------------------------------------");

		if (criterion.getType() == InformationCriterion.IC_DT)
			Utilities
					.printRed("\nWarning: The DT weights used for this model averaged phylogeny are very gross"
							+ " and should be used with caution. See the program documentation.\n");
        stream.println(" ");		
        stream.println("Selection criterion: . . . . " + criterion);
        stream.print("Confidence interval: . . . . "); 
        stream.printf("%4.2f\n", confidenceInterval);
        stream.println("Consensus type:. . . . . . . " + consensusType);
        stream.println(" ");

     // print confidence set
		stream.println(" ");
		stream.print("Using " + confidenceModels.size() + " models in the ");
		stream.printf("%4.2f ", confidenceInterval);
		stream.print("confidence interval = ");
		for (Model m : confidenceModels) {
			stream.print(m.getName() + " ");
		}
		stream.println(" ");
		
        Set<FixedBitSet> keySet = consensus.getCladeSupport().keySet();
        List<FixedBitSet> splitsInConsensus = new ArrayList<FixedBitSet>();
        List<FixedBitSet> splitsOutFromConsensus = new ArrayList<FixedBitSet>();

        for (FixedBitSet fbs : keySet) {
            if (fbs.cardinality() > 1) {
                double psupport = (1.0 * consensus.getCladeSupport().get(fbs)) / 1.0;
                if (psupport < consensusThreshold) {
                    splitsOutFromConsensus.add(fbs);
                } else {
                    splitsInConsensus.add(fbs);
                }
            }
        }

        stream.println(" ");
        stream.println("Species in order:");
        stream.println(" ");

        int numTaxa = consensus.getIdGroup().getIdCount();
        for (int i = 0; i < numTaxa; i++) {
            Identifier id = consensus.getIdGroup().getIdentifier(i);
            stream.println("    " + (i + 1) + ". " + id.getName());
        }
 
        stream.println(" ");
        stream.println("Bipartitions included in the consensus tree");
        stream.println(" ");

        stream.println(consensus.getSetsIncluded());
        stream.println(" ");
        
        Tree consensusTree = consensus.getConsensusTree();
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        TreeUtilities.printASCII(consensusTree, pw);
        pw.flush();
        stream.println(sw);
        stream.println(" ");
        String newickTree = TreeUtilities.toNewick(consensusTree, true, true, true);
        stream.println(newickTree);
        stream.println(" ");
        stream.println("Note: this tree is unrooted. Branch lengths are the expected number of "
        		+ "substitutions per site. Labels next to parentheses represent phylogenetic "
        		+ "uncertainty due to model selection (see documentation)");
	}

} // class RunConsense

