HashTable.java
package church.util;

import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;

import church.lang.ArraySupport;
import church.util.CollectionsSupport.EquivalenceRelation;
import church.util.CollectionsSupport.HashingFunction;
import church.util.CollectionsSupport.LeftFold;
import church.util.CollectionsSupport.RightFold;

@SuppressWarnings("unchecked")
public class HashTable<E, K> {
    public interface Factory<E, K> {
        <C> HashTable<E, K> hashTable(Function<E, K> entryToKey, RightFold<C, E> fold, C entries);

        static <E, K> Factory<E, K> hashTable(float loadFactor, EquivalenceRelation<K> equivalenceRelation, HashingFunction<K> hashingFunction) {
            return new Factory<E, K>() {
                @Override
                public <C> HashTable<E, K> hashTable(Function<E, K> entryToKey, RightFold<C, E> fold, C entries) {
                    return new HashTable<>(loadFactor, equivalenceRelation, hashingFunction, entryToKey, fold, entries);
                }
            };
        }

        static <E, K> Factory<E, K> hashTable(EquivalenceRelation<K> equivalenceRelation, HashingFunction<K> hashingFunction) {
            return hashTable(DEFAULT_LOAD_FACTOR, equivalenceRelation, hashingFunction);
        }

        static <E, K> Factory<E, K> hashTable() {
            return hashTable(Object::equals, Object::hashCode);
        }
    }

    // Our load factor is not an upper-bound as it is in java.util.HashMap but, because we are immutable, a
    // fixed percentage. This implementation uses 0.5 with the assumption that the battle-hardened default
    // of 0.75 in java.util.HashMap ends up leaving a HashMap roughly 50% full on average.
    private static final float DEFAULT_LOAD_FACTOR = 0.5f;

    public final  EquivalenceRelation<K> equivalenceRelation;
    @SuppressWarnings("WeakerAccess")
    public final  HashingFunction<K>     hashingFunction;
    private final Function<E, K>         entryToKey;
    private final LinkedList<E>[]        buckets;
    private       int                    size;

    @SuppressWarnings("WeakerAccess")
    protected <C> HashTable(float loadFactor, EquivalenceRelation<K> equivalenceRelation, HashingFunction<K> hashingFunction, Function<E, K> entryToKey, RightFold<C, E> fold, C entries) {
        int inputSize = fold.reduce((l, s) -> 1 + s, 0, entries);
        this.equivalenceRelation = equivalenceRelation;
        this.hashingFunction     = hashingFunction;
        this.entryToKey          = entryToKey;
        //noinspection unchecked
        this.buckets = new LinkedList[(int) (inputSize / loadFactor)];
        Arrays.fill(buckets, LinkedList.empty());
        EquivalenceRelation<E> entryEquivalence = (e1, e2) -> equivalenceRelation.equal(entryToKey.apply(e1), entryToKey.apply(e2));
        BiFunction<E, LinkedList<E>[], LinkedList<E>[]> accumulator = (entry, rest) -> {
            int           bucketIndex = getBucketIndexForEntry(entry);
            LinkedList<E> bucket      = buckets[bucketIndex];
            if (!LinkedList.contains(entryEquivalence, bucket, entry)) {
                size++;
                buckets[bucketIndex] = LinkedList.cons(entry, bucket); // note the sacrilegious modification of state
            }
            return buckets;
        };
        fold.reduce(accumulator, buckets, entries);
    }

    public static <E, K> HashTable<E, K> hashTable(Function<E, K> projection, E[] elements) {
        return Factory.<E, K>hashTable().hashTable(projection, ArraySupport.rightFold(), elements);
    }

    public static <E, K> HashTable<E, K> hashTable4(Function<E, K> projection, EquivalenceRelation<K> equivalenceRelation, HashingFunction<K> hashingFunction, E... elements) {
        return Factory.<E, K>hashTable(equivalenceRelation, hashingFunction).hashTable(projection, ArraySupport.rightFold(), elements);
    }

    public int size() {
        return size;
    }

    private int getBucketIndexForKey(K key) {
        return Integer.remainderUnsigned(hashingFunction.hashCode(key), buckets.length);
    }

    private int getBucketIndexForEntry(E entry) {
        return getBucketIndexForKey(entryToKey.apply(entry));
    }

    public LinkedList<E> getBucketForKey(K key) {
        return buckets[getBucketIndexForKey(key)];
    }

    public LinkedList<E> getBucketForEntry(E entry) {
        return getBucketForKey(entryToKey.apply(entry));
    }

    public boolean contains(K key) {
        LinkedList<E> bucketForKey = getBucketForKey(key);
        Predicate<E>  p            = entry -> equivalenceRelation.equal(entryToKey.apply(entry), key);
        return LinkedList.find(bucketForKey, p, null) != null;
    }

    // Folds

    public static <E, K> LeftFold<HashTable<E, K>, E> leftFold() {
        return CollectionsSupport.compose(ArraySupport.leftFold(), LinkedList.leftFold(), map -> map.buckets);
    }

    public static <E, K> RightFold<HashTable<E, K>, E> rightFold() {
        return CollectionsSupport.compose(ArraySupport.rightFold(), LinkedList.rightFold(), map -> map.buckets);
    }
}