package org.xbib.datastructures.immutable;

import java.util.IdentityHashMap;
import java.util.Objects;
import java.util.function.BiConsumer;
import static java.util.Objects.requireNonNull;
import static org.xbib.datastructures.immutable.ImmutableCollection.checkPositionIndex;
import static org.xbib.datastructures.immutable.ImmutableMapEntry.checkEntryNotNull;
import static org.xbib.datastructures.immutable.ImmutableMapEntry.createEntryArray;

/**
 * Implementation of {@link ImmutableMap} with two or more entries.
 */
final class RegularImmutableMap<K, V> extends ImmutableMap<K, V> {
    @SuppressWarnings("unchecked")
    static final ImmutableMap<Object, Object> EMPTY =
            new RegularImmutableMap<>((Entry<Object, Object>[]) EMPTY_ENTRY_ARRAY, null, 0);

    /**
     * Closed addressing tends to perform well even with high load factors. Being conservative here
     * ensures that the table is still likely to be relatively sparse (hence it misses fast) while
     * saving space.
     */
    static final double MAX_LOAD_FACTOR = 1.2;

    /**
     * Maximum allowed false positive probability of detecting a hash flooding attack given random
     * input.
     */
    static final double HASH_FLOODING_FPP = 0.001;

    /**
     * Maximum allowed length of a hash table bucket before falling back to a j.u.HashMap based
     * implementation. Experimentally determined.
     */
    static final int MAX_HASH_BUCKET_LENGTH = 8;

    // entries in insertion order
    final transient Entry<K, V>[] entries;
    // array of linked lists of entries
    private final transient ImmutableMapEntry<K, V>[] table;
    // 'and' with an int to get a table index
    private final transient int mask;

    static <K, V> ImmutableMap<K, V> fromEntries(Entry<K, V>... entries) {
        return fromEntryArray(entries.length, entries, /* throwIfDuplicateKeys= */ true);
    }

    /**
     * Creates an ImmutableMap from the first n entries in entryArray. This implementation may replace
     * the entries in entryArray with its own entry objects (though they will have the same key/value
     * contents), and may take ownership of entryArray.
     */
    static <K, V> ImmutableMap<K, V> fromEntryArray(
            int n, Entry<K, V>[] entryArray, boolean throwIfDuplicateKeys) {
        checkPositionIndex(n, entryArray.length, "n");
        if (n == 0) {
            @SuppressWarnings("unchecked") // it has no entries so the type variables don't matter
            ImmutableMap<K, V> empty = (ImmutableMap<K, V>) EMPTY;
            return empty;
        }
        try {
            return fromEntryArrayCheckingBucketOverflow(n, entryArray, throwIfDuplicateKeys);
        } catch (BucketOverflowException e) {
            // probable hash flooding attack, fall back to j.u.HM based implementation and use its
            // implementation of hash flooding protection
            return JdkBackedImmutableMap.create(n, entryArray, throwIfDuplicateKeys);
        }
    }

