/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
==================================================================== */

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

import org.apache.poi.ss.util.NumberToTextConverter;
import org.apache.poi.util.Internal;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

/**
 * This class is an extension to the standard math library
 * provided by java.lang.Math class. It follows the Math class
 * in that it has a private constructor and all static methods.
 */
final class MathX {

    private MathX() {
        // no instances of this class
    }


    /**
     * Returns a value rounded to p digits after decimal.
     * If p is negative, then the number is rounded to
     * places to the left of the decimal point. eg.
     * 10.23 rounded to -1 will give: 10. If p is zero,
     * the returned value is rounded to the nearest integral
     * value.
     * <p>If n is negative, the resulting value is obtained
     * as the round value of absolute value of n multiplied
     * by the sign value of n (@see MathX.sign(double d)).
     * Thus, -0.6666666 rounded to p=0 will give -1 not 0.
     * <p>If n is NaN, returned value is NaN.
     * @param n
     * @param p
     */
    public static double round(double n, int p) {
        return round(n, p, java.math.RoundingMode.HALF_UP);
    }

    public static double round(double n, double p) {
        return round(n, (int)p);
    }



    /**
     * Returns a value rounded-up to p digits after decimal.
     * If p is negative, then the number is rounded to
     * places to the left of the decimal point. eg.
     * 10.23 rounded to -1 will give: 20. If p is zero,
     * the returned value is rounded to the nearest integral
     * value.
     * <p>If n is negative, the resulting value is obtained
     * as the round-up value of absolute value of n multiplied
     * by the sign value of n (@see MathX.sign(double d)).
     * Thus, -0.2 rounded-up to p=0 will give -1 not 0.
     * <p>If n is NaN, returned value is NaN.
     * @param n
     * @param p
     */
    public static double roundUp(double n, int p) {
        return round(n, p, java.math.RoundingMode.UP);
    }

    public static double roundUp(double n, double p) {
        return roundUp(n, (int)p);
    }


    /**
     * Returns a value rounded to p digits after decimal.
     * If p is negative, then the number is rounded to
     * places to the left of the decimal point. eg.
     * 10.23 rounded to -1 will give: 10. If p is zero,
     * the returned value is rounded to the nearest integral
     * value.
     * <p>If n is negative, the resulting value is obtained
     * as the round-up value of absolute value of n multiplied
     * by the sign value of n (@see MathX.sign(double d)).
     * Thus, -0.8 rounded-down to p=0 will give 0 not -1.
     * <p>If n is NaN, returned value is NaN.
     * @param n
     * @param p
     */
    public static double roundDown(double n, int p) {
        return round(n, p, java.math.RoundingMode.DOWN);
    }

    public static double roundDown(double n, double p) {
        return roundDown(n, (int)p);
    }

    private static double round(double n, int p, java.math.RoundingMode rounding) {
        if (Double.isNaN(n) || Double.isInfinite(n)) {
            return Double.NaN;
        }
        else {
            final String excelNumber = NumberToTextConverter.toText(n);
            return new java.math.BigDecimal(excelNumber).setScale(p, rounding).doubleValue();
        }
    }


    /**
     * If d < 0, returns short -1
     * <br>
     * If d > 0, returns short 1
     * <br>
     * If d == 0, returns short 0
     * <p> If d is NaN, then 1 will be returned. It is the responsibility
     * of caller to check for d isNaN if some other value is desired.
     * @param d
     */
    public static short sign(double d) {
        return (short) ((d == 0)
                ? 0
                : (d < 0)
                        ? -1
                        : 1);
    }

    /**
     * average of all values
     * @param values
     */
    public static double average(double[] values) {
        double ave = 0;
        double sum = 0;
        for (double value : values) {
            sum += value;
        }
        ave = sum / values.length;
        return ave;
    }

    /**
     * sum of all values
     * @param values
     */
    public static double sum(double[] values) {
        double sum = 0;
        for (double value : values) {
            sum += value;
        }
        return sum;
    }

    /**
     * sum of squares of all values
     * @param values
     */
    public static double sumsq(double[] values) {
        double sumsq = 0;
        for (double value : values) {
            sumsq += value * value;
        }
        return sumsq;
    }


    /**
     * product of all values
     * @param values
     */
    public static double product(double[] values) {
        double product = 0;
        if (values!=null && values.length > 0) {
            product = 1;
            for (double value : values) {
                product *= value;
            }
        }
        return product;
    }

    /**
     * min of all values. If supplied array is zero length,
     * Double.POSITIVE_INFINITY is returned.
     * @param values
     */
    public static double min(double[] values) {
        double min = Double.POSITIVE_INFINITY;
        for (double value : values) {
            min = Math.min(min, value);
        }
        return min;
    }

    /**
     * min of all values. If supplied array is zero length,
     * Double.NEGATIVE_INFINITY is returned.
     * @param values
     */
    public static double max(double[] values) {
        double max = Double.NEGATIVE_INFINITY;
        for (double value : values) {
            max = Math.max(max, value);
        }
        return max;
    }

