/*
 *  @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.functional.Consumers.Consumer3;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.Pair;

import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;

/**
 * two-dimensional map.
 *
 * Its behaviour is similar to Map, but it takes two keys instead of just one.
 * Most of the time is equivalent to the Map with composite key.
 *
 * It offers additional reduction methods, which returns as view one-dimensional MultiMap.
 *
 * @param <K1> K1
 * @param <K2> K2
 * @param <V> V
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public interface CrossMap<K1, K2, V> {

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

    /**
     * Returns true if map is empty
     * @return bool
     */
    boolean isEmpty();

    /**
     * Checks if map contains provided key pair
     *
     * @param key1 key1
     * @param key2 key2
     * @return bool
     */
    boolean containsKey(K1 key1, K2 key2);

    /**
     * Checks if map contains provided value
     *
     * @param value value
     * @return bool
     */
    boolean containsValue(V value);

    /**
     * Returns value assigned to provided key pair.
     * Returns null if there is no such mapping.
     *
     * @param key1 key1
     * @param key2 key2
     * @return value or null
     */
    V get(K1 key1, K2 key2);

    /**
     * Returns value assigned to provided key pair.
     * Returns none if there is no such mapping.
     *
     * @param key1 key1
     * @param key2 key2
     * @return optional
     */
    Ref<V> getOptional(K1 key1, K2 key2);

    /**
     * Inserts new mapping.
     * Returns old value or null.
     *
     * @param key1 key1
     * @param key2 key2
     * @param value value
     * @return value
     */
    V put(K1 key1, K2 key2, V value);

    /**
     * Removes mapping.
     * Returns old value or null.
     *
     * @param key1 key1
     * @param key2 key2
     * @return value
     */
    V remove(K1 key1, K2 key2);

    default V putPair(Pair<K1,K2> key, V value) {
        return put(key.getFirst(), key.getSecond(), value);
    }

    default V removePair(Pair<K1,K2> key) {
        return remove(key.getFirst(), key.getSecond());
    }

    /**
     * Inserts all mapping from provided map.
     *
     * @param values values
     */
    void putAll(CrossMap<K1, K2, ? extends V> values);

    /**
     * Removes all entries
     */
    void clear();

    /**
     * Creates new MultiMap which contains all entries without first key.
     * Returns MultiMap because this operation can lead to duplicated K2 keys.
     * If reduction leads to duplicated (K2,V) pairs, returned collection will contains only one.
     *
     * @return new collection
     */
    MultiMap<K2, V> reduceFirst();

    /**
     * Creates new MultiMap which contains all entries without second key.
     * Returns MultiMap because this operation can lead to duplicated K1 keys.
     * If reduction leads to duplicated (K1,V) pairs, returned collection will contains only one.
     *
     * @return new collection
     */
    MultiMap<K1, V> reduceSecond();

    /**
     * Creates new MultiMap which contains all entries without value.
     * That means: K1 keys are mapped to K2, if there is a mapping for (K1,K2,V) tuple.
     * Returns MultiMap because this operation can lead to duplicated K1 keys.
     *
     * @return new collection
     */
    MultiMap<K1, K2> reduceValues();

    /**
     * Returns view with all (K1,K2) pairs which have associated value.
     *
     * @return view
     */
    Set<Pair<K1, K2>> keySet();

    /**
     * Returns view with all values stored inside map.
     *
     * @return view
     */
    Collection<V> values();

    /**
     * Returns view with all mappings stored inside map.
     *
     * @return view
     */
    Set<Map.Entry<Pair<K1, K2>, V>> entrySet();

    /**
     * Iterates sequentially through all entries inside map.
     * It passed to "action" two arguments: key-pair and value.
     *
     * @param action action
     */
    default void forEachPair(BiConsumer<Pair<K1, K2>, V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<Pair<K1, K2>, V> entry : entrySet()) {
            Pair<K1, K2> pair;
            V val;
            try {
                pair = entry.getKey();
                val = entry.getValue();
            } catch (IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(pair, val);
        }
    }

    /**
     * Iterates sequentially through all entries inside map.
     * It passed to "action" three arguments: key1, key2, and value.
     *
     * @param action action
     */
    default void forEach(Consumer3<K1, K2, V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<Pair<K1, K2>, V> entry : entrySet()) {
            Pair<K1, K2> pair;
            V val;
            try {
                pair = entry.getKey();
                val = entry.getValue();
            } catch (IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(pair.getFirst(), pair.getSecond(), val);
        }
    }

}
