/* UtilFns.java

	Purpose:
		
	Description:
		
	History:
		Apr 19, 2010 2:23:06 PM, Created by ashish

Copyright (C) 2010 Potix Corporation. All Rights Reserved.

*/

package org.zkoss.poi.ss.formula.functions;

import java.math.BigDecimal;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;

import org.apache.commons.math.stat.descriptive.DescriptiveStatistics;
import org.apache.commons.math.stat.regression.SimpleRegression;
import org.zkoss.poi.ss.formula.AreaEvalHelper;
import org.zkoss.poi.ss.formula.eval.AreaEval;
import org.zkoss.poi.ss.formula.eval.BoolEval;
import org.zkoss.poi.ss.formula.eval.ErrorEval;
import org.zkoss.poi.ss.formula.eval.EvaluationException;
import org.zkoss.poi.ss.formula.eval.NumberEval;
import org.zkoss.poi.ss.formula.eval.StringEval;
import org.zkoss.poi.ss.formula.eval.ValueEval;
import org.zkoss.poi.ss.usermodel.DateUtil;
import org.zkoss.util.Dates;
import org.zkoss.zpoiex.util.Objects;


/**
 * @author ashish
 *
 */
public class UtilFns {

	public final static TimeZone TZ_GMT = TimeZone.getTimeZone("GMT");
	public final static Calendar CAL_GMT = Calendar.getInstance(TZ_GMT);
	private final static Date DATE1899_12_30;
	private final static Date DATE1900_3_1;
	static {
		CAL_GMT.clear();
		CAL_GMT.set(1899, 11, 30); //1899-12-30
		DATE1899_12_30 = CAL_GMT.getTime();
		
		CAL_GMT.clear();
		CAL_GMT.set(1900, 2, 1); //1900-3-1
		DATE1900_3_1 = CAL_GMT.getTime();
	}

	/**
	 * Convert a String to Date
	 * @param arg - input value
	 * @return Date value
	 * @throws EvaluationException 
	 */	
	public static Date stringToDate(ValueEval arg) throws EvaluationException {
		if (!(arg instanceof StringEval)) {
			throw new EvaluationException(ErrorEval.VALUE_INVALID); 
		}
		SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
		Date result = null;
		try {
			result = df.parse(((StringEval)arg).getStringValue());
		} catch (ParseException e) {
			throw new EvaluationException(ErrorEval.VALUE_INVALID);
		}
		return result;
	}

	public static boolean stringToBoolean(ValueEval arg) throws EvaluationException {
		if (!(arg instanceof BoolEval)) {
			throw new EvaluationException(ErrorEval.VALUE_INVALID); 
		}
		return ((BoolEval)arg).getBooleanValue();
	}
	
	public static int dsm(Date settle, Date maturi, int basis) throws EvaluationException {
		if(settle.compareTo(maturi) > 0) {
			throw new EvaluationException(ErrorEval.NUM_ERROR); 
		} else {
			int dsm = 0;
			if(basis == 0 || basis == 4) {
				SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
				Calendar calendar = df.getCalendar();
				calendar.setTime(settle);
				int settleY = calendar.get(Calendar.YEAR);
				int settleM = calendar.get(Calendar.MONTH);
				int settleD = calendar.get(Calendar.DATE) > 30 ? 30 : calendar.get(Calendar.DATE);
				calendar.setTime(maturi);
				int maturiM = calendar.get(Calendar.MONTH);
				int maturiY = calendar.get(Calendar.YEAR);
				int maturiD = calendar.get(Calendar.DATE) > 30 ? 30 : calendar.get(Calendar.DATE);
				dsm = (maturiY - settleY)*360 + (maturiM - settleM)*30 + (maturiD - settleD);
			} else {
				dsm = (int)(DateUtil.getExcelDate(maturi) - DateUtil.getExcelDate(settle));
				
			}
			return dsm;
		}
	}
	
	/**
	 * Returns number of days since 1900-1-0; i.e. 1900-1-0 returns 0 and 1900-1-1 returns 1. 
	 * @param date the date to be calculated.
	 * @return number of days since 1900-1-0; i.e. 1900-1-0 returns 0 and 1900-1-1 returns 1.
	 */
	public static Long dateToDays(Date date) {
		// Excel got an error which has 1900-2-29 (in-existing day), its date number is 60
		// We make 60 and 61 map to the same day; i.e. both 60 and 61 map to 1900-3-01.
		long diff = Dates.subtract(date, TZ_GMT, Calendar.DATE, DATE1899_12_30);
		if (DATE1900_3_1.after(date)) --diff;
		return Long.valueOf(diff);
	}
	
