/* MathFunctionImpl.java

	Purpose:
		
	Description:
		
	History:
		Oct 05, 2010 4:23:06 PM, Created by ashish

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

*/
package org.zkoss.zssex.formula.fn;

import java.math.BigDecimal;

import org.apache.commons.math.linear.InvalidMatrixException;
import org.apache.commons.math.linear.RealMatrix;
import org.apache.commons.math.linear.RealMatrixImpl;
import org.apache.commons.math.util.MathUtils;
import org.zkoss.poi.ss.formula.eval.ErrorEval;
import org.zkoss.poi.ss.formula.eval.EvaluationException;
import org.zkoss.poi.ss.formula.eval.StringEval;
import org.zkoss.poi.ss.formula.eval.ValueEval;
import org.zkoss.poi.ss.formula.functions.Function;
import org.zkoss.poi.ss.formula.functions.NumericFunction;
import org.zkoss.poi.ss.formula.functions.TextFunction;
import org.zkoss.poi.ss.formula.functions.TextFunctionHelper;
import org.zkoss.poi.ss.formula.functions.UtilFns;
import org.zkoss.xel.fn.CommonFns;


/**
 * @author ashish
 *
 */
public class MathFunctionImpl {
	
	/* Returns double factorial */
	private static int doubleFactorial(int n){
		if(n <= 1)
			return 1;
		else if(n == 2)
			return 2;
		else
			return n*doubleFactorial(n-2);
	}

	public static final Function FACTDOUBLE = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {

			double n = NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex);
			
