/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.classfile;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.Comparator;
import java.util.WeakHashMap;

/**
 * Class representing the name of a Java class.  This class is immutable, and
 * can therefore be safely used by multiple threads.
 * <p>
 * <b>Warning:</b>  The same reference is often returned by the getClassName 
 * factory method for multiple calls which use the same type name as its
 * parameter.  However, no guarantee is made that this behavior will always 
 * be true, so the equals method should always be used to compare ClassName 
 * instances.
 *
 * @author Thomas Ball
 */
public final class ClassName implements Comparable<ClassName>, Comparator<ClassName>, Serializable {

    static final long serialVersionUID = -8444469778945723553L;

    private final String type;
    private final transient String internalName;
    private volatile transient String externalName;
    private volatile transient String packageName;
    private volatile transient String simpleName;

    private final static WeakHashMap<String,WeakReference<ClassName>> cache = 
            new WeakHashMap<String,WeakReference<ClassName>>();

    /**
     * Returns the ClassName object referenced by a class
     * type string (field descriptor), as defined in the 
     * JVM Specification, sections 4.3.2 and 4.2.  
     * <P>
     * Basically, the JVM Specification defines a class type 
     * string where the periods ('.') separating
     * a package name are replaced by forward slashes ('/').
     * Not documented in the second edition of the specification
     * is that periods separating inner and outer classes are
     * replaced with dollar signs ('$').  Array classes have one
     * or more left brackets ('[') prepending the class type.
     * For example:
     * <PRE><CODE>
     *   java.lang.String         java/lang/String
     *   java.util.HashMap.Entry  java/util/HashMap$Entry
     *   java.lang.Integer[]      [java/lang/Integer
     *   java.awt.Point[][]       [[java/awt/Point
     * </CODE><PRE>
     * <P>
     * This method also accepts type strings which contain with
     * 'L' and end with ';' characters.  This format is used
     * to reference a class in other type names, such as
     * method arguments.  These two characters are removed from the
     * type string.
     * <P>
     * Because ClassNames are immutable, multiple requests to
     * get the same type string may return identical object
     * references.  This cannot be assumed, however, and the
     * ClassName.equals() method should be used instead of
     * "==" to test for equality.
     *
     * @param classType  the class type string, as defined by the JVM spec.
     * @return the ClassName instance, or null if not found.
     * @throws IllegalArgumentException if classType isn't of the correct
     *                   format.
     */
    public static ClassName getClassName(String classType) {
        // A null superclass name is supposed to be null, but may be
        // an empty string depending on the compiler.
        if (classType == null || classType.length() == 0)
	    return null;

        ClassName cn = getCacheEntry(classType);
        synchronized (cache) {
            cn = getCacheEntry(classType);
            if (cn == null) {
                // check for valid class type
                int i = classType.indexOf('L');
                String _type;
                char lastChar = classType.charAt(classType.length()-1);
                if (i != -1 && lastChar == ';') {
                    // remove 'L' and ';' from type
                    _type = classType.substring(i+1, classType.length()-1);
                    if (i > 0)
                        // add array prefix
                        _type = classType.substring(0, i) + _type;
                    cn = getCacheEntry(_type);
                    if (cn != null)
                        return cn;
                } else {
                    _type = classType;
                }

                cn = new ClassName(_type);
                cache.put(_type, new WeakReference<ClassName>(cn));
            }
        }
	return cn;
    }

    private static ClassName getCacheEntry(String key) {
	WeakReference<ClassName> ref = cache.get(key);
	return ref != null ? ref.get() : null;
    }

    /**
     * Create a ClassName object via its internal type name, as
     * defined by the JVM spec.
     * @param type the internal type name
     */
    private ClassName(String type) {
        this.type = type;

	// internal name is a type stripped of any array designators
	int i = type.lastIndexOf('[');
	internalName = (i > -1) ? type.substring(i+1) : type;
    }

    /**
     * Returns the type string of this class, as stored in the 
     * classfile (it's "raw" form).  For example, an array of
     * Floats would have a type of "[java/lang/Float".
     * @return the raw string format of the class type
     */
    public String getType() {
        return type;
    }

    /**
     * Returns the "internal" classname, as defined by the JVM 
     * Specification, without any parameter or return type 
     * information.  For example, the name for the String
     * class would be "java/lang/String".  Inner classes are 
     * separated from their outer class with '$'; such as
     * "java/util/HashMap$Entry".  Array specifications are
     * stripped; use getType() instead.
     * @return the internal name
     */
    public String getInternalName() {
        return internalName;
    }

