/*
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.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import es.uvigo.darwin.jmodeltest.ApplicationOptions;
import es.uvigo.darwin.jmodeltest.ModelTest;
import es.uvigo.darwin.jmodeltest.ModelTestConfiguration;
import es.uvigo.darwin.jmodeltest.io.TextOutputStream;
import es.uvigo.darwin.jmodeltest.model.Model;
import es.uvigo.darwin.jmodeltest.model.ModelComparator;
import es.uvigo.darwin.jmodeltest.observer.ProgressInfo;
import es.uvigo.darwin.jmodeltest.utilities.Utilities;

/**
 * RunPhyml.java
 * 
 * Description: Makes phyml calculate likelihood scores for competing models
 * 
 * @author Diego Darriba, University of Vigo / University of A Coruna, Spain
 *         ddarriba@udc.es
 * @author David Posada, University of Vigo, Spain dposada@uvigo.es |
 *         darwin.uvigo.es
 * @version 2.1 (May 2012)
 */
public abstract class RunPhyml extends Observable implements Observer {

	// Set of variables for tuning the guided search algorithm
	private static final boolean filterFrequencies = true;
	private static final boolean filterRateMatrix = true;
	private static final boolean filterRateVariation = true;

	protected ApplicationOptions options;
	protected Model[] models;
	protected Model gtrModel = null;

	public static final String[] COMPATIBLE_VERSIONS = {
		"20130103", "20131022", 
		"20141009", "20141029", 
		"20150501", "20151222"};
	public static String PHYML_VERSION = "3.0";

	public static String PHYML_TREE_SUFFIX = "_phyml_tree_";
	public static String PHYML_STATS_SUFFIX = "_phyml_stats_";

	public static File phymlBinary;
	public static String phymlBinaryStr;
	private static String CURRENT_DIRECTORY = ModelTestConfiguration.PATH;
	private static boolean PHYML_GLOBAL = ModelTestConfiguration.isGlobalPhymlBinary();
	public static String PHYML_PATH = CURRENT_DIRECTORY + "exe/phyml/";
	private static boolean compatiblePhyml = false;
	
	protected Observer progress;

	public static boolean isCompatible() {
		return compatiblePhyml;
	}
	
	public static boolean checkBinary() {
		boolean canExecute = false;
		if (!ModelTestConfiguration.isGlobalPhymlBinary()) {
			if (!RunPhyml.phymlBinary.exists()) {
				Utilities
				.printRed("ERROR: PhyML binary cannot be found: " + RunPhyml.phymlBinary.getAbsolutePath() + "\n");
			} else if (!RunPhyml.phymlBinary.canExecute()) {
				Utilities
				.printRed("ERROR: PhyML binary exists, but it cannot be executed: " + RunPhyml.phymlBinary.getAbsolutePath() + "\n");
			} else if (!RunPhyml.isCompatible()){
				Utilities
				.printRed("WARNING: PhyML binary is not in the list of compatibility: \n");
				Utilities
				.printRed(RunPhyml.phymlBinary.getAbsolutePath() + " v" + RunPhyml.PHYML_VERSION +"\n");
				Utilities.printRed("Compatible versions: ");
				for (int i=0; i<RunPhyml.COMPATIBLE_VERSIONS.length; i++)
					Utilities.printBlue(RunPhyml.COMPATIBLE_VERSIONS[i] + " ");
				Utilities.printBlue("\n");
				Utilities
				.printRed("jModelTest will try to continue execution anyway, but it might fail.\n");
				canExecute = true;
			} else {
				Utilities
				.printBlue("PhyML binary: " + RunPhyml.phymlBinary.getAbsolutePath() + " v" + RunPhyml.PHYML_VERSION + "\n");
				canExecute = true;
			}
		}
		return canExecute;
	}
	private static boolean checkPhymlCompatibility(String binary) {
		boolean binaryFound = false;
		if (Utilities.findCurrentOS() != Utilities.OS_LINUX)
			return false;
		// get process and execute command line
		String cmd[] = {binary, "--version"};
		Runtime rt = Runtime.getRuntime();
		Process proc;
		try {
			proc = rt.exec(cmd, null, null);
			InputStream stdin = proc.getInputStream();
			InputStreamReader isr = new InputStreamReader(stdin);
			BufferedReader br = new BufferedReader(isr);

			String line = null;
			while ( (line = br.readLine()) != null) {
				if (line.toLowerCase().contains("phyml version")) {
					String[] linesplit = line.trim().replace(".", "").split(" ");
					String version = linesplit[linesplit.length - 1];
					PHYML_VERSION = version;
					if (Arrays.asList(COMPATIBLE_VERSIONS).contains(version)) {
						compatiblePhyml = true;
					} else {
						compatiblePhyml = false;
					}
					binaryFound = true;
				}
			}
			br.close();
			isr.close();
			stdin.close();
			proc.destroy();
		} catch (IOException e) {
			return false;
		}
		return binaryFound;
	}
	
	public static boolean isPhymlGlobal() {
		return PHYML_GLOBAL;
	}
	
