package org.biomoby.shared.data;

import java.util.*;

import org.biomoby.registry.meta.Registry;
import org.biomoby.shared.*;
import org.biomoby.shared.parser.MobyTags;

/**
 * This class adds to MobyPrimaryDataSet the ability to get and set instantiated MOBY objects.
 * The MOBY Collection concept that this class embodies is a mathematical bag, i.e. it can contain the
 * same value more than once.  This class implements java.util.Collection to facilitate the manipulation
 * of the bag, enforcing that all members of the bag be non-null, MobyDataObject objects.
 * The collection can be empty, but never null.
 *
 * NOTE: The order of members of the collection is meaningless, and may change throughout the 
 * life of this object. Do not rely on the order of the items at any time!
 *
 * This class uses a Vector to hold the bag elements and implement most Collection functions.
 */
public class MobyDataObjectSet extends MobyPrimaryDataSet implements MobyDataInstance, Collection<MobyDataObject>{

    Vector<MobyDataObject> bag;
    Registry registry;
    private int xmlMode = MobyDataInstance.CENTRAL_XML_MODE;

    public MobyDataObjectSet(org.w3c.dom.Element e) throws MobyException{
	this(e, null);
    }

    public MobyDataObjectSet(org.w3c.dom.Element e, Registry registry) throws MobyException{
	this(MobyDataObject.getName(e), getChildren(e, registry));	

	if(getName() == null){
 	    throw new MobyException("Anonymous collections are not allowed (need articleName), input was :\n" + e);
 	}

    }
    
    public static Collection<? extends MobyDataObject> getChildren(org.w3c.dom.Element e, Registry registry) throws MobyException{
	Vector<MobyDataObject> members = new Vector<MobyDataObject>();

	org.w3c.dom.NodeList children = MobyPrefixResolver.getChildElements(e, "*");  // wildcard
	int numChildren = children.getLength();
	for(int i = 0; i < numChildren; i++){
	    MobyDataInstance mdi = MobyDataObject.createInstanceFromDOM((org.w3c.dom.Element) children.item(i), 
                                                                        registry);
            if(mdi instanceof MobyDataObject){
               members.add((MobyDataObject) mdi);
            }
	}
	return members;
    }

    /**
     * Creates an empty collection bag with a name.
     */
    public MobyDataObjectSet(String name){
	this(name, (Registry) null);
    }

    public MobyDataObjectSet(String name, Registry reg){
	super(name);
	registry = reg;
	bag = new Vector<MobyDataObject>();
    }

    /**
     * Creates a collection with a name, initializing the members with the given array.  
     * The registry is inferred from the input values.  If the values array is blank, the default
     * registry is used. This can be avoided by using the (name, registry) c-tor instead,
     * which created a blank array for you automatically.
     *
     * @param values a set of MobyDataObjects, all from the same registry
     *
     * @throws NullPointerException if the input array is null, or contains null elements
     */
    public MobyDataObjectSet(String name, MobyDataObject[] values) throws NullPointerException{
	this(name, (Registry) (values.length == 0 ? null : values[0].getDataType().getRegistry()));
	setElements(values);
    }

    /**
     * Creates a collection with a name, initializing the members with the members of the given collection.
     *
     * @param c usually another MobyDataObjectSet, or a Vector of MobyDataObjects, all in the same namespace
     *
     * @throws ClassCastException if a member of the input collection is not a MobyDataObject
     * @throws NullPointerException if the collection or one of its members is a null object
     */
    public MobyDataObjectSet(String name, Collection<? extends MobyDataObject> c) throws ClassCastException, NullPointerException{
	this(name, (Registry) (c.isEmpty() ? null : c.iterator().next().getDataType().getRegistry()));
	addAll(c);
    }

    public MobyDataObjectSet clone(){
	return new MobyDataObjectSet(getName(), bag);
    }

