/*
 * $Id$
 * @author aakimov
 * @date   Jun 20, 2016 2:53:40 PM
 *
 * Copyright (C) 2016 Scytl Secure Electronic Voting SA
 *
 * All rights reserved.
 */
package com.scytl.math;

import static com.scytl.math.BigIntegers.modMultiply;
import static com.scytl.math.BigIntegers.modPow;
import static com.scytl.math.BigIntegers.multiply;
import static java.util.Arrays.asList;

import java.math.BigInteger;
import java.util.Random;
import java.util.StringJoiner;

import com.scytl.math.jni.JNI;

/**
 * Application which runs performance tests.
 * <p>
 * The main goal of the performance test is to estimate the speedup provided by
 * the native code.
 * 
 * @author aakimov
 */
public class PerformanceTests {
    private static final Random RANDOM = new Random();
    private static final int TEST_COUNT = 100;
    private static final int BASE_LIMIT = 64;
    private static final int BIT_LIMIT = 2048;

    private PerformanceTests() {
    }

    /**
     * Main method.
     * 
     * @param args
     */
    public static void main(String[] args) {
        testMultiply();
        testModMultiply();
        testModPow();
        testMultiExponentitation();
    }

    private static void printResult(String test, String algorithm, long time,
            Object... parameters) {
        StringJoiner joiner = new StringJoiner(",");
        joiner.add(test).add(algorithm).add(Long.toString(time));
        for (Object parameter : parameters) {
            joiner.add(parameter.toString());
        }
        System.out.println(joiner.toString());
    }

    private static BigInteger[] randomBigIntegers(BigInteger limit, int count) {
        BigInteger[] values = new BigInteger[count];
        for (int i = 0; i < values.length; i++) {
            do {
                values[i] = new BigInteger(limit.bitCount(), RANDOM);
            } while (values[i].compareTo(limit) >= 0);
        }
        return values;
    }

    private static void testModMultiply() {
        BigInteger limit = BigInteger.ONE.shiftLeft(BIT_LIMIT)
                .subtract(BigInteger.ONE);
        BigInteger[] numbers1 = randomBigIntegers(limit, TEST_COUNT);
        BigInteger[] numbers2 = randomBigIntegers(limit, TEST_COUNT);
        BigInteger[] moduluses = randomBigIntegers(limit, TEST_COUNT);
        testModMultiplyJava(numbers1, numbers2, moduluses);
        testModMultiplyNative(numbers1, numbers2, moduluses);
    }

