/*
 * @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.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.function.Predicate;
import net.ranides.assira.collection.maps.MapUtils.EmptyMultiMap;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.generic.CompareUtils;

/**
 * This is extension of Map interface with support for storing more than one mapping to key.
 *
 * @param <K> K
 * @param <V> V
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public interface MultiMap<K,V> extends Map<K,V> {

    /**
     * Empty MultiMap
     */
    @SuppressWarnings("rawtypes")
    MultiMap EMPTY = new EmptyMultiMap();

    /**
     * Returns view with all values assocciated with key
     *
     * It supports remove operation.
     * It does not support add operation.
     *
     * @param key key
     * @return view
     */
    Collection<V> getAll(Object key);

    /**
     * Finds all values assocciated with key and merges them using fold operator.
     * For example, you can give "max" as fold operator.
     *
     * @param key key
     * @param fold fold
     * @return computed value
     */
    default V get(Object key, BinaryOperator<V> fold) {
        return getAll(key).stream().reduce(fold).orElseThrow(()->new NoSuchElementException());
    }

    /**
     * Checks if map contains specified mapping
     * @param key key
     * @param value value
     * @return bool
     */
    default boolean containsEntry(Object key, Object value) {
        return getAll(key).contains(value);
    }

    /**
     * Inserts all values into map, assocciating them with one provided key.
     *
     * @param key key
     * @param values values
     */
    default void putAll(K key, Iterable<? extends V> values) {
        for(V value : values) {
            put(key, value);
        }
    }
    
    @Override
    default V replace(K key, V value) {
        V prev;
        if (((prev = get(key)) != null) || containsKey(key)) {
            removeAll(key);
            put(key, value);
        }
        return prev;
    }

    /**
     * If map does not contain mapping with "key", then it does nothing.
     * If there is existing mapping it will be replaced by provided values.
     * Return previous values assigned to key.
     *
     * @param key key
     * @param values values
     * @return values
     */
    default Collection<V> replaceAll(K key, Collection<V> values) {
        Collection<V> prev = Collections.emptyList();
        if (containsKey(key)) {
            prev = removeAll(key);
            putAll(key, values);
        }
        return prev;
    }
    
    @Override
    default boolean remove(Object key, Object value) {
        Predicate<Object> ek = CompareUtils.predicate(key);
        Predicate<Object> ev = CompareUtils.predicate(value);
        
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            if( ek.test(e.getKey()) && ev.test(e.getValue()) ) {
                i.remove();
                return true;
            }
        }
        return false;
    }

    /**
     * Removes all values associated with specific key.
     * Returns previous mappings
     *
     * @param key key
     * @return values
     */
    default Collection<V> removeAll(Object key) {
        Collection<V> prev = new ArrayList<>();
        V last;
        while(null != (last=remove(key))) {
            prev.add(last);
        }
        return prev;
    }

    /**
     * This method returns new Set with keys inside map.
     * Returned set will be "valid", that means, it will contain every key at most once.
     *
     * {@link #keySet()} could return invalid Set, which contains duplicated keys if there is more than one
     * mapping to key.
     *
     * Please note: returned Set in NOT a view.
     *
     * @return new set
     */
    default Set<K> uniqueKeySet() {
        return new OpenSet<>(keySet());
    }

    /**
     * Returns empty immutable MultiMap
     *
     * @param <K> K
     * @param <V> V
     * @return map
     */
    @SuppressWarnings("unchecked")
    static <K,V> MultiMap<K,V> empty() {
        return EMPTY;
    }
    
}
