/*
 * 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.logging.log4j.perf.nogc;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.spi.ThreadContextMap;
import org.apache.logging.log4j.util.BiConsumer;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.StringMap;
import org.apache.logging.log4j.util.TriConsumer;

/**
 * Open hash map-based implementation of the {@code ReadOnlyStringMap} interface.
 * Implementation based on <a href="http://fastutil.di.unimi.it/">fastutil</a>'s
 * <a href="http://fastutil.di.unimi.it/docs/it/unimi/dsi/fastutil/objects/Object2ObjectOpenHashMap.html">Object2ObjectOpenHashMap</a>.
 * <p>
 * A type-specific hash map with a fast, small-footprint implementation.
 *
 * <P>
 * Instances of this class use a hash table to represent a map. The table is
 * filled up to a specified <em>load factor</em>, and then doubled in size to
 * accommodate new entries. If the table is emptied below <em>one fourth</em> of
 * the load factor, it is halved in size. However, halving is not performed when
 * deleting entries from an iterator, as it would interfere with the iteration
 * process.
 *
 * <p>
 * Note that {@link #clear()} does not modify the hash table size. Rather, the
 * {@link #trim(int)} method lets you control the size of
 * the table; this is particularly useful if you reuse instances of this class.
 * <p>
 * <ul>
 *   <li>Garbage-free iteration over key-value pairs with {@code BiConsumer} and {@code TriConsumer}.</li>
 *   <li>Fast copy. If the ThreadContextMap is also an instance of {@code OpenHashStringMap},
 *     the full thread context data can be transferred with two array copies and five field updates.</li>
 * </ul>
 *
 * @since 2.7
 */
public class OpenHashStringMap<K, V> implements StringMap, ThreadContextMap {
    /** The initial default size of a hash table. */
    public static final int DEFAULT_INITIAL_SIZE = 16;

    /** The default load factor of a hash table. */
    public static final float DEFAULT_LOAD_FACTOR = .75f;

    private static final String FROZEN = "Frozen collection cannot be modified";
    private static final long serialVersionUID = -1486744623338827187L;

    /** The array of keys. */
    protected transient K[] keys;
    /** The array of values. */
    protected transient V[] values;
    /** The mask for wrapping a position counter. */
    protected transient int mask;
    /** Whether this set contains the key zero. */
    protected transient boolean containsNullKey;
    /** The current table size. */
    protected transient int arraySize;
    /**
     * Threshold after which we rehash. It must be the table size times {@link #loadFactor}.
     */
    protected transient int maxFill;
    /** Number of entries in the set (including the key zero, if present). */
    protected int size;
    /** The acceptable load factor. */
    protected final float loadFactor;

    private final V defRetValue = null;
    private boolean immutable;
    private transient boolean iterating;

    /**
     * Creates a new hash map with initial expected
     * {@link #DEFAULT_INITIAL_SIZE} entries and
     * {@link #DEFAULT_LOAD_FACTOR} as load factor.
     */
    public OpenHashStringMap() {
        this(DEFAULT_INITIAL_SIZE, DEFAULT_LOAD_FACTOR);
    }
    /**
     * Creates a new hash map with {@link #DEFAULT_LOAD_FACTOR} as load factor.
     *
     * @param expected
     *            the expected number of elements in the hash map.
     */
    public OpenHashStringMap(final int expected) {
        this(expected, DEFAULT_LOAD_FACTOR);
    }
    /**
     * Creates a new hash map.
     *
     * <p>
     * The actual table size will be the least power of two greater than
     * <code>expected</code>/<code>f</code>.
     *
     * @param expected
     *            the expected number of elements in the hash set.
     * @param f
     *            the load factor.
     */
    @SuppressWarnings("unchecked")
    public OpenHashStringMap(final int expected, final float f) {
        if (f <= 0 || f > 1) {
            throw new IllegalArgumentException("Load factor must be greater than 0 and smaller than or equal to 1");
        }
        if (expected < 0) {
            throw new IllegalArgumentException("The expected number of elements must be nonnegative");
        }
        this.loadFactor = f;
        arraySize = HashCommon.arraySize(expected, f);
        mask = arraySize - 1;
        maxFill = HashCommon.maxFill(arraySize, f);
        keys = (K[]) new Object[arraySize + 1];
        values = (V[]) new Object[arraySize + 1];
    }
    /**
     * Creates a new hash map with {@link #DEFAULT_LOAD_FACTOR} as load
     * factor copying a given one.
     *
     * @param map
     *            a {@link Map} to be copied into the new hash map.
     */
    public OpenHashStringMap(final Map<? extends K, ? extends V> map) {
        this(map, DEFAULT_LOAD_FACTOR);
    }
    /**
     * Creates a new hash map copying a given one.
     *
     * @param map
     *            a {@link Map} to be copied into the new hash map.
     * @param f
     *            the load factor.
     */
    public OpenHashStringMap(final Map<? extends K, ? extends V> map, final float f) {
        this(map.size(), f);
        putAll(map);
    }

