/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.collection.map;

import java.util.Map.Entry;
import java.util.*;
import net.ranides.assira.collection.CollectionUtils;
import net.ranides.assira.collection.ListUtils;

/**
 * Implementacja interfejsu {@link MultiMap} oparta o mapę hashującą. 
 * @author ranides
 * @param <K>
 * @param <V> 
 */
public class MultiHashMap<K,V> extends HashMap<K, Collection<V>> implements MultiMap<K,V> {

    private static final long serialVersionUID = 5L;

    /**
     * Constructs an empty <tt>MultiHashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public MultiHashMap() {
        super();
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public MultiHashMap(int initialCapacity) {
        super(initialCapacity);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public MultiHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
    }

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param source 
     * @throws  NullPointerException if the specified map is null
     */
    public MultiHashMap(MultiMap<? extends K, ? extends V> source) {
        super();
        for(Entry<? extends K, ? extends Collection<? extends V>> entry : source.entrySet()) {
            @SuppressWarnings({"PMD.ConstructorCallsOverridableMethod", "OverridableMethodCallInConstructor"})
            Collection<V> copy = createCollection();
            copy.addAll(entry.getValue());
            put(entry.getKey(), copy);
        }
    }
    
    @Override
    public boolean containsItem(V value) {
        for( Collection<V> list : values() ) {
            if( list.contains(value) ) { return true; }
        }
        return false;
    }

    @Override
    public V getFirst(K key) {
        return CollectionUtils.first(get(key));
    }

    @Override
    public V getLast(K key) {
        Collection<V> elements = get(key);
        if(elements instanceof List) {
            return ListUtils.last((List<V>)elements);
        } else {
            return CollectionUtils.last(get(key));
        }
    }

    @Override
    public void putItem(K key, V value) {
        if( containsKey(key) ) {
            Collection<V> list = get(key);
            if( !list.contains(value) ) {
                list.add(value);
            }
        } else {
            put(key, createCollection());
            get(key).add(value);
        }
    }

    @Override
    public void putItems(Map<? extends K, ? extends V> source) {
        for( Entry<? extends K, ? extends V> entry : source.entrySet() ) {
            putItem(entry.getKey(), entry.getValue() );
        }
    }

    @Override
    public void putItems(MultiMap<? extends K, ? extends V> source) {
        for(Entry<? extends K, ? extends Collection<? extends V>> item : source.entrySet() ) {
            final K key = item.getKey();
            for(V value : item.getValue() ) {
                putItem(key, value);
            }
        }
    }

    @Override
    public V removeFirst(K key) {
        Collection<V> list = get(key);
        if( null == list || list.isEmpty() ) { 
			return null; 
		}
        Iterator<V> iterator = list.iterator();
        V result = iterator.next();
		if(1 == list.size()) {
			remove(key);
		} else {
			iterator.remove();
		}
        return result;
    }

    @Override
    public V removeLast(K key) {
        Collection<V> elements = get(key);
        if( null == elements || elements.isEmpty() ) { 
			return null; 
		}
		V result = null;
        if (elements instanceof List) {
            List<V> list = (List<V>) elements;
            result = list.remove(list.size()-1);
        } else {
            Iterator<V> iterator = elements.iterator();
            while(iterator.hasNext()) { result = iterator.next(); }
            iterator.remove();
        }
		if(elements.isEmpty()) {
			remove(key);
		}
		return result;
    }

    @Override
    public boolean removeItem(K key, V value) {
        Collection<V> list = get(key);
        if( null == list || list.isEmpty() ) { return false; }
        if(!list.remove(value)) {
			return false;
		}
		if(list.isEmpty()) {
			remove(key);
		}
		return true;
    }

    @Override
    public List<V> items() {
        throw new UnsupportedOperationException("Not supported yet.");
    }
    
    @Override
    public int multiSize() {
        int result = 0;
        for(Collection<V> list : values()) { result+= list.size(); }
        return result;
    }

    /**
     * <p>
     * Jeśli do jednego klucza zostanie przypisanych kilka wartości, to zostaną
     * one umieszczone w kolekcji zwróconej przez poniższą metodę. 
     * </p><p>
     * Domyślnie tworzona jest kolekcja typu {@link HashSet}, która nie wyróżnia pierwszego i 
     * ostatniego elementu. Jeśli klasa pochodna ma w pełni implementować interfejs
     * {@link MultiMap} (w szczególności metody {@code getFirst} oraz {@code getLast}),
     * to powinna zmienić tę metodę na taką, która zachowuje porządek elementów wg
     * dowolnego kryterium, tzn implementuje interfejs {@link List}.
     * </p><p>
     * Jeśli zwrócona kolekcja nie implementuje interfejsu {@link List}, to metody
     * odnoszące się do ostatniego/pierwszego elementu wybiorą ostatni/pierwszy 
     * element zwrócony przez iterator.
     * </p>
     * @return 
     */
    protected Collection<V> createCollection() {
        return new HashSet<>();
    }
    
}
