/*
 * @(#)ORBUtility.java	1.32 02/08/13
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package com.sun.corba.se.impl.orbutil;

import java.security.PrivilegedAction;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.Map.Entry;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Enumeration;
import java.util.Properties;
import java.util.IdentityHashMap;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigInteger ;
import java.math.BigDecimal ;

public final class ObjectUtility {
    private boolean useToString ;
    private boolean isIndenting ;
    private int initialLevel ;
    private int increment ;
    private ClassMap classToPrinter = new ClassMap() ;

    private static ObjectUtility standard = new ObjectUtility( false, true, 
	0, 4 ) ;
    private static ObjectUtility compact = new ObjectUtility( true, false, 
	0, 4 ) ;

    private ObjectUtility( boolean useToString, boolean isIndenting, 
	int initialLevel, int increment )
    {
	this.useToString = useToString ;
	this.isIndenting = isIndenting ;
	this.initialLevel = initialLevel ;
	this.increment = increment ;
	classToPrinter.put( Properties.class, propertiesPrinter ) ;
	classToPrinter.put( Collection.class, collectionPrinter ) ;
	classToPrinter.put( Map.class, mapPrinter ) ;
    }

    /** Construct an Utility instance with the desired objectToString
    * behavior.
    */
    public static ObjectUtility make( boolean useToString, boolean isIndenting, 
	int initialLevel, int increment )
    {
	return new ObjectUtility( useToString, isIndenting, initialLevel, 
	    increment ) ;
    }

    /** Construct an Utility instance with the desired objectToString
    * behavior.
    */
    public static ObjectUtility make( boolean useToString, boolean isIndenting )
    {
	return new ObjectUtility( useToString, isIndenting, 0, 4 ) ;
    }

    /** Get the standard Utility object that supports objectToString with
    * indented display and no use of toString() methods.
    */
    public static ObjectUtility make()
    {
	return standard ;
    }
    
    /** A convenience method that gives the default behavior: use indenting
    * to display the object's structure and do not use built-in toString
    * methods.
    */
    public static String defaultObjectToString( java.lang.Object object )
    {
	return standard.objectToString( object ) ;
    }

    public static String compactObjectToString( java.lang.Object object )
    {
	return compact.objectToString( object ) ;
    }

    /** objectToString handles display of arbitrary objects.  It correctly
    * handles objects whose elements form an arbitrary graph.  It uses
    * reflection to display the contents of any kind of object. 
    * An object's toString() method may optionally be used, but the default
    * is to ignore all toString() methods except for those defined for
    * primitive types, primitive type wrappers, and strings.
    */
    public String objectToString(java.lang.Object obj)
    {
	IdentityHashMap printed = new IdentityHashMap() ;
	ObjectWriter result = ObjectWriter.make( isIndenting, initialLevel, 
	    increment ) ;
	objectToStringHelper( printed, result, obj ) ;
	return result.toString() ;
    }

    // Perform a deep structural equality comparison of the two objects.
    // This handles all arrays, maps, and sets specially, otherwise
    // it just calls the object's equals() method.
    public static boolean equals( java.lang.Object obj1, java.lang.Object obj2 )
    {
	// Set of pairs of objects that have been (or are being) considered for
	// equality.  Such pairs are presumed to be equals.  If they are not, 
	// this will be detected eventually and the equals method will return 
	// false.
	Set considered = new HashSet() ;    

	// Map that gives the corresponding component of obj2 for a component 
	// of obj1.  This is used to check for the same aliasing and use of 
	// equal objects in both objects.
	Map counterpart = new IdentityHashMap() ;

	return equalsHelper( counterpart, considered, obj1, obj2 ) ;
    }

    /** If arr1 and arr2 are both arrays of the same component type,
     * return an array of that component type that consists of the
     * elements of arr1 followed by the elements of arr2.
     * Throws IllegalArgumentException otherwise.
     */
    public static Object concatenateArrays( Object arr1, Object arr2 )
    {
	Class comp1 = arr1.getClass().getComponentType() ;
	Class comp2 = arr2.getClass().getComponentType() ;
	int len1 = Array.getLength( arr1 ) ;
	int len2 = Array.getLength( arr2 ) ;

	if ((comp1 == null) || (comp2 == null))	
	    throw new IllegalStateException( "Arguments must be arrays" ) ;
	if (!comp1.equals( comp2 ))
	    throw new IllegalStateException( 
		"Arguments must be arrays with the same component type" ) ;

	Object result = Array.newInstance( comp1, len1 + len2 ) ;

	int index = 0 ;

	for (int ctr=0; ctr<len1; ctr++)
	    Array.set( result, index++, Array.get( arr1, ctr ) ) ;

	for (int ctr=0; ctr<len2; ctr++)
	    Array.set( result, index++, Array.get( arr2, ctr ) ) ;

	return result ;
    }

