/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2004-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.util;

import java.io.Serializable;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Map;
import java.util.Set;


/**
 * A map whose keys are derived from an other map. The keys are derived only when
 * requested, which make it possible to backup potentially large maps. Implementations
 * need only to overrides {@link #baseToDerived} and {@link #derivedToBase} methods.
 * This set do not supports {@code null} key, since {@code null} is used
 * when no mapping from {@linkplain #base} to {@code this} exists.
 * This class is serializable if the underlying {@linkplain #base} set is serializable
 * too.
 * <p>
 * This class is <strong>not</strong> thread-safe. Synchronizations (if wanted) are user's
 * reponsability.
 *
 * @param <BK> The type of keys in the backing map.
 * @param <K>  The type of keys in this map.
 * @param <V>  The type of values in both this map and the underlying map.
 *
 * @since 2.0
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/metadata/src/main/java/org/geotools/util/DerivedMap.java $
 * @version $Id: DerivedMap.java 37298 2011-05-25 05:16:15Z mbedward $
 * @author Martin Desruisseaux (IRD)
 */
public abstract class DerivedMap<BK,K,V> extends AbstractMap<K,V> implements Serializable {
    /**
     * Serial number for interoperability with different versions.
     */
    private static final long serialVersionUID = -6994867383669885934L;

    /**
     * The base map whose keys are derived from.
     *
     * @see #baseToDerived
     * @see #derivedToBase
     */
    protected final Map<BK,V> base;

    /**
     * Key set. Will be constructed only when first needed.
     *
     * @see #keySet
     */
    private transient Set<K> keySet;

    /**
     * Entry set. Will be constructed only when first needed.
     *
     * @see #entrySet
     */
    private transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The derived key type.
     */
    private final Class<K> keyType;

    /**
     * Creates a new derived map from the specified base map.
     *
     * @param base The base map.
     * @param keyType the type of keys in the derived map.
     *
     * @since 2.5
     */
    public DerivedMap(final Map<BK,V> base, final Class<K> keyType) {
        this.base    = base;
        this.keyType = keyType;
    }

    /**
     * Transforms a key from the {@linkplain #base} map to a key in this map.
     * If there is no key in the derived map for the specified base key,
     * then this method returns {@code null}.
     *
     * @param  key A ley from the {@linkplain #base} map.
     * @return The key that this view should contains instead of {@code key},
     *         or {@code null}.
     */
    protected abstract K baseToDerived(final BK key);

    /**
     * Transforms a key from this derived map to a key in the {@linkplain #base} map.
     *
     * @param  key A key in this map.
     * @return The key stored in the {@linkplain #base} map.
     */
    protected abstract BK derivedToBase(final K key);

    /**
     * Returns the number of key-value mappings in this map.
     *
     * @return the number of key-value mappings in this map.
     */
    @Override
    public int size() {
	return super.size();
    }

    /**
     * Returns {@code true} if this map contains no key-value mappings.
     *
     * @return {@code true} if this map contains no key-value mappings.
     */
    @Override
    public boolean isEmpty() {
	return base.isEmpty() || super.isEmpty();
    }

    /**
     * Returns {@code true} if this map maps one or more keys to this value.
     * The default implementation invokes
     * <code>{@linkplain #base}.containsValue(value)</code>.
     *
     * @return {@code true} if this map maps one or more keys to this value.
     */
    @Override
    public boolean containsValue(final Object value) {
        return base.containsValue(value);
    }

    /**
     * Returns {@code true} if this map contains a mapping for the specified key.
     * The default implementation invokes
     * <code>{@linkplain #base}.containsKey({@linkplain #derivedToBase derivedToBase}(key))</code>.
     *
     * @param  key key whose presence in this map is to be tested.
     * @return {@code true} if this map contains a mapping for the specified key.
     */
    @Override
    public boolean containsKey(final Object key) {
        if (keyType.isInstance(key)) {
            return base.containsKey(derivedToBase(keyType.cast(key)));
        } else {
            return false;
        }
    }