    /**************************************************************************
     * Return the least upper bound parent class of all items in the collection.
     *************************************************************************/
    public MobyDataType getDataType() {
	MobyDataType[] lineage = null;
	boolean first = true;
	synchronized (bag) {
	    if (bag.size() > 0){
		for(MobyDataObject member: bag){
		    MobyDataType objType = member.getDataType();
		    // Unknown datatype, break because we can't assume any common lineage for all elements anymore
		    if(objType == null){
			break;
		    }

		    MobyDataType[] objLineage = objType.getLineage();
		    // Unknown lineage, break because we can't assume any common lineage for all elements anymore
		    if(objLineage == null){
			break;
		    }

		    if(first){
			lineage = objLineage;
			first = false;
		    }
		    else{
			for(int i = 0; i < lineage.length; i++){
			    if(i >= objLineage.length || !lineage[i].equals(objLineage[i])){
				// start of different family tree, truncate common lineage here
				lineage = new MobyDataType[i];
				System.arraycopy(objLineage, 0, lineage, 0, i);
				break;  //would happen anyway due to lineage resize, but let's make it explicit
			    }
			}
		    }

		    // Logical shortcut: if only sharing root Object class, the list
		    // is already as short as it can get.
		    if(lineage.length < 2){
			break;
		    }
		}
		if(lineage != null && lineage.length == 0){
		    return MobyDataType.getDataType(MobyTags.MOBYOBJECT, registry);
		}
		return lineage == null ? null : lineage[lineage.length-1];
	    }
	    else{
		// We know that any we could hold would at least be an Object...
		return MobyDataType.getDataType(MobyTags.MOBYOBJECT, registry);
	    }
	}
    }

    /**************************************************************************
     * Return namespaces that occur in all items of the collection.
     * This method relies on MobyNamespace's equals() method to be 
     * properly implemented for proper set intersection functionality.
     *************************************************************************/
    public MobyNamespace[] getNamespaces() {
	Vector<MobyNamespace> namespaces = new Vector<MobyNamespace>();
	boolean first = true;
	synchronized (bag) {
	    for (MobyDataObject member: bag){
		MobyNamespace[]	nsInstances = member.getNamespaces();
		Vector<MobyNamespace> newNames = new Vector<MobyNamespace>();
		for(int i = 0; nsInstances != null && i < nsInstances.length; i++){
		    newNames.add(nsInstances[i]);
		}

		if(first){
		    // Keep all namespaces until further notice
		    namespaces.addAll(newNames);
		    first = false;
		}
		else{
		    // If not first, keep only what is common (intersection of namespaces)
		    namespaces.retainAll(newNames);
		}

		// Logical shortcut, since more intersections can't make a larger set
		if(namespaces.size() == 0){
		    break;
		}
	    }
	}
	return (MobyNamespace[]) namespaces.toArray(new MobyNamespace[namespaces.size()]);
    }

    /**************************************************************************
     * Set given namespaces of all elements of this collection.
     *************************************************************************/
    public void setNamespaces (MobyNamespace[] values) {
	synchronized (bag) {
	    for(MobyDataObject member: bag){
		member.setNamespaces(values);
	    }
	}
    }

    /**************************************************************************
     * Add given namespace of all elements of this collection.
     *************************************************************************/
    public void addNamespace (MobyNamespace value) {
	synchronized (bag) {
	    for (MobyDataObject member: bag){
		member.addNamespace(value);
	    }
	}
    }

    /**************************************************************************
     * Remove given namespace (defined by its name) from all elements
     * of this collection.
     *************************************************************************/
    public void removeNamespace (String namespaceName) {
	synchronized (bag) {
	    for (MobyDataObject member: bag){
		member.removeNamespace(namespaceName);
	    }
	}
    }

    /**************************************************************************
     * Remove given namespace from all elements of this collection.
     *************************************************************************/
    public void removeNamespace (MobyNamespace value) {
	synchronized (bag) {
	    for(MobyDataObject member: bag){
		member.removeNamespace(value);
	    }
	}
    }