    private static <K, V> ImmutableMap<K, V> fromEntryArrayCheckingBucketOverflow(
            int n, Entry<K, V>[] entryArray, boolean throwIfDuplicateKeys)
            throws BucketOverflowException {
        /*
         * The cast is safe: n==entryArray.length means that we have filled the whole array with Entry
         * instances, in which case it is safe to cast it from an array of nullable entries to an array
         * of non-null entries.
         */
        @SuppressWarnings("nullness")
        Entry<K, V>[] entries =
                (n == entryArray.length) ? entryArray : createEntryArray(n);
        int tableSize = closedTableSize(n, MAX_LOAD_FACTOR);
        ImmutableMapEntry<K, V>[] table = createEntryArray(tableSize);
        int mask = tableSize - 1;
        // If duplicates are allowed, this IdentityHashMap will record the final Entry for each
        // duplicated key. We will use this final Entry to overwrite earlier slots in the entries array
        // that have the same key. Then a second pass will remove all but the first of the slots that
        // have this Entry. The value in the map becomes false when this first entry has been copied, so
        // we know not to copy the remaining ones.
        IdentityHashMap<Entry<K, V>, Boolean> duplicates = null;
        int dupCount = 0;
        for (int entryIndex = n - 1; entryIndex >= 0; entryIndex--) {
            // requireNonNull is safe because the first `n` elements have been filled in.
            Entry<K, V> entry = requireNonNull(entryArray[entryIndex]);
            K key = entry.getKey();
            V value = entry.getValue();
            checkEntryNotNull(key, value);
            int tableIndex = smear(key.hashCode()) & mask;
            ImmutableMapEntry<K, V> keyBucketHead = table[tableIndex];
            ImmutableMapEntry<K, V> effectiveEntry =
                    checkNoConflictInKeyBucket(key, value, keyBucketHead, throwIfDuplicateKeys);
            if (effectiveEntry == null) {
                // prepend, not append, so the entries can be immutable
                effectiveEntry =
                        (keyBucketHead == null)
                                ? makeImmutable(entry, key, value)
                                : new NonTerminalImmutableMapEntry<K, V>(key, value, keyBucketHead);
                table[tableIndex] = effectiveEntry;
            } else {
                // We already saw this key, and the first value we saw (going backwards) is the one we are
                // keeping. So we won't touch table[], but we do still want to add the existing entry that
                // we found to entries[] so that we will see this key in the right place when iterating.
                if (duplicates == null) {
                    duplicates = new IdentityHashMap<>();
                }
                duplicates.put(effectiveEntry, true);
                dupCount++;
                // Make sure we are not overwriting the original entries array, in case we later do
                // buildOrThrow(). We would want an exception to include two values for the duplicate key.
                if (entries == entryArray) {
                    entries = entries.clone();
                }
            }
            entries[entryIndex] = effectiveEntry;
        }
        if (duplicates != null) {
            // Explicit type parameters needed here to avoid a problem with nullness inference.
            entries = RegularImmutableMap.removeDuplicates(entries, n, n - dupCount, duplicates);
            int newTableSize = closedTableSize(entries.length, MAX_LOAD_FACTOR);
            if (newTableSize != tableSize) {
                return fromEntryArrayCheckingBucketOverflow(
                        entries.length, entries, /* throwIfDuplicateKeys= */ true);
            }
        }
        return new RegularImmutableMap<>(entries, table, mask);
    }

    /**
     * Constructs a new entry array where each duplicated key from the original appears only once, at
     * its first position but with its final value. The {@code duplicates} map is modified.
     *
     * @param entries    the original array of entries including duplicates
     * @param n          the number of valid entries in {@code entries}
     * @param newN       the expected number of entries once duplicates are removed
     * @param duplicates a map of canonical {@link Entry} objects for each duplicate key. This map
     *                   will be updated by the method, setting each value to false as soon as the {@link Entry} has
     *                   been included in the new entry array.
     * @return an array of {@code newN} entries where no key appears more than once.
     */
    static <K, V> Entry<K, V>[] removeDuplicates(
            Entry<K, V>[] entries, int n, int newN, IdentityHashMap<Entry<K, V>, Boolean> duplicates) {
        Entry<K, V>[] newEntries = createEntryArray(newN);
        for (int in = 0, out = 0; in < n; in++) {
            Entry<K, V> entry = entries[in];
            Boolean status = duplicates.get(entry);
            // null=>not dup'd; true=>dup'd, first; false=>dup'd, not first
            if (status != null) {
                if (status) {
                    duplicates.put(entry, false);
                } else {
                    continue; // delete this entry; we already copied an earlier one for the same key
                }
            }
            newEntries[out++] = entry;
        }
        return newEntries;
    }

    /**
     * Makes an entry usable internally by a new ImmutableMap without rereading its contents.
     */
    static <K, V> ImmutableMapEntry<K, V> makeImmutable(Entry<K, V> entry, K key, V value) {
        boolean reusable =
                entry instanceof ImmutableMapEntry && ((ImmutableMapEntry<K, V>) entry).isReusable();
        return reusable ? (ImmutableMapEntry<K, V>) entry : new ImmutableMapEntry<K, V>(key, value);
    }

