package org.apache.velocity.runtime.parser.node;

/*
 * 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.
 */

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Utility-class for all arithmetic-operations.<br><br>
 *
 * All operations (+ - / *) return a Number which type is the type of the bigger argument.<br>
 * Example:<br>
 * <code>add ( Integer.valueOf(10), Integer.valueOf(1))</code> will return an <code>Integer</code>-Object with the value 11<br>
 * <code>add ( Long.valueOf(10), Integer.valueOf(1))</code> will return an <code>Long</code>-Object with the value 11<br>
 * <code>add ( Integer.valueOf(10), Float.valueOf(1))</code> will return an <code>Float</code>-Object with the value 11<br><br>
 *
 * Overflow checking:<br>
 * For integral values (byte, short, int) there is an implicit overflow correction (the next "bigger"
 * type will be returned). For example, if you call <code>add (Integer.valueOf(Integer.MAX_VALUE), 1)</code> a
 * <code>Long</code>-object will be returned with the correct value of <code>Integer.MAX_VALUE+1</code>.<br>
 * In addition to that the methods <code>multiply</code>,<code>add</code> and <code>substract</code> implement overflow
 * checks for <code>long</code>-values. That means that if an overflow occurs while working with long values a BigInteger
 * will be returned.<br>
 * For all other operations and types (such as Float and Double) there is no overflow checking.
 *
 * @author <a href="mailto:pero@antaramusic.de">Peter Romianowski</a>
 * @since 1.5
 */
public abstract class MathUtils
{

    /**
     * A BigDecimal representing the number 0
     */
    protected static final BigDecimal DECIMAL_ZERO    = new BigDecimal ( BigInteger.ZERO );

    /**
     * The constants are used to determine in which context we have to calculate.
     */
    protected static final int BASE_LONG          = 0;
    protected static final int BASE_FLOAT         = 1;
    protected static final int BASE_DOUBLE        = 2;
    protected static final int BASE_BIGINTEGER    = 3;
    protected static final int BASE_BIGDECIMAL    = 4;

    /**
     * The <code>Class</code>-object is key, the maximum-value is the value
     */
    private static final Map<Class<? extends Number>, BigDecimal> ints = new HashMap<>();
    static
    {
        ints.put (Byte.class, BigDecimal.valueOf (Byte.MAX_VALUE));
        ints.put (Short.class, BigDecimal.valueOf (Short.MAX_VALUE));
        ints.put (Integer.class, BigDecimal.valueOf (Integer.MAX_VALUE));
        ints.put (Long.class, BigDecimal.valueOf (Long.MAX_VALUE));
        ints.put (BigInteger.class, BigDecimal.valueOf (-1));
    }

    /**
     * The "size" of the number-types - ascending.
     */
    private static final List<Class<? extends Number>> typesBySize = new ArrayList<>();
    static
    {
        typesBySize.add (Byte.class);
        typesBySize.add (Short.class);
        typesBySize.add (Integer.class);
        typesBySize.add (Long.class);
        typesBySize.add (Float.class);
        typesBySize.add (Double.class);
    }

    /**
     * Convert the given Number to a BigDecimal
     * @param n
     * @return The number as BigDecimal
     */
    public static BigDecimal toBigDecimal (Number n)
    {

        if (n instanceof BigDecimal)
        {
            return (BigDecimal)n;
        }

        if (n instanceof BigInteger)
        {
            return new BigDecimal ( (BigInteger)n );
        }

        return BigDecimal.valueOf(n.doubleValue());

    }

    /**
     * Convert the given Number to a BigInteger
     * @param n
     * @return The number as BigInteger
     */
    public static BigInteger toBigInteger (Number n)
    {

        if (n instanceof BigInteger)
        {
            return (BigInteger)n;
        }

        return BigInteger.valueOf (n.longValue());

    }

    /**
     * Compare the given Number to 0.
     * @param n
     * @return True if number is 0.
     */
    public static boolean isZero (Number n)
    {
        if (isInteger( n ) )
        {
            if (n instanceof BigInteger)
            {
                return ((BigInteger)n).compareTo (BigInteger.ZERO) == 0;
            }
            return n.doubleValue() == 0;
        }
        if (n instanceof Float)
        {
            return n.floatValue() == 0f;
        }
        if (n instanceof Double)
        {
            return n.doubleValue() == 0d;
        }
        return toBigDecimal( n ).compareTo( DECIMAL_ZERO) == 0;
    }

    /**
     * Test, whether the given object is an integer value
     * (Byte, Short, Integer, Long, BigInteger)
     * @param n
     * @return True if n is an integer.
     */
    public static boolean isInteger (Number n)
    {
        return ints.containsKey (n.getClass());
    }