	public static double basisToDouble(int basis, Date settle, Date maturi, int dsm) {
		double result = 0.0;
		if(basis == 0 || basis == 2 || basis == 4) {
			result = 360;
		} else if(basis == 1) {
			SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
			Calendar calendar = df.getCalendar();
			calendar.setTime(maturi);
			int mYear = calendar.get(Calendar.YEAR);
			int mMonth = calendar.get(Calendar.MONTH) + 1;
			int mDate = calendar.get(Calendar.DATE);
			calendar.setTime(settle);
			int sYear = calendar.get(Calendar.YEAR);
			int sMonth = calendar.get(Calendar.MONTH) + 1;
			if(dsm > 366) {
				result = 365.5;
			} else {
				if((new java.util.GregorianCalendar().isLeapYear(mYear) && (mMonth > 2 || (mMonth == 2 && mDate == 29))) 
						|| (new java.util.GregorianCalendar().isLeapYear(sYear) && sMonth < 3)) {
					result = 366;
				} else {
					result = 365;				
				}
			}
		} else if(basis == 3) {
			result = 365;
		}
		return result;
	}
	public static String padZero(String num, int places) throws EvaluationException {
		if(places <= 0) {
			throw new EvaluationException(ErrorEval.NUM_ERROR);
		} else {
			StringBuffer result = new StringBuffer();
			for(int i = 0; i < places - num.length(); i++) {
				result.append("0");
			}
			result.append(num);
			return result.toString();
		}
	}
	/**
	 * Replace the String i to i1.
	 * @param complex String of complex.
	 * @return String of a complex.
	 */
	public static String replaceiToi1(String complex, String suffix) {
		int i = complex.indexOf(suffix);
		while(i != -1) {
			if(!Character.isDigit(complex.charAt(i-1))) {
				StringBuffer sf = new StringBuffer(complex);
				complex = sf.replace(i, i+1, "1"+suffix).toString();
			}
			i = complex.indexOf(suffix, i+1);
		}
		return complex;
	}
	/**
	 * Validate given String value, return associated complex.
	 * return {@link ErrorEval#NUM_ERROR} if complex is not a format of complex.
	 * @param complex String value to be validated.
	 * @return complex value.
	 * @throws EvaluationException 
	 */
	public static Complex validateComplex(String complex) throws EvaluationException {
		Complex result = null;
		String s = "i";
		int i = complex.indexOf(s);
		if(i == -1) {
			s = "j";
			i = complex.indexOf(s);
			if(i == -1) {
				s = "k";
			}
		}
		ComplexFormat cf = new ComplexFormat(s);
		complex = replaceiToi1(complex, s);
		try {
			result = cf.parse(complex, s);
		} catch (ParseException e) {
			throw new EvaluationException(ErrorEval.NUM_ERROR);
		}
		return result;
	}
	/**
	 * Convert the complex to complex.
	 * @param c the complex.
	 * @param suffix the suffix.
	 * @return the complex.
	 */
	public static Complex cToComplex(org.apache.commons.math.complex.Complex c, String suffix) {
		Complex result = new Complex(c.getReal(), c.getImaginary(), suffix);
		return result;
	}

	/**
	 * format Complex number to match Excel format
	 * @param c the complex number
	 * @return the formated Complex number that match Excel format
	 */
	public static String format(Complex c) {
		NumberFormat nfr = NumberFormat.getInstance();
		nfr.setMaximumFractionDigits(14);
		NumberFormat nfi = NumberFormat.getInstance();
		nfi.setMaximumFractionDigits(14);
		ComplexFormat cf = new ComplexFormat(nfr, nfi);
		// 20130912 kuroridoplayer@gmail.com. There is no space between operator and numbers in Excel, so replace it.
		return cf.format(c).replace(" ", "");
	}
	/**
	 * Convert an object array to an int array
	 * @param objs an object array
	 * @return int array
	 * @throws EvaluationException 
	 */
	public static int[] toIntArray(Object[] objs) throws EvaluationException{
		int[] in = new int[objs.length];
		for(int i =0; i < objs.length; i++){
			if (objs[i] instanceof ErrorEval) {
				throw new EvaluationException((ErrorEval) objs[i]);
			}else{
				in[i] = ((Number)org.zkoss.lang.Classes.coerce(int.class, objs[i])).intValue();
			}
		}
		return in;
	}
	public static double[][] toDoubleMatrix(ValueEval arg,int srcRowIndex,int srcColumnIndex) throws EvaluationException {
		if (arg instanceof AreaEval) {
			return AreaEvalHelper.toDoubleMatrix(arg, srcRowIndex, srcColumnIndex);
		} else {
			throw new EvaluationException(ErrorEval.VALUE_INVALID);
		}
	}
	/**
	 * Force convert a double to Double with 15 digits precision. Use specified rounding mode
	 * @param dbl the double number to be converted
	 * @param roundingMode the rounding mode when doing the conversion
	 * @return the converted Double with 15 digits precision.
	 */
	private static final int PRECISION_SIZE = 15;
	public static double toDouble15(double dbl, int roundingMode) {
		final boolean neg = dbl < 0;
		final String dbs = ""+dbl;
		BigDecimal bdx = new BigDecimal(dbs); 
		final int sz = dbs.length();
		int dotj = -1;
		int expj = -1;
		int scale = 0;
		if (sz > PRECISION_SIZE) {
			for (int j = 0; j < sz; ++j) {
				final char ch = dbs.charAt(j);
				if ('.' == ch) {
					dotj = j;
				} else if ('E' == ch) {
					expj = j;
					break;
				}
			}
			int useful = expj >= 0 ? expj : sz;
			int expsz = expj >=0 ? Integer.parseInt(dbs.substring(expj+1)) : 0;
			
			if (dotj >= 0) {
				--useful;
			}
			if (neg) {
				--useful;
				--dotj;
			}
			if (useful > PRECISION_SIZE) {
				if (dotj < 0) dotj = useful;
				scale = PRECISION_SIZE - dotj - expsz;
				bdx = bdx.setScale(scale, roundingMode);
			}
		}
		return bdx.doubleValue();
	}