			if(n < 0)
				throw new EvaluationException(ErrorEval.NUM_ERROR);
			else{
				return doubleFactorial((int)n);
			}
			
		}
	};
	
	/**
	 * Returns the greatest common divisor
	 * @param m - number 1
	 * @param n - number 2
	 * @return the greatest common divisor 
	 * @throws EvaluationException 
	 */
	public static int gcd(int m, int n) throws EvaluationException {
		if(m < 0 || n < 0) {
			throw new EvaluationException(ErrorEval.NUM_ERROR);
		}
		return MathUtils.gcd(m, n);
    }
	
	public static final Function GCD = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			return gcd((int) NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex),
					(int) NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex));
		}
	};

	
	public static final Function LCM = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			int m = (int) NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex);
			int n = (int) NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex);
			return m * n / gcd(m,n);
		}
	};
	
	public static final Function MDETERM = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			double[][] d = UtilFns.toDoubleMatrix(args[0],srcRowIndex, srcColumnIndex);
			//not a valid square matrix, return #VALUE!
			if(d.length != d[0].length)  
			{
				throw new EvaluationException(ErrorEval.VALUE_INVALID);
			} else {
				RealMatrix m = new RealMatrixImpl(d);
				try{
					double result = m.getDeterminant();
					return UtilFns.toDouble15(result, BigDecimal.ROUND_HALF_UP);
				}
				//shouldn't happen to run into this exception. because we already check the matrix if it is square.
				catch(InvalidMatrixException e)
				{
					throw new EvaluationException(ErrorEval.VALUE_INVALID);
				}
				
			}
		}
	};

	public static final Function MINVERSE = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			double[][] array = UtilFns.toDoubleMatrix(args[0],srcRowIndex, srcColumnIndex);
			if(array.length != array[0].length)  
			{
				throw new EvaluationException(ErrorEval.VALUE_INVALID);
			} else {
				RealMatrix m = new RealMatrixImpl(array);
				try{
					RealMatrix invM = m.inverse();
					Double result[][] = new Double[invM.getRowDimension()][invM.getColumnDimension()];
					for(int row =0; row < invM.getRowDimension(); row++)
					{
						for(int column =0; column < invM.getColumnDimension(); column++)
						{
							// put the answer to return matrix and use toDouble15() to get around the floating-point arithm conversion problem.	
							result[row][column] = UtilFns.toDouble15(invM.getEntry(row, column), BigDecimal.ROUND_HALF_UP);
						}
					}
					return result[0][0];
				}
				// non-invertible square matrix
				catch(InvalidMatrixException e)
				{
					throw new EvaluationException(ErrorEval.VALUE_INVALID);
				}
			}
		}
	};
	
	public static final Function MMULT = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			double[][] array1 = UtilFns.toDoubleMatrix(args[0],srcRowIndex, srcColumnIndex);
			double[][] array2 = UtilFns.toDoubleMatrix(args[1],srcRowIndex, srcColumnIndex);
			if(array1[0].length != array2.length)  
			{
				throw new EvaluationException(ErrorEval.VALUE_INVALID);
			} else {
				RealMatrix m1 = new RealMatrixImpl(array1);
				RealMatrix m2 = new RealMatrixImpl(array2);

				try{
					//now do the really matrix multiplication.
					RealMatrix mmultResult = m1.multiply(m2);
					Double result[][] = new Double[mmultResult.getRowDimension()][mmultResult.getColumnDimension()];
		
					for(int row =0; row < mmultResult.getRowDimension(); row++)
					{
						for(int column =0; column < mmultResult.getColumnDimension(); column++)
						{
							// put the answer to return matrix and use toDouble15() to get around the floating-point arithm conversion problem.	
							result[row][column] = UtilFns.toDouble15(mmultResult.getEntry(row, column), BigDecimal.ROUND_HALF_UP);
						}
					}
					return result[0][0];
				}
				//shouldn't happen to run into this exception. because the dimensions had been checked above.
				catch(InvalidMatrixException e)
				{
					throw new EvaluationException(ErrorEval.VALUE_INVALID);
				}
				
			}
		}
	};
	
	public static final Function MROUND = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			double number = NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex);
			double multiple = NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex);
			//if number and multiple have different sign, MROUND returns #NUM! error.
			if((number > 0 && multiple < 0) || (number < 0 && multiple > 0))
			{
				throw new EvaluationException(ErrorEval.NUM_ERROR);
			}
			else
			{
				if(multiple == 0)//weird result for MSFT Excel compatible.
				{
					return 0;
				}
				else
				{
					return multiple * Math.floor(number / multiple + 0.5);
				}
			}
		}
	};
	
	/* Returns factorial */
	private static int factorial(int n){
		if(n <= 1)
			return 1;
		else
			return n * factorial(n-1);
	}

	public static final Function MULTINOMIAL = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			int[] number = new int[args.length];
			for(int i =0; i < args.length; i++){
				if (args[i] instanceof ErrorEval) {
					throw new EvaluationException((ErrorEval) args[i]);
				} else {
					number[i] = (int)NumericFunction.singleOperandEvaluate(args[i], srcRowIndex, srcColumnIndex);
				}
			}

			int sum = 0;
			int product = 1;
			for(int i = 0; i < number.length; ++i) {
				if(number[i] < 0 || number[i] >= 255) {
					throw new EvaluationException(ErrorEval.NUM_ERROR);
				}
				sum += number[i];
				product *= factorial(number[i]);
			}
			return (factorial(sum)/product);
		}
	};
	
	public static final Function QUOTIENT = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			double n2 = (int) NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex);
			if (n2 == 0) {
				throw new EvaluationException(ErrorEval.DIV_ZERO);
			}
			double n1 = (int) NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex);
			return Math.rint(n1 / n2);
		}
	};
	
	public static final Function RANDBETWEEN = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			double bottom = NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex);
			double top = NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex);
			if (bottom > top) {
				throw new EvaluationException(ErrorEval.NUM_ERROR);
			}
			
			return Math.floor(bottom + Math.random() * (top - bottom));
		}
	};
	public static final Function ROMAN = new TextFunction() {

		@Override
		protected ValueEval evaluateFunc(ValueEval[] args, int srcCellRow,
				int srcCellCol) throws EvaluationException {
			// Roman numerical explain:
			// http://www.web40571.clarahost.co.uk/roman/howtheywork.htm

			int numberHA = TextFunctionHelper.evaluateIntArg(args[0],
					srcCellRow, srcCellCol);
			int form = 0;
			int paraNum = args.length;
			if (paraNum == 2) {
				form = TextFunctionHelper.evaluateIntArg(args[1], srcCellRow,
						srcCellCol);
			}
			// if the input is negative or > 3999, or wrong style parameter,
			// return #VALUE! as MSFT Excel.
			if (numberHA < 0 || numberHA > 3999 || form < 0 || form > 4) {
				throw new EvaluationException(ErrorEval.VALUE_INVALID);
			} else {
				// convert Arabic numerical to Roman.
				char[] unit = { 'I', 'X', 'C', 'M' };
				char[] five = { 'V', 'L', 'D' };
				char[] result = new char[16]; // Accumulator for results
				short resultPosn = 15; // Right-to-left index
				short digit; // Current digit
				boolean five_sw; // Cur. digit needs initial five
				short posn;
				for (posn = 0; numberHA > 0; posn++, numberHA /= 10) {
					digit = (short) (numberHA % 10);

					if (digit == 9) {
						result[resultPosn--] = unit[posn + 1];
						digit = 1;
					} else if (digit == 4) {
						result[resultPosn--] = five[posn];
						digit = 1;
					}

					five_sw = (digit >= 5);
					digit %= 5; // Number of units in this position
					while (digit-- > 0)
						result[resultPosn--] = unit[posn];

					if (five_sw)
						result[resultPosn--] = five[posn];
				}
				String numberRoman = new String(result)
						.substring(resultPosn + 1);
				// using default "Classic" style
				if (paraNum == 1 || form == 0)// ||
												// CommonFns.toBoolean(args[1]))
				{
					return new StringEval(numberRoman);
				} else {
					/*
					 * MSFT Excel Roman simplify rule: (don't know why it would
					 * be like this.) use ROMAN(499) , ROMAN(499,1) ,
					 * ROMAN(499,2) , ROMAN(499,3) , ROMAN(499,4) to verify.
					 * =CDXCIX =LDVLIV =XDIX =VDIV =ID =ROMAN(499,0)
					 * =ROMAN(499,false) =ROMAN(499,ture)
					 * 
					 * <1> for all simplified format(form > 0): XLV => VL XCV =>
					 * VC CDL => LD CML => LM CMVC => LMVL
					 */

					if (numberRoman.contains("XLV")) {
						numberRoman = numberRoman.replaceAll("XLV", "VL");
					}
					if (numberRoman.contains("XCV")) {
						numberRoman = numberRoman.replaceAll("XCV", "VC");
					}
					if (numberRoman.contains("CDL")) {
						numberRoman = numberRoman.replaceAll("CDL", "LD");
					}
					if (numberRoman.contains("CML")) {
						numberRoman = numberRoman.replaceAll("CML", "LM");
					}
					if (numberRoman.contains("CMVC")) {
						numberRoman = numberRoman.replaceAll("CMVC", "LMVL");
					}

					/*
					 * <2> for form == 1: CDXC => LDXL CDVC => LDVL CMXC => LMXL
					 * XCIX => VCIV XLIX => VLIV
					 */
					if (form == 1) {
						if (numberRoman.contains("CDXC")) {
							numberRoman = numberRoman
									.replaceAll("CDXC", "LDXL");
						}
						if (numberRoman.contains("CDVC")) {
							numberRoman = numberRoman
									.replaceAll("CDVC", "LDVL");
						}
						if (numberRoman.contains("CMXC")) {
							numberRoman = numberRoman
									.replaceAll("CMXC", "LMXL");
						}
						if (numberRoman.contains("XCIX")) {
							numberRoman = numberRoman
									.replaceAll("XCIX", "VCIV");
						}
						if (numberRoman.contains("XLIX")) {
							numberRoman = numberRoman
									.replaceAll("XLIX", "VLIV");
						}
					}

					/*
					 * <3> for form > 1: XLIX => IL XCIX => IC CDXC => XD CDVC
					 * => XDV CDIC => XDIX LMVL => XMV CMIC => XMIX CMXC => XM
					 */
					if (form > 1) {
						if (numberRoman.contains("XLIX")) {
							numberRoman = numberRoman.replaceAll("XLIX", "IL");
						}
						if (numberRoman.contains("XCIX")) {
							numberRoman = numberRoman.replaceAll("XCIX", "IC");
						}
						if (numberRoman.contains("CDXC")) {
							numberRoman = numberRoman.replaceAll("CDXC", "XD");
						}
						if (numberRoman.contains("CDVC")) {
							numberRoman = numberRoman.replaceAll("CDVC", "XDV");
						}
						if (numberRoman.contains("CDIC")) {
							numberRoman = numberRoman
									.replaceAll("CDIC", "XDIX");
						}
						if (numberRoman.contains("LMVL")) {
							numberRoman = numberRoman.replaceAll("LMVL", "XMV");
						}
						if (numberRoman.contains("CMIC")) {
							numberRoman = numberRoman
									.replaceAll("CMIC", "XMIX");
						}
						if (numberRoman.contains("CMXC")) {
							numberRoman = numberRoman.replaceAll("CMXC", "XM");
						}
					}

					/*
					 * for form > 2: XDV => VD XDIX => VDIV XMV => VM XMIX =>
					 * VMIV
					 */
					if (form > 2) {
						if (numberRoman.contains("XDV")) {
							numberRoman = numberRoman.replaceAll("XDV", "VD");
						}
						if (numberRoman.contains("XDIX")) {
							numberRoman = numberRoman
									.replaceAll("XDIX", "VDIV");
						}
						if (numberRoman.contains("XMV")) {
							numberRoman = numberRoman.replaceAll("XMV", "VM");
						}
						if (numberRoman.contains("XMIX")) {
							numberRoman = numberRoman
									.replaceAll("XMIX", "VMIV");
						}
					}
					/*
					 * for form == 4 (the most Simplified format.): VDIV => ID
					 * VMIV => IM
					 */
					if (form == 4 || !CommonFns.toBoolean(args[1])) //
					{
						if (numberRoman.contains("VDIV")) {
							numberRoman = numberRoman.replaceAll("VDIV", "ID");
						}
						if (numberRoman.contains("VMIV")) {
							numberRoman = numberRoman.replaceAll("VMIV", "IM");
						}
					}
					return new StringEval(numberRoman);
				}
			}
		}
	};
	
	public static final Function SQRTPI = new NumericFunction() {

		@Override
		public double eval(ValueEval[] args, int srcRowIndex, int srcColumnIndex) throws EvaluationException {
			double number =  NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex);
			if(number < 0) {
				throw new EvaluationException(ErrorEval.NUM_ERROR);
			} else {
				return Math.sqrt(number*Math.PI);
			}
		}
	};

}
