/*
 * 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 com.sun.jini.lookup.entry;

import net.jini.core.entry.Entry;
import net.jini.lookup.entry.ServiceControlled;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.rmi.MarshalledObject;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;

/**
 * Some simple utilities for manipulating lookup service attributes.
 * These are not high-performance operations; it is expected that
 * they are called relatively infrequently.
 *
 * 
 *
 */
public class LookupAttributes {

    private LookupAttributes() {}

    /** Comparator for sorting fields */
    private static final FieldComparator comparator = new FieldComparator();
    private static final Class[] noArg = new Class[0];

    /**
     * Returns a new array containing the elements of the 
     * <code>addAttrSets</code> parameter (that are not duplicates of
     * any of the elements already in the <code>attrSets</code> parameter)
     * added to the elements of <code>attrSets</code>. The parameter
     * arrays are not modified.
     * <p>
     * Note that attribute equality is defined in terms of 
     * <code>MarshalledObject.equals</code> on field values.  The 
     * parameter arrays are not modified.
     * <p>
     * Throws an <code>IllegalArgumentException</code> if any element of
     * <code>addAttrSets</code> is not an instance of a valid
     * <code>Entry</code> class (the class is not public, or does not have a
     * no-arg constructor, or has primitive public non-static non-final
     * fields).
     */
    public static Entry[] add(Entry[] attrSets, Entry[] addAttrSets) {
	return add(attrSets, addAttrSets, false);
    }

    /**
     * Returns a new array containing the elements of the 
     * <code>addAttrSets</code> parameter (that are not duplicates of
     * any of the elements already in the <code>attrSets</code> parameter)
     * added to the elements of <code>attrSets</code>. The parameter
     * arrays are not modified.
     * <p>
     * Note that attribute equality is defined in terms of 
     * <code>MarshalledObject.equals</code> on field values.  The 
     * parameter arrays are not modified.
     * <p>
     * If the <code>checkSC</code> parameter is <code>true</code>,
     * then a <code>SecurityException</code> is thrown if any elements
     * of the <code>addAttrSets</code> parameter are instanceof 
     * <code>ServiceControlled</code>.
     * <p>
     * Throws an <code>IllegalArgumentException</code> if any element of
     * <code>addAttrSets</code> is not an instance of a valid
     * <code>Entry</code> class (the class is not public, or does not have a
     * no-arg constructor, or has primitive public non-static non-final
     * fields).
     */
    public static Entry[] add(Entry[] attrSets,
			      Entry[] addAttrSets,
			      boolean checkSC)
    {
	check(addAttrSets, false);
	Entry[] newSets = concat(attrSets, addAttrSets);
	for (int i = newSets.length; --i >= attrSets.length; ) {
	    if (checkSC)
		check(newSets[i]);
	    if (isDup(newSets, i))
		newSets = delete(newSets, i);
	}
	return newSets;
    }

    /**
     * Returns a new array that contains copies of the attributes in the
     * <code>attrSets</code> parameter, modified according to the contents
     * of both the <code>attrSetTmpls</code> parameter and the 
     * <code>modAttrSets</code> parameter. The parameter arrays and
     * their <code>Entry</code> instances are not modified.
     * <p>
     * Throws an <code>IllegalArgumentException</code> if any element of
     * <code>attrSetTmpls</code> or <code>modAttrSets</code> is not an
     * instance of a valid <code>Entry</code> class (the class is not public,
     * or does not have a no-arg constructor, or has primitive public
     * non-static non-final fields).
     */
    public static Entry[] modify(Entry[] attrSets,
				 Entry[] attrSetTmpls,
				 Entry[] modAttrSets)
    {
	return modify(attrSets, attrSetTmpls, modAttrSets, false);
    }