    /**
     * Replace whatever is in the collection right now with the values in the input array.
     *
     * @throws NullPointerException if the input array is null, or contains null elements
     */
    public void setElements(MobyDataObject[] values) throws NullPointerException{
	if(values == null){
	    throw new NullPointerException("An attempt was made to set a  MobyDataObjectSet's members " +
					   "from a null array, which is disallowed");
	}

	for(int i = 0; i < values.length; i++){
	    if(values[i] == null){
		throw new NullPointerException("An attempt was made to set a MobyDataObjectSet's members " +
					       "from an array, but the element at index " + i + 
					       " was null, which is disallowed.");
	    }
	}

	// The array is okay if it got this far
	bag.clear();
	for(int i = 0; i < values.length; i++){
	    bag.add(values[i]);
	}
    }

    /**
     * @return a Vector with the MobyDataObjects
     */
    public Object getObject(){
	return bag;
    }
  
    public MobyPrimaryDataSimple[] getElements(){
	return getElementInstances();
    }

    /**
     * @return the MobyDataObjects that comprise the collection
     */
    public MobyDataObject[] getElementInstances(){
	MobyDataObject instances[] = new MobyDataObject[bag.size()];
	return (MobyDataObject[]) bag.toArray(instances);
    }
    
    private void checkInputClass(String action, Object mdsi) throws ClassCastException, NullPointerException{
	if(mdsi == null){
	    throw new NullPointerException("An attempt to " + action + " a null object was made, but null " +
					   "objects are disallowed in MobyDataObjectSet collections.");
	}
	if(!(mdsi instanceof MobyDataObject)){
	    throw new ClassCastException("The MobyDataObjectSet collection can only " +
					 "contain MobyDataObject objects, but an " +
					 "attempt to " + action + " a " + mdsi.getClass().getName() + 
					 " was made.");
	}
    }

    private void checkCollectionClass(String action, Collection c) throws ClassCastException, NullPointerException{
	if(c == null){
	    throw new NullPointerException("An attempt to " + action + " a null collection was made, but null " +
					   "collection operations in MobyDataObjectSet are disallowed.");
	}

	// Make sure all of the elements in the collection are MobyDataObjects
	Iterator iterator = c.iterator();
	while(iterator.hasNext()){
	    Object element = iterator.next();
	    if(!(element instanceof MobyDataObject)){
		throw new ClassCastException("Attempted to " + action + " a " + element.getClass().getName() +
					     "to a MobyDataObjectSet as part of a collection (" +
					     c.getClass().getName() + "), which is disallowed");
	    }
	}
    }

    /**
     * Add a single MobyDataObject to the collection.
     *
     * @throws NullPointerException if the input is a null object
     */
    public boolean add(MobyDataObject mdo) throws NullPointerException{
	checkInputClass("add", mdo);

	return bag.add(mdo);
    }
    
    /**
     * Convenient way to add the input collection of MobyDataObjects 
     * (usually a MobyDataObjectSet, or a Vector of MobyDataObjects) 
     * to this collection.
     *
     * @throws NullPointerException if the collection or one of its members is a null object
     * @return true if the collection changes as a result of the operation
     */
    public boolean addAll(Collection<? extends MobyDataObject> c) throws NullPointerException{
	checkCollectionClass("add", c);
	
	return bag.addAll(c);
    }

    /**
     * Removes all of the MobyDataObject elements from this collection.
     */
    public void clear(){
	bag.clear();
    }

    /**
     * @return true if this collection contains an element with the exact same value (equivalent in MOBY XML representation, including name)
     * @throws ClassCastException if the input is not a MobyDataObject
     * @throws NullPointerException if the input is a null object
     */
    public boolean contains(Object mdsi) throws ClassCastException, NullPointerException{
	checkInputClass("check for the presence of", mdsi);

	return bag.contains(mdsi);
    }

    /**
     * @return true if each element in the input collection returns true when contains(Object o) is called
     * @throws ClassCastException if a member of the input collection is not a MobyDataObject
     * @throws NullPointerException if the collection or one of its members is a null object
     */
    public boolean containsAll(Collection c) throws ClassCastException, NullPointerException{
	checkCollectionClass("check for presence of", c);
	
	return bag.containsAll(c);
    }

