/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.maps;

import java.io.IOException;
import net.ranides.assira.generic.HashUtils;
import java.util.Map;
import net.ranides.assira.collection.utils.HashCollection;
import net.ranides.assira.collection.HashFunction;

/**
 * A generic hash map implementation with {@link HashFunction hashing strategy} 
 * specified at creation time.
 *
 * <P>Instances of this class use a hash table to represent a map. The table is
 * enlarged as needed by doubling its size when new entries are created, but it
 * is <em>never</em> made smaller (even on a {@link #clear()}). A family of {@linkplain #trim() trimming
 * methods} lets you control the size of the table; this is particularly useful
 * if you reuse instances of this class.
 *
 * @param <K> K
 * @param <V> V
 */
public final class CustomMap<K, V> extends AOpenMap<K, V> {

    private static final long serialVersionUID = 0L;

    /**
     * The hash strategy of this custom map.
     */
    protected transient HashFunction<K> strategy;

    /**
     * Creates a new hash map.
     *
     * <p>The actual table size will be the least power of two greater than
     * {@code expected}/
     * {@code f}.
     *
     * @param expected the expected number of elements in the hash set.
     * @param factor the load factor.
     * @param strategy the strategy.
     */
    public CustomMap(int expected, float factor, HashFunction<K> strategy) {
        super(expected, factor);
        this.strategy = strategy;
    }

    /**
     * Creates a new hash map with {@link HashCollection#LOAD_FACTOR} as load
     * factor.
     *
     * @param expected the expected number of elements in the hash map.
     * @param strategy the strategy.
     */
    public CustomMap(int expected, HashFunction<K> strategy) {
        this(expected, HashCollection.LOAD_FACTOR, strategy);
    }

    /**
     * Creates a new hash map with initial expected
     * {@link HashCollection#INITIAL_SIZE} entries and
     * {@link HashCollection#LOAD_FACTOR} as load factor.
     *
     * @param strategy the strategy.
     */
    public CustomMap(HashFunction<K> strategy) {
        this(HashCollection.INITIAL_SIZE, HashCollection.LOAD_FACTOR, strategy);
    }

    /**
     * Creates a new hash map copying a given one.
     *
     * @param values a {@link Map} to be copied into the new hash map.
     * @param factor the load factor.
     * @param strategy the strategy.
     */
    public CustomMap(Map<? extends K, ? extends V> values, float factor, HashFunction<K> strategy) {
        this(values.size(), factor, strategy);
        putAll(values);
    }

    /**
     * Creates a new hash map with {@link HashCollection#LOAD_FACTOR} as load
     * factor copying a given one.
     *
     * @param values a {@link Map} to be copied into the new hash map.
     * @param strategy the strategy.
     */
    public CustomMap(Map<? extends K, ? extends V> values, HashFunction<K> strategy) {
        this(values, HashCollection.LOAD_FACTOR, strategy);
    }

    /**
     * Creates a new hash map using the elements of two parallel arrays.
     *
     * @param keys the array of keys of the new hash map.
     * @param values the array of corresponding values in the new hash map.
     * @param factor the load factor.
     * @param strategy the strategy.
     * @throws IllegalArgumentException if {@code k} and {@code v}
     * have different lengths.
     */
    public CustomMap(K[] keys, V[] values, float factor, HashFunction<K> strategy) {
        this(keys.length, factor, strategy);
        if (keys.length != values.length) {
            throw new IllegalArgumentException("The key array and the value array have different lengths (" + keys.length + " and " + values.length + ")");
        }
        for (int i = 0; i < keys.length; i++) {
            this.put(keys[ i], values[ i]);
        }
    }

    /**
     * Creates a new hash map with {@link HashCollection#LOAD_FACTOR} as load
     * factor using the elements of two parallel arrays.
     *
     * @param keys the array of keys of the new hash map.
     * @param values the array of corresponding values in the new hash map.
     * @param strategy the strategy.
     * @throws IllegalArgumentException if {@code k} and {@code v}
     * have different lengths.
     */
    public CustomMap(K[] keys, V[] values, HashFunction<K> strategy) {
        this(keys, values, HashCollection.LOAD_FACTOR, strategy);
    }

    /**
     * Returns the hashing strategy.
     *
     * @return the hashing strategy of this custom hash map.
     */
    public HashFunction<K> strategy() {
        return strategy;
    }

    @Override
    protected int hash(K key) {
        try {
            return HashUtils.murmurHash3(strategy.hashCode(key));
        } catch(ClassCastException ex) {
            // "unchecked" generic methods could pass something strange here
            return 0;
        }
    }

    @Override
    protected boolean compare(K key1, V value1, K key2, V value2) {
        try {
            return strategy.equals(key1, key2);
        } catch(ClassCastException ex) {
            // "unchecked" generic methods could pass something strange here
            return false;
        }
    }
    
    @SuppressWarnings("unchecked")
    @Override
    protected void readObjectDetails(java.io.ObjectInputStream istream) throws IOException, ClassNotFoundException {
        this.strategy = (HashFunction<K>)istream.readObject();
    }
    
    @Override
    protected void writeObjectDetails(java.io.ObjectOutputStream ostream) throws IOException {
        ostream.writeObject(this.strategy);
    }

}