    /**
     * Returns a new array that contains copies of the attributes in the
     * <code>attrSets</code> parameter, modified according to the contents
     * of both the <code>attrSetTmpls</code> parameter and the 
     * <code>modAttrSets</code> parameter. The parameter arrays and
     * their <code>Entry</code> instances are not modified.
     * <p>
     * If the <code>checkSC</code> parameter is <code>true</code>, then a
     * <code>SecurityException</code> is thrown if any elements of the
     * <code>attrSets</code> parameter that would be deleted or modified
     * are instanceof <code>ServiceControlled</code>.
     * <p>
     * Throws an <code>IllegalArgumentException</code> if any element of
     * <code>attrSetTmpls</code> or <code>modAttrSets</code> is not an
     * instance of a valid <code>Entry</code> class (the class is not public,
     * or does not have a no-arg constructor, or has primitive public
     * non-static non-final fields).
     */
    public static Entry[] modify(Entry[] attrSets,
				 Entry[] attrSetTmpls,
				 Entry[] modAttrSets,
				 boolean checkSC)
    {
	if (attrSetTmpls.length != modAttrSets.length)
	    throw new IllegalArgumentException(
				       "attribute set length mismatch");
	for (int i = modAttrSets.length; --i >= 0; ) {
	    if (modAttrSets[i] != null &&
		!isAssignableFrom(modAttrSets[i].getClass(),
				  attrSetTmpls[i].getClass()))
		throw new IllegalArgumentException(
					   "attribute set type mismatch");
	}
	check(attrSetTmpls, false);
	check(modAttrSets, true);
	attrSets = (Entry[])attrSets.clone();
	for (int i = attrSets.length; --i >= 0; ) {
	    Entry pre = attrSets[i];
	    for (int j = attrSetTmpls.length; --j >= 0; ) {
		if (matches(attrSetTmpls[j], pre)) {
		    if (checkSC)
			check(pre);
		    Entry mods = modAttrSets[j];
		    if (mods == null) {
			attrSets = delete(attrSets, i);
			break;
		    } else {
			attrSets[i] = update(attrSets[i], mods);
		    }
		}
	    }
	}
	for (int i = attrSets.length; --i >= 0; ) {
	    if (isDup(attrSets, i))
		attrSets = delete(attrSets, i);
	}
	return attrSets;
    }

    /**
     * Test that two entries are the same type, with the same
     * public fields. Attribute equality is defined in terms of
     * <code>MarshalledObject.equals</code> on field values.
     */
    public static boolean equal(Entry e1, Entry e2) {
	if (!equal(e1.getClass(), e2.getClass()))
	    return false;
	Field[] fields1 = getFields(e1);
	Field[] fields2 = getFields(e2, e1, fields1);
	try {
	    for (int i = fields1.length; --i >= 0; ) {
		if (!equal(fields1[i].get(e1), fields2[i].get(e2)))
		    return false;
	    }
	} catch (IllegalAccessException ex) {
	    throw new IllegalArgumentException(
				       "unexpected IllegalAccessException");
	}
	return true;
    }

    /** Tests that two <code>Entry[]</code> arrays are the same. */
    public static boolean equal(Entry[] attrSet1, Entry[] attrSet2) {
	return contains(attrSet1, attrSet2) && contains(attrSet2, attrSet1);
    }

    /**
     * Test if the parameter <code>tmpl</code> is the same class as, or a
     * superclass of, the parameter <code>e</code>, and that every 
     * non-<code>null</code> public field of <code>tmpl</code> is the
     * same as the corresponding field of <code>e</code>. Attribute equality
     * is defined in terms of <code>MarshalledObject.equals</code> on
     * field values.
     */
    public static boolean matches(Entry tmpl, Entry e) {
	if (!isAssignableFrom(tmpl.getClass(), e.getClass()))
	    return false;
	Field[] tfields = getFields(tmpl);
	Field[] efields = getFields(e, tmpl, tfields);
	try {
	    for (int i = tfields.length; --i >= 0; ) {
		Object val = tfields[i].get(tmpl);
		if (val != null && !equal(val, efields[i].get(e)))
		    return false;
	    }
	} catch (IllegalAccessException ex) {
	    throw new IllegalArgumentException
                                      ("unexpected IllegalAccessException");
	}
	return true;
    }

