/*
 * Copyright (C) 2015 The Guava Authors
 *
 * 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.
 */

package com.google.common.hash;

import com.google.caliper.BeforeExperiment;
import com.google.caliper.Benchmark;
import com.google.caliper.Param;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Random;
import org.jspecify.annotations.NullUnmarked;

/**
 * Benchmarks for comparing the various {@link HashCode#equals} methods.
 *
 * <p>Parameters for the benchmark are:
 *
 * <ul>
 *   <li>size: the length of the byte array to hash
 *   <li>whereToDiffer: where in the array the bytes should differ
 *   <li>equalsImpl: which implementation of array equality to use
 * </ul>
 *
 * <p><b>Important note:</b> the primary goal of this benchmark is to ensure that varying {@code
 * whereToDiffer} produces no observable change in performance. We want to make sure that the array
 * equals implementation is *not* short-circuiting to prevent timing-based attacks. Being fast is
 * only a secondary goal.
 *
 * @author Kurt Alfred Kluever
 */
@NullUnmarked
public class HashCodeBenchmark {

  // Use a statically configured random instance for all of the benchmarks
  private static final Random random = new Random(42);

  @Param({"1000", "100000"})
  private int size;

  @Param WhereToDiffer whereToDiffer;

  @Param EqualsImplementation equalsImpl;

  private enum WhereToDiffer {
    ONE_PERCENT_IN,
    LAST_BYTE,
    NOT_AT_ALL;
  }

  private enum EqualsImplementation {
    ANDING_BOOLEANS {
      @Override
      boolean doEquals(byte[] a, byte[] b) {
        if (a.length != b.length) {
          return false;
        }
        boolean areEqual = true;
        for (int i = 0; i < a.length; i++) {
          areEqual &= a[i] == b[i];
        }
        return areEqual;
      }
    },
    XORING_TO_BYTE {
      @Override
      boolean doEquals(byte[] a, byte[] b) {
        if (a.length != b.length) {
          return false;
        }
        byte result = 0;
        for (int i = 0; i < a.length; i++) {
          result = (byte) (result | a[i] ^ b[i]);
        }
        return result == 0;
      }
    },
    XORING_TO_INT {
      @Override
      boolean doEquals(byte[] a, byte[] b) {
        if (a.length != b.length) {
          return false;
        }
        int result = 0;
        for (int i = 0; i < a.length; i++) {
          result |= a[i] ^ b[i];
        }
        return result == 0;
      }
    },
    MESSAGE_DIGEST_IS_EQUAL {
      @Override
      boolean doEquals(byte[] a, byte[] b) {
        return MessageDigest.isEqual(a, b);
      }
    },
    ARRAYS_EQUALS {
      @Override
      boolean doEquals(byte[] a, byte[] b) {
        return Arrays.equals(a, b);
      }
    };

    abstract boolean doEquals(byte[] a, byte[] b);
  }

  private byte[] testBytesA;
  private byte[] testBytesB;

  @BeforeExperiment
  void setUp() {
    testBytesA = new byte[size];
    random.nextBytes(testBytesA);
    testBytesB = Arrays.copyOf(testBytesA, size);
    int indexToDifferAt = -1;
    switch (whereToDiffer) {
      case ONE_PERCENT_IN:
        indexToDifferAt = (int) (size * 0.01);
        break;
      case LAST_BYTE:
        indexToDifferAt = size - 1;
        break;
      case NOT_AT_ALL:
    }
    if (indexToDifferAt != -1) {
      testBytesA[indexToDifferAt] = (byte) (testBytesB[indexToDifferAt] - 1);
    }
  }

  @Benchmark
  boolean hashFunction(int reps) {
    boolean result = true;
    for (int i = 0; i < reps; i++) {
      result ^= equalsImpl.doEquals(testBytesA, testBytesB);
    }
    return result;
  }
}