	static {
		if (PHYML_GLOBAL) {
			PHYML_PATH = "";
			phymlBinaryStr = "phyml";
		} else {
			/* check the local paths */
			String path = ModelTestConfiguration.getExeDir();
			if (!path.startsWith(File.separator)) {
				PHYML_PATH = CURRENT_DIRECTORY + File.separator + path;
			} else {
				PHYML_PATH = path;
			}
			if (!PHYML_PATH.endsWith(File.separator)) {
				PHYML_PATH += File.separator;
			}
			phymlBinary = new File(PHYML_PATH + "phyml");
			if (phymlBinary.exists() && phymlBinary.canExecute()) {
				phymlBinaryStr = phymlBinary.getAbsolutePath();
				compatiblePhyml = true;
			} else {
				phymlBinaryStr = PHYML_PATH + Utilities.getBinaryVersion();
			}
			/* Check if binary exists and is compatible */
			phymlBinary = new File(phymlBinaryStr);
			if (!checkPhymlCompatibility(phymlBinaryStr)) {
				/* Check for system wide PhyML */
				if (!(phymlBinary.exists() && phymlBinary.canExecute()))
				{
					if(checkPhymlCompatibility("/usr/bin/phyml")) {
						PHYML_PATH = "/usr/bin";
						phymlBinary = new File(PHYML_PATH + "/phyml");
						phymlBinaryStr = phymlBinary.getAbsolutePath();						
					}
				}
			}
		}
	}
	public RunPhyml(Observer progress, ApplicationOptions options,
			Model[] models) {
		if (models != null)
			this.models = new Model[models.length];
		else
			this.models = new Model[0];
		for (int i = 0; i < this.models.length; i++)
			this.models[i] = models[i];
		this.options = options;
		this.progress = progress;
		this.addObserver(progress);
		Arrays.sort(this.models, new ModelComparator());
	}

	public void execute() {
		// remove stuff from exe directories before starting
		deleteFiles();
		printSettings(ModelTest.getMainConsole());

		// locate GTR model
		String searchFor;
		int gtrParams;
		if (options.doI && options.doG) {
			searchFor="GTR+I+G";
			gtrParams = 10;
		} else if(options.doI) {
			searchFor="GTR+I";
			gtrParams = 9;
		} else if(options.doG) {
			searchFor="GTR+G";
			gtrParams = 9;
		} else {
			searchFor="GTR";
			gtrParams = 8;
		}
		for (int i = (models.length - 1); i >= 0; i--) {
			if (models[i].getName().startsWith(searchFor)) {
				gtrModel = models[i];
				break;
			}
		}
		if (gtrModel == null) {
			gtrModel = new Model(0, searchFor, "012345", gtrParams, false, false, false, true, options.doI, options.doG, 2, 4);
		}
		// estimate a NJ-JC tree if needed
		if (options.fixedTopology) {
			Model jcModel = null;
			for (Model model : models) {
				if (model.getName().equals("JC")) {
					jcModel = model;
					break;
				}
			}

			if (jcModel != null) {
				notifyObservers(ProgressInfo.BASE_TREE_INIT, 0, jcModel, null);

				PhymlSingleModel jcModelPhyml = new PhymlSingleModel(jcModel,
						0, true, false, options);
				jcModelPhyml.addObserver(this);
				jcModelPhyml.run();

				// create JCtree file
				TextOutputStream JCtreeFile = new TextOutputStream(options
						.getTreeFile().getAbsolutePath(), false);
				JCtreeFile.print(jcModel.getTreeString() + "\n");
				JCtreeFile.close();

				notifyObservers(ProgressInfo.BASE_TREE_COMPUTED, 0, jcModel,
						null);
			}

		}

		if (options.isGuidedSearch()) {

			if (gtrModel != null) {
				// compute GTR model
				notifyObservers(ProgressInfo.GTR_OPTIMIZATION_INIT, models.length, gtrModel, null);
				PhymlSingleModel gtrPhymlModel = new PhymlSingleModel(
						gtrModel, 0, false, false, options);
				gtrPhymlModel.run();
				notifyObservers(ProgressInfo.GTR_OPTIMIZATION_COMPLETED, models.length, gtrModel, null);
				
				GuidedSearchManager gsm = new GuidedSearchManager(
						options.getGuidedSearchThreshold(), gtrModel,
						filterFrequencies, filterRateMatrix,
						filterRateVariation);
	
				models = gsm.filterModels(models);
				ModelTest.setCandidateModels(models);
			} else {
				notifyObservers(ProgressInfo.GTR_NOT_FOUND, models.length, models[0], null);
			}
		}

		// compute likelihood scores for all models

		notifyObservers(ProgressInfo.OPTIMIZATION_INIT, 0, models[0], null);

		doPhyml();

	}

	public void executeIgnoreGaps(Model[] models) {
		notifyObservers(ProgressInfo.REOPTIMIZATION_INIT, models.length, models[0], null);
		parallelExecute(models, true);
		notifyObservers(ProgressInfo.REOPTIMIZATION_COMPLETED, models.length, null, null);
	}
	
