/*
 * @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.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import net.ranides.assira.generic.HashUtils;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.BinaryOperator;
import net.ranides.assira.collection.ACollection;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.collection.sets.IntOpenSet;
import net.ranides.assira.collection.sets.IntSet;
import net.ranides.assira.collection.utils.HashCollection;
import net.ranides.assira.functional.special.Fold;
import net.ranides.assira.generic.CompareUtils;

/**
 * A type-specific hash map with primitive int mkeys.
 *
 * @param <V> type of value
 */
public class IntOpenMultiMap<V> extends AIntOpenMap<V> implements IntMultiMap<V> {

    private static final long serialVersionUID = 6L;

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

    /**
     * Creates a new hash map with {@link HashCollection#LOAD_FACTOR} as load
     * factor.
     *
     * @param expected the expected number of elements in the hash map.
     */
    public IntOpenMultiMap(int expected) {
        super(expected, HashCollection.LOAD_FACTOR);
    }

    /**
     * Creates a new hash map with initial expected
     * {@link HashCollection#INITIAL_SIZE} mentries and
     * {@link HashCollection#LOAD_FACTOR} as load factor.
     */
    public IntOpenMultiMap() {
        super(HashCollection.INITIAL_SIZE, HashCollection.LOAD_FACTOR);
    }

    /**
     * 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.
     */
    public IntOpenMultiMap(Map<? extends Integer, ? extends V> values, float factor) {
        super(values.size(), factor);
        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.
     */
    public IntOpenMultiMap(Map<? extends Integer, ? extends V> values) {
        this(values, HashCollection.LOAD_FACTOR);
    }

    /**
     * Creates a new hash map copying a given type-specific one.
     *
     * @param values a type-specific map to be copied into the new hash map.
     * @param factor the load factor.
     */
    public IntOpenMultiMap(IntMap<V> values, float factor) {
        this(values.size(), factor);
        putAll(values);
    }

    /**
     * Creates a new hash map with {@link HashCollection#LOAD_FACTOR} as load
     * factor copying a given type-specific one.
     *
     * @param values a type-specific map to be copied into the new hash map.
     */
    public IntOpenMultiMap(IntMap<V> values) {
        this(values, HashCollection.LOAD_FACTOR);
    }

    /**
     * Creates a new hash map using the elements of two parallel arrays.
     *
     * @param keys the array of mkeys of the new hash map.
     * @param values the array of corresponding mvalues in the new hash map.
     * @param factor the load factor.
     * @throws IllegalArgumentException if {@code k} and {@code v}
     * have different lengths.
     */
    public IntOpenMultiMap(int[] keys, V[] values, float factor) {
        super(keys.length, factor);
        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++) {
            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 mkeys of the new hash map.
     * @param values the array of corresponding mvalues in the new hash map.
     * @throws IllegalArgumentException if {@code k} and {@code v}
     * have different lengths.
     */
    public IntOpenMultiMap(int[] keys, V[] values) {
        this(keys, values, HashCollection.LOAD_FACTOR);
    }
	
    @Override
	protected final int hash(int key) {
		return HashUtils.murmurHash3(key);
	}
    
    @Override
    protected final boolean compare(int key1, V value1, int key2, V value2) {
		return key1 == key2 && CompareUtils.equals(value1, value2);
	}
    
    @Override
    public Collection<V> getAll(int key) {
        return new MCollection(key);
    }

    @Override
    public V get(int key, BinaryOperator<V> fold) {
        return Fold.apply(new MIterator(key), fold);
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean containsEntry(int key, Object value) {
        return used[ifind(key, (V)value)];
    }

    @Override
    public void putAll(int key, Iterable<? extends V> values) {
        for(V value : values) {
            put(key, value);
        }
    }

    @Override
    public Collection<V> removeAll(int key) {
        List<V> list = new ArrayList<>();
        int pos;
        while(used[pos = ifind(key)]) {
            list.add(ishift(pos));
        }
        return list;
    }
    
    @Override
    public IntSet uniqueKeySet() {
        return new IntOpenSet(keySet());
    }

    private class MCollection extends ACollection<V> {
        
        private final int key;

        public MCollection(int key) {
            this.key = key;
        }

        @Override
        public Iterator<V> iterator() {
            return new MIterator(key);
        }

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

        @Override
        public boolean isEmpty() {
            return !iterator().hasNext();
        }
        
    }
    
    private class MIterator implements Iterator<V> {
        
        private int pos;
        private final int key;
        
        public MIterator(int key) {
            this.key = key;
            this.pos = ifind(key);
        }

        @Override
        public boolean hasNext() {
            return used[pos];
        }

        @Override
        public V next() {
            if(!used[pos]) {
                throw new NoSuchElementException();
            }
            V last = values[pos];
            do {
                pos = inext(pos);
            } while(used[pos] && !compare(key, keys[pos]));
            return last;
        }
        
    }

}
