/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.prototype;

import net.ranides.assira.annotations.Meta;
import net.ranides.assira.collection.maps.ArrayMap;
import net.ranides.assira.collection.maps.MultiMap;
import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.maps.OrderedMap;
import net.ranides.assira.collection.maps.RandomAccessMap;

import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
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 <M> map
 */
public class GenericMapBuilder<K, M extends GenericMap<K>> {

    private final M map;

    private GenericMapBuilder(M target) {
        this.map = target;
    }

    /**
     * Creates new builder which inserts all entries into provided map
     *
     * @param target target
     * @param <K> K
     * @param <M> M
     * @return this
     */
    public static <K, M extends GenericMap<K>> GenericMapBuilder<K,M> into(M target) {
        return new GenericMapBuilder<>(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> GenericMapBuilder<K, OpenGenericMap<K>> hash() {
        return new GenericMapBuilder<>(new OpenGenericMap<>());
    }

    /**
     * Returns constructed map
     * Please note that returned by is still shared by builder
     *
     * @return map
     */
    public M result() {
        return 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 <V> GenericMapBuilder<K,M> append(GenericKey<K,V> key, V value) {
        return put(key, value);
    }

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

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

    /**
     * Inserts new values to map
     *
     * @param values values
     * @return this
     */
    public GenericMapBuilder<K,M> putAll(GenericMap<? extends K> 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 <V> GenericMapBuilder<K,M> putIfAbsent(GenericKey<K, V> 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 <V> GenericMapBuilder<K,M> replace(GenericKey<K, V> 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 <V> GenericMapBuilder<K,M> replace(GenericKey<K, V> 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 <V> GenericMapBuilder<K,M> replaceAll(BiFunction<? super GenericKey<K,?>, Object, Object> 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 function remapping function
     * @return this
     */
    public <V> GenericMapBuilder<K,M> merge(GenericKey<K,V> key, V value, BiFunction<? super V, ? super V, ? extends V> function) {
        map.merge(key, value, function);
        return this;
    }

    /**
     * Removes provided key
     *
     * @param key key
     * @return this
     */
    public GenericMapBuilder<K,M> remove(GenericKey<K,?> 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 <V> GenericMapBuilder<K,M> remove(GenericKey<K,V> key, V value) {
        map.remove(key, value);
        return this;
    }

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

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

    /**
     * Associates every key with corresponding value from provided lists.
     *
     * @param keys keys
     * @param values values
     * @return this
     */
    @SuppressWarnings("unchecked")
    public GenericMapBuilder<K,M> zip(List<GenericKey<K,?>> keys, List<?> values) {
        if( keys.size() != values.size() ) {
            throw new IllegalArgumentException("keys & values have different sizes");
        }
        for(int i=0, n=keys.size(); i<n; i++) {
            GenericKey<K, ?> key = keys.get(i);
            Object val = values.get(i);
            if(!key.type().isInstance(val)) {
                throw new ClassCastException("Key " + key + " can't store value " + val);
            }
            map.put((GenericKey) key, val);
        }
        return this;
    }

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

}
