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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.StringTokenizer;

import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;

import pal.alignment.Alignment;
import es.uvigo.darwin.jmodeltest.ModelTest;
import es.uvigo.darwin.jmodeltest.gui.XManager;

public final class Utilities {

	public static final int NA = Integer.MIN_VALUE;
	public static final int OS_LINUX = 1;
	public static final int OS_OSX = 2;
	public static final int OS_WINDOWS = 3;

	public Utilities() {
	}

	public static String firstNumericToken(String str) {
		StringTokenizer st = new StringTokenizer(str);
		String token = "";
		boolean found = false;
		while (st.hasMoreTokens() && !found) {
			token = st.nextToken();
			found |= isNumber(token);
		}
		if (found)
			return token;
		else
			return null;
	}
	
	public static String lastToken(String str) {
		StringTokenizer st = new StringTokenizer(str);
		String token = "";
		while (st.hasMoreTokens()) {
			token = st.nextToken();
		}
		return token;
	}

	public static boolean isWindows() {
		if (System.getProperty("os.name").startsWith("Window"))
			return true;
		return false;
	}

	public static int findCurrentOS() {
		String os = System.getProperty("os.name");
		if (os.startsWith("Mac"))
			return OS_OSX;
		else if (os.startsWith("Windows"))
			return OS_WINDOWS;
		else
			return OS_LINUX;
	}

	public static String getBinaryVersion() {
		String arch = System.getProperty("os.arch");
		String bit = System.getProperty("sun.arch.data.model");

		String binaryName = null;
		switch (findCurrentOS()) {

		case OS_OSX:
			if (arch.startsWith("ppc")) {
				System.err
						.println("Sorry, PowerPC architecture is no longer supported");
				System.exit(0);
			} else {
				binaryName = "PhyML_3.0_macOS_i386";
			}
			break;
		case OS_LINUX:
			if (bit.startsWith("64"))
				binaryName = "PhyML_3.0_linux64";
			else
				binaryName = "PhyML_3.0_linux32";
			break;
		case OS_WINDOWS:
			binaryName = "PhyML_3.0_win32.exe";
			break;
		default:
			binaryName = null;
		}

		return binaryName;
	}

	public static String calculateRuntimeMinutes(long startTime, long endTime) {
		long seconds = Math.round((endTime - startTime) / 1000.0);
		int hours = (int) (seconds / 3600.0);
		int rest1 = (int) (seconds % (3600.0));
		int minutes = (int) (rest1 / 60.0);
		seconds = (int) (seconds - (hours * 3600 + minutes * 60));
		String h = "" + hours;
		String m = "" + minutes;
		String s = "" + seconds;
		if (minutes < 10)
			m = "0" + m;
		if (seconds < 10)
			s = "0" + s;
		return h + "h:" + m + ":" + s + "";
	}

	public static String getCurrentTime(String format) {
		Calendar cal = new GregorianCalendar();
	    SimpleDateFormat date_format = new SimpleDateFormat(format);
	    return date_format.format(cal.getTime());
	}
	
	public static String displayRuntime(long time) {
		long decimes = Math.round(time / 100.0);
		int hours = (int) (decimes / 36000.0);
		int rest1 = (int) (decimes % 36000.0);
		int minutes = (int) (rest1 / 600.0);
		int rest2 = (int) (rest1 % 600.0);
		int seconds = (int) (rest2 / 10.0);
		decimes = (int) (decimes - (hours * 36000 + minutes * 600 + seconds * 10));
		String h = "" + hours;
		String m = "" + minutes;
		String s = "" + seconds;
		String d = "0" + decimes;
		if (hours < 10)
			h = "0" + h;
		if (minutes < 10)
			m = "0" + m;
		if (seconds < 10)
			s = "0" + s;
		return h + "h:" + m + ":" + s + ":" + d + "";
	}

	public static String calculateRuntime(long startTime, long endTime) {
		return displayRuntime(endTime - startTime);
	}

	public static boolean isNumber(String s) {
		try {
			Double.parseDouble(s);
			return true;
		} catch (NumberFormatException ex) {
			return false;
		}
	}

	public static void toConsoleEnd() {
		if (ModelTest.buildGUI)
			XManager.getInstance()
					.getPane()
					.setCaretPosition(
							XManager.getInstance().getPane().getDocument()
									.getLength());
	}

	/**
	 * copyFile
	 * 
	 * Copies src file to dst file. If the dst file does not exist, it is
	 * created modified from The Java Developers Almanac 1.4:
	 * http://www.exampledepot.com/egs/java.io/CopyFile.html
	 */