    private static void testModMultiplyJava(BigInteger[] numbers1,
            BigInteger[] numbers2, BigInteger[] moduluses) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < numbers1.length; i++) {
            numbers1[i].multiply(numbers2[i]).mod(moduluses[i]);
        }
        printResult("modMultiply", "Java", System.currentTimeMillis() - start);
    }

    private static void testModMultiplyNative(BigInteger[] numbers1,
            BigInteger[] numbers2, BigInteger[] moduluses) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < numbers1.length; i++) {
            modMultiply(numbers1[i], numbers2[i], moduluses[i]);
        }
        printResult("modMultiply", "Native",
                System.currentTimeMillis() - start);
    }

    private static void testModPow() {
        BigInteger modulus = BigInteger.probablePrime(BIT_LIMIT, RANDOM);
        BigInteger[] bases = randomBigIntegers(modulus, TEST_COUNT);
        BigInteger[] exponents = randomBigIntegers(
                modulus.subtract(BigInteger.ONE), TEST_COUNT);
        testModPowJava(bases, exponents, modulus);
        testModPowNative(bases, exponents, modulus);
    }

    private static void testModPowJava(BigInteger[] bases,
            BigInteger[] exponents, BigInteger modulus) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < bases.length; i++) {
            bases[i].modPow(exponents[i], modulus);
        }
        printResult("modPow", "Java", System.currentTimeMillis() - start);
    }

    private static void testModPowNative(BigInteger[] bases,
            BigInteger[] exponents, BigInteger modulus) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < bases.length; i++) {
            modPow(bases[i], exponents[i], modulus);
        }
        printResult("modPow", "Native", System.currentTimeMillis() - start);
    }

    private static void testMultiExponentiationBrickell(BigInteger[][] bases,
            BigInteger[][] exponents, BigInteger[] moduluses, int bitCount) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < bases.length; i++) {
            MultiExponentiator exponentiator = new BrickellMultiExponentiator(
                    asList(bases[i]), moduluses[i], BIT_LIMIT);
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "BG", System.currentTimeMillis() - start,
                bases[0].length, bitCount);
    }

    private static void testMultiExponentiationNative(BigInteger[][] bases,
            BigInteger[][] exponents, BigInteger[] moduluses, int bitCount) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < bases.length; i++) {
            MultiExponentiator exponentiator = new NativeMultiExponentiator(
                    asList(bases[i]), moduluses[i], BIT_LIMIT);
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "Native", System.currentTimeMillis() - start,
                bases[0].length, bitCount);
    }

    private static void testMultiExponentiatorBrickellReused(BigInteger[] bases,
            BigInteger[][] exponents, BigInteger modulus, int bitCount) {
        long start = System.currentTimeMillis();
        MultiExponentiator exponentiator = new BrickellMultiExponentiator(
                asList(bases), modulus, BIT_LIMIT);
        for (int i = 0; i < exponents.length; i++) {
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "Reused BG",
                System.currentTimeMillis() - start, bases.length, bitCount);
    }

    private static void testMultiExponentiatorLimLee(BigInteger[][] bases,
            BigInteger[][] exponents, BigInteger[] moduluses, int bitCount) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < bases.length; i++) {
            MultiExponentiator exponentiator = new LimLeeMultiExponentiator(
                    asList(bases[i]), moduluses[i], BIT_LIMIT);
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "LL", System.currentTimeMillis() - start,
                bases[0].length, bitCount);
    }

    private static void testMultiExponentiatorLimLeeReused(BigInteger[] bases,
            BigInteger[][] exponents, BigInteger modulus, int bitCount) {
        long start = System.currentTimeMillis();
        MultiExponentiator exponentiator = new LimLeeMultiExponentiator(
                asList(bases), modulus, BIT_LIMIT);
        for (int i = 0; i < exponents.length; i++) {
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "Reused LL",
                System.currentTimeMillis() - start, bases.length, bitCount);
    }

    private static void testMultiExponentiatorSimple(BigInteger[][] bases,
            BigInteger[][] exponents, BigInteger[] moduluses, int bitCount) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < bases.length; i++) {
            MultiExponentiator exponentiator = new SimpleMultiExponentiator(
                    asList(bases[i]), moduluses[i], BIT_LIMIT);
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "BU", System.currentTimeMillis() - start,
                bases[0].length, bitCount);
    }

    private static void testMultiExponentiatorSlidingWindow(
            BigInteger[][] bases, BigInteger[][] exponents,
            BigInteger[] moduluses, int bitCount) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < bases.length; i++) {
            MultiExponentiator exponentiator = new SlidingWindowMultiExponentiator(
                    asList(bases[i]), moduluses[i], BIT_LIMIT);
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "WU", System.currentTimeMillis() - start,
                bases[0].length, bitCount);
    }

    private static void testMultiExponentiatorSlidingWindowReused(
            BigInteger[] bases, BigInteger[][] exponents, BigInteger modulus,
            int bitCount) {
        long start = System.currentTimeMillis();
        MultiExponentiator exponentiator = new SlidingWindowMultiExponentiator(
                asList(bases), modulus, BIT_LIMIT);
        for (int i = 0; i < exponents.length; i++) {
            exponentiator.exponentiate(asList(exponents[i]));
        }
        printResult("multiModPow", "Reused WU",
                System.currentTimeMillis() - start, bases.length, bitCount);
    }

    private static void testMultiExponentitation() {
        for (int bitCount = BIT_LIMIT; bitCount > 1; bitCount >>= 1) {
            for (int baseCount = BASE_LIMIT; baseCount > 1; baseCount >>= 1) {
                BigInteger[][] bases = new BigInteger[TEST_COUNT][baseCount];
                BigInteger[][] exponents = new BigInteger[TEST_COUNT][baseCount];
                BigInteger[] moduluses = new BigInteger[TEST_COUNT];
                for (int i = 0; i < TEST_COUNT; i++) {
                    do {
                        moduluses[i] = new BigInteger(bitCount, RANDOM);
                    } while (moduluses[i].compareTo(BigInteger.ONE) <= 0);
                    bases[i] = randomBigIntegers(moduluses[i], baseCount);
                    exponents[i] = randomBigIntegers(moduluses[i], baseCount);
                }
                testMultiExponentiatorSimple(bases, exponents, moduluses,
                        bitCount);
                testMultiExponentiatorSlidingWindow(bases, exponents, moduluses,
                        bitCount);
                testMultiExponentiatorSlidingWindowReused(bases[0], exponents,
                        moduluses[0], bitCount);
                testMultiExponentiatorLimLee(bases, exponents, moduluses,
                        bitCount);
                testMultiExponentiatorLimLeeReused(bases[0], exponents,
                        moduluses[0], bitCount);
                testMultiExponentiationBrickell(bases, exponents, moduluses,
                        bitCount);
                testMultiExponentiatorBrickellReused(bases[0], exponents,
                        moduluses[0], bitCount);
                if (JNI.isAvailable()) {
                    testMultiExponentiationNative(bases, exponents, moduluses,
                            bitCount);
                }
            }
        }
    }

    private static void testMultiply() {
        BigInteger limit = BigInteger.ONE.shiftLeft(BIT_LIMIT)
                .subtract(BigInteger.ONE);
        BigInteger[] factors1 = randomBigIntegers(limit, TEST_COUNT);
        BigInteger[] factors2 = randomBigIntegers(limit, TEST_COUNT);
        testMultiplyJava(factors1, factors2);
        testMultiplyNative(factors1, factors2);
    }

    private static void testMultiplyJava(BigInteger[] factors1,
            BigInteger[] factors2) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < factors1.length; i++) {
            factors1[i].multiply(factors2[i]);
        }
        printResult("Multiply", "Java", System.currentTimeMillis() - start);
    }

    private static void testMultiplyNative(BigInteger[] factors1,
            BigInteger[] factors2) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < factors1.length; i++) {
            multiply(factors1[i], factors2[i]);
        }
        printResult("Multiply", "Native", System.currentTimeMillis() - start);
    }
}