    /**
     * Throws an <code>IllegalArgumentException</code> if any element of
     * the array is not an instance of a valid <code>Entry</code> class
     * (the class is not public, or does not have a no-arg constructor, or
     * has primitive public non-static non-final fields).  If
     * <code>nullOK</code> is <code>false</code>, and any element of the
     * array is <code>null</code>, a <code>NullPointerException</code>
     * is thrown.
     */
    public static void check(Entry[] attrs, boolean nullOK) {
	for (int i = attrs.length; --i >= 0; ) {
	    Entry e = attrs[i];
	    if (e == null ){
		if (nullOK) continue;
                throw new NullPointerException("null Entry not ok");
            }
	    Class c = e.getClass();
	    if (!Modifier.isPublic(c.getModifiers()))
		throw new IllegalArgumentException("entry class " +
						   c.getName() +
						   " is not public");
	    try {
		c.getConstructor(noArg);
	    } catch (NoSuchMethodException ex) {
		throw new IllegalArgumentException("entry class " +
						   c.getName() +
			        " does not have a public no-arg constructor");
	    }
	    Field[] fields = c.getFields();
	    for (int j = fields.length; --j >= 0; ) {
		if ((fields[j].getModifiers() &
		     (Modifier.STATIC|Modifier.FINAL|Modifier.TRANSIENT)) == 0
		    &&
		    fields[j].getType().isPrimitive())
		    throw new IllegalArgumentException("entry class " +
						       c.getName() +
						   " has a primitive field");
	    }
	}
    }

    /** 
     * Throws a <code>SecurityException</code> if parameter <code>e</code>
     * is instanceof <code>ServiceControlled</code>.
     */
    private static void check(Entry e) {
	if (e instanceof ServiceControlled)
	    throw new SecurityException
               ("attempt to add or modify a ServiceControlled attribute set");
    }

    /**
     * Test if the set at the given <code>index</code> is equal to any
     * other set earlier in the <code>Entry[]</code> array parameter.
     */
    private static boolean isDup(Entry[] attrs, int index) {
	Entry set = attrs[index];
	for (int i = index; --i >= 0; ) {
	    if (equal(set, attrs[i]))
		return true;
	}
	return false;
    }

    /**
     * Return a new entry that, for each non-<code>null</code> field of
     * the parameter <code>mods</code>, has the same field value as
     * <code>mods</code>, else the same field value as the parameter 
     * <code>e</code>.
     */
    private static Entry update(Entry e, Entry mods) {
	try {
	    Entry ec = (Entry)e.getClass().newInstance();
	    Field[] mfields = getFields(mods);
	    Field[] efields = getFields(e, mods, mfields);
	    for (int i = efields.length; --i >= 0; ) {
		efields[i].set(ec, efields[i].get(e));
	    }
	    for (int i = mfields.length; --i >= 0; ) {
		Object val = mfields[i].get(mods);
		if (val != null)
		    efields[i].set(ec, val);
	    }
	    return ec;
	} catch (InstantiationException ex) {
	    throw new IllegalArgumentException(
				       "unexpected InstantiationException");
	} catch (IllegalAccessException ex) {
	    throw new IllegalArgumentException(
				       "unexpected IllegalAccessException");
	}
    }

    /** 
     * Returns <code>true</code> if the two input objects are the same in
     * <code>MarshalledObject</code> form, <code>false</code> otherwise.
     */
    private static boolean equal(Object o1, Object o2) {
	if (o1 == o2)
	    return true;
	if (o1 == null || o2 == null)
	    return false;
	Class c = o1.getClass();
	if (c == String.class ||
	    c == Integer.class ||
	    c == Boolean.class ||
	    c == Character.class ||
	    c == Long.class ||
	    c == Float.class ||
	    c == Double.class ||
	    c == Byte.class ||
	    c == Short.class)
	    return o1.equals(o2);
	try {
	    return new MarshalledObject(o1).equals(new MarshalledObject(o2));
	} catch (IOException ex) {
	    throw new IllegalArgumentException("unexpected IOException");
	}
    }

    /**
     * Tests if two classes are equal, using the class equivalence
     * semantics of the lookup service: same name.
     */
    private static boolean equal(Class c1, Class c2) {
	return c1.equals(c2) || c1.getName().equals(c2.getName());
    }

    /**
     * Tests if class <code>c1</code> is equal to, or a superclass of,
     * class <code>c2</code>, using the class equivalence semantics of
     * the lookup service: same name.
     */
    private static boolean isAssignableFrom(Class c1, Class c2) {
	if (c1.isAssignableFrom(c2))
	    return true;
	String n1 = c1.getName();
	for (Class sup = c2; sup != null; sup = sup.getSuperclass()) {
	    if (n1.equals(sup.getName()))
		return true;
	}
	return false;
    }