	public static List toList(ValueEval[] args, int srcRowIndex,
			int srcColumnIndex) throws EvaluationException {
		// TODO Auto-generated method stub
		final List result = new ArrayList();
		
		for(int i =0; i < args.length;i++){
			toNumber(result,args[i],srcRowIndex, srcColumnIndex);
		}		
		return result;
	}
	
	private static void toNumber(List result, ValueEval arg, int srcRowIndex,
			int srcColumnIndex) throws EvaluationException {
		if (arg instanceof StringEval) {
			final Double val = stringToDouble(TextFunctionHelper.evaluateStringArg(arg, srcRowIndex, srcColumnIndex), true);
			if(val == null) {
				result.add(Objects.ZERO_DOUBLE);
			} else {
				result.add(val);
			}
		} else if(arg instanceof NumberEval) {
			final Double val = new Double(TextFunctionHelper.evaluateDoubleArg(arg, srcRowIndex, srcColumnIndex));
			result.add(val);
		} else if(arg instanceof BoolEval) {
			final Double val = new Double(((BoolEval)arg).getNumberValue());
			result.add(val);
		} else if(arg instanceof AreaEval) {
			result.addAll(AreaEvalHelper.toDoubleList(arg, srcRowIndex, srcColumnIndex));
		}
	}
	
	/**
	 * Convert a string to double
	 * @param str - input value
	 * @param nullable - flag to indicate the return could be null or not
	 * @return double value
	 * @throws EvaluationException 
	 */
	public static Double stringToDouble(String str, boolean nullable) throws EvaluationException {
		try {
			return new Double(Double.parseDouble(str));
		} catch (NumberFormatException ex) {
			if (nullable) {
				return null;
			} else {
				throw new EvaluationException(ErrorEval.NUM_ERROR);
			}
		}
	}
	/**
	 * Convert a list to a double array
	 * @param ls a list 
	 * @return a double array
	 * @throws EvaluationException 
	 */
	public static double[] toDoubleArray(List ls) throws EvaluationException{
		return toDoubleArray(ls.toArray());
	}
	
	/**
	 * Convert an object array to a double array
	 * @param objs an object array
	 * @return double array
	 * @throws EvaluationException 
	 */
	public static double[] toDoubleArray(Object[] objs) throws EvaluationException{
		double[] da = new double[objs.length];
		//System.out.println(a.length);
		for(int i =0; i < objs.length; i++){
			if (objs[i] instanceof ErrorEval) {
				throw new EvaluationException((ErrorEval) objs[i]);
			} else {
				da[i] = ((Number)org.zkoss.lang.Classes.coerce(double.class, objs[i])).doubleValue();
			}
		}
		return da;
	}
	/**
	 * Return a commons math DescriptiveStatistics class which is contructed from double array d
	 * @param d samples
	 * @return a DescriptiveStatistics class which is contructed from double array d
	 */
	public static DescriptiveStatistics getStats(double[] d){
		// upgrade to common math 2.2, it doesn't have newInstance
		DescriptiveStatistics stats = new DescriptiveStatistics();
		for(int i =0; i < d.length; i++){
			stats.addValue(d[i]);
		}
		return stats;
	}
	/**
	 * Return a commons math Regression class
	 * @param xs x-coordinates
	 * @param ys y-coordinates
	 * @return a commons math Regression class
	 * @throws EvaluationException 
	 */
	public static SimpleRegression getRegre(double[] xs, double[] ys) throws EvaluationException{
		if(xs.length != ys.length){
			throw new EvaluationException(ErrorEval.NA);
		}else{
			SimpleRegression sr = new SimpleRegression();
			for(int i =0; i < xs.length; i++){
				sr.addData(xs[i], ys[i]);
			}
			return sr;
		}
		
	}
	/**
	 * Returns a {@link Date} of a given number of days since 1900-1-1 (inclusive); i.e. returns Date at 1900-1-1 given 1
	 * @param arg number of days since 1900-1-1.
	 * @return Date per the given number of days since 1900-1-1.
	 */
	public static Date daysToDate(int arg) {
		// Excel got an error which has 1900-2-29 (in-existing day), its date number is 60
		// We make 60 and 61 map to the same day; i.e. both 60 and 61 map to 1900-3-01.
		if (arg < 61) ++arg;
		return Dates.add(DATE1899_12_30, TZ_GMT, Calendar.DATE, arg);
	}

}

