/*
 * 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 net.jini.entry;

import net.jini.core.entry.Entry;
import java.lang.reflect.*;

import java.util.WeakHashMap;
import java.util.ArrayList;

/**
 * An abstract implementation of {@link Entry} that provides useful
 * implementations of <code>equals</code>, <code>hashCode</code>, and
 * <code>toString</code>. Implementations of the <code>Entry</code>
 * interface may, but are not required to, extend this class. <p>
 *
 * The methods of this class consult the <em>entry fields</em> of the
 * entries they process. The entry fields of an <code>Entry</code> are
 * its public, non-primitive, non-static, non-transient, non-final
 * fields.<p>
 *
 * 
 * @see Entry 
 */
public abstract class AbstractEntry implements Entry {
    static final long serialVersionUID = 5071868345060424804L;

    /**
     * Creates an instance of this class.
     */
    protected AbstractEntry() { }

    /**
     * Compares this <code>AbstractEntry</code> to the specified
     * object. If <code>other</code> is <code>null</code> or not an
     * instance of {@link Entry} returns <code>false</code>,
     * otherwise returns the result of calling {@link #equals(Entry,Entry)
     * AbstractEntry.equals(this, (Entry) other)}.
     * @param other the object to compare this <code>AbstractEntry</code>
     *              against
     * @return <code>true</code> if this object is equivalent
     *         to <code>other</code> and <code>false</code> otherwise
     */
    public boolean equals(Object other) {
	if (!(other instanceof Entry))
	    return false;
	return equals(this, (Entry) other);
    }

    /**
     * Returns <code>true</code> if the two arguments are of the same
     * class and for each entry field <em>F</em>, the arguments'
     * values for <em>F</em> are either both <code>null</code> or the
     * invocation of <code>equals</code> on one argument's value for
     * <em>F</em> with the other argument's value for <em>F</em> as
     * its parameter returns <code>true</code>. Will also return
     * <code>true</code> if both arguments are <code>null</code>. In
     * all other cases an invocation of this method will return
     * <code>false</code>.<p>
     *
     * @param e1  an entry object to compare to e2
     * @param e2  an entry object to compare to e1
     * @return <code>true</code> if the two arguments are equivalent 
     */
    public static boolean equals(Entry e1, Entry e2) {
	if (e1 == e2)
	    return true;

	// Note, if both e1 and e2 are null the previous test would
	// have returned true.
	if (e1 == null || e2 == null)
	    return false;

	if (e1.getClass() != e2.getClass())
	    return false;

	Field[] fields = fieldInfo(e1);
	try {
	    // compare each field
	    for (int i = 0; i < fields.length; i++) {

		// f works for other since other is the same type as this
		Field f = fields[i];
		Object ov = f.get(e1);
		Object tv = f.get(e2);

		if (tv == ov)			// same obj or both null is OK
		    continue;
		if (tv == null || ov == null)	// if only one is null, not OK
		    return false;
		if (!tv.equals(ov))		// not equals is not OK
		    return false;
	    }
	    return true;
	} catch (IllegalAccessException e) {
	    // should never happen, all entry fields are public
	    throw new AssertionError(e);
	}
    }

    /**
     * Returns the result of calling {@link #hashCode(Entry)
     * AbstractEntry.hashCode(this)}.
     * @return the result of calling {@link #hashCode(Entry)
     * AbstractEntry.hashCode(this)}
     */
    public int hashCode() {
	return hashCode(this);
    }