	public static void copyFile(String source, String destination)
			throws IOException {
		File src = new File(source);
		File dst = new File(destination);

		InputStream in = new FileInputStream(src);
		OutputStream out = new FileOutputStream(dst);

		// Transfer bytes from in to out
		byte[] buf = new byte[1024];
		int len;
		while ((len = in.read(buf)) > 0) {
			out.write(buf, 0, len);
		}
		in.close();
		out.close();
	}

	/**
	 * CheckNA
	 * 
	 * If value is NA it prints "-"
	 */
	public static String checkNA(double value) {
		if (value == NA)
			return "  -  ";
		else {
			String s = String.format(Locale.ENGLISH, "%8.4f", value);
			return s;
		}
	}

	/**
	 * deleteFile
	 * 
	 * Delete a given file
	 */
	public static void deleteFile(String fileName) {

		File f;

		try {
			f = new File(fileName);
		} catch (Exception e) {
			throw new IllegalArgumentException("DeleteFiles: deletion of "
					+ fileName + " failed");
		}

		// Make sure the file or directory exists and isn't write protected
		if (f.exists()) {
			if (!f.canWrite())
				throw new IllegalArgumentException(
						"DeleteFiles: write protected: " + fileName);

			// if it is a directory, make sure it is empty
			if (f.isDirectory()) {
				String[] files = f.list();
				if (files.length > 0)
					throw new IllegalArgumentException(
							"DeleteFiles: directory not empty: " + fileName);
			}

			// explicit close associated stream for windows
			//
			// attempt to delete it
			boolean success = f.delete();

			if (!success)
				throw new IllegalArgumentException("DeleteFiles: deletion of "
						+ fileName + " failed");
		}
	}

	private static void printColor(String text, SimpleAttributeSet color) {
		if (ModelTest.buildGUI) {
			try {
				Document doc = XManager.getInstance().getPane().getDocument();
				doc.insertString(doc.getLength(), text, color);
			} catch (javax.swing.text.BadLocationException e) {
				System.err.println("Bad Location Exception");
			}
		} else {
			System.out.print(text);
		}
	}
	
	/**
	 * printRed
	 * 
	 * Prints to the main console text in red
	 */
	public static void printRed(String text) {
		printColor(text, XManager.redText);
	}
	
	/**
	 * printBlue
	 * 
	 * Prints to the main console text in blue
	 */

	public static void printBlue(String text) {
		printColor(text, XManager.blueText);
	}

	/******************
	 * RoundDoubleTo **************************
	 * 
	 * Rounds a double to a number of significant digits
	 *********************************************************/

	public static double roundDoubleTo(double decimal, int dplaces) {
		BigDecimal bd = new BigDecimal(decimal);
		bd = bd.setScale(dplaces, BigDecimal.ROUND_UP);
		return bd.doubleValue();
	}

	public static String asPercent(double decimal) {
		if (decimal > 100.0d) {
			decimal = 100.0d;
		}
		return format(decimal, 6, 2, false) + "%";
	}

	public static String format(double number, int totalLength, int decimalPlaces, boolean exp) {
		StringBuffer sb;
		String format;
		if (exp) {
			format = "%"+totalLength+"."+decimalPlaces+"e";
		} else {
			format = "%"+totalLength+"."+decimalPlaces+"f";
		}
		sb = new StringBuffer(String.format(Locale.ENGLISH, format, number));
		// normalize string to size 6
		for (int i = sb.length(); i < totalLength; i++) {
			sb.insert(0, " ");
		}

		return sb.toString();
	}
	
	/******************
	 * putSlashBeforeSpaces **************************
	 * 
	 * Inserts slash before paces in a path (MacOS X)
	 *********************************************************/

	public static String putSlashBeforeSpaces(String originalPath) {
		if (originalPath == null)
			return "";

		StringBuffer s = new StringBuffer(originalPath);

		for (int i = 0; i < s.length(); i++) {
			if (s.charAt(i) == ' ')
				s.insert(i++, '\\');
		}
		return s.toString();
	}

	/******************
	 * substituteSpaces **************************
	 * 
	 * substitutes %20 in spaces in a path (MacOS X)
	 *********************************************************/

	public static String substituteSpaces(String originalPath) {
		if (originalPath == null)
			return "";

		StringBuffer s = new StringBuffer(originalPath);

		for (int i = 0; i < s.length(); i++) {
			if (s.charAt(i) == ' ') {
				s.setCharAt(i, '0');
				s.insert(i++, '%');
				s.insert(i++, '2');
			}
		}
		return s.toString();
	}

	/******************
	 * putQuotesAroundSpaces **************************
	 * 
	 * Inserts quotes at correct locations in nominated path string. (Windows)
	 *********************************************************/

