/*
 * Copyright (C) 2019 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.collect;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Objects;
import com.google.common.primitives.Ints;
import java.util.Arrays;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;

/**
 * Helper classes and static methods for implementing compact hash-based collections.
 *
 * @author Jon Noack
 */
@GwtIncompatible
final class CompactHashing {
  private CompactHashing() {}

  /** Indicates blank table entries. */
  static final byte UNSET = 0;

  /** Number of bits used to store the numbers of hash table bits (max 30). */
  private static final int HASH_TABLE_BITS_MAX_BITS = 5;

  /** Use high bits of metadata for modification count. */
  static final int MODIFICATION_COUNT_INCREMENT = (1 << HASH_TABLE_BITS_MAX_BITS);

  /** Bitmask that selects the low bits of metadata to get hashTableBits. */
  static final int HASH_TABLE_BITS_MASK = (1 << HASH_TABLE_BITS_MAX_BITS) - 1;

  /** Maximum size of a compact hash-based collection (2^30 - 1 because 0 is UNSET). */
  static final int MAX_SIZE = Ints.MAX_POWER_OF_TWO - 1;

  /** Default size of a compact hash-based collection. */
  static final int DEFAULT_SIZE = 3;

  /**
   * Minimum size of the hash table of a compact hash-based collection. Because small hash tables
   * use a byte[], any smaller size uses the same amount of memory due to object padding.
   */
  private static final int MIN_HASH_TABLE_SIZE = 4;

  private static final int BYTE_MAX_SIZE = 1 << Byte.SIZE; // 2^8 = 256
  private static final int BYTE_MASK = (1 << Byte.SIZE) - 1; // 2^8 - 1 = 255

  private static final int SHORT_MAX_SIZE = 1 << Short.SIZE; // 2^16 = 65_536
  private static final int SHORT_MASK = (1 << Short.SIZE) - 1; // 2^16 - 1 = 65_535

  /**
   * Returns the power of 2 hashtable size required to hold the expected number of items or the
   * minimum hashtable size, whichever is greater.
   */
  static int tableSize(int expectedSize) {
    // We use entries next == 0 to indicate UNSET, so actual capacity is 1 less than requested.
    return Math.max(MIN_HASH_TABLE_SIZE, Hashing.closedTableSize(expectedSize + 1, 1.0f));
  }

  /** Creates and returns a properly-sized array with the given number of buckets. */
  static Object createTable(int buckets) {
    if (buckets < 2
        || buckets > Ints.MAX_POWER_OF_TWO
        || Integer.highestOneBit(buckets) != buckets) {
      throw new IllegalArgumentException("must be power of 2 between 2^1 and 2^30: " + buckets);
    }
    if (buckets <= BYTE_MAX_SIZE) {
      return new byte[buckets];
    } else if (buckets <= SHORT_MAX_SIZE) {
      return new short[buckets];
    } else {
      return new int[buckets];
    }
  }

  static void tableClear(Object table) {
    if (table instanceof byte[]) {
      Arrays.fill((byte[]) table, (byte) 0);
    } else if (table instanceof short[]) {
      Arrays.fill((short[]) table, (short) 0);
    } else {
      Arrays.fill((int[]) table, 0);
    }
  }

  static int tableGet(Object table, int index) {
    if (table instanceof byte[]) {
      return ((byte[]) table)[index] & BYTE_MASK; // unsigned read
    } else if (table instanceof short[]) {
      return ((short[]) table)[index] & SHORT_MASK; // unsigned read
    } else {
      return ((int[]) table)[index];
    }
  }

  static void tableSet(Object table, int index, int entry) {
    if (table instanceof byte[]) {
      ((byte[]) table)[index] = (byte) entry; // unsigned write
    } else if (table instanceof short[]) {
      ((short[]) table)[index] = (short) entry; // unsigned write
    } else {
      ((int[]) table)[index] = entry;
    }
  }

  /**
   * Returns a larger power of 2 hashtable size given the current mask.
   *
   * <p>For hashtable sizes less than or equal to 32, the returned power of 2 is 4x the current
   * hashtable size to reduce expensive rehashing. Otherwise the returned power of 2 is 2x the
   * current hashtable size.
   */
  static int newCapacity(int mask) {
    return ((mask < 32) ? 4 : 2) * (mask + 1);
  }

  /** Returns the hash prefix given the current mask. */
  static int getHashPrefix(int value, int mask) {
    return value & ~mask;
  }

  /** Returns the index, or 0 if the entry is "null". */
  static int getNext(int entry, int mask) {
    return entry & mask;
  }

  /** Returns a new value combining the prefix and suffix using the given mask. */
  static int maskCombine(int prefix, int suffix, int mask) {
    return (prefix & ~mask) | (suffix & mask);
  }

  static int remove(
      @NullableDecl Object key,
      @NullableDecl Object value,
      int mask,
      Object table,
      int[] entries,
      Object[] keys,
      @NullableDecl Object[] values) {
    int hash = Hashing.smearedHash(key);
    int tableIndex = hash & mask;
    int next = tableGet(table, tableIndex);
    if (next == UNSET) {
      return -1;
    }
    int hashPrefix = getHashPrefix(hash, mask);
    int lastEntryIndex = -1;
    do {
      int entryIndex = next - 1;
      int entry = entries[entryIndex];
      if (getHashPrefix(entry, mask) == hashPrefix
          && Objects.equal(key, keys[entryIndex])
          && (values == null || Objects.equal(value, values[entryIndex]))) {
        int newNext = getNext(entry, mask);
        if (lastEntryIndex == -1) {
          // we need to update the root link from table[]
          tableSet(table, tableIndex, newNext);
        } else {
          // we need to update the link from the chain
          entries[lastEntryIndex] = maskCombine(entries[lastEntryIndex], newNext, mask);
        }

        return entryIndex;
      }
      lastEntryIndex = entryIndex;
      next = getNext(entry, mask);
    } while (next != UNSET);
    return -1;
  }
}
