/*******************************************************************************
 * Copyright 2019 2Fi Business Solutions Ltd.
 *
 * This code is part of the Core project.
 *
 * This code is copyrighted. Under no circumstances should any party, people,
 * or organization should redistribute any portions of this code in any form,
 * either verbatim or through electronic media, to any third parties, unless
 * under explicit written permission by 2Fi Business Solutions Ltd.
 ******************************************************************************/
package com.ffii.core.utils;

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

import org.apache.commons.lang3.ArrayUtils;

/**
 * NumberUtils extends from Apache Commons, and incl some methods from MathUtils
 *
 * @author Patrick
 */
public abstract class NumberUtils extends org.apache.commons.lang3.math.NumberUtils {

	/**
	 * Round the given value to the specified number of decimal places. The value is rounded using the given method which is any method defined in
	 * {@link BigDecimal}.
	 *
	 * @param x
	 *            the value to round
	 * @param scale
	 *            the number of digits to the right of the decimal point
	 * @param roundingMethod
	 *            the rounding method as defined in {@link RoundingMode}
	 * @return the rounded value
	 */
	public static double round(double x, int scale, RoundingMode roundingMethod) {
		try {
			return BigDecimal.valueOf(x).setScale(scale, roundingMethod).doubleValue();
		} catch (NumberFormatException ex) {
			if (Double.isInfinite(x)) {
				return x;
			} else {
				return Double.NaN;
			}
		}
	}

	/**
	 * Round the given value to the specified number of decimal places. The value is rounded using the {@link RoundingMode#HALF_UP} method.
	 *
	 * @param x
	 *            the value to round
	 * @param scale
	 *            the number of digits to the right of the decimal point
	 * @return the rounded value
	 * @see org.apache.commons.math.util.MathUtils#round(double, int)
	 */
	public static double round(double x, int scale) {
		return round(x, scale, RoundingMode.HALF_UP);
	}

	/**
	 * Round up the given value to the specified number of decimal places. The value is rounded up using the {@link RoundingMode#UP} method.
	 *
	 * @param x
	 *            the value to round up
	 * @param scale
	 *            the number of digits to the right of the decimal point
	 * @return the rounded up value
	 */
	public static double roundUp(double x, int scale) {
		return round(x, scale, RoundingMode.UP);
	}

	/**
	 * Round down the given value to the specified number of decimal places. The value is rounded down using the {@link RoundingMode#DOWN} method.
	 *
	 * @param x
	 *            the value to round down
	 * @param scale
	 *            the number of digits to the right of the decimal point
	 * @return the rounded down value
	 */
	public static double roundDown(double x, int scale) {
		return round(x, scale, RoundingMode.DOWN);
	}

	/**
	 * Return the {@code int} value of an {@code Object}, or the default value if the object is either {@code null} or not an instance of {@code Number}.
	 *
	 * @param obj
	 *            the {@code Object}
	 * @param defaultValue
	 *            the default value
	 * @return the {@code int} value of the {@code Object}, or the default if the object is either {@code null} or not an instance of {@code Number}
	 * @see Number#intValue()
	 */
	public static int intValue(Object obj, int defaultValue) {
		if (obj instanceof Number) {
			return ((Number) obj).intValue();
		} else if (obj instanceof String) {
			try {
				return Integer.parseInt((String) obj);
			} catch (NumberFormatException nfe) {
			}
		}
		return defaultValue;
	}

	/**
	 * Return the {@code int} value of an {@code Object}, or {@code zero} if the object is either {@code null} or not an instance of {@code Number}.
	 *
	 * @param obj
	 *            the {@code Object}
	 * @return the {@code int} value of the {@code Object}, or {@code zero} if the object is either {@code null} or not an instance of {@code Number}
	 * @see Number#intValue()
	 */
	public static int intValue(Object obj) {
		return intValue(obj, 0);
	}

