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

package net.ranides.assira.collection;

import java.util.*;
import net.ranides.assira.collection.map.MultiHashMap;
import net.ranides.assira.collection.map.MultiMap;
import net.ranides.assira.collection.map.VirtualMap;
import net.ranides.assira.generic.BiFunction;
import net.ranides.assira.generic.Function;
import net.ranides.assira.text.Strings;


/**
 *
 * @author ranides
 */
public final class MapUtils {

    /**
     * Pusta mapa, której nie można modyfikować.
     */
    public static final MultiMap EMPTY_MULTIMAP = new EmptyMultiMap();

    private MapUtils() { }

    /**
     * Zwraca pustę mapę, której nie można modyfikować. Metoda nie tworzy nowego
     * obiektu lecz reuse only one internal immutable object.
     * @param <K>
     * @param <V>
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <K, V> MultiMap<K, V> emptyMap() {
        return EMPTY_MULTIMAP;
    }

    /**
     * Wstawia do docelowej mapy klucze {@code keys} z wartościami {@values}. Przekazane
     * listy kluczy i wartości muszą mieć taki sam rozmiar.
     * @param <K>
     * @param <V>
     * @param target
     * @param keys
     * @param values
     * @return {@code target}
     */
    public static <K,V> Map<K,V> addAll(Map<K,V> target, List<K> keys, List<V> values) {
        if( !ListUtils.equalSize(keys, values) ) {
            throw new IllegalArgumentException("keys & values have different sizes");
        }
        for(int i=0, n=keys.size(); i<n; i++) {
            target.put(keys.get(i), values.get(i));
        }
        return target;
    }

    /**
     * Tworzy nową mapę na podstawie list zawierających klucze i odpowiadające im
     * wartości. Przekazane listy muszą mieć taki sam rozmiar.
     * @param <K>
     * @param <V>
     * @param keys
     * @param values
     * @return
     */
    public static <K,V> Map<K,V> join(List<K> keys, List<V> values) {
        return addAll(new HashMap<K, V>() , keys, values);
    }

    /**
     * Tworzy nową multi-mapę na podstawie list zawierających klucze i odpowiadające im
     * wartości. Przekazane listy muszą mieć taki sam rozmiar. Klucze mogą się powstarzać.
     * @param <K>
     * @param <V>
     * @param keys
     * @param values
     * @return
     */
    public static <K,V> MultiMap<K,V> multiJoin(List<K> keys, List<V> values) {
        MultiMap<K, V> target = new MultiHashMap<>();
        for(int i=0, n=keys.size(); i<n; i++) {
            target.putItem(keys.get(i), values.get(i) );
        }
        return target;
    }

    /**
     * Tworzy adapter, który podaną funkcję prezentuje jako interfejs {@code Map}.
     * Uwaga: zwrócona mapa nie obsługuje większości metod:
     * <ul>
     *   <li>nie można modyfikować</li>
     *   <li>nie można iterować po wartościach</li>
     *   <li>nie można sprawdzić rozmiaru</li>
     *   <li>można sprawdzić istnienie klucza</li>
     *   <li>można odczytać wartość przypisaną do klucza</li>
     * </ul>
     *
     * @param <K>
     * @param <V>
     * @param generator
     * @return
     */
    public static <K,V> Map<K,V> produce(Function<V,K> generator) {
        return VirtualMap.produce(generator);
    }

    /**
     * Tworzy adapter, który podaną funkcję prezentuje jako interfejs {@code Map}.
     * Mapa obsługuje większość metod, jednak nie można modyfikować wartości
     * przypisanych do kluczy ani dodawać nowych. Jeśli przekazany argument
     * {@code domain} pozwala na usuwanie elementów, to można z mapy przypisania usuwać.
     *
     * @param <K>
     * @param <V>
     * @param domain
     * @param generator
     * @return
     */
    public static <K,V> Map<K,V> produce(Set<K> domain, BiFunction<V,K> generator) {
        return VirtualMap.produce(domain, generator);
    }

    /**
     * Spłaszcza mapę, która zawiera wartości oraz zagnieżdżone mapy w ten sposób,
     * że w wyniku znajdują się wartości wyciągnięte z wszystkich zagnieżdżonych map,
     * ale nie same mapy. Klucz dla każdej wartości jest generowany tak, że klucze
     * z kolejno zagnieżdżonych map są konwertowane na {@code String} a następnie
     * sklejane razem. Na przykład:
     * <pre>
     * {@code source.get(17).get("a").get(".value") ==> target.get("17a.value") }
     * </pre>
     * @param source
     * @return
     */
    public static Map<String,Object> flat(Map<?,?> source) {
        return flat(new HashMap<String, Object>(), source, "");
    }