    /**
     * Note: this function is different from java.lang.Math.floor(..).
     * <p>
     * When n and s are "valid" arguments, the returned value is: Math.floor(n/s) * s;
     * <br>
     * n and s are invalid if any of following conditions are true:
     * <ul>
     * <li>s is zero</li>
     * <li>n is negative and s is positive</li>
     * <li>n is positive and s is negative</li>
     * </ul>
     * In all such cases, Double.NaN is returned.
     * @param n
     * @param s
     */
    public static double floor(double n, double s) {
        if (s==0 && n!=0) {
            return Double.NaN;
        } else {
            if (n == 0.0 || s == 0.0) {
                return 0.0;
            } else if (s == 1.0) {
                return Math.floor(n);
            } else if (s < 0.0 && n >= 0.0) {
                return Double.NaN;
            } else {
                return scaledRoundUsingBigDecimal(n, s, RoundingMode.FLOOR);
            }
        }
    }

    /**
     * Note: this function is different from java.lang.Math.ceil(..).
     * <p>
     * When n and s are "valid" arguments, the returned value is: Math.ceiling(n/s) * s;
     * <br>
     * n and s are invalid if any of following conditions are true:
     * <ul>
     * <li>s is zero</li>
     * <li>n is negative and s is positive</li>
     * <li>n is positive and s is negative</li>
     * </ul>
     * In all such cases, Double.NaN is returned.
     * @param n
     * @param s
     */
    public static double ceiling(double n, double s) {
        if (n>0 && s<0) {
            return Double.NaN;
        } else {
            if (n == 0.0 || s == 0.0) {
                return 0.0;
            } else if (s == 1.0) {
                return Math.ceil(n);
            } else {
                return scaledRoundUsingBigDecimal(n, s, RoundingMode.CEILING);
            }
        }
    }

    @Internal
    public static double scaledRoundUsingBigDecimal(double xval, double multiplier, RoundingMode mode) {
        BigDecimal multiplierDecimal = BigDecimal.valueOf(multiplier);
        BigDecimal bd = BigDecimal.valueOf(xval).divide(multiplierDecimal, MathContext.DECIMAL128)
                .setScale(0, mode)
                .multiply(multiplierDecimal);
        return bd.doubleValue();
    }

    /**
     * <br> for all n >= 1; factorial n = n * (n-1) * (n-2) * ... * 1
     * <br> else if n == 0; factorial n = 1
     * <br> else if n < 0; factorial n = Double.NaN
     * <br> Loss of precision can occur if n is large enough.
     * If n is large so that the resulting value would be greater
     * than Double.MAX_VALUE; Double.POSITIVE_INFINITY is returned.
     * If n < 0, Double.NaN is returned.
     * @param n
     */
    public static double factorial(int n) {
        double d = 1;

        if (n >= 0) {
            if (n <= 170) {
                for (int i=1; i<=n; i++) {
                    d *= i;
                }
            }
            else {
                d = Double.POSITIVE_INFINITY;
            }
        }
        else {
            d = Double.NaN;
        }
        return d;
    }

    public static double factorial(double d) {
        return factorial((int)d);
    }


    /**
     * returns the remainder resulting from operation:
     * n / d.
     * <br> The result has the sign of the divisor.
     * <br> Examples:
     * <ul>
     * <li>mod(3.4, 2) = 1.4</li>
     * <li>mod(-3.4, 2) = 0.6</li>
     * <li>mod(-3.4, -2) = -1.4</li>
     * <li>mod(3.4, -2) = -0.6</li>
     * </ul>
     * If d == 0, result is NaN
     * @param n
     * @param d
     */
    public static double mod(double n, double d) {
        if (d == 0) {
            return Double.NaN;
        }
        else if (sign(n) == sign(d)) {
            return n % d;
        }
        else {
            return ((n % d) + d) % d;
        }
    }

    /**
     * inverse hyperbolic cosine
     * @param d
     */
    public static double acosh(double d) {
        return Math.log(Math.sqrt(Math.pow(d, 2) - 1) + d);
    }

    /**
     * inverse hyperbolic sine
     * @param d
     */
    public static double asinh(double d) {
        return Math.log(Math.sqrt(d*d + 1) + d);
    }

    /**
     * inverse hyperbolic tangent
     * @param d
     */
    public static double atanh(double d) {
        return Math.log((1 + d)/(1 - d)) / 2;
    }

    /**
     * hyperbolic cosine
     * @param d
     */
    public static double cosh(double d) {
        double ePowX = Math.pow(Math.E, d);
        double ePowNegX = Math.pow(Math.E, -d);
        return (ePowX + ePowNegX) / 2;
    }

    /**
     * hyperbolic sine
     * @param d
     */
    public static double sinh(double d) {
        double ePowX = Math.pow(Math.E, d);
        double ePowNegX = Math.pow(Math.E, -d);
        return (ePowX - ePowNegX) / 2;
    }

    /**
     * hyperbolic tangent
     * @param d
     */
    public static double tanh(double d) {
        double ePowX = Math.pow(Math.E, d);
        double ePowNegX = Math.pow(Math.E, -d);
        return (ePowX - ePowNegX) / (ePowX + ePowNegX);
    }


    /**
     * returns the total number of combinations possible when
     * k items are chosen out of total of n items. If the number
     * is too large, loss of precision may occur (since returned
     * value is double). If the returned value is larger than
     * Double.MAX_VALUE, Double.POSITIVE_INFINITY is returned.
     * If either of the parameters is negative, Double.NaN is returned.
     * @param n
     * @param k
     */
    public static double nChooseK(int n, int k) {
        double d = 1;
        if (k < 0 || n < k) {
            d= Double.NaN;
        }
        else {
            int minnk = Math.min(n-k, k);
            int maxnk = Math.max(n-k, k);
            for (int i=maxnk; i<n; i++) {
                d *= i+1;
            }
            d /= factorial(minnk);
        }

        return d;
    }
}