	/**
	 * Convert an {@code Object} to an {@code Integer} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the
	 * conversion fails.
	 * <p>
	 * If the object is {@code null}, the default value is returned.
	 *
	 * @param obj
	 *            the object to convert, may be {@code null}
	 * @param defaultValue
	 *            the default value, may be {@code null}
	 * @return the Integer represented by the object, or the default if conversion fails
	 */
	public static Integer toInt(Object obj, Integer defaultValue) {
		if (obj instanceof Number)
			return Integer.valueOf(((Number) obj).intValue());
		else if (obj instanceof String)
			try {
				return Integer.valueOf((String) obj);
			} catch (NumberFormatException nfe) {
				return defaultValue;
			}
		else
			return defaultValue;
	}

	/**
	 * Return the {@code long} value of a {@code Long} object, or a default value if the object is {@code null}.
	 *
	 * @param obj
	 *            the object (can be {@code null})
	 * @param defaultValue
	 *            the default value
	 * @return the {@code long} value of the object if it's a number, or the default value if the object is {@code null} or {@code NaN}
	 * @see Long#longValue()
	 */
	public static long longValue(Object obj, long defaultValue) {
		return (obj instanceof Number) ? ((Number) obj).longValue() : defaultValue;
	}

	/**
	 * Return the {@code long} value of a {@code Long} object, or {@code zero} if the object is {@code null}.
	 *
	 * @param obj
	 *            the object (can be {@code null})
	 * @return the {@code long} value of the object if it's a number, or {@code zero} if the object is {@code null} or {@code NaN}
	 * @see Long#longValue()
	 */
	public static long longValue(Object obj) {
		return longValue(obj, 0l);
	}

	/**
	 * Convert an {@code Object} to a {@code Long} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the
	 * conversion fails.
	 * <p>
	 * If the object is {@code null}, the default value is returned.
	 *
	 * @param obj
	 *            the object to convert, may be {@code null}
	 * @param defaultValue
	 *            the default value, may be {@code null}
	 * @return the Long represented by the object, or the default if conversion fails
	 */
	public static Long toLong(Object obj, Long defaultValue) {
		if (obj instanceof Number)
			return Long.valueOf(((Number) obj).longValue());
		else if (obj instanceof String)
			try {
				return Long.valueOf((String) obj);
			} catch (NumberFormatException nfe) {
				return defaultValue;
			}
		else
			return defaultValue;
	}

	/**
	 * @param obj
	 *            the object (can be {@code null})
	 * @param defaultValue
	 *            the default value
	 * @return the {@code double} value of the object if it's a number, or the default value if the object is {@code null} or {@code NaN}
	 * @see Double#doubleValue()
	 */
	public static double doubleValue(Object obj, double defaultValue) {
		return (obj instanceof Number) ? ((Number) obj).doubleValue() : defaultValue;
	}

	/**
	 * @param obj
	 *            the object (can be {@code null})
	 * @param defaultValue
	 *            the default value
	 * @return the {@code double} value of the object if it's a number, or {@code zero} if the object is {@code null} or {@code NaN}
	 * @see Double#doubleValue()
	 */
	public static double doubleValue(Object obj) {
		return doubleValue(obj, 0.0d);
	}

	/**
	 * Convert an {@code Object} to a {@code Double} (only if the object is an instance of {@code Number} or {@code String}), returning a default value if the
	 * conversion fails.
	 * <p>
	 * If the object is {@code null}, the default value is returned.
	 *
	 * @param obj
	 *            the object to convert, may be {@code null}
	 * @param defaultValue
	 *            the default value, may be {@code null}
	 * @return the Double represented by the object, or the default if conversion fails
	 */
	public static Double toDouble(Object obj, Double defaultValue) {
		if (obj instanceof Number)
			return Double.valueOf(((Number) obj).doubleValue());
		else if (obj instanceof String)
			try {
				return Double.valueOf((String) obj);
			} catch (NumberFormatException nfe) {
				return defaultValue;
			}
		else
			return defaultValue;
	}

