/**
 * Copyright (c) 2004-2005, Regents of the University of California
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * Neither the name of the University of California, Los Angeles nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package cck.util;

import cck.text.StringUtil;

import java.util.*;

/**
 * The <code>ClassMap</code> is a class that maps short names (i.e. short, lower case strings) to java classes
 * and can instantiate them. This is useful for dynamic resolution of classes but with a small set of known
 * defaults that have a short name. If the short name is not in the default set, this class will treat the
 * short name as a fully qualified Java class name and load it. This class does the requisite checking--that
 * the class exists, that it can be loaded, that it is of the appropriate type, that it can be instantiated,
 * etc.
 *
 * @author Ben L. Titzer
 */
public class ClassMap {

    /**
     * The <code>type</code> field stores a string that represents the name of the "type" that this map
     * contains. For example, a class map for actions might be called "Action" and for input formats might be
     * called "Input Format".
     */
    protected final String type;

    /**
     * The <code>clazz</code> field stores a reference to the Java class of which the objects stored in this
     * map are instances of.
     */
    protected final Class<?> clazz;

    /**
     * The <code>classMap</code> field is a hash map that maps a string to a Java class.
     */
    protected final HashMap<String, Class<?>> classMap;

    /**
     * The <code>reverseMap</code> field is a hash map that maps a Java class back to its alias.
     */
    protected final HashMap<Class<?>, String> reverseMap;

    /**
     * The <code>objMap</code> field is a hash map that maps a string to an instance of a particular class,
     * i.e. an object.
     */
    protected final HashMap<String, Object> objMap;

    /**
     * The constructor for the <code>ClassMap</code> class creates a new class map with the specified type,
     * which maps strings to instances of the specified class.
     *
     * @param t   the name of the type of this class as a string
     * @param clz the class which objects should be instances of
     */
    public ClassMap(String t, Class<?> clz) {
        clazz = clz;
        classMap = new HashMap<String, Class<?>>();
        reverseMap = new HashMap<Class<?>, String>();
        objMap = new HashMap<String, Object>();
        type = t;
    }

    /**
     * The <code>addClass()</code> method adds a short name (alias) for the specified class to the set of
     * default class names.
     *
     * @param alias the string representation of the alias of the class
     * @param clz   the class to which the alias maps
     */
    public void addClass(String alias, Class<?> clz) {
        classMap.put(alias, clz);
        reverseMap.put(clz, alias);
    }

    /**
     * The <code>addInstance()</code> method adds a mapping between a short name (alias) and an object that is
     * the instance of the class represented by that short name.
     *
     * @param alias the alias for the instance
     * @param o     the actual object that will be returned from <code>getObjectOfClass()</code> when the
     *              parameters is equal to the alias.
     */
    public void addInstance(String alias, Object o) {
        Class<?> cz = o.getClass();
        if (!(clazz.isAssignableFrom(cz)))
            throw Util.failure("Object of class " + StringUtil.quote(cz) + " is not an instance of " + clazz.getName());

        objMap.put(alias, o);
        classMap.put(alias, cz);
        reverseMap.put(cz, alias);
    }

    /**
     * The <code>getClass()</code> method gets the Java class representing the class returned for a given
     * short name. If there is no short name (alias) for the passed argument, this method will assume that the
     * parameter is the fully qualified name of a class. It will then load that class and instantiate an
     * instance of that class. That instance will be returned in subsequent calls to
     * <code>getObjectOfClass()</code>.
     *
     * @param shortName the short name of the class
     * @return a Java class representing the class for that alias or fully qualified name
     */
    public Class<?> getClass(String shortName) {
        Object o = objMap.get(shortName);
        if (o != null) return o.getClass();
        return (Class<?>) classMap.get(shortName);
    }

    /**
     * The <code>getObjectOfClass()</code> method looks up the string name of the class in the alias map
     * first, and if not found, attempts to load the class using <code>Class.forName()</code> and instantiates
     * one object.
     *
     * @param name the name of the class or alias
     * @return an instance of the specified class
     * @throws Util.Error if there is a problem finding or instantiating the class
     */
    public Object getObjectOfClass(String name) {
        Object o = objMap.get(name);
        if (o != null) return o;

        String clname = StringUtil.quote(name);

        Class<?> c = (Class<?>) classMap.get(name);
        if (c == null) {
            try {
                c = Class.forName(name);
            } catch (ClassNotFoundException e) {
                Util.userError(type + " class not found", clname);
            }
        } else {
            clname = clname + " (" + c.toString() + ')';
        }

        if (!(clazz.isAssignableFrom(c)))
            Util.userError("The specified class does not extend " + clazz.getName(), clname);

        try {
            return c.newInstance();
        } catch (InstantiationException e) {
            Util.userError("The specified class does not have a default constructor", clname);
        } catch (IllegalAccessException e) {
            Util.userError("Illegal access to class", clname);
        }

        // UNREACHABLE
        throw Util.unreachable();
    }

    /**
     * The <code>getAlias()</code> method returns the alias of a particular class or object
     * that corresponds to the short name for the class. If there is no such alias for this
     * object, this method will return the name of the class of the object.
     * @param o the object for which to look up the alias
     * @return the string name of the alias for the object
     */
    public String getAlias(Object o) {
        String s = (String)reverseMap.get(o);
        if ( s == null ) s = (String)reverseMap.get(o.getClass());
        if ( s == null ) s = o.getClass().getName();
        return s;
    }

    /**
     * The <code>getSortedList()</code> method returns a sorted list of the short names (aliases) stored in
     * this class map.
     *
     * @return an alphabetically sorted list that contains all the aliases in this map
     */
    public List<String> getSortedList() {
        List<String> list = Collections.list(Collections.enumeration(classMap.keySet()));
        Collections.sort(list, String.CASE_INSENSITIVE_ORDER);
        return list;
    }

    /**
     * The <code>getIterator()</code> method returns an interator over all of the key values (short
     * names or aliases) of this class map.
     *
     * @return an iterator over all the short names in this map
     */
    public Iterator<String> getIterator() {
        return classMap.keySet().iterator();
    }

    /**
     * The <code>iterator()</code> method returns an interator over the short names (aliases) stored in this
     * map.
     *
     * @return an instance of <code>java.util.Iterator</code> which can be used to iterate over each alias in
     *         this map.
     */
    public Iterator<String> iterator() {
        return classMap.keySet().iterator();
    }
}
