package com.pearson.statsagg.utilities.math_utils;

import com.pearson.statsagg.utilities.core_utils.StackTrace;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Jeffrey Schmidt
 */
public class MathUtilities {
    
    private static final Logger logger = LoggerFactory.getLogger(MathUtilities.class.getName());
    
    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
    
    public static double computeSmallerNumber(double number1, double number2) {
        if (number1 <= number2) {
            return number1;
        }
        else {
            return number2;
        }
    }
    
    public static double computeLargerNumber(double number1, double number2) {
        if (number1 >= number2) {
            return number1;
        }
        else {
            return number2;
        }
    }

    public static double computePercentChange(double oldValue, double newValue) {
        
        if (oldValue == 0) {
            throw new IllegalArgumentException("oldValue cannot be 0");
        }
        
        double percentChange = ((newValue - oldValue) / oldValue) * 100;
        
        return percentChange;
    }
    
    public static boolean areBigDecimalsNumericallyEqual(BigDecimal bigDecimal1, BigDecimal bigDecimal2) {
        
        boolean isEqual = false;
        
        if ((bigDecimal1 != null) && (bigDecimal2 != null)) {
            isEqual = (bigDecimal1.compareTo(bigDecimal2) == 0);
        }
        else if (bigDecimal1 == null) {
            isEqual = (bigDecimal2 == null);
        }
        
        return isEqual;
    }
    
    public static Long computeMedianOfLongs(List<Long> numbers) {
        
        if ((numbers == null) || (numbers.isEmpty())) {
            return null;
        }
        
        if (numbers.size() == 1) {
            return numbers.get(0);
        }
        
        Long median;
        
        List<Long> localNumbers = new ArrayList(numbers);
        Collections.sort(localNumbers);
        
        boolean isOddSized = (localNumbers.size() % 2) == 1;
        int medianIndex = localNumbers.size() / 2;

        if (isOddSized) {
            median = localNumbers.get(medianIndex);
        }
        else {
            median = (localNumbers.get(medianIndex - 1) + localNumbers.get(medianIndex)) / 2;
        }        
    
        return median;
    }
    
    public static Double computeMedianOfDoubles(List<Double> numbers) {
        
        if ((numbers == null) || (numbers.isEmpty())) {
            return null;
        }
        
        if (numbers.size() == 1) {
            return numbers.get(0);
        }
        
        Double median;
        
        List<Double> localNumbers = new ArrayList(numbers);
        Collections.sort(localNumbers);
        
        boolean isOddSized = (localNumbers.size() % 2) == 1;
        int medianIndex = localNumbers.size() / 2;

        if (isOddSized) {
            median = localNumbers.get(medianIndex);
        }
        else {
            median = (localNumbers.get(medianIndex - 1) + localNumbers.get(medianIndex)) / 2;
        }        
    
        return median;
    }
    
    public static BigDecimal computeMedianOfBigDecimals(List<BigDecimal> numbers, MathContext mathContext, boolean areNumbersAlreadySorted) {
        
        if ((numbers == null) || (numbers.isEmpty())) {
            return null;
        }
        
        if (numbers.size() == 1) {
            return numbers.get(0);
        }
        
        BigDecimal median;
        
        List<BigDecimal> sortedNumbers;
        if (!areNumbersAlreadySorted) {
            sortedNumbers = new ArrayList(numbers);
            Collections.sort(sortedNumbers);
        }
        else {
            sortedNumbers = numbers;
        }
        
        boolean isOddSized = (sortedNumbers.size() % 2) == 1;
        int medianIndex = sortedNumbers.size() / 2;

        if (isOddSized) {
            median = sortedNumbers.get(medianIndex);
        }
        else {
            median = sortedNumbers.get(medianIndex - 1).add(sortedNumbers.get(medianIndex));
            median = median.divide(new BigDecimal(2), mathContext);
        }        
    
        return median;
    }

    public static BigDecimal smartBigDecimalScaleChange(BigDecimal number, int scale, RoundingMode roundingMode) {
        
        if ((number == null) || (roundingMode == null)) {
            return null;
        }
        
        if ((number.scale() >= 0) && (scale >= 0)) {
            if (number.scale() <= scale) {
                return number;
            }
            else {
                return number.setScale(scale, roundingMode);
            }
        }
        else {
            return number.setScale(scale, roundingMode);
        }
    }
    
    public static BigDecimal computePopulationStandardDeviationOfBigDecimals(List<BigDecimal> numbers) {
        
        if ((numbers == null) || numbers.isEmpty()) {
            return null;
        }
        
        try {
            double[] doublesArray = new double[numbers.size()];

            for (int i = 0; i < doublesArray.length; i++) {
                doublesArray[i] = numbers.get(i).doubleValue();
            }

            StandardDeviation standardDeviation = new StandardDeviation();
            standardDeviation.setBiasCorrected(false);
            BigDecimal standardDeviationResult = new BigDecimal(standardDeviation.evaluate(doublesArray));

            return standardDeviationResult;
        }
        catch (Exception e) {
            logger.error(e.toString() + System.lineSeparator() + StackTrace.getStringFromStackTrace(e));
            return null;
        }
    }
    
    public static Long getSmallestValue(List<Long> values) {
        
        if ((values == null) || values.isEmpty()) {
            return null;
        }
        
        Long returnValue = null;
        int counter = 0;
        
        for (Long currentValue : values) {
            if (counter == 0) {
                returnValue = currentValue;
            }
            else if ((counter > 0) && (returnValue > currentValue)) {
                returnValue = currentValue;
            }
            
            counter++;        
        }
        
        return returnValue;
    }

