package org.bouncycastle.crypto.test.speedy;

import java.security.SecureRandom;

import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.KeyGenerationParameters;
import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.engines.NullEngine;
import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
import org.bouncycastle.crypto.macs.CMac;
import org.bouncycastle.crypto.macs.GMac;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.macs.Poly1305;
import org.bouncycastle.crypto.macs.SipHash;
import org.bouncycastle.crypto.macs.SkeinMac;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

/**
 * Microbenchmark of MACs on short, medium, long messages, with optional object creation cost.
 */
public class MacThroughputTest
{

    private static final long CLOCK_SPEED = 2400000000L;

    private static final SecureRandom RANDOM = new SecureRandom();
    private static Poly1305KeyGenerator kg = new Poly1305KeyGenerator();;

    private static final byte[] SHORT_MESSAGE = new byte[16];
    private static final byte[] MEDIUM_MESSAGE = new byte[256];
    private static final byte[] LONG_MESSAGE = new byte[8192];
    static
    {
        RANDOM.nextBytes(SHORT_MESSAGE);
        RANDOM.nextBytes(MEDIUM_MESSAGE);
        RANDOM.nextBytes(LONG_MESSAGE);
    }

    private static final int SHORT_MESSAGE_COUNT = 20000000;
    private static final int MEDIUM_MESSAGE_COUNT = 2200000;
    private static final int LONG_MESSAGE_COUNT = 80000;

    static
    {
        kg.init(new KeyGenerationParameters(RANDOM, 256));
    }

    private static KeyParameter generatePoly1305Key()
    {
        return new KeyParameter(kg.generateKey());
    }

    public static void main(String[] args)
    {
        testMac(new HMac(new SHA1Digest()), new KeyParameter(generateNonce(20)), 3);
        testMac(new SkeinMac(SkeinMac.SKEIN_512, 128), new KeyParameter(generateNonce(64)), 2);
        testMac(new SipHash(), new KeyParameter(generateNonce(16)), 1);
        testMac(new CMac(AESEngine.newInstance()), new KeyParameter(generateNonce(16)), 3);
        testMac(new GMac(new GCMBlockCipher(AESEngine.newInstance())), new ParametersWithIV(new KeyParameter(
                generateNonce(16)), generateNonce(16)), 5);
        testMac(new Poly1305(new NullEngine(16)), new ParametersWithIV(generatePoly1305Key(), generateNonce(16)), 1);
        testMac(new Poly1305(AESEngine.newInstance()), new ParametersWithIV(generatePoly1305Key(), generateNonce(16)), 1);
        testMac(new Poly1305Reference(new NullEngine(16)), new ParametersWithIV(generatePoly1305Key(),
                generateNonce(16)), 1);
    }

    private static byte[] generateNonce(int sizeBytes)
    {
        byte[] nonce = new byte[16];
        RANDOM.nextBytes(nonce);
        return nonce;
    }

    private static void testMac(Mac mac, CipherParameters params, int rateFactor)
    {
        System.out.println("=========================");

        long total = testRun(mac, params, false, MEDIUM_MESSAGE, adjust(MEDIUM_MESSAGE_COUNT, rateFactor));
        System.out.printf("%s Warmup 1 run time: %,d ms\n", mac.getAlgorithmName(), total / 1000000);
        total = testRun(mac, params, false, MEDIUM_MESSAGE, adjust(MEDIUM_MESSAGE_COUNT, rateFactor));
        System.out.printf("%s Warmup 2 run time: %,d ms\n", mac.getAlgorithmName(), total / 1000000);
        System.gc();
        try
        {
            Thread.sleep(1000);
        } catch (InterruptedException e)
        {
        }

        test("Short", mac, params, false, SHORT_MESSAGE, adjust(SHORT_MESSAGE_COUNT, rateFactor));
        // test("Short", mac, params, true, SHORT_MESSAGE, adjust(SHORT_MESSAGE_COUNT, rateFactor));
        test("Medium", mac, params, false, MEDIUM_MESSAGE, adjust(MEDIUM_MESSAGE_COUNT, rateFactor));
        // test("Medium", mac, params, true, MEDIUM_MESSAGE, adjust(MEDIUM_MESSAGE_COUNT,
        // rateFactor));
        test("Long", mac, params, false, LONG_MESSAGE, adjust(LONG_MESSAGE_COUNT, rateFactor));
        // test("Long", mac, params, true, LONG_MESSAGE, adjust(LONG_MESSAGE_COUNT, rateFactor));
    }

    private static int adjust(int iterationCount, int rateFactor)
    {
        return (int)(iterationCount * (1.0f / rateFactor));
    }

    private static void test(String name,
                             Mac mac,
                             CipherParameters params,
                             boolean initPerMessage,
                             byte[] message,
                             int adjustedCount)
    {
        System.out.println("=========================");
        long total = testRun(mac, params, initPerMessage, message, adjustedCount);

        long averageRuntime = total / adjustedCount;
        System.out.printf("%s %-7s%s Total run time:   %,d ms\n", mac.getAlgorithmName(), name, initPerMessage ? "*"
                : " ", total / 1000000);
        System.out.printf("%s %-7s%s Average run time: %,d ns\n", mac.getAlgorithmName(), name, initPerMessage ? "*"
                : " ", averageRuntime);
        final long mbPerSecond = (long)((double)message.length / averageRuntime * 1000000000 / (1024 * 1024));
        System.out.printf("%s %-7s%s Average speed:    %,d MB/s\n", mac.getAlgorithmName(), name, initPerMessage ? "*"
                : " ", mbPerSecond);
        System.out.printf("%s %-7s%s Average speed:    %,f c/b\n", mac.getAlgorithmName(), name, initPerMessage ? "*"
                : " ", CLOCK_SPEED / (double)(mbPerSecond * (1024 * 1024)));
    }

    private static long testRun(Mac mac,
                                CipherParameters params,
                                boolean initPerMessage,
                                byte[] message,
                                int adjustedCount)
    {
        byte[] out = new byte[mac.getMacSize()];

        if (!initPerMessage)
        {
            mac.init(params);
        }
        long start = System.nanoTime();

        for (int i = 0; i < adjustedCount; i++)
        {
            if (initPerMessage)
            {
                mac.init(params);
            }
            mac.update(message, 0, message.length);
            mac.doFinal(out, 0);
        }
        long total = System.nanoTime() - start;
        return total;
    }
}
