package de.renew.util;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * HashedRelations represents a relationship between a hash key generated from the type <code>K</code> to a {@link Set}
 * containing all the objects of type <code>V</code> mapped from that Hash. This means a single Hash may return a set of
 * or multiple objects sharing that hash.
 *
 * @param <K> The type of the key from which hashes are generated
 * @param <V> The type of the sets mapped onto via the hash
 */
public class HashedRelation<K, V> implements Serializable {
    /**
     * The map which maps Hashes generated from type K to Sets containing or multiple objects of type V.
     */
    private final Map<K, Set<V>> _map;

    /**
     * Generate a new HashedRelations mapping. The mapping is empty at start.
     */
    public HashedRelation() {
        _map = new HashMap<>();
    }

    /**
     * Insert a new element into the hashed Relation using the key as the base for the hash on which to retrieve the element.
     *
     * @param key the key which will be used to map to the Set of all elements sharing this key, including the newly submitted element
     * @param elem the element which to append to the Set of all other objects that are mapped from the hash of the submitted key
     */
    public synchronized void put(K key, V elem) {
        Set<V> set;
        if (_map.containsKey(key)) {
            set = _map.get(key);
        } else {
            set = new HashSet<>();
            _map.put(key, set);
        }
        set.add(elem);
    }

    /**
     * Removes a specific element stored under a specific key in the HashedRelations. If the element is not stored under
     * the submitted key, the method will still execute. If the key is not mapped from, the method will
     * fail with a {@link NullPointerException} due to calling {@link Set#remove(Object)} on a null object.
     *
     * @param key the key under which to attempt removal of the submitted element
     * @param elem the object which to remove from the Set mapped to by the key.
     */
    public synchronized void remove(K key, V elem) {
        Set<V> set = _map.get(key);
        set.remove(elem);
        if (set.isEmpty()) {
            _map.remove(key);
        }
    }

    /**
     * Returns a {@link Set} of all key of type <code>K</code> which are mapped from in the hashed relation.
     * @return a Set of all keys of type <code>V</code> mapped from the hashed relation.
     */
    public synchronized Set<K> keys() {
        return _map.keySet();
    }

    /**
     * Retrieve the Set of all objects of type <code>K</code> which are stored under the submitted key in the hashedRelation.
     * If the submitted key maps to no set in the relationship, an empty Set is returned.
     * @param key The key for which to retrieve the mapped-to Set of objects
     * @return the mapped-to set; an empty set if the key isn't mapping to any extant Set in the relationship.
     */
    public synchronized Set<V> elementsAt(K key) {
        return _map.getOrDefault(key, Collections.emptySet());
    }

    /**
     * Returns the number of elements of the Set of objects of type <code>K</code> mapped to from the key.
     * If the key doesn't map to an extant set, size 0 is returned.
     * @param key which may map to an extant Set of objects of that key in the relationship
     * @return the number of objects stored in the Set under the mapping key; 0 if no extant mapping relationship with a
     * Set of at least size 1 exists for the key.
     */
    public synchronized int sizeAt(Object key) {
        if (_map.containsKey(key)) {
            Set<V> set = _map.get(key);
            return set.size();
        } else {
            return 0;
        }
    }

    /**
     * <p>
     * Print the entire content of the hashedRelation as a string. For all extant keys in the map, print
     * the string representation of all objects which are mapped to by that key in their encapsulating set. This is
     * subject to the limitations of the underlying {@link HashSet} storing the objects: there is no
     * guarantee that the order of elements mapped under the same key in the hashedRelation will stay the same over time.
     * </p>
     * <p>
     *     The  string representation encapsulates in brackets. Inside the outer capsule <code>de.renew.util.HashedRelation()</code>
     *     it will contain substrings of the format <code>key -> (list,of,elements) </code>
     * </p>
     * @return a String representation of the entire content of hashedRelation
     */
    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("de.renew.util.HashedRelation( ");
        for (K key : _map.keySet()) {
            buffer.append(key);
            buffer.append(" -> (");
            boolean first = true;
            for (V value : _map.get(key)) {
                if (first) {
                    first = false;
                } else {
                    buffer.append(", ");
                }
                buffer.append(value);
            }
            buffer.append(") ");
        }
        buffer.append(")");
        return buffer.toString();
    }
}