    /**
     * Creates a new hash map with {@link #DEFAULT_LOAD_FACTOR} as load
     * factor copying a given type-specific one.
     *
     * @param contextData
     *            a type-specific map to be copied into the new hash map.
     */
    public OpenHashStringMap(final ReadOnlyStringMap contextData) {
        this(contextData, DEFAULT_LOAD_FACTOR);
    }
    /**
     * Creates a new hash map copying a given type-specific one.
     *
     * @param contextData
     *            a type-specific map to be copied into the new hash map.
     * @param f
     *            the load factor.
     */
    public OpenHashStringMap(final ReadOnlyStringMap contextData, final float f) {
        this(contextData.size(), f);
        if (contextData instanceof OpenHashStringMap) {
            initFrom0((OpenHashStringMap) contextData);
        } else {
            contextData.forEach(PUT_ALL, this);
        }
    }

    private static final TriConsumer<String, Object, StringMap> PUT_ALL =
            (key, value, contextData) -> contextData.putValue(key, value);

    private void assertNotFrozen() {
        if (immutable) {
            throw new UnsupportedOperationException(FROZEN);
        }
    }

    private void assertNoConcurrentModification() {
        if (iterating) {
            throw new ConcurrentModificationException();
        }
    }

    @SuppressWarnings("unchecked")
    private void initFrom0(final OpenHashStringMap other) {
        // this.loadFactor = other.loadFactor; // final field
        this.arraySize = other.arraySize;
        this.size = other.size;
        this.containsNullKey = other.containsNullKey;
        this.mask = other.mask;
        this.maxFill = other.maxFill;
        keys = (K[]) Arrays.copyOf(other.keys, arraySize + 1);
        values = (V[]) Arrays.copyOf(other.values, arraySize + 1);
    }

    private int realSize() {
        return containsNullKey ? size - 1 : size;
    }

    private void ensureCapacity(final int capacity) {
        final int needed = HashCommon.arraySize(capacity, loadFactor);
        if (needed > arraySize) {
            rehash(needed);
        }
    }

    private void tryCapacity(final long capacity) {
        final int needed =
                Math.min(1 << 30, Math.max(2, HashCommon.nextPowerOfTwo((int) Math.ceil(capacity / loadFactor))));
        if (needed > arraySize) {
            rehash(needed);
        }
    }

    @Override
    public Map<String, String> toMap() {
        final Map<String, String> result = new HashMap<>(size);
        forEach(COPY_INTO_MAP, result);
        return result;
    }

    private static final TriConsumer<String, Object, Map<String, String>> COPY_INTO_MAP =
            (k, v, map) -> map.put(k, v == null ? null : v.toString());

    /*
     * Removes all elements from this map.
     *
     * <P>To increase object reuse, this method does not change the table size.
     * If you want to reduce the table size, you must use {@link #trim()}.
     */
    @Override
    public void clear() {
        if (size == 0) {
            return;
        }
        assertNotFrozen();
        assertNoConcurrentModification();

        size = 0;
        containsNullKey = false;
        Arrays.fill(keys, (null));
        Arrays.fill(values, null);
    }

    @Override
    public boolean containsKey(final String key) {
        return containsObjectKey(key);
    }

