/*-
 * #%L
 * LmdbJava Benchmarks
 * %%
 * Copyright (C) 2016 - 2020 The LmdbJava Open Source Project
 * %%
 * Licensed 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.
 * #L%
 */

package org.lmdbjava.bench;

import static java.lang.Boolean.TRUE;
import static java.lang.System.setProperty;
import static java.nio.ByteBuffer.allocateDirect;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static net.openhft.hashing.LongHashFunction.xx_r39;
import static org.agrona.concurrent.UnsafeBuffer.DISABLE_BOUNDS_CHECKS_PROP_NAME;
import static org.lmdbjava.CopyFlags.MDB_CP_COMPACT;
import static org.lmdbjava.DirectBufferProxy.PROXY_DB;
import static org.lmdbjava.GetOp.MDB_SET_KEY;
import static org.lmdbjava.PutFlags.MDB_APPEND;
import static org.lmdbjava.SeekOp.MDB_FIRST;
import static org.lmdbjava.SeekOp.MDB_LAST;
import static org.lmdbjava.SeekOp.MDB_NEXT;
import static org.lmdbjava.SeekOp.MDB_PREV;
import static org.openjdk.jmh.annotations.Level.Invocation;
import static org.openjdk.jmh.annotations.Level.Trial;
import static org.openjdk.jmh.annotations.Mode.SampleTime;
import static org.openjdk.jmh.annotations.Scope.Benchmark;

import java.io.IOException;

import org.agrona.DirectBuffer;
import org.agrona.MutableDirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;
import org.lmdbjava.Cursor;
import org.lmdbjava.PutFlags;
import org.lmdbjava.Txn;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.BenchmarkParams;
import org.openjdk.jmh.infra.Blackhole;

@OutputTimeUnit(MILLISECONDS)
@Fork(1)
@Warmup(iterations = 3)
@Measurement(iterations = 3)
@BenchmarkMode(SampleTime)
@SuppressWarnings({"checkstyle:javadoctype", "checkstyle:designforextension"})
public class LmdbJavaAgrona {

  @Benchmark
  public void readCrc(final Reader r, final Blackhole bh) {
    r.crc.reset();
    bh.consume(r.c.seek(MDB_FIRST));
    do {
      r.txn.key().getBytes(0, r.keyBytes, 0, r.keySize);
      r.txn.val().getBytes(0, r.valBytes, 0, r.valSize);
      r.crc.update(r.keyBytes);
      r.crc.update(r.valBytes);
    } while (r.c.seek(MDB_NEXT));
    bh.consume(r.crc.getValue());
  }

  @Benchmark
  public void readKey(final Reader r, final Blackhole bh) {
    for (final int key : r.keys) {
      if (r.intKey) {
        r.rwKey.putInt(0, key);
      } else {
        r.rwKey.putStringWithoutLengthUtf8(0, r.padKey(key));
      }
      bh.consume(r.c.get(r.rwKey, MDB_SET_KEY));
      bh.consume(r.txn.val());
    }
  }

  @Benchmark
  public void readRev(final Reader r, final Blackhole bh) {
    bh.consume(r.c.seek(MDB_LAST));
    do {
      bh.consume(r.txn.val());
    } while (r.c.seek(MDB_PREV));
  }

  @Benchmark
  public void readSeq(final Reader r, final Blackhole bh) {
    bh.consume(r.c.seek(MDB_FIRST));
    do {
      bh.consume(r.txn.val());
    } while (r.c.seek(MDB_NEXT));
  }

  @Benchmark
  public void readXxh64(final Reader r, final Blackhole bh) {
    long result = 0;
    bh.consume(r.c.seek(MDB_FIRST));
    do {
      result += xx_r39().hashMemory(r.txn.key().addressOffset(), r.keySize);
      result += xx_r39().hashMemory(r.txn.val().addressOffset(), r.valSize);
    } while (r.c.seek(MDB_NEXT));
    bh.consume(result);
  }

  @Benchmark
  public void write(final Writer w, final Blackhole bh) {
    w.write();
  }

  @State(Benchmark)
  @SuppressWarnings("checkstyle:visibilitymodifier")
  public static class LmdbJava extends CommonLmdbJava<DirectBuffer> {

    /**
     * CRC scratch (memory-mapped MDB can't return a byte[] or ByteBuffer).
     */
    byte[] keyBytes;

    MutableDirectBuffer rwKey;
    MutableDirectBuffer rwVal;
    /**
     * CRC scratch (memory-mapped MDB can't return a byte[] or ByteBuffer).
     */
    byte[] valBytes;

    static {
      setProperty(DISABLE_BOUNDS_CHECKS_PROP_NAME, TRUE.toString());
    }

    @Override
    public void setup(final BenchmarkParams b, final boolean sync) throws
        IOException {
      super.setup(b, sync);
      keyBytes = new byte[keySize];
      valBytes = new byte[valSize];
      rwKey = new UnsafeBuffer(allocateDirect(keySize).order(LITTLE_ENDIAN));
      rwVal = new UnsafeBuffer(allocateDirect(valSize));
    }

    @SuppressWarnings("PMD.NullAssignment")
    void write() {
      try (Txn<DirectBuffer> tx = env.txnWrite()) {
        try (Cursor<DirectBuffer> c = db.openCursor(tx);) {
          final PutFlags flags = sequential ? MDB_APPEND : null;
          final int rndByteMax = RND_MB.length - valSize;
          int rndByteOffset = 0;
          for (final int key : keys) {
            if (intKey) {
              rwKey.putInt(0, key);
            } else {
              rwKey.putStringWithoutLengthUtf8(0, padKey(key));
            }
            if (valRandom) {
              rwVal.putBytes(0, RND_MB, rndByteOffset, valSize);
              rndByteOffset += valSize;
              if (rndByteOffset >= rndByteMax) {
                rndByteOffset = 0;
              }
            } else {
              rwVal.putInt(0, key);
            }
            c.put(rwKey, rwVal, flags);
          }
        }
        tx.commit();
      }
    }

  }

  @State(Benchmark)
  @SuppressWarnings("checkstyle:visibilitymodifier")
  public static class Reader extends LmdbJava {

    Cursor<DirectBuffer> c;
    Txn<DirectBuffer> txn;

    @Setup(Trial)
    @Override
    public void setup(final BenchmarkParams b) throws IOException {
      bufferProxy = PROXY_DB;
      super.setup(b, false);
      super.write();
      final int maxValSizeForCopy = 4_081; // 2nd copy requires *2 /tmp space
      if (valSize <= maxValSizeForCopy && tmp.getName().contains(".readKey-")) {
        env.copy(compact, MDB_CP_COMPACT);
        reportSpaceUsed(compact, "compacted");
      }
      txn = env.txnRead();
      c = db.openCursor(txn);
    }

    @TearDown(Trial)
    @Override
    public void teardown() throws IOException {
      c.close();
      txn.abort();
      super.teardown();
    }
  }

  @State(Benchmark)
  @SuppressWarnings("checkstyle:visibilitymodifier")
  public static class Writer extends LmdbJava {

    /**
     * Whether <code>MDB_NOSYNC</code> is used.
     */
    @Param("false")
    boolean sync;

    @Setup(Invocation)
    @Override
    public void setup(final BenchmarkParams b) throws IOException {
      bufferProxy = PROXY_DB;
      super.setup(b, sync);
    }

    @TearDown(Invocation)
    @Override
    public void teardown() throws IOException {
      super.teardown();
    }
  }

}