	public static String putQuotesAroundSpaces(String originalPath) {
		StringTokenizer st = new StringTokenizer(originalPath, File.separator);
		String newPath = "\"";
		while (st.hasMoreTokens()) {
			String token = st.nextToken();
			if (token.contains(" ")) {
				newPath += File.separator + "\"" + token + "\"";
			} else {
				newPath += File.separator + token;
			}
		}
		return newPath.substring(1, newPath.length());
	}

	/******************
	 * specialConcatStringArrays **************************
	 * 
	 * concatenates two string arrays but removinf the first string from the
	 * second
	 *********************************************************/

	public static String[] specialConcatStringArrays(String[] A, String[] B) {
		String[] C = new String[A.length + B.length - 1];
		System.arraycopy(A, 0, C, 0, A.length);
		System.arraycopy(B, 1, C, A.length, B.length - 1);
		return C;
	}

	/**
	 * Calculate invariable sites.
	 * 
	 * @param alignment the alignment
	 * 
	 * @return the invariable sites in the alignment
	 */
	public static int calculateInvariableSites (Alignment alignment) {

		//use this function to estimate a good starting value for the InvariableSites distribution.
		int numSites    = alignment.getSiteCount();
		int numSeqs     = alignment.getSequenceCount();
		int inv         = 0;
		int tmp;
		boolean tmpInv = true;
		for(int i=0; i < numSites; i++) {
			tmp     = indexOfChar(alignment.getData(0,i));
			tmpInv  = true;
			for(int j=0; j < numSeqs; j++) {
				if(indexOfChar(alignment.getData(j,i)) != tmp) { //if at least one difference in column i:
					j = numSeqs;    //we exit this for.
					tmpInv = false; //i is not an invariable site.
				}
			}
			if(tmpInv)
				inv++;
		}
		return inv;
	}
	
	private static int indexOfChar (char c) {
		char[] charSet = {'A', 'C', 'G', 'T', 'a', 'c', 'g', 't'};
		for (int charIndex = 0; charIndex < charSet.length; charIndex++)
			if (charSet[charIndex] == c)
				return charIndex % 4;

		return -1;
	}
	
	public static double calculateShannonSampleSize (Alignment alignment, boolean doJustShannonEntropy) {

		int numSites = alignment.getSiteCount();
		int numTaxa = alignment.getSequenceCount();
		
		//int    pattern[][]    = new int   [numSites][numSeqs];
		double freqs  [][]    = new double[numSites][4];
		byte   state  [][]    = new byte  [numSites][4];
		double siteS  []      = new double[numSites];
		int    sequences[]    = new int[numSites];
		double shannonEntropy = 0.0;

		//We simply count bases at positions and store in state[][]
		for(int i=0; i < numSites; i++) {
			for(int j=0; j < numTaxa; j++) {
				//state[i][indexOfchar(alignment.getData(j,i))]++;
				int index = indexOfChar(alignment.getData(j,i));
				if(index >= 0) {
					state[i][index]++;
					sequences[i]++;
				}
				//state[i][sP.pattern[j][i]]++;
			}
		}

		//For each alignment position, we calculate aminoacid frequencies. And also...
		//For each alignment position, we calculate Shannon Entropy based on previous frequencies.
		for(int i=0; i < numSites; i++) {
			for(int j=0; j < 4; j++) {
				//freqs[i][j] = (double)state[i][j]/(double)numSeqs;
				freqs[i][j] = (double)state[i][j]/(double)sequences[i];
				if(freqs[i][j] > 0)
					siteS[i]   += freqs[i][j]*Math.log(freqs[i][j])/Math.log(2);
			}
		}

		//We sum positions entropies over the whole alignment.
		for(int i=0; i < numSites; i++) {
			shannonEntropy += siteS[i];
		}

		if(doJustShannonEntropy) {
			return -1.0*shannonEntropy; //sum of shannon Entropy positions.
		} else { //if Options.SHANNON_NxL
			double meanShannonEntropy;
			double maxShannonEntropy = 0;
			double normalizedShannonEntropy = 0;
			meanShannonEntropy = -1.0*shannonEntropy/(double)numSites; //mean S for sites
			//let's normalize ShannonEntropy from 0 to 1:
			for(int i=0; i<4; i++) {
				maxShannonEntropy += (1.0/(double)4)*Math.log(1.0/(double)4)/Math.log(2);
			}
			//by this moment we normalize by a "rule of three"
			normalizedShannonEntropy = -1.0*meanShannonEntropy/maxShannonEntropy;
			return (double)numSites*(double)numTaxa*normalizedShannonEntropy; //NxL x averaged Shannon entropy
		}
	}
} // end of class
