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

import java.util.Random;

import es.uvigo.darwin.jmodeltest.ApplicationOptions;
import es.uvigo.darwin.jmodeltest.io.TextOutputStream;
import es.uvigo.darwin.jmodeltest.model.Model;

public class AICc extends InformationCriterion {

	// constructor
	public AICc(boolean mwritePAUPblock, boolean mdoImportances,
			boolean mdoModelAveraging, double minterval) {
		super(mwritePAUPblock, mdoImportances, mdoModelAveraging, minterval);
	}

	/****************************
	 * computeAICc ******************************* * Computes the corrected AIC
	 * (AICc) for small sample sizes (relative * to the number of parameters)
	 * and finds out the model with the * minimum AICc * *
	 ************************************************************************/

	public void compute() {

		boolean sorted;
		int temp2, pass;
		double[] tempw = new double[options.getNumModels()];
		double min, sumExp, cum, temp1;

		// Calculate AICc and minAICc
		min = computeAicc(models[0], options);

		if (doCheckAgainstULK) {
			unconstrainedModel.setAICc(computeSingle(unconstrainedModel));
		}
		minModel = models[0];
		for (Model model : models) {
			model.setAICc(computeAicc(model, options));

			if (model.getAICc() < min) {
				min = model.getAICc();
				minModel = model;
			}
			
			if (doCheckAgainstULK) {
				model.setUAICcd(model.getAICc() - unconstrainedModel.getAICc());
			}
		}

		validResults = min > 0;
		
		// Calculate Akaike differences
		sumExp = 0;
		for (int i = 0; i < numModels; i++) {
			models[i].setAICcd(models[i].getAICc() - minModel.getAICc());
			sumExp += Math.exp(-0.5 * models[i].getAICcd());
		}

		// Calculate Akaike weights
		for (int i = 0; i < numModels; i++) {
			if (models[i].getAICcd() > 1000)
				models[i].setAICcw(0.0);
			else
				models[i].setAICcw(Math.exp(-0.5 * models[i].getAICcd()) / sumExp);
			tempw[i] = models[i].getAICc();
			order[i] = i;
		}

		// Get order for Akaike weights and calculate cumWeigths
		sorted = false;
		pass = 1;
		while (!sorted) {
			sorted = true;
			for (int i = 0; i < (numModels - pass); i++)
				if (tempw[i] > tempw[i + 1]) {
					temp1 = tempw[i + 1];
					tempw[i + 1] = tempw[i];
					tempw[i] = temp1;

					temp2 = order[i + 1];
					order[i + 1] = order[i];
					order[i] = temp2;

					sorted = false;
				}
			pass++;
		}

		cum = 0;
		for (int i = 0; i < numModels; i++) {
			cum += models[order[i]].getAICcw();
			models[order[i]].setCumAICcw(cum);
		}

		// confidence interval
		buildConfidenceInterval();

		// parameter importances
		if (doImportances || doModelAveraging)
			parameterImportance();

		// model averaging
		if (doModelAveraging)
			averageModels();

	}

	public double computeSingle(Model model) {
		return computeAicc(model, options);
	}
	
	public static double computeAicc(Model model, ApplicationOptions options) {
		int K;
		if (options.countBLasParameters)
			K = model.getK();
		else
			K = model.getK() - options.getNumBranches();

		double aicc;
		if ((options.getSampleSize() - K - 1) == 0) {
			aicc = 0;
		} else {
			aicc = 2 * (model.getLnL() + K)
				+ ((double) 2 * K * (K + 1) / (options.getSampleSize() - K - 1));
		}
		return aicc;
	}
	
	public static double computeAicc(double lnL, int k, ApplicationOptions options) {
		int K;
		if (options.countBLasParameters)
			K = k;
		else
			K = k - options.getNumBranches();

		double aicc;
		if ((options.getSampleSize() - K - 1) == 0) {
			aicc = 0;
		} else {
			aicc = 2 * (lnL + K)
				+ ((double) 2 * K * (K + 1) / (options.getSampleSize() - K - 1));
		}
		System.out.println("AICC 2 IS " + aicc);
		return aicc;
	}
	