    /**
     * Returns public fields, in super to subclass order, sorted
     * alphabetically within a given class.
     */
    private static Field[] getFields(Entry e) {
	Field[] fields = e.getClass().getFields();
	Arrays.sort(fields, comparator);
	int len = 0;
	for (int i = 0; i < fields.length; i++) {
	    if ((fields[i].getModifiers() &
		 (Modifier.STATIC|Modifier.FINAL|Modifier.TRANSIENT)) == 0)
		fields[len++] = fields[i];
	}
	if (len < fields.length) {
	    Field[] nfields = new Field[len];
	    System.arraycopy(fields, 0, nfields, 0, len);
	    fields = nfields;
	}
	return fields;
    }

    /** Comparator for sorting fields. */
    private static class FieldComparator implements Comparator, Serializable {
        private static final long serialVersionUID = 1L;
	public FieldComparator() {}

	/**
	 * Sorts superclass fields before subclass fields, and sorts
	 * fields alphabetically within a given class.
	 */
	public int compare(Object o1, Object o2) {
	    Field f1 = (Field)o1;
	    Field f2 = (Field)o2;
	    if (f1 == f2)
		return 0;
	    if (f1.getDeclaringClass() == f2.getDeclaringClass())
		return f1.getName().compareTo(f2.getName());
	    if (f1.getDeclaringClass().isAssignableFrom(
						     f2.getDeclaringClass()))
		return -1;
	    return 1;
	}
    }

    /**
     * Returns the public fields of the parameter <code>e</code>.  If 
     * <code>e</code> and parameter <code>oe</code> have the same class,
     * then returns parameter <code>ofields</code>, otherwise ensures that
     * <code>e</code> has at least as many fields as does parameter
     * <code>ofields</code>.
     */
    private static Field[] getFields(Entry e, Entry oe, Field[] ofields) {
	if (e.getClass().equals(oe.getClass()))
	    return ofields;
	Field[] fields = getFields(e);
	if (fields.length < ofields.length)
	    throw new IllegalArgumentException("type mismatch");
	return fields;
    }

    /** Return a concatenation of the two arrays. */
    private static Entry[] concat(Entry[] attrs1, Entry[] attrs2) {
	Entry[] nattrs = new Entry[attrs1.length + attrs2.length];
	System.arraycopy(attrs1, 0, nattrs, 0, attrs1.length);
	System.arraycopy(attrs2, 0, nattrs, attrs1.length, attrs2.length);
	return nattrs;
    }

    /** Return a new array containing all but the given element. */
    private static Entry[] delete(Entry[] attrs, int i) {
	int len = attrs.length - 1;
	Entry[] nattrs = new Entry[len];
	System.arraycopy(attrs, 0, nattrs, 0, i);
	System.arraycopy(attrs, i + 1, nattrs, i, len - i);
	return nattrs;
    }

    /** 
     * Returns <code>true</code> if the <code>Entry</code> parameter
     * <code>e</code> is an element of the <code>Entry[]</code> array
     * parameter <code>eSet</code>; returns <code>false</code> otherwise.
     */
    private static boolean contains(Entry[] eSet, Entry e) {
	for(int i=0; i<eSet.length; i++) {
	    if(equal(eSet[i], e))
	       return true;
	}
	return false;
    }

    /** 
     * Returns <code>true</code> if the <code>Entry[]</code> array parameter 
     * <code>eSet1</code> contains the <code>Entry[]</code> array parameter 
     * <code>eSet2</code>; returns <code>false</code> otherwise. That is, 
     * this method determines if <code>eSet2</code> is a subset of
     * <code>eSet1</code>.
     */
    private static boolean contains(Entry[] eSet1, Entry[] eSet2) {
	int len1=0, len2=0;
	if(eSet1 != null)
	    len1 = eSet1.length;
	if(eSet2 != null)
	    len2 = eSet2.length;

	if(len1 < len2)
	    return false;
	for(int i=0; i<len2; i++) {
	    if(!contains(eSet1, eSet2[i]))
	       return false;
	}
	return true;
    }
}