	protected boolean parallelExecute(Model models[], boolean ignoreGaps) {
		
		ExecutorService threadPool = Executors.newFixedThreadPool(options
				.getNumberOfThreads());
		Collection<Callable<Object>> c = new ArrayList<Callable<Object>>();
		int current = 0;
		for (Model model : models) {
			if (model != null) {
				PhymlSingleModel psm = new PhymlSingleModel(model, current, false,
						ignoreGaps, options);
				psm.addObserver(this);
				c.add(Executors.callable(psm));
	
				current++;
			}
		}

		Collection<Future<Object>> futures = null;
		try {
			futures = threadPool.invokeAll(c);
		} catch (InterruptedException e) {
			notifyObservers(ProgressInfo.INTERRUPTED, 0, null, null);
		}

		if (futures != null) {
			for (Future<Object> f : futures) {
				try {
					f.get();
				} catch (InterruptedException ex) {
					notifyObservers(ProgressInfo.INTERRUPTED, 0, null, null);
					ex.printStackTrace();
					return false;
				} catch (ExecutionException ex) {
					// Internal exception while computing model.
					// Let's continue with errors
					ex.printStackTrace();
					return false;
				}
			}
		}
		return true;
	}
	
	/***************************
	 * printSettings ***************************** * Prints the settings for the
	 * likelihood calculation * * *
	 ***********************************************************************/

	protected void printSettings(TextOutputStream stream) {

		stream.println(" ");
		stream.println(" ");
		stream.println("---------------------------------------------------------------");
		stream.println("*                                                             *");
		stream.println("*        COMPUTATION OF LIKELIHOOD SCORES WITH PHYML          *");
		stream.println("*                                                             *");
		stream.println("---------------------------------------------------------------");
		stream.println(" ");
		stream.println("::Settings::");
		stream.println(" ");
		stream.println(" Phyml version = " + PHYML_VERSION);
		stream.println(" Phyml binary = " + phymlBinary.getName());
		stream.println(" Phyml path = " + phymlBinary.getAbsolutePath()
				.substring(0, phymlBinary.getAbsolutePath().lastIndexOf(File.separator)) + File.separator);
		stream.println(" Candidate models = " + models.length);
		stream.print("   number of substitution schemes = ");

		if (options.getSubstTypeCode() == 0)
			stream.println("3");
		else if (options.getSubstTypeCode() == 1)
			stream.println("5");
		else if (options.getSubstTypeCode() == 2)
			stream.println("7");
		else
			stream.println("11");

		if (options.doF)
			stream.println("   including models with equal/unequal base frequencies (+F)");
		else
			stream.println("   including only models with equal base frequencies");

		if (options.doI)
			stream.println("   including models with/without a proportion of invariable sites (+I)");
		else
			stream.println("   including only models without a proportion of invariable sites");

		if (options.doG)
			stream.println("   including models with/without rate variation among sites (+G)"
					+ " (nCat = " + options.numGammaCat + ")");
		else
			stream.println("   including only models without rate variation among sites");

		stream.print(" Optimized free parameters (K) =");
		stream.print(" substitution parameters");
		if (options.countBLasParameters)
			stream.print(" + " + options.getNumBranches() + " branch lengths");
		if (options.optimizeMLTopology)
			stream.print(" + topology");
		stream.println(" ");

		stream.print(" Base tree for likelihood calculations = ");
		if (options.userTopologyExists) {
			stream.println("fixed user tree topology.");
			stream.println(" ");
			stream.print("User tree " + "("
					+ options.getInputTreeFile().getName() + ") = ");
			stream.println(options.getUserTree());
			stream.println(" ");
		} else if (options.fixedTopology) {
			stream.println("fixed BIONJ-JC tree topology");
		} else if (options.optimizeMLTopology) {
			stream.println("ML tree");
		} else {
			stream.println("BIONJ tree");
		}

		if (options.optimizeMLTopology) {
			stream.print(" Tree topology search operation = ");
			switch (options.treeSearchOperations) {
			case NNI:
				stream.println("NNI");
				break;
			case SPR:
				stream.println("SPR");
				break;
			case BEST:
				stream.println("BEST");
				break;
			}
		}
		if (options.isClusteringSearch()) {
			stream.println(" Using hill-climbing hierarchical clustering");
		}
		
		if (options.isGuidedSearch()) {
			stream.println(" Using heuristic model filtering ");
		}
		stream.println(" ");
	}

	protected abstract Object doPhyml();

	/***********************************************************************
	 * interruptThread
	 * 
	 * Interrupts the calculation of likelihood scores
	 ***********************************************************************/
	public void interruptThread() {
		notifyObservers(ProgressInfo.OPTIMIZATION_COMPLETED_INTERRUPTED, 0,
				null, null);
		// workerPhyml.interrupt();
		// IS_INTERRUPTED = true;
	}

	private void deleteFiles() {
		/* phymlFolder */
		//if (options.getLogFile() != null)
			options.getLogFile().delete();
	}

	protected void notifyObservers(int type, int value, Model model,
			String message) {
		setChanged();
		notifyObservers(new ProgressInfo(type, value, model, message));
	}

	@Override
	public void update(Observable o, Object arg) {
		setChanged();
		notifyObservers(arg);
	}

} // class RunPhyml