    /**
     * Makes an entry usable internally by a new ImmutableMap.
     */
    static <K, V> ImmutableMapEntry<K, V> makeImmutable(Entry<K, V> entry) {
        return makeImmutable(entry, entry.getKey(), entry.getValue());
    }

    private RegularImmutableMap(Entry<K, V>[] entries, ImmutableMapEntry<K, V>[] table, int mask) {
        this.entries = entries;
        this.table = table;
        this.mask = mask;
    }

    /**
     * Checks if the given key already appears in the hash chain starting at {@code keyBucketHead}. If
     * it does not, then null is returned. If it does, then if {@code throwIfDuplicateKeys} is true an
     * {@code IllegalArgumentException} is thrown, and otherwise the existing {@link Entry} is
     * returned.
     *
     * @throws IllegalArgumentException if another entry in the bucket has the same key and {@code
     *                                  throwIfDuplicateKeys} is true
     * @throws BucketOverflowException  if this bucket has too many entries, which may indicate a hash
     *                                  flooding attack
     */
    static <K, V> ImmutableMapEntry<K, V> checkNoConflictInKeyBucket(
            Object key,
            Object newValue,
            ImmutableMapEntry<K, V> keyBucketHead,
            boolean throwIfDuplicateKeys)
            throws BucketOverflowException {
        int bucketSize = 0;
        for (; keyBucketHead != null; keyBucketHead = keyBucketHead.getNextInKeyBucket()) {
            if (keyBucketHead.getKey().equals(key)) {
                if (throwIfDuplicateKeys) {
                    checkNoConflict(/* safe= */ false, "key", keyBucketHead, key + "=" + newValue);
                } else {
                    return keyBucketHead;
                }
            }
            if (++bucketSize > MAX_HASH_BUCKET_LENGTH) {
                throw new BucketOverflowException();
            }
        }
        return null;
    }

    static class BucketOverflowException extends Exception {
    }

    @Override
    public V get(Object key) {
        return get(key, table, mask);
    }

    static <V> V get(Object key, ImmutableMapEntry<?, V>[] keyTable,
                     int mask) {
        if (key == null || keyTable == null) {
            return null;
        }
        int index = smear(key.hashCode()) & mask;
        for (ImmutableMapEntry<?, V> entry = keyTable[index];
             entry != null;
             entry = entry.getNextInKeyBucket()) {
            Object candidateKey = entry.getKey();

            /*
             * Assume that equals uses the == optimization when appropriate, and that
             * it would check hash codes as an optimization when appropriate. If we
             * did these things, it would just make things worse for the most
             * performance-conscious users.
             */
            if (key.equals(candidateKey)) {
                return entry.getValue();
            }
        }
        return null;
    }

    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Entry<K, V> entry : entries) {
            action.accept(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public int size() {
        return entries.length;
    }

    @Override
    boolean isPartialView() {
        return false;
    }

    @Override
    ImmutableSet<Entry<K, V>> createEntrySet() {
        return new RegularEntrySet<>(this, entries);
    }

    @Override
    ImmutableSet<K> createKeySet() {
        return new RegularImmutableMapKeySet<>(this);
    }

    @Override
    ImmutableCollection<V> createValues() {
        return new RegularImmutableMapValues<>(this);
    }

    private static int closedTableSize(int expectedEntries, double loadFactor) {
        // Get the recommended table size.
        // Round down to the nearest power of 2.
        expectedEntries = Math.max(expectedEntries, 2);
        int tableSize = Integer.highestOneBit(expectedEntries);
        // Check to make sure that we will not exceed the maximum load factor.
        if (expectedEntries > (int) (loadFactor * tableSize)) {
            tableSize <<= 1;
            return (tableSize > 0) ? tableSize : MAX_TABLE_SIZE;
        }
        return tableSize;
    }

    private static final int MAX_POWER_OF_TWO = 1 << (Integer.SIZE - 2);

    private static final int MAX_TABLE_SIZE = MAX_POWER_OF_TWO;

    private static int smear(int hashCode) {
        return (int) (C2 * Integer.rotateLeft((int) (hashCode * C1), 15));
    }

    private static final long C1 = 0xcc9e2d51;

    private static final long C2 = 0x1b873593;
}
