/*
 * @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 net.ranides.assira.annotations.Meta;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;

/**
 * Fluent builder for maps.
 * It is parameterized by M type because can build different types of maps: HashMap, TreeMap, etc
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @param <K> key
 * @param <V> value
 * @param <M> map
 */
public class MapBuilder<K, V, M extends Map<K,V>> {
    
    private final boolean array;

    private final M map;

    private MapBuilder(boolean array, M target) {
        this.array = array;
        this.map = target;
    }

    /**
     * Creates new builder which inserts all entries into provided map
     *
     * @param target target
     * @param <K> K
     * @param <V> V
     * @param <M> M
     * @return this
     */
    public static <K, V, M extends Map<K,V>> MapBuilder<K,V,M> into(M target) {
        return new MapBuilder<>(false, target);
    }

    /**
     * Creates new builder which construct map with undefined order (some implementation of HashMap)
     *
     * @param <K> K
     * @param <V> V
     * @return this
     */
    public static <K, V> MapBuilder<K, V, Map<K,V>> hash() {
        return new MapBuilder<>(false, new OpenMap<>());
    }

    /**
     * Creates new builder which construct map preserving order of insertions.
     *
     * @param <K> K
     * @param <V> V
     * @return this
     */
    public static <K, V> MapBuilder<K, V, OrderedMap<K,V>> ordered() {
        return new MapBuilder<>(false, new RandomAccessMap<>());
    }

    /**
     * Creates new builder which construct map of strings preserving order of insertions.
     *
     * @return this
     */
    public static MapBuilder<String, Object, OrderedMap<String,Object>> dictionary() {
        return new MapBuilder<>(false, new RandomAccessMap<>());
    }

    /**
     * Creates new builder which construct TreeMap with default comparator
     * @param <K> K
     * @param <V> V
     * @return this
     */
    public static <K extends Comparable<K>, V> MapBuilder<K, V, TreeMap<K,V>> tree() {
        return new MapBuilder<>(false, new TreeMap<>());
    }

    /**
     * Creates new builder which construct TreeMap with provided comparator
     *
     * @param cmp cmp
     * @param <K> K
     * @param <V> V
     * @return this
     */
    public static <K, V> MapBuilder<K, V, TreeMap<K,V>> tree(Comparator<? super K> cmp) {
        return new MapBuilder<>(false, new TreeMap<>(cmp));
    }

    /**
     * Creates new builder which construct ArrayMap with default comparator
     *
     * @param <K> K
     * @param <V> V
     * @return builder
     */
    public static <K extends Comparable<K>, V> MapBuilder<K, V, Map<K,V>> array() {
        return new MapBuilder<>(true, new TreeMap<>());
    }

    /**
     * Creates new builder which construct ArrayMap with provided comparator
     *
     * @param cmp cmp
     * @param <K> K
     * @param <V> V
     * @return builder
     */
    public static <K, V> MapBuilder<K, V, Map<K,V>> array(Comparator<? super K> cmp) {
        return new MapBuilder<>(true, new TreeMap<>(cmp));
    }

    /**
     * Returns constructed map
     * Please note that returned by is still shared by builder
     *
     * @return map
     */
    @SuppressWarnings("unchecked")
    public M result() {
        return array ? (M)new ArrayMap<>((SortedMap<K,V>)map) : map;
    }

    /**
     * Alias for {@link #result()}
     *
     * @return map
     */
    @Meta.Alias("result")
    public M map() {
        return result();
    }

    /**
     * Inserts new value to map
     *
     * @param key key
     * @param value value
     * @return this
     */
    @Meta.Alias("put")
    public MapBuilder<K,V,M> append(K key, V value) {
        return put(key, value);
    }

    /**
     * Inserts new values to map
     *
     * @param values values
     * @return this
     */
    @Meta.Alias("putAll")
    public MapBuilder<K,V,M> append(Map<? extends K, ? extends V> values) {
        return putAll(values);
    }

    /**
     * Inserts new value to map
     *
     * @param key key
     * @param value value
     * @return this
     */
    public MapBuilder<K,V,M> put(K key, V value) {
        map.put(key, value);
        return this;
    }

    /**
     * Inserts new values to map
     *
     * @param values values
     * @return this
     */
    public MapBuilder<K,V,M> putAll(Map<? extends K, ? extends V> values) {
        map.putAll(values);
        return this;
    }

