/*
 * @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.collection.CollectionUtils;
import net.ranides.assira.collection.sets.OpenSet;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.Ref;

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

/**
 * GenericMap interface represents heterogenous Map which can store multiple
 * value types in type-safe way. GenericMap uses composite keys: GenericKey.
 *
 * GenericKeys could be used inside ordinary maps too, but GenericMap offers better
 * static type-safety by inferring allowed types of values.
 *
 * Please note, that GenericMap does not put any restrictions on values on its own,
 * only those imposed by GenericKeys.
 *
 * GenericMap by default has MultiMap behaviour and support assigning multiple values to one key.
 *
 * @param <K> K
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public interface GenericMap<K> {

    static <K> GenericMap<K> wrap(Map<GenericKey<K,?>, Object> map) {
        return new OpenGenericMap<>(map);
    }

    /**
     * Returns size of map
     *
     * @return int
     */
    int size();

    /**
     * Returns true if map is empty
     *
     * @return bool
     */
    default boolean isEmpty() {
        return 0 == size();
    }

    /**
     * Checks if map contains provided key.
     *
     * @param key key
     * @return bool
     */
    boolean containsKey(GenericKey<K,?> key);

    /**
     * Checks if map contains provided value
     *
     * @param value value
     * @return bool
     */
    default boolean containsValue(Object value) {
        return CollectionUtils.first(values(), CompareUtils.predicate(value)).isPresent();
    }

    /**
     * Checks if map contains provided (key,value) pair
     *
     * @param key key
     * @param value value
     * @return bool
     * @param <V> V
     */
    default <V> boolean containsValue(GenericKey<K,V> key, V value) {
        V v = get(key);
        return v != null && v.equals(value);
    }

    /**
     * Returns value assigned with provided key, or null
     *
     * @param key key
     * @param <V> V
     * @return value or null
     */
    <V> V get(GenericKey<K,V> key);

    /**
     * Returns value assigned with provided key, or empty
     *
     * @param key key
     * @param <V> V
     * @return optional
     */
    default <V> Ref<V> getOptional(GenericKey<K,V> key) {
        if(containsKey(key)) {
            return Ref.of(get(key));
        } else {
            return Ref.empty();
        }
    }

    /**
     * Inserts new value into map
     * Returns previously stored value or null
     *
     * @param key key
     * @param value value
     * @param <V> V
     * @return value or null
     */
    <V> V put(GenericKey<K,V> key, V value);

    /**
     * Inserts new value into map
     * Returns previously stored value or null
     *
     * It provided optional is empty then it does not modify map.
     *
     * @param key key
     * @param value value
     * @param <V> V
     * @return value or null
     */
    default <V> V put(GenericKey<K,V> key, Ref<V> value) {
        if(value.isPresent()) {
            return put(key, value.get());
        } else {
            return get(key);
        }
    }

    /**
     * Removes mapping for provided key.
     * Returns previously stored value or null.
     *
     * @param key key
     * @param <V> V
     * @return value or null
     */
    <V> V remove(GenericKey<K,V> key);

    /**
     * Inserts all mapings stored inside provided map
     *
     * @param m m
     */
    @SuppressWarnings("unchecked")
    default void putAll(GenericMap<? extends K> m)  {
        for(GenericEntry<? extends K> e : m.entrySet()) {
            GenericKey kk = e.getKey();
            put(kk, e.getValue());
        }
    }

    /**
     * Removes all values from map
     */
    default void clear() {
        Iterator<GenericEntry<K>> i = entrySet().iterator();
        while(i.hasNext()) {
            i.remove();
        }
    }

    /**
     * Returns read-only view of keys stored inside map.
     *
     * @return view
     */
    Set<GenericKey<K,?>> keySet();

    /**
     * Returns read-only view of values stored inside map.
     *
     * @return view
     */
    Collection<?> values();

    /**
     * Returns read-only view of entries stored inside map.
     *
     * @return view
     */
    Set<GenericEntry<K>> entrySet();

    @Override
    boolean equals(Object o);

    @Override
    int hashCode();

    /**
     * Returns value assigned with provided key or "default" if there is no mapping.
     *
     * @param key key
     * @param ddefault ddefault
     * @param <V> V
     * @return value
     */
    default <V> V getOrDefault(GenericKey<K,V> key, V ddefault) {
        V v;
        return (((v = get(key)) != null) || containsKey(key)) ? v : ddefault;
    }

    /**
     * Iterates sequentially through all entries inside map.
     *
     * @param action action
     */
    default void forEach(BiConsumer<? super GenericKey<K,?>, Object> action) {
        Objects.requireNonNull(action);
        for (GenericEntry<K> entry : entrySet()) {
            GenericKey<K,?> k;
            Object v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

    /**
     * Replaces each entry's value with the result of invoking the given function on that entry
     *
     * @param function function
     */
    default void replaceAll(BiFunction<? super GenericKey<K,?>, Object, Object> function) {
        Objects.requireNonNull(function);
        for (GenericEntry<K> entry : entrySet()) {
            GenericKey<K,?> k;
            Object v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }

            // ise thrown from function is not a cme.
            v = function.apply(k, v);

            try {
                entry.setValue(k.type().cast(v));
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
        }
    }

    /**
     * Inserts new value to map if it is not already inserted
     * Returns null or entry already stored inside map.
     *
     * @param key key
     * @param value value
     * @param <V> V
     * @return value
     */
    default <V> V putIfAbsent(GenericKey<K,V> key, V value) {
        V v = get(key);
        if (v == null) {
            return put(key, value);
        }
        return v;
    }

    /**
     * Removes provided (key,value) pair from map.
     * Returns true, if map was modified.
     *
     * @param key key
     * @param value value
     * @param <V> V
     * @return boolean
     */
    default <V> boolean remove(GenericKey<K,V> key, V value) {
        Object current = get(key);
        if (!Objects.equals(current, value) || (current == null && !containsKey(key))) {
            return false;
        }
        remove(key);
        return true;
    }

    /**
     * Inserts new value to map only if there is already value for specified key
     * Returns true, if map was modified.
     *
     * @param key key
     * @param oldValue oldValue
     * @param value value
     * @param <V> V
     * @return boolean
     */
    default <V> boolean replace(GenericKey<K,V> key, V oldValue, V value) {
        Object current = get(key);
        if (!Objects.equals(current, oldValue) || (current == null && !containsKey(key))) {
            return false;
        }
        put(key, value);
        return true;
    }

    /**
     * Inserts new value to map only if there is already value for specified key
     * Returns previously stored value.
     *
     * @param key key
     * @param value value
     * @param <V> V
     * @return value
     */
    default <V> V replace(GenericKey<K,V> key, V value) {
        V current;
        if (((current = get(key)) != null) || containsKey(key)) {
            current = put(key, value);
        }
        return current;
    }

    /**
     * If the specified key is not already associated with a value,
     * attempts to compute its value using the given mapping function and inserts it.
     * If the mapping function returns null, no mapping is recorded.
     *
     * Returns computed value.
     *
     * @param key key
     * @param function function
     * @param <V> V
     * @return value
     */
    default <V> V computeIfAbsent(GenericKey<K,V> key, Function<? super GenericKey<K,V>, ? extends V> function) {
        Objects.requireNonNull(function);
        V prev;
        if (null == (prev = get(key)) && !containsKey(key)) {
            V value = function.apply(key);
            put(key, value);
            return value;
        }
        return prev;
    }

    /**
     * If the specified key is already associated with a value,
     * attempts to compute new value using the given mapping function and inserts it.
     * If the mapping function returns null, no mapping is recorded.
     *
     * Returns computed value.
     *
     * @param key key
     * @param function function
     * @param <V> V
     * @return value
     */
    default <V> V computeIfPresent(GenericKey<K,V> key, BiFunction<? super GenericKey<K,?>, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        V prev;
        if (null != (prev = get(key)) || containsKey(key)) {
            V value = function.apply(key, prev);
            put(key, value);
            return value;
        } else {
            return null;
        }
    }

    /**
     * Inserts new computed value into map.
     * New value is computed using provided key and value currently stored inside map.
     *
     * Returns computed value.
     *
     * @param key key
     * @param function function
     * @param <V> V
     * @return value
     */
    default <V> V compute(GenericKey<K,V> key, BiFunction<? super GenericKey<K,V>, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        V value = function.apply(key, get(key));
        put(key, value);
        return value;
    }

    /**
     * Inserts new computed value into map.
     * New value is computed using value currently associated with provided key and provided new value.
     *
     * Returns computed value.
     *
     * @param key key
     * @param value value
     * @param function function
     * @param <V> V
     * @return value
     */
    default <V> V merge(GenericKey<K,V> key, V value, BiFunction<? super V, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        V prev = get(key);
        V newValue = (prev == null) ? value : function.apply(prev, value);
        put(key, newValue);
        return newValue;
    }

    /**
     * Checks if map contains provided (key,value) pair
     *
     * @param key key
     * @param value value
     * @param <V> V
     * @return boolean
     */
    default <V> boolean containsEntry(GenericKey<K,V> key, Object value) {
        return Objects.equals(value, get(key));
    }

    /**
     * Inserts all values into map, assocciating them with one provided key.
     *
     * @param key key
     * @param values values
     * @param <V> V
     */
    default <V> void putAll(GenericKey<K,V> key, Iterable<? extends V> values) {
        for(V value : values) {
            put(key, value);
        }
    }

    /**
     * Removes all values associated with specific key.
     * Returns previous mappings
     *
     * @param key key
     * @param <V> V
     * @return values
     */
    default <V> Collection<V> removeAll(GenericKey<K,V> 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<GenericKey<K,?>> uniqueKeySet() {
        return new OpenSet<>(keySet());
    }
}
