/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.util;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A specialized map that stores types by both their full and short (without
 * package prefixes) names. If an incoming type shares the same name (but
 * different package/prefix) with a type already in the map then an
 * IllegalArgumentException will be thrown since any subsequent retrievals by
 * said short name could be in error.
 *
 * @author Brian Remedios
 */
public class TypeMap {

    private Map<String, Class<?>> typesByName;

    /**
     * Constructor for TypeMap.
     *
     * @param initialSize
     *            int
     */
    public TypeMap(int initialSize) {
        typesByName = new HashMap<>(initialSize);
    }

    /**
     * Constructor for TypeMap that takes in an initial set of types.
     *
     * @param types
     *            Class[]
     */
    public TypeMap(Class<?>... types) {
        this(types.length);
        add(types);
    }

    /**
     * Adds a type to the receiver and stores it keyed by both its full and
     * short names. Throws an exception if the short name of the argument
     * matches an existing one already in the map for a different class.
     *
     * @param type
     *            Class
     * @throws IllegalArgumentException
     */
    @SuppressWarnings("PMD.CompareObjectsWithEquals")
    public void add(Class<?> type) {
        final String shortName = ClassUtil.withoutPackageName(type.getName());
        Class<?> existingType = typesByName.get(shortName);
        if (existingType == null) {
            typesByName.put(type.getName(), type);
            typesByName.put(shortName, type);
            return;
        }

        if (existingType != type) {
            throw new IllegalArgumentException(
                    "Short name collision between existing " + existingType + " and new " + type);
        }
    }

    /**
     * Returns whether the type is known to the receiver.
     *
     * @param type
     *            Class
     * @return boolean
     */
    public boolean contains(Class<?> type) {
        return typesByName.containsValue(type);
    }

    /**
     * Returns whether the typeName is known to the receiver.
     *
     * @param typeName
     *            String
     * @return boolean
     */
    public boolean contains(String typeName) {
        return typesByName.containsKey(typeName);
    }

    /**
     * Returns the type for the typeName specified.
     *
     * @param typeName
     *            String
     * @return Class
     */
    public Class<?> typeFor(String typeName) {
        return typesByName.get(typeName);
    }

    /**
     * Adds an array of types to the receiver at once.
     *
     * @param types
     *            Class[]
     */
    public void add(Class<?>... types) {
        for (Class<?> element : types) {
            add(element);
        }
    }

    /**
     * Creates and returns a map of short type names (without the package
     * prefixes) keyed by the classes themselves.
     *
     * @return Map
     */
    public Map<Class<?>, String> asInverseWithShortName() {
        Map<Class<?>, String> inverseMap = new HashMap<>(typesByName.size() / 2);

        Iterator<Map.Entry<String, Class<?>>> iter = typesByName.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, Class<?>> entry = iter.next();
            storeShortest(inverseMap, entry.getValue(), entry.getKey());
        }

        return inverseMap;
    }

    /**
     * Returns the total number of entries in the receiver. This will be exactly
     * twice the number of types added.
     *
     * @return the total number of entries in the receiver
     */
    public int size() {
        return typesByName.size();
    }

    /**
     * Store the shorter of the incoming value or the existing value in the map
     * at the key specified.
     *
     * @param map
     * @param key
     * @param value
     */
    private void storeShortest(Map<Class<?>, String> map, Class<?> key, String value) {
        String existingValue = map.get(key);

        if (existingValue == null) {
            map.put(key, value);
            return;
        }

        if (existingValue.length() < value.length()) {
            return;
        }

        map.put(key, value);
    }
}
