/*
 * $Id$
 * @author aakimov
 * @date   Jun 16, 2016 5:27:12 PM
 *
 * Copyright (C) 2016 Scytl Secure Electronic Voting SA
 *
 * All rights reserved.
 */
package com.scytl.math;

import java.math.BigInteger;

import javax.annotation.Nonnegative;
import javax.annotation.concurrent.ThreadSafe;

import com.scytl.math.jni.JNI;

/**
 * Utility class which provides some extensions and optimizations for standard
 * {@link BigInteger}.
 * 
 * @author aakimov
 */
@ThreadSafe
public final class BigIntegers {
    private BigIntegers() {
    }

    /**
     * Returns whether a given number is negative.
     * 
     * @param number
     *            the number
     * @return the number is negative.
     */
    public static boolean isNegative(BigInteger number) {
        return number.signum() == -1;
    }

    /**
     * Returns whether a given number is non-negative.
     * 
     * @param number
     *            the number
     * @return the number is non-negative.
     */
    public static boolean isNonNegative(BigInteger number) {
        return number.signum() >= 0;
    }

    /**
     * Returns whether a given number is non-positive.
     * 
     * @param number
     *            the number
     * @return the number is non-positive.
     */
    public static boolean isNonPositive(BigInteger number) {
        return number.signum() <= 0;
    }

    /**
     * Returns whether a given number is non-zero.
     * 
     * @param number
     *            the number
     * @return the number is non-zero.
     */
    public static boolean isNonZero(BigInteger number) {
        return number.signum() != 0;
    }

    /**
     * Returns whether a given number is one.
     * 
     * @param number
     *            the number
     * @return the number is one.
     */
    public static boolean isOne(BigInteger number) {
        return BigInteger.ONE.equals(number);
    }

    /**
     * Returns whether a given number is positive.
     * 
     * @param number
     *            the number
     * @return the number is positive.
     */
    public static boolean isPositive(BigInteger number) {
        return number.signum() == 1;
    }

    /**
     * Returns whether a given number is zero.
     * 
     * @param number
     *            the number
     * @return the number is zero.
     */
    public static boolean isZero(BigInteger number) {
        return number.signum() == 0;
    }

    /**
     * Multiplies two numbers modulo given modulus. This is a shortcut for
     * {@code a.multiply(b).mod(modulus);}.
     * 
     * @param number1
     *            the first number
     * @param number2
     *            the second number
     * @param modulus
     *            the modulus, must be positive
     * @return the product
     * @throws ArithmeticException
     *             the modulus is not positive.
     */
    public static BigInteger modMultiply(BigInteger number1, BigInteger number2,
            BigInteger modulus) {
        BigInteger value;
        if (JNI.isAvailable()) {
            if (!isPositive(modulus)) {
                throw new ArithmeticException("Modulus is not positive.");
            }
            number1 = number1.abs();
            number2 = number2.abs();
            value = new BigInteger(modMultiplyNative(number1.toByteArray(),
                    number2.toByteArray(), modulus.toByteArray()));
        } else {
            value = number1.multiply(number2).mod(modulus);
        }
        return value;
    }

    /**
     * Executes {@link BigInteger#modPow(BigInteger, BigInteger)} for given
     * base, exponent and modulus. See
     * {@link BigInteger#modPow(BigInteger, BigInteger)} for details.
     * 
     * @param base
     *            the base
     * @param exponent
     *            the exponent
     * @param modulus
     *            the modulus, must be positive
     * @return the value
     * @throws ArithmeticException
     *             the modulus is not positive or the exponent is negative and
     *             the base is not relatively prime to the modulus.
     */
    public static @Nonnegative BigInteger modPow(BigInteger base,
            BigInteger exponent, @Nonnegative BigInteger modulus) {
        BigInteger value;
        if (JNI.isAvailable()) {
            if (!isPositive(modulus)) {
                throw new ArithmeticException("Modulus is not positive.");
            }
            base = base.abs();
            if (isNegative(exponent)) {
                base = base.modInverse(modulus);
                exponent = exponent.negate();
            }
            value = new BigInteger(modPowNative(base.toByteArray(),
                    exponent.toByteArray(), modulus.toByteArray()));
        } else {
            value = base.modPow(exponent, modulus);
        }
        return value;
    }

    /**
     * Squares a number modulo given modulus.
     * 
     * @param number
     *            the number
     * @param modulus
     *            the modulus, must be positive
     * @return the square
     * @throws ArithmeticException
     *             the modulus is not positive.
     */
    public static BigInteger modSquare(BigInteger number, BigInteger modulus) {
        return modMultiply(number, number, modulus);
    }

    /**
     * Multiplies two given factors. See {@link BigInteger#multiply(BigInteger)}
     * for details.
     * 
     * @param number1
     *            the first factor
     * @param number2
     *            the second factor
     * @return the product.
     */
    public static BigInteger multiply(BigInteger number1, BigInteger number2) {
        BigInteger value;
        if (JNI.isAvailable()) {
            int signum = number1.signum() * number2.signum();
            number1 = number1.abs();
            number2 = number2.abs();
            value = new BigInteger(multiplyNative(number1.toByteArray(),
                    number2.toByteArray()));
            if (signum < 0) {
                value = value.negate();
            }
        } else {
            value = number1.multiply(number2);
        }
        return value;
    }

    /**
     * Squares a given number.
     * 
     * @param number
     *            the number
     * @return the square.
     */
    public static BigInteger square(BigInteger number) {
        return multiply(number, number);
    }

    private static native byte[] modMultiplyNative(byte[] number1,
            byte[] number2, byte[] modulus);

    private static native byte[] modPowNative(byte[] base, byte[] exponent,
            byte[] modulus);

    private static native byte[] multiplyNative(byte[] number1, byte[] number2);
}