	/**
	 * Return the {@code BigDecimal} object, or {@code zero} if the object is {@code null}.
	 *
	 * @param obj
	 *            the {@code BigDecimal} object
	 * @return the {@code BigDecimal} object, or {@code zero} if the object is {@code null}
	 */
	public static BigDecimal decimalValue(BigDecimal obj) {
		return decimalValue(obj, BigDecimal.ZERO);
	}

	/**
	 * Return the {@code BigDecimal} object, or a default value if the object is {@code null}.
	 *
	 * @param obj
	 *            the {@code BigDecimal} object
	 * @param defaultValue
	 *            the default value
	 * @return the {@code BigDecimal} object, or the default if the object is {@code null}
	 */
	public static BigDecimal decimalValue(BigDecimal obj, BigDecimal defaultValue) {
		return obj == null ? defaultValue : obj;
	}

	/**
	 * Convert an {@code Object} to a {@code BigDecimal}, returning {@code BigDecimal.ZERO} if the conversion fails (e.g. the object is not an instance of
	 * {@code Number} nor {@code String}).
	 * <p>
	 * If the object is {@code null}, {@code BigDecimal.ZERO} is returned.
	 *
	 * @param obj
	 *            the object to convert, may be {@code null}
	 * @return the BigDecimal represented by the object, or {@code BigDecimal.ZERO} if conversion fails
	 */
	public static BigDecimal toDecimal(Object obj) {
		return toDecimal(obj, BigDecimal.ZERO);
	}

	/**
	 * Convert an {@code Object} to a {@code BigDecimal}, returning a default value if the conversion fails (e.g. the object is not an instance of
	 * {@code Number} nor {@code String}).
	 * <p>
	 * If the object is {@code null}, the default value is returned.
	 *
	 * @param obj
	 *            the object to convert, may be {@code null}
	 * @param defaultValue
	 *            the default value, may be {@code null}
	 * @return the BigDecimal represented by the object, or the default if conversion fails
	 */
	public static BigDecimal toDecimal(Object obj, BigDecimal defaultValue) {
		if (obj instanceof BigDecimal)
			return (BigDecimal) obj;
		else if (obj instanceof Number)
			return BigDecimal.valueOf(((Number) obj).doubleValue());
		else if (obj instanceof String)
			try {
				return new BigDecimal((String) obj);
			} catch (NumberFormatException nfe) {
				return defaultValue;
			}
		else
			return defaultValue;
	}

	/**
	 * Null-safe method to check if the two {@code Integer} objects have the same value.
	 *
	 * <ol>
	 * <li>Returns {@code true} if {@code a} and {@code b} are both {@code null}.
	 * <li>Returns {@code false} if only one of them is {@code null}.
	 * <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have the same {@code int} value, else returns {@code false}.
	 * </ol>
	 *
	 * @param a
	 *            Integer obj, may be {@code null}
	 * @param b
	 *            Integer obj, may be {@code null}
	 */
	public static boolean isEqual(Integer a, Integer b) {
		return a == null ? (b == null ? true : false) : (b == null ? false : a.equals(b));
	}

	/**
	 * Null-safe method to check if the two {@code Integer} objects have different values.
	 *
	 * <ol>
	 * <li>Returns {@code false} if {@code a} and {@code b} are both {@code null}.
	 * <li>Returns {@code true} if only one of them is {@code null}.
	 * <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have different {@code int} values, else returns {@code false}.
	 * </ol>
	 *
	 * @param a
	 *            Integer obj, may be {@code null}
	 * @param b
	 *            Integer obj, may be {@code null}
	 */
	public static boolean isNotEqual(Integer a, Integer b) {
		return !isEqual(a, b);
	}

	/**
	 * Null-safe method to check if the two {@code BigDecimal} objects have the same value.
	 * <p>
	 * Two {@code BigDecimal} objects that are equal in value but have a different scale (like 2.0 and 2.00) are considered equal by this method.
	 *
	 * <ol>
	 * <li>Returns {@code true} if {@code a} and {@code b} are both {@code null}.
	 * <li>Returns {@code false} if only one of them is {@code null}.
	 * <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have the same {@code decimal} value, else returns {@code false}.
	 * </ol>
	 *
	 * @param a
	 *            BigDecimal obj, may be {@code null}
	 * @param b
	 *            BigDecimal obj, may be {@code null}
	 */
	public static boolean isEqual(BigDecimal a, BigDecimal b) {
		return a == null ? (b == null ? true : false) : (b == null ? false : a.compareTo(b) == 0);
	}