    /**
     * Returns the "external" classname, as defined by the 
     * Java Language Specification, without any parameter
     * or return type information.  For example, the name for the
     * String class would be "java.lang.String".  Inner classes
     * are separated from their outer class with '.'; such as
     * "java.util.HashMap.Entry".  Arrays are shown as having one
     * or more "[]" characters behind the base classname, such
     * as "java.io.Files[]".
     * @return the external name, with array characters if this ClassName 
     *         is an array
     */
    public String getExternalName() {
        return getExternalName(false);
    }

    /**
     * Returns the "external" classname, as defined by the 
     * Java Language Specification, without any parameter
     * or return type information.  For example, the name for the
     * String class would be "java.lang.String".  Inner classes
     * are separated from their outer class with '.'; such as
     * "java.util.HashMap.Entry".  Unless suppressed, arrays are 
     * shown as having one or more "[]" characters behind the 
     * base classname, such as "java.io.Files[]".
     * @param suppressArrays true if array characters should be included
     * @return the external name
     */
    public String getExternalName(boolean suppressArrays) {
        initExternalName();
        int i;
        if (suppressArrays && (i = externalName.indexOf('[')) != -1)
	    return externalName.substring(0, i);
        return externalName;
    }
    
    private synchronized void initExternalName() {
        if (externalName == null) 
            externalName = externalizeClassName();
    }

    /**
     * Return the package portion of this classname.
     * @return the package name
     */
    public String getPackage() {
        if (packageName == null)
            initPackage();
	return packageName;
    }

    private synchronized void initPackage() {
        int i = internalName.lastIndexOf('/');
        packageName = (i != -1) ? 
            internalName.substring(0, i).replace('/', '.') : "";
    }

    /**
     * Returns the classname without any package specification.
     * @return the simple name
     */
    public String getSimpleName() {
        if (simpleName == null)
            initSimpleName();
	return simpleName;
    }
    
    private synchronized void initSimpleName() {
        String pkg = getPackage();
        int i = pkg.length();
        String extName = getExternalName();
        if (i == 0)
            simpleName = extName;  // no package
        else
            simpleName = extName.substring(i + 1);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
	    return true;
	return (obj instanceof ClassName) && 
            type.equals(((ClassName) obj).type);
    }

    /**
     * Compares this ClassName to another Object.  If the Object is a 
     * ClassName, this function compares the ClassName's types.  Otherwise,
     * it throws a <code>ClassCastException</code>.
     *
     * @param   obj the <code>Object</code> to be compared.
     * @return  the value <code>0</code> if the argument is a string
     *		lexicographically equal to this string; a value less than
     *		<code>0</code> if the argument is a string lexicographically 
     *		greater than this string; and a value greater than
     *		<code>0</code> if the argument is a string lexicographically
     *		less than this string.
     * @see     java.lang.Comparable
     */
    public int compareTo(ClassName obj) {
        // If obj isn't a ClassName, the correct ClassCastException
        // will be thrown by the cast.
        return type.compareTo(obj.type);
    }

    /**
     * Compares its two arguments for order.  Returns a negative integer,
     * zero, or a positive integer as the first argument is less than, equal
     * to, or greater than the second.<p>
     *
     * @param o1 the first object to be compared.
     * @param o2 the second object to be compared.
     * @return a negative integer, zero, or a positive integer as the
     * 	       first argument is less than, equal to, or greater than the
     *	       second. 
     * @throws ClassCastException if the arguments' types prevent them from
     * 	       being compared by this Comparator.
     */
    public int compare(ClassName o1, ClassName o2) {
        return o1.compareTo(o2);
    }

    @Override
    public int hashCode() {
        return type.hashCode();
    }

    @Override
    public String toString() {
        return getExternalName();
    }

    // Called from synchronization block, do not call out!
    private String externalizeClassName() {
        StringBuffer sb = new StringBuffer(type);
	int arrays = 0;
	boolean atBeginning = true;
	int length = sb.length();
	for (int i = 0; i < length; i++) {
	    char ch = sb.charAt(i);
	    switch (ch) {
	      case '[': 
		if (atBeginning)
		    arrays++; 
		break;

	      case '/':
	      case '$':
		sb.setCharAt(i, '.');
		atBeginning = false;
		break;

	      default:
		atBeginning = false;
	    }
	}

	if (arrays > 0) {
	    sb.delete(0, arrays);
	    for (int i = 0; i < arrays; i++)
	      sb.append("[]");
	}

        return sb.toString();
    }
    
    /**
     * Empties the cache -- used by unit tests.
     */
    static void clearCache() {
        cache.clear();
    }

    /*
     * Suppress multiple instances of the same type, as well as any
     * immutability attacks (unlikely as that might be).  For more information
     * on this technique, check out Effective Java, Item 57, by Josh Bloch.
     */
    private Object readResolve() throws ObjectStreamException {
        return getClassName(internalName);
    }
}