    /**
     * Returns the value to which this map maps the specified key.
     * The default implementation invokes
     * <code>{@linkplain #base}.get({@linkplain #derivedToBase derivedToBase}(key))</code>.
     *
     * @param  key key whose associated value is to be returned.
     * @return the value to which this map maps the specified key.
     */
    @Override
    public V get(final Object key) {
        if (keyType.isInstance(key)) {
            return base.get(derivedToBase(keyType.cast(key)));
        } else {
            return null;
        }
    }

    /**
     * Associates the specified value with the specified key in this map.
     * The default implementation invokes
     * <code>{@linkplain #base}.put({@linkplain #derivedToBase derivedToBase}(key), value)</code>.
     *
     * @param  key key with which the specified value is to be associated.
     * @param  value value to be associated with the specified key.
     * @return previous value associated with specified key, or {@code null}
     *	       if there was no mapping for key.
     * @throws UnsupportedOperationException if the {@linkplain #base} map doesn't
     *         supports the {@code put} operation.
     */
    @Override
    public V put(final K key, final V value) throws UnsupportedOperationException {
        return base.put(derivedToBase(key), value);
    }

    /**
     * Removes the mapping for this key from this map if present.
     * The default implementation invokes
     * <code>{@linkplain #base}.remove({@linkplain #derivedToBase derivedToBase}(key))</code>.
     *
     * @param  key key whose mapping is to be removed from the map.
     * @return previous value associated with specified key, or {@code null}
     *	       if there was no entry for key.
     * @throws UnsupportedOperationException if the {@linkplain #base} map doesn't
     *         supports the {@code remove} operation.
     */
    @Override
    public V remove(final Object key) throws UnsupportedOperationException {
        if (keyType.isInstance(key)) {
            return base.remove(derivedToBase(keyType.cast(key)));
        } else {
            return null;
        }
    }

    /**
     * Returns a set view of the keys contained in this map.
     *
     * @return a set view of the keys contained in this map.
     */
    @Override
    public Set<K> keySet() {
        if (keySet == null) {
            keySet = new KeySet(base.keySet());
        }
        return keySet;
    }

    /**
     * Returns a collection view of the values contained in this map.
     *
     * @return a collection view of the values contained in this map.
     */
    @Override
    public Collection<V> values() {
        return base.values();
    }

    /**
     * Returns a set view of the mappings contained in this map.
     *
     * @return a set view of the mappings contained in this map.
     */
    @Override
    @SuppressWarnings("unchecked")
    public Set<Map.Entry<K,V>> entrySet() {
        if (entrySet == null) {
            entrySet = (Set) new EntrySet(base.entrySet());
        }
        return entrySet;
    }

    /**
     * The key set.
     */
    private final class KeySet extends DerivedSet<BK,K> {
        private static final long serialVersionUID = -2931806200277420177L;

        public KeySet(final Set<BK> base) {
            super(base, keyType);
        }

        protected K baseToDerived(final BK element) {
            return DerivedMap.this.baseToDerived(element);
        }

        protected BK derivedToBase(final K element) {
            return DerivedMap.this.derivedToBase(element);
        }
    }

    /**
     * The entry set.
     */
    private final class EntrySet extends DerivedSet<Map.Entry<BK,V>, Entry<BK,K,V>> {
        private static final long serialVersionUID = -2931806200277420177L;

        @SuppressWarnings("unchecked")
        public EntrySet(final Set<Map.Entry<BK,V>> base) {
            super(base, (Class) Entry.class);
        }

        protected Entry<BK,K,V> baseToDerived(final Map.Entry<BK,V> entry) {
            final K derived = DerivedMap.this.baseToDerived(entry.getKey());
            return derived!=null ? new Entry<BK,K,V>(entry, derived) : null;
        }

        protected Map.Entry<BK,V> derivedToBase(final Entry<BK,K,V> element) {
            return element.entry;
        }
    }

    /**
     * The entry element.
     */
    private static final class Entry<BK,K,V> implements Map.Entry<K,V> {
        public final Map.Entry<BK,V> entry;
        private final K derived;

        public Entry(final Map.Entry<BK,V> entry, final K derived) {
            this.entry   = entry;
            this.derived = derived;
        }

        public K getKey() {
            return derived;
        }

        public V getValue() {
            return entry.getValue();
        }

        public V setValue(V value) {
            return entry.setValue(value);
        }
    }
}