    /**
     * Returns zero XORed with the result of invoking
     * <code>hashCode</code> on each of the argument's
     * non-<code>null</code> entry fields. Returns <code>0</code> if
     * the argument is <code>null</code>.
     *
     * @param entry the <code>Entry</code> for which to generate a
     *              hash code
     * @return a hash code formed by XORing the hash codes of
     *         <code>entry</code>'s non-<code>null</code> entry field,
     *         or <code>0</code> if <code>entry</code> is
     *         <code>null</code> 
     */
    public static int hashCode(Entry entry) {
	if (entry == null)
	    return 0;
	
	int hash = 0;
	Field[] fields = fieldInfo(entry);
	try {
	    for (int i = 0; i < fields.length; i++) {
		Object tv = fields[i].get(entry);
		if (tv != null)
		    hash ^= tv.hashCode();
	    }
	    return hash;
	} catch (IllegalAccessException e) {
	    // should never happen, all entry fields are public
	    throw new AssertionError(e);
	}
    }

    /**
     * Returns the result of calling {@link #toString(Entry)
     * AbstractEntry.toString(this)}.
     * @return the result of calling {@link #toString(Entry)
     * AbstractEntry.toString(this)}
     */
    public String toString() {
	return toString(this);
    }

    /**
     * Returns a <code>String</code> representation of its argument
     * that will contain the name of the argument's class and a
     * representation of each of the argument's entry fields. The
     * representation of each entry field will include the field's
     * name and a representation of its value. If passed
     * <code>null</code> will return the string <code>"null"</code>. <p>
     *
     * @param entry  an entry to represent as a String
     * @return a <code>String</code> representation of
     *         <code>entry</code> that contains the name of the
     *         <code>entry</code>'s class and a representation of each
     *         of <code>entry</code>'s entry fields
     */
    public static String toString(Entry entry) {
	if (entry == null)
	    return "null";

	Field[] fields = fieldInfo(entry);
	StringBuffer str = new StringBuffer(entry.getClass().getName());
	str.append('(');	// later matched with a ')'
	for (int i = 0; i < fields.length; i++) {
	    try {
		Field f = fields[i];
		if (i > 0)
		    str.append(',');
		str.append(f.getName());
		str.append('=');
		str.append(f.get(entry));
	    } catch (IllegalAccessException e) {
		// should never happen, all entry fields are public
		throw new AssertionError(e);
	    }
	}
	// now add the ending '('
	str.append(')');
	return str.toString();
    }

    private static WeakHashMap fieldArrays;

    /**
     * Calculate the list of usable fields for this type
     */
    private static Field[] fieldInfo(Entry entry) {
	Field[] fields = null;

	synchronized (AbstractEntry.class) {
	    if (fieldArrays == null)
		fieldArrays = new WeakHashMap();
	    else {
		fields = (Field[]) fieldArrays.get(entry.getClass());
		if (fields != null)
		    return fields;
	    }
	}

	/*
	 * Scan the array to see if we can use it or if we must build up
	 * a smaller array because we must skip some fields.  If so, we
	 * create an ArrayList and add the unskippable fields to it, and
	 * then fetch the array back out of it.
	 */
	final int SKIP_MODIFIERS =
	    (Modifier.STATIC | Modifier.TRANSIENT | Modifier.FINAL);
	fields = entry.getClass().getFields();
	ArrayList usable = null;
	for (int i = 0; i < fields.length; i++) {
	    // exclude this one?
	    if ((fields[i].getModifiers() & SKIP_MODIFIERS) != 0 ||
		(fields[i].getType().isPrimitive())) 
	    {
		if (usable == null) {		// first excluded: set up for it
		    usable = new ArrayList();	// allocate the list of usable
		    for (int j = 0; j < i; j++)	// earlier fields are usable
			usable.add(fields[j]);
		}
	    } else {				// not excluded
		if (usable != null)		// tracking usable fields?
		    usable.add(fields[i]);
	    }
	}
	if (usable != null)
	    fields = (Field[]) usable.toArray(new Field[usable.size()]);

	synchronized (AbstractEntry.class) {
	    // We could check to make sure someone else 
	    // has not already stuck a value for entry.getClass() in,
	    // fieldArrays but there should be no harm in an overwrite
	    // and if anything likely to be less efficient
	    fieldArrays.put(entry.getClass(), fields);
	}

	return fields;
    }
}