    /**
     * Compare two MobyDataObject collections.  Note that this may be a very expensive 
     * operation if the lists are long, the equals() method is complicated, or collection is hard
     * to convert into an array for sorting.
     *
     * @return true if and only if the input object is a MobyDataObjectSet, and the two collections contain exactly equal elements from a MOBY XML perspective
     * @throws ClassCastException if the input is not a Collection of MobyDataObjects
     * @throws NullPointerException if the input is a null object
     */
    public boolean equals(Object set) throws ClassCastException, NullPointerException{
	// Easy case, they are the same object reference in the JVM
	if(this == set){
	    return true;
	}

	if(!(set instanceof Collection)){
	    throw new ClassCastException("An attempt to check the equivalency of a " + set.getClass().getName() +
					 " to a MobyDataObjectSet was made, but that class is not a Collection.");
	}

	checkInputClass("check for equivalency of", set);

	// Normally we cannot simple use the Vector class equals method, because it requires the objects to 
	// be in the same order in both lists.  Element order in MOBY collections does not affect semantic equivalency.
	// Luckily, MobyDataObjects are Comparable, which means that we can sort them. Only if someone calls 
	// this method will we bother sorting the Vector.

	MobyDataObject[] mdsis = getElementInstances();
	Arrays.sort(mdsis);

	// This only works because we know internally to the class that setElements() doesn't 
	// reorder the elements passed in.
	// If we wanted to be really efficient, we might keep a variable around saying whether the list
	// is already sorted or not.  Maybe later.
	setElements(mdsis); //now bag has a sorted members list

	MobyDataObject[] inputMdsis = (MobyDataObject[]) ((Collection) set).toArray();
	Arrays.sort(inputMdsis);
	Vector<MobyDataObject> sortedInput = new Vector<MobyDataObject>();
	for(int i = 0; i < inputMdsis.length; i++){
	    sortedInput.add(inputMdsis[i]);
	}	

	return bag.equals(sortedInput);
    }
	
    /**
     * Implemented solely because of the general contract that c1.equals(c2) implies that c1.hashCode()==c2.hashCode()
     */
    public int hashCode(){
	return bag == null ? super.hashCode() : bag.hashCode();
    }

    /**
     * Logically equivalent to size() == 0.
     */
    public boolean isEmpty(){
	return bag.isEmpty();
    }

    /**
     * @return an iterator over the non-null MobyDataObjects in this collection
     */
    public Iterator<MobyDataObject> iterator(){
	return bag.iterator();
    }

    /**
     * Removes a single instance of an element from this collection, if it satisfies mdsi.equals().
     * 
     * @param mdsi the MobyDataObject whose value equivalent (not object reference) is to be removed
     * @return true if the element was found and removed
     * @throws ClassCastException if the input is not a MobyDataObject
     * @throws NullPointerException if the input is a null object
     */
    public boolean remove(Object mdsi) throws ClassCastException, NullPointerException{
	checkInputClass("remove", mdsi);

	return bag.remove(mdsi);
    }

    /**
     * Set theory subtraction operator implementation.
     *
     * @return true if this collection changed as a result of the call
     * @throws ClassCastException if a member of the input collection is not a MobyDataObject
     * @throws NullPointerException if the collection or one of its members is a null object
     */
    public boolean removeAll(Collection c) throws ClassCastException, NullPointerException{
	checkCollectionClass("remove", c);

	return bag.removeAll(c);
    }

    /**
     * Set theory intersection operator implementation.
     * 
     * @return true if this collection changed as a result of the call
     * @throws ClassCastException if a member of the input collection is not a MobyDataObject
     * @throws NullPointerException if the collection or one of its members is a null object
     */
    public boolean retainAll(Collection c){
	checkCollectionClass("intersect", c);

	return bag.retainAll(c);
    }

    /**
     * @return the number of elements in this collection
     */
    public int size(){
	return bag.size();
    }