    @SuppressWarnings("unchecked")
    private boolean containsObjectKey(final Object k) {
        if (k == null) {
            return containsNullKey;
        }
        K curr;
        final K[] key = this.keys;
        int pos;
        // The starting point.
        if ((curr = key[pos = HashCommon.mix(k.hashCode()) & mask]) == null) {
            return false;
        }
        if (k.equals(curr)) {
            return true;
        }
        // There's always an unused entry.
        while (true) {
            if ((curr = key[pos = (pos + 1) & mask]) == null) {
                return false;
            }
            if (k.equals(curr)) {
                return true;
            }
        }
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof ReadOnlyStringMap)) {
            return false;
        }
        if (size() != ((ReadOnlyStringMap) obj).size()) {
            return false;
        }

        // Convert to maps and compare
        final Map<String, String> thisMap = toMap();
        final Map<String, String> otherMap = ((ReadOnlyStringMap) obj).toMap();
        return thisMap.equals(otherMap);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <VAL> void forEach(final BiConsumer<String, ? super VAL> action) {
        final int startSize = size;
        final K myKeys[] = this.keys;
        int pos = arraySize;

        iterating = true;
        try {
            if (containsNullKey) {
                action.accept((String) myKeys[pos], (VAL) values[pos]);
                if (size != startSize) {
                    throw new ConcurrentModificationException();
                }
            }
            --pos;
            for (; pos >= 0; pos--) {
                if (myKeys[pos] != null) {
                    action.accept((String) myKeys[pos], (VAL) values[pos]);
                    if (size != startSize) {
                        throw new ConcurrentModificationException();
                    }
                }
            }
        } finally {
            iterating = false;
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public <VAL, STATE> void forEach(final TriConsumer<String, ? super VAL, STATE> action, final STATE state) {
        final int startSize = size;
        final K myKeys[] = this.keys;
        int pos = arraySize;

        iterating = true;
        try {
            if (containsNullKey) {
                action.accept((String) myKeys[pos], (VAL) values[pos], state);
                if (size != startSize) {
                    throw new ConcurrentModificationException();
                }
            }
            --pos;
            for (; pos >= 0; pos--) {
                if (myKeys[pos] != null) {
                    action.accept((String) myKeys[pos], (VAL) values[pos], state);
                    if (size != startSize) {
                        throw new ConcurrentModificationException();
                    }
                }
            }
        } finally {
            iterating = false;
        }
    }

    @Override
    public String get(final String key) {
        return (String) getObjectValue(key);
    }

    @SuppressWarnings("unchecked")
    private V getObjectValue(final Object k) {
        if (k == null) {
            return containsNullKey ? values[arraySize] : defRetValue;
        }
        K curr;
        final K[] key = this.keys;
        int pos;
        // The starting point.
        if ((curr = key[pos = HashCommon.mix(k.hashCode()) & mask]) == null) {
            return defRetValue;
        }
        if (k.equals(curr)) {
            return values[pos];
        }
        // There's always an unused entry.
        while (true) {
            if (((curr = key[pos = (pos + 1) & mask]) == null)) {
                return defRetValue;
            }
            if (k.equals(curr)) {
                return values[pos];
            }
        }
    }

    @Override
    public Map<String, String> getCopy() {
        return toMap();
    }

    @Override
    public Map<String, String> getImmutableMapOrNull() {
        return isEmpty() ? null : Collections.unmodifiableMap(toMap());
    }

    @Override
    public <VAL> VAL getValue(final String key) {
        return (VAL) getObjectValue(key);
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void put(final String key, final String value) {
        putObjectValue((K) key, (V) value);
    }

    private int insert(final K k, final V v) {
        int pos;
        if (k == null) {
            if (containsNullKey) {
                return arraySize;
            }
            containsNullKey = true;
            pos = arraySize;
        } else {
            K curr;
            final K[] key = this.keys;
            // The starting point.
            if (!((curr = key[pos = HashCommon.mix(k.hashCode()) & mask]) == null)) {
                if (curr.equals(k)) {
                    return pos;
                }
                while (!((curr = key[pos = (pos + 1) & mask]) == null)) {
                    if (curr.equals(k)) {
                        return pos;
                    }
                }
            }
        }
        keys[pos] = k;
        values[pos] = v;
        if (size++ >= maxFill) {
            rehash(HashCommon.arraySize(size + 1, loadFactor));
        }
        return -1;
    }

    @Override
    public void putAll(final ReadOnlyStringMap source) {
        assertNotFrozen();
        assertNoConcurrentModification();

        if (isEmpty() && source instanceof OpenHashStringMap) {
            initFrom0((OpenHashStringMap) source);
        } else if (source != null) {
            source.forEach(PUT_ALL, this);
        }
    }

    /** {@inheritDoc} */
    public void putAll(final Map<? extends K, ? extends V> map) {
        if (loadFactor <= .5) {
            // The resulting map will be sized for m.size() elements
            ensureCapacity(map.size());
        } else {
            // The resulting map will be tentatively sized for size() +  m.size() elements
            tryCapacity(size() + map.size());
        }
        for (final Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
            putObjectValue(entry.getKey(), entry.getValue());
        }
    }

    private V putObjectValue(final K k, final V v) {
        assertNotFrozen();
        assertNoConcurrentModification();

        final int pos = insert(k, v);
        if (pos < 0) {
            return defRetValue;
        }
        final V oldValue = values[pos];
        values[pos] = v;
        return oldValue;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void putValue(final String key, final Object value) {
        putObjectValue((K) key, (V) value);
    }

    @Override
    public void remove(final String key) {
        removeObjectKey(key);
    }

    @Override
    public void freeze() {
        immutable = true;
    }

    @Override
    public boolean isFrozen() {
        return immutable;
    }

    @SuppressWarnings("unchecked")
    private V removeObjectKey(final Object k) {
        assertNotFrozen();
        assertNoConcurrentModification();

        if (k == null) {
            if (containsNullKey) {
                return removeNullEntry();
            }
            return defRetValue;
        }
        final K[] key = this.keys;
        int pos = HashCommon.mix(k.hashCode()) & mask;
        K curr = key[pos & mask];
        // The starting point.
        if (curr == null) {
            return defRetValue;
        }
        if (k.equals(curr)) {
            return removeEntry(pos);
        }
        while (true) {
            if ((curr = key[pos = (pos + 1) & mask]) == null) {
                return defRetValue;
            }
            if (k.equals(curr)) {
                return removeEntry(pos);
            }
        }
    }

    private V removeEntry(final int pos) {
        final V oldValue = values[pos];
        values[pos] = null;
        size--;
        shiftKeys(pos);
        if (size < maxFill / 4 && arraySize > DEFAULT_INITIAL_SIZE) {
            rehash(arraySize / 2);
        }
        return oldValue;
    }

    private V removeNullEntry() {
        containsNullKey = false;
        keys[arraySize] = null;
        final V oldValue = values[arraySize];
        values[arraySize] = null;
        size--;
        if (size < maxFill / 4 && arraySize > DEFAULT_INITIAL_SIZE) {
            rehash(arraySize / 2);
        }
        return oldValue;
    }
    /**
     * Shifts left entries with the specified hash code, starting at the
     * specified position, and empties the resulting free entry.
     *
     * @param pos
     *            a starting position.
     */
    private void shiftKeys(int pos) {
        // Shift entries with the same hash.
        int last, slot;
        K curr;
        final K[] myKeys = this.keys;
        for (; ; ) {
            pos = ((last = pos) + 1) & mask;
            for (; ; ) {
                if (((curr = myKeys[pos]) == null)) {
                    myKeys[last] = (null);
                    values[last] = null;
                    return;
                }
                slot = HashCommon.mix(curr.hashCode()) & mask;
                if (last <= pos ? (last >= slot || slot > pos) : (last >= slot && slot > pos)) {
                    break;
                }
                pos = (pos + 1) & mask;
            }
            myKeys[last] = curr;
            values[last] = values[pos];
        }
    }

    @Override
    public int size() {
        return size;
    }

    /**
     * Rehashes this map if the table is too large.
     *
     * <P>
     * Let <var>N</var> be the smallest table size that can hold
     * <code>max(n,{@link #size()})</code> entries, still satisfying the load
     * factor. If the current table size is smaller than or equal to
     * <var>N</var>, this method does nothing. Otherwise, it rehashes this map
     * in a table of size <var>N</var>.
     *
     * <P>
     * This method is useful when reusing maps. {@linkplain #clear() Clearing a
     * map} leaves the table size untouched. If you are reusing a map many times,
     * you can call this method with a typical size to avoid keeping around a
     * very large table just because of a few large transient maps.
     *
     * @param n
     *            the threshold for the trimming.
     * @return true if there was enough memory to trim the map.
     */
    public boolean trim(final int n) {
        final int l = HashCommon.nextPowerOfTwo((int) Math.ceil(n / loadFactor));
        if (l >= n || size > HashCommon.maxFill(l, loadFactor)) {
            return true;
        }
        try {
            rehash(l);
        } catch (final OutOfMemoryError cantDoIt) { // unusual to catch OOME but in this case appropriate
            return false;
        }
        return true;
    }
    /**
     * Rehashes the map.
     *
     * <P>
     * This method implements the basic rehashing strategy, and may be overriden
     * by subclasses implementing different rehashing strategies (e.g.,
     * disk-based rehashing). However, you should not override this method
     * unless you understand the internal workings of this class.
     *
     * @param newN
     *            the new size
     */
    @SuppressWarnings("unchecked")
    protected void rehash(final int newN) {
        final K myKeys[] = this.keys;
        final V myValues[] = this.values;
        final int mask = newN - 1; // Note that this is used by the hashing
        // macro
        final K newKey[] = (K[]) new Object[newN + 1];
        final V newValue[] = (V[]) new Object[newN + 1];
        int i = arraySize, pos;
        for (int j = realSize(); j-- != 0; ) {
            while (myKeys[--i] == null) {
                // advance i until we find an existing key
            }
            if (newKey[pos = HashCommon.mix(myKeys[i].hashCode()) & mask] != null) { // rehash & check slot availability
                while (newKey[pos = (pos + 1) & mask] != null) {
                    // find available slot at (or immediately following) pos
                }
            }
            newKey[pos] = myKeys[i];
            newValue[pos] = myValues[i];
        }
        newValue[newN] = myValues[arraySize];
        arraySize = newN;
        this.mask = mask;
        maxFill = HashCommon.maxFill(arraySize, loadFactor);
        this.keys = newKey;
        this.values = newValue;
    }

    /**
     * Returns a hash code for this map.
     *
     * @return a hash code for this map.
     */
    @Override
    public int hashCode() {
        return toMap().hashCode();
    }

    @SuppressWarnings("unchecked")
    private void readObject(final ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        arraySize = HashCommon.arraySize(size, loadFactor);
        maxFill = HashCommon.maxFill(arraySize, loadFactor);
        mask = arraySize - 1;
        final K key[] = this.keys = (K[]) new Object[arraySize + 1];
        final V value[] = this.values = (V[]) new Object[arraySize + 1];
        K k;
        V v;
        for (int i = size, pos; i-- != 0; ) {
            k = (K) s.readObject();
            v = (V) s.readObject();
            if (k == null) {
                pos = arraySize;
                containsNullKey = true;
            } else {
                pos = HashCommon.mix(k.hashCode()) & mask;
                while (key[pos] != null) {
                    pos = (pos + 1) & mask;
                }
            }
            key[pos] = k;
            value[pos] = v;
        }
    }

    private void writeObject(final ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        try {
            forEach(SERIALIZER, s);
        } catch (final RuntimeException runex) {
            if (runex.getCause() instanceof IOException) {
                throw (IOException) runex.getCause();
            }
            throw runex;
        }
    }

    private static final TriConsumer<String, Object, ObjectOutputStream> SERIALIZER = (k, v, objectOutputStream) -> {
        try {
            objectOutputStream.writeObject(k);
            objectOutputStream.writeObject(v);
        } catch (final IOException ioex) {
            throw new IllegalStateException(ioex);
        }
    };

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder(256);
        sb.append('{');
        final K myKeys[] = this.keys;
        int pos = arraySize;
        boolean first = true;
        if (containsNullKey) {
            sb.append(myKeys[pos] == this ? "(this map)" : myKeys[pos]);
            sb.append('=');
            sb.append(values[pos] == this ? "(this map)" : values[pos]);
            first = false;
        }
        --pos;
        for (; pos >= 0; pos--) {
            if (myKeys[pos] != null) {
                if (first) {
                    first = false;
                } else {
                    sb.append(", ");
                }
                sb.append(myKeys[pos] == this ? "(this map)" : myKeys[pos]);
                sb.append('=');
                sb.append(values[pos] == this ? "(this map)" : values[pos]);
            }
        }
        sb.append('}');
        return sb.toString();
    }

    private static final class HashCommon {
        private HashCommon() {}

        /** 2<sup>32</sup> &middot; &phi;, &phi; = (&#x221A;5 &minus; 1)/2. */
        private static final int INT_PHI = 0x9E3779B9;

        /** The reciprocal of {@link #INT_PHI} modulo 2<sup>32</sup>. */
        private static final int INV_INT_PHI = 0x144cbc89;

        /** Avalanches the bits of an integer by applying the finalisation step of MurmurHash3.
         *
         * <p>This method implements the finalisation step of Austin Appleby's
         * <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>.
         * Its purpose is to avalanche the bits of the argument to within 0.25% bias.
         *
         * @param x an integer.
         * @return a hash value with good avalanching properties.
         */
        public static int murmurHash3(final int x) {
            int h = x;
            h ^= h >>> 16;
            h *= 0x85ebca6b;
            h ^= h >>> 13;
            h *= 0xc2b2ae35;
            h ^= h >>> 16;
            return h;
        }

        /**
         * Quickly mixes the bits of an integer.
         *
         * <p>This method mixes the bits of the argument by multiplying by the golden ratio and
         * xorshifting the result. It is borrowed from <a href="https://github.com/OpenHFT/Koloboke">Koloboke</a>, and
         * it has slightly worse behaviour than {@link #murmurHash3(int)} (in open-addressing hash tables the average
         * number of probes is slightly larger), but it's much faster.
         *
         * @param x an integer.
         * @return a hash value obtained by mixing the bits of {@code x}.
         * @see #invMix(int)
         */
        public static int mix(final int x) {
            final int h = x * INT_PHI;
            return h ^ (h >>> 16);
        }

        /** The inverse of {@link #mix(int)}. This method is mainly useful to create unit tests.
         *
         * @param x an integer.
         * @return a value that passed through {@link #mix(int)} would give {@code x}.
         */
        public static int invMix(final int x) {
            return (x ^ x >>> 16) * INV_INT_PHI;
        }

        /** Return the least power of two greater than or equal to the specified value.
         *
         * <p>Note that this function will return 1 when the argument is 0.
         *
         * @param x an integer smaller than or equal to 2<sup>30</sup>.
         * @return the least power of two greater than or equal to the specified value.
         */
        public static int nextPowerOfTwo(final int x) {
            if (x == 0) {
                return 1;
            }
            int r = x;
            r--;
            r |= r >> 1;
            r |= r >> 2;
            r |= r >> 4;
            r |= r >> 8;
            return (r | r >> 16) + 1;
        }

        /** Return the least power of two greater than or equal to the specified value.
         *
         * <p>Note that this function will return 1 when the argument is 0.
         *
         * @param x a long integer smaller than or equal to 2<sup>62</sup>.
         * @return the least power of two greater than or equal to the specified value.
         */
        public static long nextPowerOfTwo(final long x) {
            if (x == 0) {
                return 1;
            }
            long r = x;
            r--;
            r |= r >> 1;
            r |= r >> 2;
            r |= r >> 4;
            r |= r >> 8;
            r |= r >> 16;
            return (r | r >> 32) + 1;
        }

        /** Returns the maximum number of entries that can be filled before rehashing.
         *
         * @param n the size of the backing array.
         * @param f the load factor.
         * @return the maximum number of entries before rehashing.
         */
        public static int maxFill(final int n, final float f) {
            /* We must guarantee that there is always at least
             * one free entry (even with pathological load factors). */
            return Math.min((int) Math.ceil(n * f), n - 1);
        }

        /**
         * Returns the least power of two smaller than or equal to 2<sup>30</sup> and larger than or equal to
         * <code>Math.ceil( expected / f )</code>.
         *
         * @param expected the expected number of elements in a hash table.
         * @param f the load factor.
         * @return the minimum possible size for a backing array.
         * @throws IllegalArgumentException if the necessary size is larger than 2<sup>30</sup>.
         */
        public static int arraySize(final int expected, final float f) {
            final long result = Math.max(2, nextPowerOfTwo((long) Math.ceil(expected / f)));
            if (result > (1 << 30)) {
                throw new IllegalArgumentException(
                        "Too large (" + expected + " expected elements with load factor " + f + ")");
            }
            return (int) result;
        }
    }
}