    /**
     * Inserts new value to map if it is not already inserted
     *
     * @param key key
     * @param value value
     * @return this
     */
    public MapBuilder<K,V,M> putIfAbsent(K key, V value) {
        map.putIfAbsent(key, value);
        return this;
    }

    /**
     * Inserts new value to map only if there is already value for specified key
     *
     * @param key key
     * @param value value
     * @return this
     */
    public MapBuilder<K,V,M> replace(K key, V value) {
        map.replace(key, value);
        return this;
    }

    /**
     * Inserts new value to map only if there is already value for specified key
     *
     * @param key key
     * @param oldValue oldValue
     * @param newValue newValue
     * @return this
     */
    public MapBuilder<K,V,M> replace(K key, V oldValue, V newValue) {
        map.replace(key, oldValue, newValue);
        return this;
    }

    /**
     * Replaces each entry's value with the result of invoking the given function on that entry.
     *
     * @param function function
     * @return this
     */
    public MapBuilder<K,V,M> replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        map.replaceAll(function);
        return this;
    }

    /**
     * If the specified key is not already associated, insert new value.
     *
     * Otherwise, replaces the associated value with the results of the given remapping function.
     *
     * If the remapping function returns null, the mapping is removed.
     *
     * @param key key
     * @param value value
     * @param remappingFunction remappingFunction
     * @return this
     */
    public MapBuilder<K,V,M> merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        map.merge(key, value, remappingFunction);
        return this;
    }

    /**
     * Removes provided key
     *
     * @param key key
     * @return this
     */
    public MapBuilder<K,V,M> remove(Object key) {
        map.remove(key);
        return this;
    }

    /**
     * Removes provided mapping only if the key is associates to equal "value"
     *
     * @param key key
     * @param value value
     * @return this
     */
    public MapBuilder<K,V,M> remove(Object key, Object value) {
        map.remove(key, value);
        return this;
    }

    /**
     * Removes all content from builder
     * @return this
     */
    public MapBuilder<K,V,M> clear() {
        map.clear();
        return this;
    }

    /**
     * Performs the given action for each entry in this map
     *
     * @param action action
     * @return this
     */
    public MapBuilder<K,V,M> forEach(BiConsumer<? super K, ? super V> action) {
        map.forEach(action);
        return this;
    }

    /**
     * Inserts all provided values.
     *
     * It works only if constructed map is MultiMap.
     *
     * @param key key
     * @param values values
     * @return this
     */
    @SuppressWarnings("unchecked")
    public MapBuilder<K,V,M> putAll(K key, Iterable<? extends V> values) {
        ((MultiMap<K,V>)map).putAll(key, values);
        return this;
    }

    /**
     * Inserts all provided values only if there is already defined mapping. Former mapping is removed.
     *
     * It works only if constructed map is MultiMap.
     *
     * @param key key
     * @param values values
     * @return this
     */
    @SuppressWarnings("unchecked")
    public MapBuilder<K,V,M> replaceAll(K key, Collection<V> values) {
        ((MultiMap<K,V>)map).replaceAll(key, values);
        return this;
    }

    /**
     * Removes all mappings for key
     *
     * @param key key
     * @return this
     */
    @SuppressWarnings("unchecked")
    public MapBuilder<K,V,M> removeAll(Object key) {
        if(map instanceof MultiMap) {
            ((MultiMap<K,V>)map).removeAll(key);
        } else {
            map.remove(key);
        }
        return this;
    }

    /**
     * Associates every key with corresponding value from provided lists.
     *
     * @param keys keys
     * @param values values
     * @return this
     */
    public MapBuilder<K,V,M> zip(List<K> keys, List<V> values) {
        if( keys.size() != values.size() ) {
            throw new IllegalArgumentException("keys & values have different sizes");
        }
        for(int i=0, n=keys.size(); i<n; i++) {
            map.put(keys.get(i), values.get(i));
        }
        return this;
    }

    /**
     * Associates every key with corresponding value from provided arrays.
     *
     * @param keys keys
     * @param values values
     * @return this
     */
    public MapBuilder<K,V,M> zip(K[] keys, V[] values) {
        return zip(Arrays.asList(keys), Arrays.asList(values));
    }

}