    /**
     * Spłaszcza mapę, która zawiera wartości oraz zagnieżdżone mapy w ten sposób,
     * że w wyniku znajdują się wartości wyciągnięte z wszystkich zagnieżdżonych map,
     * ale nie same mapy. Klucz dla każdej wartości jest generowany tak, że klucze
     * z zagnieżdżonych map są sklejane przez funkcję {@code joiner}.
     *
     * <p>
     * Funkcja sklejająca jest wywoływana z dwoma argumentami: kluczem-prefixem [P]
     * oraz kluczem najbardziej zagnieżdżonym [K]. Tak obliczony klucz [KP] może
     * zostać w późniejszym czasie przekazany jako prefix [P] do funkcji sklejającej,
     * jeśli pod kluczem (K) znajdowała się kolejna zagnieżdżona mapa z wartościami.
     * </p><p>
     * Klucze najwyższego poziomu są generowane jako sklejenie {@code seed}'u
     * oraz klucza, któremu przypisano wartość.
     * </p><p>
     * Uwaga: funkcja może założyć, że jako pierwszy argument zawsze dostaje wartość
     * typu K, ale drugi argument może być całkowicie dowolny - zależy to tylko
     * od zawartości map. Funkcja powinna być przygotowana na konwersje {@code Object -> K}
     * lub bardziej szczegółowe, jeśli istnieją jakieś gwarancje co do kluczy w
     * mapie źródłowej.
     * </p>
     * @param <K>
     * @param source
     * @param seed
     * @param joiner
     * @return
     */
    public static <K> Map<K,Object> flat(Map<?,?> source, K seed, KeyJoiner<K> joiner) {
        return flat(new HashMap<K, Object>(), source, seed, joiner);
    }

    private static Map<String,Object> flat(Map<String,Object> target, Map<?,?> source, String prefix) {
        return flat(target, source, prefix, new KeyJoiner<String>() {
            @Override
            public String join(String first, Object second) {
                return Strings.or(first,"") + Strings.or(second,"");
            }
        });
    }

    @SuppressWarnings("unchecked")
    private static <K> Map<K,Object> flat(Map<K,Object> target, Map<?,?> source, K seed, KeyJoiner<K> joiner) {
        for(Map.Entry<?,?> entry : source.entrySet()) {
            K key = joiner.join(seed, entry.getKey());
            if(entry.getValue() instanceof Map) {
                flat(target, (Map)entry.getValue(), key, joiner);
            } else {
                target.put(key, entry.getValue());
            }
        }
        return target;
    }

    private static final class EmptyMultiMap<K,V> extends AbstractMap<K,Collection<V>> implements MultiMap<K, V> {

        @Override public int size() { return 0; }

        @Override public boolean isEmpty() {return true;}

        @Override public boolean containsKey(Object key) {return false;}

        @Override public boolean containsValue(Object value) {return false;}

        @Override public Collection<V> get(Object key) { return null; }

        @Override public Set<K> keySet() { return Collections.emptySet();}

        @Override public Collection<Collection<V>> values() { return Collections.emptySet();}

        @Override public Set<Map.Entry<K,Collection<V>>> entrySet() { return Collections.emptySet(); }

        @Override public boolean equals(Object object) { return (object instanceof Map) && ((Map)object).isEmpty(); }

        @Override public int hashCode() { return 0; }

        // Preserves singleton property
        private Object readResolve() {
            return EMPTY_MULTIMAP;
        }

        @Override public boolean containsItem(V value) { return false; }

        @Override public V getFirst(K key) { return null; }

        @Override public V getLast(K key) { return null; }

        @Override public void putItem(K key, V value) { throw new UnsupportedOperationException("Immutable map"); }

        @Override public V removeFirst(K key) { return null; }

        @Override public V removeLast(K key) { return null; }

        @Override public boolean removeItem(K key, V value) { return false; }

        @Override public void putItems(Map<? extends K, ? extends V> source) { throw new UnsupportedOperationException("Immutable map"); }

        @Override public void putItems(MultiMap<? extends K, ? extends V> source) { throw new UnsupportedOperationException("Immutable map"); }

        @Override public List<V> items() { return Collections.emptyList(); }

        @Override public int multiSize() { return 0; }

    }
    
    public interface KeyJoiner<K> {
        
        K join(K first, Object second);
        
    }

}
