package org.xbib.datastructures.immutable;

import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
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;
import static org.xbib.datastructures.immutable.RegularImmutableMap.checkNoConflictInKeyBucket;

/**
 * Bimap with zero or more mappings.
 */
final class RegularImmutableBiMap<K, V> extends ImmutableBiMap<K, V> {
    @SuppressWarnings("unchecked")
    static final RegularImmutableBiMap<Object, Object> EMPTY =
            new RegularImmutableBiMap<>(
                    null, null, (Entry<Object, Object>[]) EMPTY_ENTRY_ARRAY, 0, 0);

    static final double MAX_LOAD_FACTOR = 1.2;

    private final transient ImmutableMapEntry<K, V>[] keyTable;

    private final transient ImmutableMapEntry<K, V>[] valueTable;

    final transient Entry<K, V>[] entries;

    private final transient int mask;

    private final transient int hashCode;

    @SuppressWarnings("unchecked")
    static <K, V> ImmutableBiMap<K, V> fromEntries(Entry<K, V>... entries) {
        return fromEntryArray(entries.length, entries);
    }

    static <K, V> ImmutableBiMap<K, V> fromEntryArray(int n, Entry<K, V>[] entryArray) {
        checkPositionIndex(n, entryArray.length, "n");
        int tableSize = closedTableSize(n, MAX_LOAD_FACTOR);
        int mask = tableSize - 1;
        ImmutableMapEntry<K, V>[] keyTable = createEntryArray(tableSize);
        ImmutableMapEntry<K, V>[] valueTable = createEntryArray(tableSize);
        /*
         * 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 hashCode = 0;

        for (int i = 0; i < n; i++) {
            // requireNonNull is safe because the first `n` elements have been filled in.
            Entry<K, V> entry = requireNonNull(entryArray[i]);
            K key = entry.getKey();
            V value = entry.getValue();
            checkEntryNotNull(key, value);
            int keyHash = key.hashCode();
            int valueHash = value.hashCode();
            int keyBucket = smear(keyHash) & mask;
            int valueBucket = smear(valueHash) & mask;

            ImmutableMapEntry<K, V> nextInKeyBucket = keyTable[keyBucket];
            ImmutableMapEntry<K, V> nextInValueBucket = valueTable[valueBucket];
            try {
                checkNoConflictInKeyBucket(key, value, nextInKeyBucket, /* throwIfDuplicateKeys= */ true);
                checkNoConflictInValueBucket(value, entry, nextInValueBucket);
            } catch (RegularImmutableMap.BucketOverflowException e) {
                return JdkBackedImmutableBiMap.create(n, entryArray);
            }
            ImmutableMapEntry<K, V> newEntry =
                    (nextInValueBucket == null && nextInKeyBucket == null)
                            ? RegularImmutableMap.makeImmutable(entry, key, value)
                            : new NonTerminalImmutableBiMapEntry<>(
                            key, value, nextInKeyBucket, nextInValueBucket);
            keyTable[keyBucket] = newEntry;
            valueTable[valueBucket] = newEntry;
            entries[i] = newEntry;
            hashCode += keyHash ^ valueHash;
        }
        return new RegularImmutableBiMap<>(keyTable, valueTable, entries, mask, hashCode);
    }

    private RegularImmutableBiMap(ImmutableMapEntry<K, V>[] keyTable, ImmutableMapEntry<K, V>[] valueTable,
                                  Entry<K, V>[] entries,
                                  int mask,
                                  int hashCode) {
        this.keyTable = keyTable;
        this.valueTable = valueTable;
        this.entries = entries;
        this.mask = mask;
        this.hashCode = hashCode;
    }

    // checkNoConflictInKeyBucket is static imported from RegularImmutableMap

    /**
     * @throws IllegalArgumentException                    if another entry in the bucket has the same key
     * @throws RegularImmutableMap.BucketOverflowException if this bucket has too many entries, which may indicate a hash
     *                                                     flooding attack
     */
    private static void checkNoConflictInValueBucket(
            Object value, Entry<?, ?> entry, ImmutableMapEntry<?, ?> valueBucketHead)
            throws RegularImmutableMap.BucketOverflowException {
        int bucketSize = 0;
        for (; valueBucketHead != null; valueBucketHead = valueBucketHead.getNextInValueBucket()) {
            checkNoConflict(!value.equals(valueBucketHead.getValue()), "value", entry, valueBucketHead);
            if (++bucketSize > RegularImmutableMap.MAX_HASH_BUCKET_LENGTH) {
                throw new RegularImmutableMap.BucketOverflowException();
            }
        }
    }

    @Override
    public V get(Object key) {
        return RegularImmutableMap.get(key, keyTable, mask);
    }

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

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

    @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
    boolean isHashCodeFast() {
        return true;
    }

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

    @Override
    boolean isPartialView() {
        return false;
    }

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

    private transient ImmutableBiMap<V, K> inverse;

    @Override
    public ImmutableBiMap<V, K> inverse() {
        if (isEmpty()) {
            return of();
        }
        ImmutableBiMap<V, K> result = inverse;
        return (result == null) ? inverse = new Inverse() : result;
    }

    private final class Inverse extends ImmutableBiMap<V, K> {

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

        @Override
        public ImmutableBiMap<K, V> inverse() {
            return RegularImmutableBiMap.this;
        }

        @Override
        public void forEach(BiConsumer<? super V, ? super K> action) {
            Objects.requireNonNull(action);
            RegularImmutableBiMap.this.forEach((k, v) -> action.accept(v, k));
        }

        @Override
        public K get(Object value) {
            if (value == null || valueTable == null) {
                return null;
            }
            int bucket = smear(value.hashCode()) & mask;
            for (ImmutableMapEntry<K, V> entry = valueTable[bucket];
                 entry != null;
                 entry = entry.getNextInValueBucket()) {
                if (value.equals(entry.getValue())) {
                    return entry.getKey();
                }
            }
            return null;
        }

        @Override
        ImmutableSet<V> createKeySet() {
            return new ImmutableMapKeySet<>(this);
        }

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

        final class InverseEntrySet extends ImmutableMapEntrySet<V, K> {
            @Override
            ImmutableMap<V, K> map() {
                return Inverse.this;
            }

            @Override
            boolean isHashCodeFast() {
                return true;
            }

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

            @Override
            public UnmodifiableIterator<Entry<V, K>> iterator() {
                return asList().iterator();
            }

            @Override
            public void forEach(Consumer<? super Entry<V, K>> action) {
                asList().forEach(action);
            }

            @Override
            ImmutableList<Entry<V, K>> createAsList() {
                return new ImmutableAsList<Entry<V, K>>() {
                    @Override
                    public Entry<V, K> get(int index) {
                        Entry<K, V> entry = entries[index];
                        return new ImmutableMapEntry<>(entry.getValue(), entry.getKey());
                    }

                    @Override
                    ImmutableCollection<Entry<V, K>> delegateCollection() {
                        return InverseEntrySet.this;
                    }
                };
            }
        }

        @Override
        boolean isPartialView() {
            return false;
        }
    }

    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;
}