    /**
     * @return a MobyDataObject[] with the collection members
     */
    public Object[] toArray(){
	MobyDataObject[] mdsis = new MobyDataObject[bag.size()];
	return bag.toArray(mdsis);
    }
    
    /**
     * Don't use this method if at all possible.  It is only defined because of
     * the Collection interface requirement.  Use toArray() instead.
     *
     * This method will only work if the input array is a MobyDataObject[], or
     * MobyDataInt[], MobyDataFloat[], etc. if you know for sure that all of the 
     * collection elements are of a particular MOBY primitive subtype.  Otherwise
     * an ArrayStoreException will be thrown.
     */
    public <T extends Object>T[] toArray(T[] classArray) throws ArrayStoreException, NullPointerException{
	return bag.toArray(classArray);
    }
    
    /**
     * Determined whether toXML will return a Central template value or a service call instance value.
     *
     * @param mode one of MobyDataInstance.CENTRAL_XML_MODE or MobyDataInstance.SERVICE_XML_MODE
     * @throws IllegalArgumentException if the mode is not one of the specified values
     */
    public void setXmlMode(int mode) throws IllegalArgumentException{
 	if(mode != MobyDataInstance.CENTRAL_XML_MODE && mode != MobyDataInstance.SERVICE_XML_MODE){
	    throw new IllegalArgumentException("Value passed to setXmlMode was neither " +
                                               "MobyDataInstance.CENTRAL_XML_MODE nor " +
                                               "MobyDataInstance.SERVICE_XML_MODE");
	}
        xmlMode = mode;
    }

    /**
     * Report whether toXML will produce Central template or service call instance XML.
     *
     * @return one of MobyDataInstance.CENTRAL_XML_MODE or MobyDataInstance.SERVICE_XML_MODE
     */
    public int getXmlMode(){
	return xmlMode;
    }

    /**
     * Wraps the simple instances' XML in a MOBY Collection tag
     */
    public String toXML(){
	StringBuffer collectionXml = new StringBuffer();
	
        if(xmlMode == MobyDataInstance.SERVICE_XML_MODE){
	    collectionXml.append("<moby:" + MobyTags.COLLECTION + " xmlns:moby=\""+MobyPrefixResolver.MOBY_XML_NAMESPACE+
				 (getName() != null ? "\" moby:articleName=\"" + getName() : "") + 
				 "\">\n");
	    
	    for(MobyDataObject mdsi: bag){
		mdsi.setXmlMode(xmlMode);

		collectionXml.append("<"+MobyTags.SIMPLE+">"+mdsi.toXML()+"</"+MobyTags.SIMPLE+">");
	    }
	    collectionXml.append("\n</moby:" + MobyTags.COLLECTION + ">\n");
        }
	
        // Otherwise it's MOBY Central query mode
        // We need to find out what object classes are present in the array, and 
        // enumerate them (the types, not the instances).
        else{
	    collectionXml.append("<"+MobyTags.COLLECTION+">\n");

	    MobyPrimaryDataSimple commonDataTemplate = new MobyPrimaryDataSimple("");
	    commonDataTemplate.setDataType(getDataType());
	    commonDataTemplate.setNamespaces(getNamespaces());
	    collectionXml.append(commonDataTemplate.toXML());

	    //	    Hashtable printed = new Hashtable();
// 	    for(MobyDataObject mdsi: bag){
// 		// Could the DataType be null?  I hope not!
// 		String objectClass = mdsi.getDataType().getName();

// 		// A new data type for the collection?  Note to self (Paul), this doesn't seem right, should have one type only ...since the 0.86 spec
// 		if(!printed.containsKey(objectClass)){
// 		    mdsi.setXmlMode(xmlMode);

// 		    collectionXml.append(mdsi.toXML());
		    
// 		    printed.put(objectClass, "printed object type in MOBY central input collection");
// 		}
// 	    }
	    

	    collectionXml.append("</"+MobyTags.COLLECTION+">\n");
        }
	
	return collectionXml.toString();
    }
}