    public static BigDecimal correctOutOfRangePercentage(BigDecimal input) {
        
        if (input == null) {
            return null;
        }
        
        if (input.compareTo(ONE_HUNDRED) > 0) {
            return ONE_HUNDRED;
        }
        
        if (input.compareTo(BigDecimal.ZERO) < 0) {
            return BigDecimal.ZERO;
        }
        
        return input;
    }
    
    public static BigDecimal safeGetBigDecimal(String numericString) {
        
        if (numericString == null) {
            return null;
        }
        
        BigDecimal bigDecimal;

        try {
            bigDecimal = new BigDecimal(numericString);
        }
        catch (Exception e) {
            logger.error(e.toString() + System.lineSeparator() + StackTrace.getStringFromStackTrace(e));
            bigDecimal = null;
        }

        return bigDecimal;
    }

    public static String getFastPlainStringWithNoTrailingZeros(BigDecimal bigDecimal) {
        
        if (bigDecimal == null) return null;
        
        String numericString = bigDecimal.toPlainString();
        if ((numericString == null) || (numericString.length() <= 1)) return numericString;
            
        try {
            for (int i=(numericString.length()-1); i >= 0; i--) {
                char currentChar = numericString.charAt(i);
                if (currentChar == '0') continue;
                if (currentChar == '.') return numericString.substring(0, i);
                if (currentChar != '0') {
                    for (int j = 0; j < i; j++) {
                        if (numericString.charAt(j) == '.') {
                            return numericString.substring(0, (i+1));
                        }
                    }
                    return numericString;
                }
            }
            
            return numericString;
        }
        catch (Exception e) {
            logger.error(e.toString() + System.lineSeparator() + StackTrace.getStringFromStackTrace(e));
            return numericString;
        }
        
    }
    
    public static String convertNumericObjectToString(Object numericObject, boolean treatBooleanAsNumeric) {
        
        if (numericObject == null) return null;
        
        String valueString = null;
        
        if (numericObject instanceof Integer) valueString = Integer.toString((Integer) numericObject);
        else if (numericObject instanceof Long) valueString = Long.toString((Long) numericObject);
        else if (numericObject instanceof Short) valueString = Short.toString((Short) numericObject);
        else if (numericObject instanceof Byte) valueString = Byte.toString((Byte) numericObject);
        else if (numericObject instanceof Double) valueString = Double.toString((Double) numericObject);
        else if (numericObject instanceof Float) valueString = Float.toString((Float) numericObject);
        else if (numericObject instanceof BigDecimal) {
            BigDecimal numericObjectBigDecimal = (BigDecimal) numericObject;
            valueString = numericObjectBigDecimal.stripTrailingZeros().toPlainString();
        }
        else if (numericObject instanceof BigInteger) {
            BigInteger numericObjectBigInteger = (BigInteger) numericObject;
            valueString = numericObjectBigInteger.toString();
        }
        else if ((numericObject instanceof Boolean) && treatBooleanAsNumeric) {
            Boolean numberObjectBoolean = (Boolean) numericObject;
            if (numberObjectBoolean) valueString = "1";
            else valueString = "0";
        }
        else if ((numericObject instanceof Boolean) && !treatBooleanAsNumeric) {
            Boolean numberObjectBoolean = (Boolean) numericObject;
            if (numberObjectBoolean) valueString = "true";
            else valueString = "false";
        }
        
        return valueString;
    }
        
    public static BigDecimal convertNumericObjectToBigDecimal(Object numericObject, boolean treatBooleanAsNumeric) {
        
        if (numericObject == null) return null;
        
        BigDecimal valueBigDecimal = null;
        
        if (numericObject instanceof BigDecimal) valueBigDecimal = (BigDecimal) numericObject;
        else if (numericObject instanceof Integer) valueBigDecimal = new BigDecimal(Integer.toString((Integer) numericObject));
        else if (numericObject instanceof Long) valueBigDecimal = new BigDecimal(Long.toString((Long) numericObject));
        else if (numericObject instanceof Short) valueBigDecimal = new BigDecimal(Short.toString((Short) numericObject));
        else if (numericObject instanceof Byte) valueBigDecimal = new BigDecimal(Byte.toString((Byte) numericObject));
        else if (numericObject instanceof Double) valueBigDecimal = new BigDecimal(Double.toString((Double) numericObject));
        else if (numericObject instanceof Float) valueBigDecimal = new BigDecimal(Float.toString((Float) numericObject));
        else if (numericObject instanceof BigInteger) valueBigDecimal = new BigDecimal((BigInteger) numericObject);
        else if ((numericObject instanceof Boolean) && treatBooleanAsNumeric) {
            Boolean numberObjectBoolean = (Boolean) numericObject;
            if (numberObjectBoolean) valueBigDecimal = BigDecimal.ONE;
            else valueBigDecimal = BigDecimal.ZERO;
        }

        return valueBigDecimal;
    }
    
    public static boolean isObjectNumericType(Object object, boolean treatBooleanAsNumeric) {
        
        if (object == null) return false;
                
        if (object instanceof Integer) return true;
        else if (object instanceof Long) return true;
        else if (object instanceof Short) return true;
        else if (object instanceof Byte) return true;
        else if (object instanceof Double) return true;
        else if (object instanceof Float) return true;
        else if (object instanceof BigDecimal) return true;
        else if (object instanceof BigInteger) return true;
        else if ((object instanceof Boolean) && treatBooleanAsNumeric) return true;
        
        return false;
    }
    
}