    /**
     * Wrap the given primitive into the given class if the value is in the
     * range of the destination type. If not the next bigger type will be chosen.
     * @param value
     * @param type
     * @return Number object representing the primitive.
     */
    public static Number wrapPrimitive (long value, Class<?> type)
    {
        if (type == Byte.class)
        {
            if (value > Byte.MAX_VALUE || value < Byte.MIN_VALUE)
            {
                type = Short.class;
            }
            else
            {
                return (byte) value;
            }
        }
        if (type == Short.class)
        {
            if (value > Short.MAX_VALUE || value < Short.MIN_VALUE)
            {
                type = Integer.class;
            }
            else
            {
                return (short) value;
            }
        }
        if (type == Integer.class)
        {
            if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE)
            {
                type = Long.class;
            }
            else
            {
                return (int) value;
            }
        }
        if (type == Long.class)
        {
            return value;
        }
        return BigInteger.valueOf(value);
    }

    /**
     * Wrap the result in the object of the bigger type.
     *
     * @param value result of operation (as a long) - used to check size
     * @param op1 first operand of binary operation
     * @param op2 second operand of binary operation
     * @return Number object of appropriate size to fit the value and operators
     */
    private static Number wrapPrimitive (long value, Number op1, Number op2)
    {
        if ( typesBySize.indexOf( op1.getClass()) > typesBySize.indexOf( op2.getClass()))
        {
            return wrapPrimitive(value, op1.getClass());
        }
        return wrapPrimitive(value, op2.getClass());
    }

    /**
     * Find the common Number-type to be used in calculations.
     *
     * @param op1 first operand of binary operation
     * @param op2 second operand of binary operation
     * @return constant indicating type of Number to use in calculations
     */
    private static int findCalculationBase (Number op1, Number op2)
    {

        boolean op1Int = isInteger(op1);
        boolean op2Int = isInteger(op2);

        if ( (op1 instanceof BigDecimal || op2 instanceof BigDecimal) ||
             ( (!op1Int || !op2Int) && (op1 instanceof BigInteger || op2 instanceof BigInteger)) )
        {
            return BASE_BIGDECIMAL;
        }

        if (op1Int && op2Int) {
            if (op1 instanceof BigInteger || op2 instanceof BigInteger)
            {
                return BASE_BIGINTEGER;
            }
            return BASE_LONG;
        }

        if ((op1 instanceof Double) || (op2 instanceof Double))
        {
            return BASE_DOUBLE;
        }
        return BASE_FLOAT;
    }

    /**
     * Find the Number-type to be used for a single number
     *
     * @param op operand
     * @return constant indicating type of Number to use in calculations
     */
    public static int findCalculationBase(Number op)
    {
        if (isInteger(op))
        {
            if (op instanceof BigInteger)
            {
                return BASE_BIGINTEGER;
            }
            return BASE_LONG;
        } else if (op instanceof BigDecimal)
        {
            return BASE_BIGDECIMAL;
        } else if (op instanceof Double)
        {
            return BASE_DOUBLE;
        }
        return BASE_FLOAT;
    }

    /**
     * Add two numbers and return the correct value / type.
     * Overflow detection is done for integer values (byte, short, int, long) only!
     * @param op1
     * @param op2
     * @return Addition result.
     */
    public static Number add (Number op1, Number op2)
    {

        int calcBase = findCalculationBase( op1, op2);
        switch (calcBase)
        {
            case BASE_BIGINTEGER:
                return toBigInteger( op1 ).add( toBigInteger( op2 ));
            case BASE_LONG:
                long l1 = op1.longValue();
                long l2 = op2.longValue();
                long result = l1+l2;

                // Overflow check
                if ((result ^ l1) < 0 && (result ^ l2) < 0)
                {
                    return toBigInteger( op1).add( toBigInteger( op2));
                }
                return wrapPrimitive( result, op1, op2);
            case BASE_FLOAT:
                return op1.floatValue() + op2.floatValue();
            case BASE_DOUBLE:
                return op1.doubleValue() + op2.doubleValue();

            // Default is BigDecimal operation
            default:
                return toBigDecimal( op1 ).add( toBigDecimal( op2 ));
        }
    }

    /**
     * Subtract two numbers and return the correct value / type.
     * Overflow detection is done for integer values (byte, short, int, long) only!
     * @param op1
     * @param op2
     * @return Subtraction result.
     */
    public static Number subtract (Number op1, Number op2) {

        int calcBase = findCalculationBase( op1, op2);
        switch (calcBase) {
            case BASE_BIGINTEGER:
                return toBigInteger( op1 ).subtract( toBigInteger( op2 ));
            case BASE_LONG:
                long l1 = op1.longValue();
                long l2 = op2.longValue();
                long result = l1-l2;

                // Overflow check
                if ((result ^ l1) < 0 && (result ^ ~l2) < 0) {
                    return toBigInteger( op1).subtract( toBigInteger( op2));
                }
                return wrapPrimitive( result, op1, op2);
            case BASE_FLOAT:
                return op1.floatValue() - op2.floatValue();
            case BASE_DOUBLE:
                return op1.doubleValue() - op2.doubleValue();

            // Default is BigDecimal operation
            default:
                return toBigDecimal( op1 ).subtract( toBigDecimal( op2 ));
        }
    }

    /**
     * Multiply two numbers and return the correct value / type.
     * Overflow detection is done for integer values (byte, short, int, long) only!
     * @param op1
     * @param op2
     * @return Multiplication result.
     */
    public static Number multiply (Number op1, Number op2) {

        int calcBase = findCalculationBase( op1, op2);
        switch (calcBase) {
            case BASE_BIGINTEGER:
                return toBigInteger( op1 ).multiply( toBigInteger( op2 ));
            case BASE_LONG:
                long l1 = op1.longValue();
                long l2 = op2.longValue();
                long result = l1*l2;

                // Overflow detection
                if ((l2 != 0) && (result / l2 != l1)) {
                    return toBigInteger( op1).multiply( toBigInteger( op2));
                }
                return wrapPrimitive( result, op1, op2);
            case BASE_FLOAT:
                return op1.floatValue() * op2.floatValue();
            case BASE_DOUBLE:
                return op1.doubleValue() * op2.doubleValue();

            // Default is BigDecimal operation
            default:
                return toBigDecimal( op1 ).multiply( toBigDecimal( op2 ));
        }
    }

    /**
     * Divide two numbers. The result will be returned as Integer-type if and only if
     * both sides of the division operator are Integer-types. Otherwise a Float, Double,
     * or BigDecimal will be returned.
     * @param op1
     * @param op2
     * @return Division result.
     */
    public static Number divide (Number op1, Number op2) {

        int calcBase = findCalculationBase( op1, op2);
        switch (calcBase) {
            case BASE_BIGINTEGER:
                BigInteger b1 = toBigInteger( op1 );
                BigInteger b2 = toBigInteger( op2 );
                return b1.divide( b2);

            case BASE_LONG:
                long l1 = op1.longValue();
                long l2 = op2.longValue();
                return wrapPrimitive( l1 / l2, op1, op2);

            case BASE_FLOAT:
                return op1.floatValue() / op2.floatValue();
            case BASE_DOUBLE:
                return op1.doubleValue() / op2.doubleValue();

            // Default is BigDecimal operation
            default:
                return toBigDecimal( op1 ).divide( toBigDecimal( op2 ), BigDecimal.ROUND_HALF_DOWN);
        }
    }

    /**
     * Modulo two numbers.
     * @param op1
     * @param op2
     * @return Modulo result.
     *
     * @throws ArithmeticException If at least one parameter is a BigDecimal
     */
    public static Number modulo (Number op1, Number op2) throws ArithmeticException {

        int calcBase = findCalculationBase( op1, op2);
        switch (calcBase) {
            case BASE_BIGINTEGER:
                return toBigInteger( op1 ).mod( toBigInteger( op2 ));
            case BASE_LONG:
                return wrapPrimitive( op1.longValue() % op2.longValue(), op1, op2);
            case BASE_FLOAT:
                return op1.floatValue() % op2.floatValue();
            case BASE_DOUBLE:
                return op1.doubleValue() % op2.doubleValue();

            // Default is BigDecimal operation
            default:
                throw new ArithmeticException( "Cannot calculate the modulo of BigDecimals.");
        }
    }

    /**
     * Compare two numbers.
     * @param op1
     * @param op2
     * @return 1 if n1 &gt; n2, -1 if n1 &lt; n2 and 0 if equal.
     */
    public static int compare (Number op1, Number op2) {

        int calcBase = findCalculationBase( op1, op2);
        switch (calcBase) {
            case BASE_BIGINTEGER:
                return toBigInteger( op1 ).compareTo( toBigInteger( op2 ));
            case BASE_LONG:
                long l1 = op1.longValue();
                long l2 = op2.longValue();
                if (l1 < l2) {
                    return -1;
                }
                if (l1 > l2) {
                    return 1;
                }
                return 0;
            case BASE_FLOAT:
                float f1 = op1.floatValue();
                float f2 = op2.floatValue();
                if (f1 < f2) {
                    return -1;
                }
                if (f1 > f2) {
                    return 1;
                }
                return 0;
            case BASE_DOUBLE:
                double d1 = op1.doubleValue();
                double d2 = op2.doubleValue();
                if (d1 < d2) {
                    return -1;
                }
                if (d1 > d2) {
                    return 1;
                }
                return 0;

            // Default is BigDecimal operation
            default:
                return toBigDecimal( op1 ).compareTo( toBigDecimal ( op2 ));
        }
    }

    /**
     * Negate a number
     * @param op n
     * @return -n (unary negation of n)
     */
    public static Number negate(Number op)
    {
        int calcBase = findCalculationBase( op);
        switch (calcBase) {
            case BASE_BIGINTEGER:
                return toBigInteger(op).negate();
            case BASE_LONG:
                long l = op.longValue();
                /* overflow check */
                if (l == Long.MIN_VALUE)
                {
                    return toBigInteger(l).negate();
                }
                return wrapPrimitive(-l, op.getClass());
            case BASE_FLOAT:
                float f = op.floatValue();
                return -f;
            case BASE_DOUBLE:
                double d = op.doubleValue();
                return -d;
            // Default is BigDecimal operation
            default:
                return toBigDecimal(op).negate();
        }
    }
}