	/**
	 * Null-safe method to check if the two {@code BigDecimal} objects have different values.
	 * <p>
	 * Two {@code BigDecimal} objects that are equal in value but have a different scale (like 2.0 and 2.00) are considered equal by this method.
	 *
	 * <ol>
	 * <li>Returns {@code false} if {@code a} and {@code b} are both {@code null}.
	 * <li>Returns {@code true} if only one of them is {@code null}.
	 * <li>Returns {@code true} if {@code a} and {@code b} are not {@code null} and have different {@code decimal} values, else returns {@code false}.
	 * </ol>
	 *
	 * @param a
	 *            BigDecimal obj, may be {@code null}
	 * @param b
	 *            BigDecimal obj, may be {@code null}
	 */
	public static boolean isNotEqual(BigDecimal a, BigDecimal b) {
		return !isEqual(a, b);
	}

	/**
	 * Check if {@code BigDecimal} object {@code a} is greater than {@code BigDecimal} object {@code b}.
	 *
	 * @param a
	 *            non-{@code null} BigDecimal obj
	 * @param b
	 *            non-{@code null} BigDecimal obj
	 */
	public static boolean isGreaterThan(BigDecimal a, BigDecimal b) {
		return a.compareTo(b) > 0;
	}

	/**
	 * Check if {@code BigDecimal} object {@code a} is greater than or equals to {@code BigDecimal} object {@code b}.
	 *
	 * @param a
	 *            non-{@code null} BigDecimal obj
	 * @param b
	 *            non-{@code null} BigDecimal obj
	 */
	public static boolean isGreaterThanOrEqual(BigDecimal a, BigDecimal b) {
		return a.compareTo(b) >= 0;
	}

	/**
	 * Check if {@code BigDecimal} object {@code a} is less than {@code BigDecimal} object {@code b}.
	 *
	 * @param a
	 *            non-{@code null} BigDecimal obj
	 * @param b
	 *            non-{@code null} BigDecimal obj
	 */
	public static boolean isLessThan(BigDecimal a, BigDecimal b) {
		return a.compareTo(b) < 0;
	}

	/**
	 * Check if {@code BigDecimal} object {@code a} is less than or equals to {@code BigDecimal} object {@code b}.
	 *
	 * @param a
	 *            non-{@code null} BigDecimal obj
	 * @param b
	 *            non-{@code null} BigDecimal obj
	 */
	public static boolean isLessThanOrEqual(BigDecimal a, BigDecimal b) {
		return a.compareTo(b) <= 0;
	}

	/**
	 * 
	 * <pre>
	 * NumberUtils.equalsAny(null, (Integer[]) null) = false
	 * NumberUtils.equalsAny(null, null, null)       = true
	 * NumberUtils.equalsAny(null, 1, 2)             = false
	 * NumberUtils.equalsAny(1, null, 2)             = false
	 * NumberUtils.equalsAny(1, 1, 2)                = true
	 * </pre>
	 *
	 * @param int
	 *            to compare, may be {@code null}.
	 * @param searchInts
	 *            a int, may be {@code null}.
	 * @return {@code true} if the num is equal to any other element of <code>searchInts</code>; {@code false} if <code>searchInts</code> is null or contains no
	 *         matches.
	 */
	public static boolean equalsAny(final int num, int... searchInts) {
		if (ArrayUtils.isNotEmpty(searchInts)) {
			for (int next : searchInts) {
				if (num == next) {
					return true;
				}
			}
		}
		return false;
	}

	public static double sum(double... nums) {
		BigDecimal rs = BigDecimal.ZERO;
		for (double num : nums)
			rs = rs.add(BigDecimal.valueOf(num));
		return rs.doubleValue();
	}
}
