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

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import net.ranides.assira.collection.ACollection;
import net.ranides.assira.collection.sets.ASet;
import net.ranides.assira.generic.HashUtils;
import net.ranides.assira.generic.CompareUtils;

/**
 * An abstract class providing basic methods for maps implementing a
 * type-specific interface.
 *
 * <P>Optional operations just throw an {@link
 * UnsupportedOperationException}. Generic versions of accessors delegate to the
 * corresponding type-specific counterparts following the interface rules (they
 * take care of returning <code>null</code> on a missing key).
 *
 * <P>As a further help, this class provides a {@link BasicEntry BasicEntry}
 * inner class that implements a type-specific version of
 * {@link java.util.Map.Entry}; it is particularly useful for those classes that
 * do not implement their own entries (e.g., most immutable maps).
 */
public abstract class AMap<K, V> implements Map<K, V>, java.io.Serializable {

    private static final long serialVersionUID = 2L;

    protected AMap() {
        // do nothing
    }

    /**
     * Puts all pairs in the given map. If the map implements the interface of
     * this map, it uses the faster iterators.
     *
     * @param values a map.
     */
    @SuppressWarnings("unchecked")
    @Override
    public void putAll(Map<? extends K, ? extends V> values) {
        int n = values.size();
        Iterator<? extends Entry<? extends K, ? extends V>> iterator = values.entrySet().iterator();
        while (n-- != 0) {
            Entry<? extends K, ? extends V> entry = iterator.next();
            put(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public boolean isEmpty() {
        return size() == 0;
    }

    /**
     * This class provides a basic but complete type-specific entry class for
     * all those maps implementations that do not have entries on their own
     * (e.g., most immutable maps).
     *
     * <P>This class does not implement
     * {@link java.util.Map.Entry#setValue(Object) setValue()}, as the
     * modification would not be reflected in the base map.
     */
    protected static class BasicEntry<K, V> implements Entry<K, V> {

        public K key;
        public V value;

        public BasicEntry(K key, V value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof Map.Entry)) {
                return false;
            }
            Map.Entry<?, ?> entry = (Map.Entry<?, ?>) other;
            return CompareUtils.equals(key, entry.getKey()) &&
                   CompareUtils.equals(value, entry.getValue());
        }

        @Override
        public int hashCode() {
            return HashUtils.hashPair(key, value);
        }

        @Override
        public String toString() {
            return key + "->" + value;
        }
    }

    @Override
    public Set<K> keySet() {
        return new ASet<K>() {
            @Override
            @SuppressWarnings("element-type-mismatch")
            public boolean contains(Object key) {
                return containsKey(key);
            }

            @Override
            public int size() {
                return AMap.this.size();
            }

            @Override
            public void clear() {
                AMap.this.clear();
            }

            @Override
            public boolean remove(Object value) {
                int prev = AMap.this.size();
                AMap.this.remove(value);
                return prev != AMap.this.size();
            }
            
            @Override
            public Iterator<K> iterator() {
                return new Iterator<K>() {
                    private final Iterator<Map.Entry<K, V>> riv = entrySet().iterator();

                    @Override
                    public K next() {
                        return riv.next().getKey();
                    }

                    @Override
                    public boolean hasNext() {
                        return riv.hasNext();
                    }

                    @Override
                    public void remove() {
                        riv.remove();
                    }
                };
            }
        };
    }

    @Override
    public Collection<V> values() {
        return new ACollection<V>() {
            @Override
            @SuppressWarnings("element-type-mismatch")
            public boolean contains(Object value) {
                return containsValue(value);
            }

            @Override
            public int size() {
                return AMap.this.size();
            }

            @Override
            public void clear() {
                AMap.this.clear();
            }

            @Override
            public Iterator<V> iterator() {
                return new Iterator<V>() {
                    private final Iterator<Map.Entry<K, V>> riv = entrySet().iterator();

                    @Override
                    public V next() {
                        return riv.next().getValue();
                    }

                    @Override
                    public boolean hasNext() {
                        return riv.hasNext();
                    }

                    @Override
                    public void remove() {
                        riv.remove();
                    }
                };
            }
        };
    }

    @Override
    public abstract Set<Map.Entry<K, V>> entrySet();

    /**
     * Returns a hash code for this map.
     *
     * The hash code of a map is computed by summing the hash codes of its
     * entries.
     *
     * @return a hash code for this map.
     */
    @Override
    public int hashCode() {
        int h = 0, n = size();
        final Iterator<? extends Map.Entry<K, V>> i = entrySet().iterator();
        while (n-- != 0) {
            h += i.next().hashCode();
        }
        return h;
    }

    /**
     * Checks if content of passed map is equal. Order of entries is irrevelant.
     * @param other
     * @return 
     */
    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }
        if (!(other instanceof Map)) {
            return false;
        }
        Map<?, ?> map = (Map<?, ?>) other;
        if (map.size() != size()) {
            return false;
        }
        return entrySet().containsAll(map.entrySet());
    }

    @Override
    public String toString() {
        final StringBuilder result = new StringBuilder();
        final Iterator<? extends Map.Entry<K, V>> i = entrySet().iterator();
        int n = size();
        Entry<K, V> entry;
        boolean first = true;
        result.append('{');
        while (n-- != 0) {
            if (first) {
                first = false;
            } else {
                result.append(", ");
            }
            entry = (Entry<K, V>) i.next();
            if (this == entry.getKey()) {
                result.append("(this map)");
            } else {
                result.append(String.valueOf(entry.getKey()));
            }
            result.append("=>");
            if (this == entry.getValue()) {
                result.append("(this map)");
            } else {
                result.append(String.valueOf(entry.getValue()));
            }
        }
        result.append('}');
        return result.toString();
    }

    @Override
    public V put(K key, V value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public V remove(Object key) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void clear() {
        entrySet().clear();
    }

}