	protected void printHeader(TextOutputStream stream) {
		stream.println("\n\n\n---------------------------------------------------------------");
		stream.println("*                                                             *");
		stream.println("*        CORRECTED AKAIKE INFORMATION CRITERION (AICc)        *");
		stream.println("*                                                             *");
		stream.println("---------------------------------------------------------------");
		stream.println(" ");
		stream.println(" Sample size: " + options.getSampleSize());
	}
	
	protected void printFooter(TextOutputStream stream) {
		stream.println("-lnL:\tnegative log likelihod");
		stream.println(" K:\tnumber of estimated parameters");
		stream.println(" AICc:\tCorrected Akaike Information Criterion");
		stream.println(" delta:\tAICc difference");
		stream.println(" weight:\tAICc weight");
		stream.println(" cumWeight:\tcumulative AICc weight");
	}
	

	/**************
	 * buildConfidenceInterval ************************
	 * 
	 * Builts 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)
	 ****************************************************************/

	public void buildConfidenceInterval() {
		int i;
		Model tmodel = models[0];
		cumWeight = 0;

		// first construct the confidence interval for models
		if (confidenceInterval >= 1.0d) {
			for (i = 0; i < numModels; i++) {
				tmodel = models[order[i]];
				tmodel.setInAICcinterval(true);
				confidenceModels.add(tmodel);
			}
			cumWeight = 1.0;
		} else {
			for (i = 0; i < numModels; i++) {
				tmodel = models[order[i]];
				if (tmodel.getCumAICcw() <= confidenceInterval) {
					tmodel.setInAICcinterval(true);
					confidenceModels.add(tmodel);
					cumWeight += tmodel.getAICcw();
				} else
					break;
			}

			// lets decide whether the model that just passed the confidence
			// interval should be included (suggested by John Huelsenbeck)
			double probOut = (tmodel.getCumAICcw() - confidenceInterval)
					/ tmodel.getAICcw();
			double probIn = 1.0 - probOut;
			Random generator = new Random();
			double randomNumber = generator.nextDouble();
			if (randomNumber <= probIn) {
				tmodel.setInAICcinterval(true);
				if (!confidenceModels.contains(tmodel)) confidenceModels.add(tmodel);
				cumWeight += tmodel.getAICcw();
			} else
				tmodel.setInAICcinterval(false);

			/*
			 * System.out.print("name=" + tmodel.name + " w=" + tmodel.AICcw +
			 * " cumw=" + tmodel.cumAICw); System.out.print(" in=" + probIn +
			 * " out=" + probOut); System.out.print(" r=" + randomNumber +
			 * " isIn=" + tmodel.isInAICcinterval + " ");
			 */
		}

		/*
		 * System.out.print(confidenceInterval + " confidence interval (" +
		 * numModels + " models) = ["); for (Enumeration
		 * e=confidenceModels.elements(); e.hasMoreElements();) { Model m =
		 * (Model)e.nextElement(); System.out.print(m.name + " "); }
		 * System.out.print("]");
		 */
	}

	public double getMinModelValue() {
		return minModel.getAICc();
	}

	public double getMinModelWeight() {
		return minModel.getAICcw();
	}
	
	@Override
	public double getValue(Model m) {
		return m.getAICc();
	}
	
	@Override
	public double getWeight(Model m) {
		return m.getAICcw();
	}
	
	@Override
	public double getDelta(Model m) {
		return m.getAICcd();
	}
	
	@Override
	public double getUDelta(Model m) {
		return m.getUAICcd();
	}
	
	@Override
	public double setUDelta(Model m) {
		m.setUAICcd(computeAicc(m.getLnLIgnoringGaps(), m.getK(), options)
				- computeAicc(unconstrainedModel.getLnL(), 
						unconstrainedModel.getK(), options));
		return m.getUAICcd();
	}
	
	@Override
	public double getCumWeight(Model m) {
		return m.getCumAICcw();
	}
	
	@Override
	public int getType() {
		return IC_AICc;
	}

} // class AICc