//===========================================================================
//  Implementation
//===========================================================================

    private void objectToStringHelper( IdentityHashMap printed, 
	ObjectWriter result, java.lang.Object obj)
    {
	if (obj==null) {
	    result.append( "null" ) ;
	    result.endElement() ;
	} else {
	    Class cls = obj.getClass() ;
	    result.startObject( obj ) ;

	    if (printed.keySet().contains( obj )) {
		result.endObject( "*VISITED*" ) ;
	    } else {
		printed.put( obj, null ) ;

		if (mustUseToString(cls)) {
		    result.endObject( obj.toString() ) ;
		} else {
		    // First, handle any classes that have special printer 
		    // methods defined.  This is useful when the class 
		    // overrides toString with something that
		    // is not sufficiently detailed.
		    ObjectPrinter printer = (ObjectPrinter)(classToPrinter.get(
			cls )) ;
		    if (printer != null) {
			printer.print( printed, result, obj ) ;
			result.endObject() ;
		    } else {
			Class compClass = cls.getComponentType() ;

			if (compClass == null)
			    // handleObject always calls endObject
			    handleObject( printed, result, obj ) ;
			else {
			    handleArray( printed, result, obj ) ;
			    result.endObject() ;
			}
		    }
		}
	    }
	}
    }

    private static interface ObjectPrinter {
	void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj ) ;
    }

    private ObjectPrinter propertiesPrinter = new ObjectPrinter() {
	public void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj )
	{
	    if (!(obj instanceof Properties)) 
		throw new Error() ;

	    Properties props = (Properties)obj ;
	    Enumeration keys = props.propertyNames() ;
	    while (keys.hasMoreElements()) {
		String key = (String)(keys.nextElement()) ;
		String value = props.getProperty( key ) ;
		buff.startElement() ;
		buff.append( key ) ;
		buff.append( "=" ) ;
		buff.append( value ) ;
		buff.endElement() ;
	    }
	}
    } ;

    private ObjectPrinter collectionPrinter = new ObjectPrinter() {
	public void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj )
	{
	    if (!(obj instanceof Collection)) 
		throw new Error() ;
	    
	    Collection coll = (Collection)obj ;
	    Iterator iter = coll.iterator() ;
	    while (iter.hasNext()) {
		java.lang.Object element = iter.next() ;
		buff.startElement() ;
		objectToStringHelper( printed, buff, element ) ;
		buff.endElement() ;
	    }
	}
    } ;

    private ObjectPrinter mapPrinter = new ObjectPrinter() {
	public void print( IdentityHashMap printed, ObjectWriter buff, 
	    java.lang.Object obj )
	{
	    if (!(obj instanceof Map)) 
		throw new Error() ;

	    Map map = (Map)obj ;
	    Iterator iter = map.entrySet().iterator() ;
	    while (iter.hasNext()) {
		Entry entry = (Entry)(iter.next()) ;
		buff.startElement() ;
		objectToStringHelper( printed, buff, entry.getKey() ) ;
		buff.append( "=>" ) ;
		objectToStringHelper( printed, buff, entry.getValue() ) ;
		buff.endElement() ;
	    }
	}
    } ;

    private static class ClassMap {
	ArrayList data ;

	public ClassMap()
	{
	    data = new ArrayList() ;
	}

	/** Return the first element of the ClassMap that is assignable to cls.
	* The order is determined by the order in which the put method was 
	* called.  Returns null if there is no match.
	*/
	public java.lang.Object get( Class cls )
	{
	    Iterator iter = data.iterator() ;
	    while (iter.hasNext()) {
		java.lang.Object[] arr = (java.lang.Object[])(iter.next()) ;
		Class key = (Class)(arr[0]) ;
		if (key.isAssignableFrom( cls ))
		    return arr[1] ;
	    }

	    return null ;
	}

	/** Add obj to the map with key cls.  Note that order matters,
	 * as the first match is returned.
	 */
	public void put( Class cls, java.lang.Object obj )
	{
	    java.lang.Object[] pair = { cls, obj } ;
	    data.add( pair ) ;
	}
    }

    private boolean mustUseToString( Class cls )
    {
	// These probably never occur
	if (cls.isPrimitive())
	    return true ;

	// We must use toString for all primitive wrappers, since 
	// otherwise the code recurses endlessly (access value field
	// inside Integer, returns another Integer through reflection).
	if ((cls == Integer.class) ||
	    (cls == BigInteger.class) ||
	    (cls == BigDecimal.class) ||
	    (cls == String.class) ||
	    (cls == StringBuffer.class) ||
	    (cls == Long.class) ||
	    (cls == Short.class) ||
	    (cls == Byte.class) ||
	    (cls == Character.class) ||
	    (cls == Float.class) ||
	    (cls == Double.class) ||
	    (cls == Boolean.class))
	    return true ;

	if (useToString) {
	    try {
		cls.getDeclaredMethod( "toString", null ) ;
		return true ;
	    } catch (Exception exc) {
		return false ;
	    }
	}

	return false ;
    }

    private void handleObject( IdentityHashMap printed, ObjectWriter result,
	java.lang.Object obj ) 
    {
	Class cls = obj.getClass() ;

	try {
	    Field[] fields = cls.getDeclaredFields() ;

	    for (int ctr=0; ctr<fields.length; ctr++ ) {
		final Field fld = fields[ctr] ;
		int modifiers = fld.getModifiers() ;

		// Do not display field if it is static, since these fields
		// are always the same for every instances.  This could
		// be made configurable, but I don't think it is 
		// useful to do so.
		if (!Modifier.isStatic( modifiers )) {
		    result.startElement() ;
		    result.append( fld.getName() ) ;
		    result.append( ":" ) ;

		    try {
			// Make sure that we can read the field if it is 
			// not public
			AccessController.doPrivileged( new PrivilegedAction() {
			    public Object run() {
				fld.setAccessible( true ) ;
				return null ;
			    } 
			} ) ;

			java.lang.Object value = fld.get( obj ) ;
			objectToStringHelper( printed, result, value ) ;
		    } catch (Exception exc2) {
			result.append( "???" ) ;
		    }

		    result.endElement() ;
		}
	    }

	    result.endObject() ;
	} catch (Exception exc2) {
	    result.endObject( obj.toString() ) ;
	}
    }

    private void handleArray( IdentityHashMap printed, ObjectWriter result,
	java.lang.Object obj ) 
    {
	Class compClass = obj.getClass().getComponentType() ;
	if (compClass == boolean.class) {
	    boolean[] arr = (boolean[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == byte.class) {
	    byte[] arr = (byte[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == short.class) {
	    short[] arr = (short[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == int.class) {
	    int[] arr = (int[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == long.class) {
	    long[] arr = (long[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == char.class) {
	    char[] arr = (char[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == float.class) {
	    float[] arr = (float[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else if (compClass == double.class) {
	    double[] arr = (double[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		result.append( arr[ctr] ) ;
		result.endElement() ;
	    }
	} else { // array of object
	    java.lang.Object[] arr = (java.lang.Object[])obj ;
	    for (int ctr=0; ctr<arr.length; ctr++) {
		result.startElement() ;
		objectToStringHelper( printed, result, arr[ctr] ) ;
		result.endElement() ;
	    }
	}
    }

    private static class Pair 
    {
	private java.lang.Object obj1 ;
	private java.lang.Object obj2 ;

	Pair( java.lang.Object obj1, java.lang.Object obj2 )
	{
	    this.obj1 = obj1 ;
	    this.obj2 = obj2 ;
	}

	public boolean equals( java.lang.Object obj )
	{
	    if (!(obj instanceof Pair))
		return false ;

	    Pair other = (Pair)obj ;
	    return other.obj1 == obj1 && other.obj2 == obj2 ;
	}

	public int hashCode()
	{
	    return System.identityHashCode( obj1 ) ^ 
		System.identityHashCode( obj2 ) ;
	}
    }

    private static boolean equalsHelper( Map counterpart, Set considered, 
	java.lang.Object obj1, java.lang.Object obj2 )
    {
	if ((obj1 == null) || (obj2 == null))
	    return obj1 == obj2 ;

	java.lang.Object other2 = counterpart.get( obj1 ) ;
	if (other2 == null) {
	    other2 = obj2 ;
	    counterpart.put( obj1, other2 ) ;
	}

	if (obj1 == other2)
	    return true ;

	if (obj2 != other2)
	    return false ;
	
	Pair pair = new Pair( obj1, obj2 ) ;
	if (considered.contains( pair ))
	    return true ;
	else
	    considered.add( pair ) ;

	if (obj1 instanceof java.lang.Object[] && 
	    obj2 instanceof java.lang.Object[])
	    return equalArrays( counterpart, considered, 
		(java.lang.Object[])obj1, (java.lang.Object[])obj2 ) ;
	else if (obj1 instanceof Map && obj2 instanceof Map)
	    return equalMaps( counterpart, considered, 
		(Map)obj1, (Map)obj2 ) ;
	else if (obj1 instanceof Set && obj2 instanceof Set)
	    return equalSets( counterpart, considered, 
		(Set)obj1, (Set)obj2 ) ;
	else if (obj1 instanceof List && obj2 instanceof List)
	    return equalLists( counterpart, considered, 
		(List)obj1, (List)obj2 ) ;
	else if (obj1 instanceof boolean[] && obj2 instanceof boolean[])
	    return Arrays.equals( (boolean[])obj1, (boolean[])obj2 ) ;
	else if (obj1 instanceof byte[] && obj2 instanceof byte[])
	    return Arrays.equals( (byte[])obj1, (byte[])obj2 ) ;
	else if (obj1 instanceof char[] && obj2 instanceof char[])
	    return Arrays.equals( (char[])obj1, (char[])obj2 ) ;
	else if (obj1 instanceof double[] && obj2 instanceof double[])
	    return Arrays.equals( (double[])obj1, (double[])obj2 ) ;
	else if (obj1 instanceof float[] && obj2 instanceof float[])
	    return Arrays.equals( (float[])obj1, (float[])obj2 ) ;
	else if (obj1 instanceof int[] && obj2 instanceof int[])
	    return Arrays.equals( (int[])obj1, (int[])obj2 ) ;
	else if (obj1 instanceof long[] && obj2 instanceof long[])
	    return Arrays.equals( (long[])obj1, (long[])obj2 ) ;
	else {
	    Class cls = obj1.getClass() ;
	    if (cls != obj2.getClass())
		return obj1.equals( obj2 ) ;
	    else
		return equalsObject( counterpart, considered, cls, obj1, obj2 ) ;
	}
    }

    private static boolean equalsObject( Map counterpart, Set considered, 
	Class cls, java.lang.Object obj1, java.lang.Object obj2 ) 
    {
	Class objectClass = java.lang.Object.class ;
	if (cls == objectClass)
	    return true ;

	Class[] equalsTypes = { objectClass } ;
	try {
	    Method equalsMethod = cls.getDeclaredMethod( "equals", 
		equalsTypes ) ;
	    return obj1.equals( obj2 ) ;
	} catch (Exception exc) {
	    if (equalsObjectFields( counterpart, considered, 
		    cls, obj1, obj2 ))
		return equalsObject( counterpart, considered, 
		    cls.getSuperclass(), obj1, obj2 ) ;
	    else
		return false ;
	}
    }

    private static boolean equalsObjectFields( Map counterpart, Set considered,
	Class cls, java.lang.Object obj1, java.lang.Object obj2 ) 
    {
	Field[] fields = cls.getDeclaredFields() ;
	for (int ctr=0; ctr<fields.length; ctr++) {
	    try {
		final Field field = fields[ctr] ;
		// Ignore static fields
		if (!Modifier.isStatic( field.getModifiers())) {
		    AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
			    field.setAccessible( true ) ;
			    return null ;
			} 
		    } ) ;

		    java.lang.Object value1 = field.get( obj1 ) ;
		    java.lang.Object value2 = field.get( obj2 ) ;
		    if (!equalsHelper( counterpart, considered, value1, 
			value2 ))
			return false ;
		}
	    } catch (IllegalAccessException exc) {
		return false ;
	    }
	}

	return true ;
    }

    private static boolean equalArrays( Map counterpart, Set considered,
	java.lang.Object[] arr1, java.lang.Object[] arr2 ) 
    {
	int len = arr1.length ;
	if (len != arr2.length)
	    return false ;

	for (int ctr = 0; ctr<len; ctr++ )
	    if (!equalsHelper( counterpart, considered, arr1[ctr], arr2[ctr] ))
		return false ;

	return true ;
    }

    private static boolean equalMaps( Map counterpart, Set considered,
	Map map1, Map map2 )
    {
	if (map2.size() != map1.size())
	    return false;

        try {
            Iterator i = map1.entrySet().iterator();
            while (i.hasNext()) {
                Entry e = (Entry) i.next();
                java.lang.Object key = e.getKey();
                java.lang.Object value = e.getValue();
                if (value == null) {
                    if (!(map2.get(key)==null && map2.containsKey(key)))
                        return false;
                } else {
                    if (!equalsHelper( counterpart, considered, 
			value, map2.get(key)))
                        return false;
                }
            }
        } catch(ClassCastException unused)   {
            return false;
        } catch(NullPointerException unused) {
            return false;
        }

	return true;
    }

    // Obviously this is an inefficient quadratic algorithm.
    // This is taken pretty directly from AbstractSet and AbstractCollection
    // in the JDK.
    // For HashSet, an O(n) (with a good hash function) algorithm
    // is possible, and likewise TreeSet, since it is 
    // ordered, is O(n).  But this is not worth the effort here.
    // Note that the inner loop uses equals, not equalsHelper.
    // This is needed because of the searching behavior of this test.
    // However, note that this will NOT correctly handle sets that
    // contain themselves as members, or that have members that reference
    // themselves.  These cases will cause infinite regress!
    private static boolean equalSets( Map counterpart, Set considered,
	Set set1, Set set2 )
    {
	if (set1.size() != set2.size())
	    return false ;

	Iterator e1 = set1.iterator() ;
	while (e1.hasNext()) {
	    java.lang.Object obj1 = e1.next() ;

	    boolean found = false ;
	    Iterator e2 = set2.iterator() ;
	    while (e2.hasNext() && !found) {
		java.lang.Object obj2 = e2.next() ;
		found = equals( obj1, obj2 ) ;
	    }

	    if (!found)
		return false ;
	}

	return true ;
    }

    private static boolean equalLists( Map counterpart, Set considered,
	List list1, List list2 )
    {
	ListIterator e1 = list1.listIterator();
	ListIterator e2 = list2.listIterator();
	while(e1.hasNext() && e2.hasNext()) {
	    java.lang.Object o1 = e1.next();
	    java.lang.Object o2 = e2.next();
	    if (!(o1==null ? o2==null : equalsHelper( 
		counterpart, considered, o1, o2)))
		return false;
	}
	return !(e1.hasNext() || e2.hasNext());
    }
}
