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

package org.apache.bookkeeper.proto.checksum;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.apache.bookkeeper.proto.DataFormats.LedgerMetadataFormat.DigestType;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;

/**
 * Microbenchmarks for different digest type
 * getting started:
 * 1. http://tutorials.jenkov.com/java-performance/jmh.html
 * 2. http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
 * 3. google
 * To run:
 * build project from command line.
 * execute ./run.sh
 */
public class DigestTypeBenchmark {

    /**
     * BufferType.
     */
    public enum BufferType {
        ARRAY_BACKED,
        NOT_ARRAY_BACKED,
        BYTE_BUF_DEFAULT_ALLOC
    }

    /**
     * Digest.
     */
    public enum Digest {
        MAC,
        CRC32,
        CRC32_C,
    }

    static byte[] randomBytes(int sz) {
        byte[] b = new byte[sz];
        ThreadLocalRandom.current().nextBytes(b);
        return b;
    }

    /**
     * MyState.
     */
    @State(Scope.Thread)
    public static class MyState {

        @Param
        public BufferType bufferType;
        @Param
        public Digest digest;
        @Param({"64", "1024", "4086", "8192", "16384", "65536"})
        public int entrySize;

        private DigestManager crc32;
        private DigestManager crc32c;
        private DigestManager mac;

        private ByteBuf arrayBackedBuffer;
        private CompositeByteBuf notArrayBackedBuffer;
        private ByteBuf byteBufDefaultAlloc;

        public ByteBuf digestBuf;

        @Setup(Level.Trial)
        public void doSetup() throws Exception {
            final byte[] password = "password".getBytes(StandardCharsets.UTF_8);
            crc32 = DigestManager.instantiate(ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE),
                    password, DigestType.CRC32, PooledByteBufAllocator.DEFAULT, true);

            crc32c = DigestManager.instantiate(ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE),
                    password, DigestType.CRC32C, PooledByteBufAllocator.DEFAULT, true);

            mac = DigestManager.instantiate(ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE),
                    password, DigestType.HMAC, PooledByteBufAllocator.DEFAULT, true);

            digestBuf = Unpooled.buffer(getDigestManager(digest).getMacCodeLength());

            arrayBackedBuffer = Unpooled.wrappedBuffer(randomBytes(entrySize));

            final int headerSize = 32 + getDigestManager(digest).getMacCodeLength();
            notArrayBackedBuffer = new CompositeByteBuf(ByteBufAllocator.DEFAULT, true, 2);
            notArrayBackedBuffer.addComponent(Unpooled.wrappedBuffer(randomBytes(headerSize)));
            notArrayBackedBuffer.addComponent(Unpooled.wrappedBuffer((randomBytes(entrySize - headerSize))));

            byteBufDefaultAlloc = ByteBufAllocator.DEFAULT.buffer(entrySize, entrySize);
            byteBufDefaultAlloc.writeBytes(randomBytes(entrySize));

            if (!arrayBackedBuffer.hasArray() || notArrayBackedBuffer.hasArray()) {
                throw new IllegalStateException("buffers in invalid state");
            }
        }

        @TearDown(Level.Trial)
        public void doTearDown() {
        }

        public ByteBuf getByteBuff(BufferType bType) {
            switch (bType) {
                case ARRAY_BACKED:
                    return arrayBackedBuffer;
                case NOT_ARRAY_BACKED:
                    return notArrayBackedBuffer;
                case BYTE_BUF_DEFAULT_ALLOC:
                    return byteBufDefaultAlloc;
                default:
                    throw new IllegalArgumentException("unknown buffer type " + bType);
            }
        }

        public DigestManager getDigestManager(Digest digest) {
            switch (digest) {
                case CRC32:
                    return crc32;
                case CRC32_C:
                    return crc32c;
                case MAC:
                    return mac;
                default:
                    throw new IllegalArgumentException("unknown digest " + digest);
            }
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    @Warmup(iterations = 2, time = 3, timeUnit = TimeUnit.SECONDS)
    @Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
    @Threads(2)
    @Fork(value = 1, warmups = 1)
    public void digestManager(MyState state) {
        final ByteBuf buff = state.getByteBuff(state.bufferType);
        final DigestManager dm = state.getDigestManager(state.digest);
        int digest = dm.update(0, buff, 0, buff.readableBytes());
        state.digestBuf.clear();
        dm.populateValueAndReset(digest, state.digestBuf);
    }

}
