
package org.biomoby.shared.data;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.*;

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

/**
 * This is the class that represents any non-primitive object instance 
 * (i.e. anything except object, boolean, integer, float, date-time, or string)
 * from the MOBY data type ontology.  The members of the composite
 * object are stored in a Java Map, and can be either primitives, or 
 * other composites.
 *
 * This class implements the ConcurrentMap interface to allow for the easy
 * editing of a composite object's members at runtime.  
 */

public class MobyDataComposite extends MobyDataObject implements ConcurrentMap<String, MobyDataObject>{

    private ConcurrentHashMap<String, MobyDataObject> members;

    /**
     * Construct the object using a DOM fragment.
     *
     * @throws MobyException if the element is not a MobyObject tag
     */
    public MobyDataComposite(org.w3c.dom.Element element) throws MobyException{
	this(element, null);
    }

    public MobyDataComposite(org.w3c.dom.Element element, Registry registry) throws MobyException{
	this(element.getLocalName(), 
	     getName(element), 
	     MobyPrefixResolver.getAttr(element, "namespace"), 
	     getId(element));
	if(getDataType() == null){
	    throw new MobyException("Attempted to build a composite MOBY object from XML, " +
				    "but the tag (" + element.getLocalName() + ") does not " +
				    "correspond to an existing data type in the registry (" +
				    (registry == null ? "default" : registry.getLongName()) + 
				    ").  Please check the spelling and capitalization of the " +
				    "XML tag, to match a registered data type.");
	}

	populateMembersFromDOM(element, registry);
    }

    protected void populateMembersFromDOM(org.w3c.dom.Element element, Registry registry) throws MobyException{
	// Decompose the children
	org.w3c.dom.NodeList substructures = MobyPrefixResolver.getChildElements(element, "*"); //wildcard
	int numSubstructures = substructures.getLength();
	for(int i = 0; i < numSubstructures; i++){
	    org.w3c.dom.Element child = (org.w3c.dom.Element) substructures.item(i);
	    if(child.getLocalName().equals(MobyTags.CROSSREFERENCE)){
		addCrossReferences(child, registry);
	    }
 	    else if(child.getLocalName().equals(MobyTags.PROVISIONINFORMATION)){
 		addProvisionInfo(child);		
 	    }
	    else{
		// If the child element doesn't have a name, this
		// will be a problem for member association (i.e. it's an anonymous variable)
		String fieldName = getName(child);
		if(fieldName == null || fieldName.length() == 0){
		    throw new MobyException("The subelement with index " + i +
					    " does not have an article " +
					    "name, which is required (tag "+element.getNodeName()+")");
		}
		MobyDataObject childObject = (MobyDataObject) createInstanceFromDOM(child, registry);
		if(childObject == null){
		    throw new MobyException("The object member '" + fieldName + "' for object '"+ getName() +
					    " could not be properly parsed into a MOBY object");
		}
		put(fieldName, childObject);
	    }
	}
    }

    public MobyDataComposite(MobyDataType type, String name, MobyNamespace namespace, String id){
	super(namespace.getName(), id, type.getRegistry());
	setName(name);
	setDataType(type);
	members = new ConcurrentHashMap<String, MobyDataObject>();
    }

    public MobyDataComposite(MobyDataType type, String name, String namespace, String id){
	super(namespace, id, type.getRegistry());
	setName(name);
	setDataType(type);
	members = new ConcurrentHashMap<String, MobyDataObject>();
    }

    /**
     * Only call this constructor if you are sure that the data type name is in the 
     * ontology, otherwise the datatype will be null.
     */
    public MobyDataComposite(String typeName, String name, String namespace, String id){
	this(typeName, name, namespace, id, (Registry) null);
    }

    public MobyDataComposite(String typeName, String name, String namespace, String id, Registry registry){
	super(namespace, id, registry);
	setName(name);
	setDataType(MobyDataType.getDataType(typeName, registry));
	members = new ConcurrentHashMap<String, MobyDataObject>();
    }

    /**
     * Instantiates a composite with a variable number of members using Java 1.5's varargs.
     * @param memberStrings an even number of strings, representing name, object, name, object, ...
     *
     * @throws IllegalArgumentException if an odd number of memberStrings is provided
     * @throws MobyException if one of the values provided cannot be cast into the required type according to the DataType definition for the composite, or if a required member is missing.
     */
    public MobyDataComposite(MobyDataType type, MobyNamespace namespace, String id, Object... memberStrings) throws IllegalArgumentException, MobyException{
	super(namespace == null ? null : namespace.getName(), id);
	
	if(type == null){
	    throw new IllegalArgumentException("MOBY data type given in composite object " +
					       "constructor was null (not in the ontology?)");
	}

	setDataType(type);
	members = new ConcurrentHashMap<String, MobyDataObject>();

	MobyDataType dt = MobyDataType.getDataType(type.getName(), type.getRegistry());
	MobyRelationship[] children = dt.getAllChildren();

	// If one arg, resolve it's name, set it, and we're done
	if(memberStrings.length == 1){
	    if(children.length != 1){
		throw new IllegalArgumentException("Only one member was specified to " +
						   " MobyDataComposite constructor, but " +
						   children.length + " are required " +
						   "for data type " + type.getName());
	    }
	    // Take object as is if already a data instance.
	    if(memberStrings[0] instanceof MobyDataObject){
		put(children[0].getName(), (MobyDataObject) memberStrings[0]);
	    }
	    // Otherwise we need to convert the object into a string, 
	    // which will be the basis for the member.
	    else{
		put(children[0].getName(), 
		    MobyDataObject.createInstanceFromString(children[0].getDataTypeName(), 
							    memberStrings[0].toString()));
	    }
	    return;
	}

	// Otherwise, process the args list more throroughly
	for(int i = 0; i < memberStrings.length; i++){
	    Object arg1 = memberStrings[i];
	    if(!(arg1 instanceof String)){
		throw new IllegalArgumentException("Argument was not a string (member name) as expected: " + arg1);
	    }
	    String name = (String) arg1;

	    Object arg2 = memberStrings[++i];

	    for(int j = 0; i < memberStrings.length && j < children.length; j++){
		MobyRelationship relationship = children[j];
		if(name.equals(relationship.getName())){
		    int relationshipType = relationship.getRelationshipType();
			
		    // Already exists?
		    if(get(name) != null){
			throw new IllegalArgumentException("Datatype member " + name + 
							   " is repeated in the constructor");
		    }

		    // Possibly more than one value
		    if(relationshipType == Central.iHAS){
			do{			    
			    if(arg2 instanceof MobyDataObject){
				if(!((MobyDataObject) arg2).getDataType().inheritsFrom(relationship.getDataTypeName())){
				    throw new IllegalArgumentException("Argument " + arg2 + 
								       "is not of the required data type (" + 
								       relationship.getDataTypeName() + ")");
				}
				put(name, (MobyDataObject) arg2);
			    }
			    else{
				put(name, MobyDataObject.createInstanceFromString(relationship.getDataTypeName(), 
									    arg2.toString()));
			    }			    

			}while(i+1 < memberStrings.length && memberStrings[++i] != null);
			// grab values until we each the end of the arg list, or a null value: whichever comes first 
		    }
		    // Must be HASA (single value)
		    else{
			if(arg2 instanceof MobyDataObject){
			    if(!((MobyDataObject) arg2).getDataType().inheritsFrom(relationship.getDataTypeName())){
				throw new IllegalArgumentException("Argument " + arg2 + 
								   "is not of the required data type (" + 
								   relationship.getDataTypeName() + ")");
			    }
			    put(name, (MobyDataObject) arg2);
			}
			else{
			    put(name, MobyDataObject.createInstanceFromString(relationship.getDataTypeName(), 
									      arg2.toString()));
			}
		    }
		}  // if name = member name
	    }  // for members of the object
	} // for arguments
    }

    public MobyDataComposite(MobyDataType type, String name){
	this(type, name, "", "");
    }

    public MobyDataComposite(String typeName, String name){
	this(typeName, name, (Registry) null);
    }

    public MobyDataComposite(String typeName, String name, Registry r){
	this(MobyDataType.getDataType(typeName, r), name);
    }

    public MobyDataComposite(MobyDataType type){
	this(type, "");
    }

    public MobyDataComposite(String typeName){
	this(typeName, (Registry) null);
    }

    public MobyDataComposite(String typeName, Registry r){
	this(MobyDataType.getDataType(typeName, r));
    }

    public MobyDataComposite clone(){
	MobyDataComposite theClone = new MobyDataComposite(getDataType(), getName(), "", getId());
	theClone.setNamespaces(getNamespaces());
	theClone.putAll(members);
	return theClone;
    }

    /**
     * Report whether all required fields for the object's datatype have been instantiated 
     * (i.e. the object is ready to use as input to a service).
     *
     * NOT YET IMPLEMENTED
     */
    public boolean isObjectValid(){
	return true;
    }

    public String toXML(){
	// xmlMode is a protected variable in the parent class
	if(xmlMode == MobyDataInstance.CENTRAL_XML_MODE){
	    return super.toXML();
	}
	else if(isEmpty()){
	    return super.toXML(); //build the default XML for a base object if we have no fields instantiated
	}
	else{

	    // Print the fields in a sorted order (by name) so that equivalent objects
	    // will always have the same XML output	
	    Object[] fieldNames = members.keySet().toArray();
	    Arrays.sort(fieldNames);
	    StringBuffer instanceXML = new StringBuffer();

	    // Open tag
	    instanceXML.append("<"+getDataType().getName()+" "+
			       getAttrXML() + ">\n");  //getAttrXML defined in super

	    // Add the info blocks if available
	    instanceXML.append(getCRIBXML());
	    instanceXML.append(getProvisionInfo() == null ? "" : getProvisionInfo().toXML());

	    // Print out the sorted fields' XML
	    for(int i = 0; i < fieldNames.length; i++){
		MobyDataObject mdsi = members.get(fieldNames[i]);
		// ensure the articleName is set correctly for the requirements of this object
		if(!fieldNames[i].toString().equals(mdsi.getName())){
		    mdsi.setName(fieldNames[i].toString());
		}
		int oldXmlMode = mdsi.getXmlMode();
		if(oldXmlMode != MobyDataInstance.SERVICE_XML_MODE){
		    mdsi.setXmlMode(MobyDataInstance.SERVICE_XML_MODE);
		}
		instanceXML.append(mdsi.toXML()+"\n");
		if(oldXmlMode != MobyDataInstance.SERVICE_XML_MODE){
		    mdsi.setXmlMode(oldXmlMode);
		}
	    }

	    // End of data, close tag
	    instanceXML.append("</"+getDataType().getName()+">");

	    return instanceXML.toString();
	}
    }
  
    /**
     * @return HashMap of member names to MobyDataObjects
     */
    public Object getObject(){
	return members;
    }
  
    // Below, to the end of the class file, are the methods that must be 
    // implemented to satisfy the ConcurrentMap interface

    /**
     * Effectively deletes the composite, leaving you with a blank base object
     */
    public void clear(){
	members.clear();
    }

    /**
     * To check for the presence of a field with a given name
     */
    public boolean containsKey(Object fieldName){
	return members.containsKey(fieldName);
    }

    /**
     * To check for the presence of a value in one of the members (e.g Integer, Float, String, Calendar)
     */
    public boolean containsValue(Object value){
	return members.containsValue(value);
    }

    /**
     * Retrieves each field name/MobyDataObject pair for the members of the composite object
     */
    public Set<Map.Entry<String,MobyDataObject>> entrySet(){
	return members.entrySet();
    }

    /**
     * Returns true if and only if both objects have the same fields with the same values, and the same object ID
     */
    public boolean equals(Object o){

	// Make sure we are comparing apples to apples...
	MobyDataComposite other = null;
	if(o instanceof MobyDataComposite){
	    other = (MobyDataComposite) o;
	}
	// If a service associated instance, get the object it shadows for comparison
	else if(o instanceof MobyDataObjectSAI){
	    if(((MobyDataObjectSAI) o).getDataInstance() instanceof MobyDataComposite){
		other = (MobyDataComposite) ((MobyDataObjectSAI) o).getDataInstance();
	    }
	    else{
		// Composite object compared to non-composite SAI 
		return false;
	    }
	}
	else{
	    // Composite object compared to non-composite;
	    return false;
	}

	// Must have same data type...
	if(other.getDataType() != getDataType()){
	    return false;
	}

	// ...and same field names/values
	return members.equals(other);
    }

    /**
     * Retrieves a member of the composite with a given field name.
     */
    public MobyDataObject get(Object fieldName){
	//public Object get(Object fieldName){
	return members.get(fieldName);
    }

    public int hashCode(){
	return members.hashCode();
    }

    /**
     * Is this a blank, uninstantiated object?
     */
    public boolean isEmpty(){
	return members.isEmpty();
    }

    /**
     * Retrieves a list of the field names in this object
     */
    public Set<String> keySet(){
	return members.keySet();
    }

    /**
     * Add a field to the composite.  This is the most common operation.
     * e.g. creating a GenericSequence involves putting a 'length' member (MobyDataInt), 
     * then putting a 'sequence' member (MobyDataString).
     *
     * This method will throw an IllegalArgumentException if the value does not inherit from the type defined by Moby Central  
     *
     * NOTE: if the field already exists and is defined with the HAS relationship in the DataType ontology, 
     * the previous value is NOT overwritten, rather the MobyDataObject is switched into a MobyDataObjectVector.
     * Replace mode will be used unless the data type and relationship are properly defined.
     */
    public MobyDataObject put(String fieldName, MobyDataObject value){
	if(value == null){
	    return null;
	}

	MobyRelationship relationship = getDataType().getChild(fieldName);
	if(relationship == null){
	    MobyDataType t = MobyDataType.getDataType(getDataType().getName(), getDataType().getRegistry());
	    if(t == null){
		System.err.println("Data type " + getDataType().getName() + " is not in the registry: " +
                                   "validity of member put() operation will not be confirmed");
		relationship = new MobyRelationship(fieldName, 
						    value.getDataType().getName(), 
						    members.containsKey(fieldName) ? Central.iHAS : Central.iHASA);
	    }
	    else{
		relationship = t.getChild(fieldName);
	    }
	    if(relationship == null){
		String memberNames = "";
		for(MobyRelationship rel: getDataType().getChildren()){
		    memberNames += " "+rel.getName();
		}
		throw new IllegalArgumentException("The member '" + fieldName + "' for object '"+ getName() +
						   "' does not exist in the Moby ontology definition for "+
						   getDataType().getName() + ", valid member names are:" + memberNames);
	    }
	}
	MobyDataType childDataType = MobyDataType.getDataType(relationship.getDataTypeName(), getDataType().getRegistry());
	if(!value.getDataType().inheritsFrom(childDataType)){
	    // Incompatible types
	    throw new IllegalArgumentException("The member '" + fieldName + "' for object '"+ getName() +
					       "' does not inherit from the required data type " +
					       "(found data type " + value.getDataType().getName()+
					       ", but require subclass of " + 
					       childDataType.getName() +")");
	}

	// If the field already exists, see if the HAS (1-to-many) relationship exists
	if(members.containsKey(fieldName)){
	    MobyDataObject existingMember = members.get(fieldName);

	    // Easy, it's already a list, so add the new element to it
	    if(existingMember instanceof MobyDataObjectVector){
		((MobyDataObjectVector) existingMember).add(value);
		return existingMember;
	    }
	    
	    // Use replace mode if the data type or relationship is unavailable
	    MobyDataType dt = getDataType();
	    if(dt == null){
		return members.put(fieldName, value);
	    }
	    if(relationship == null){
		return members.put(fieldName, value);
	    }
	    int relationshipType = relationship.getRelationshipType();
	    
	    // Possibly more than one value (1-to-many relationship)
	    if(relationshipType == Central.iHAS){		

		// If we got here, there was a previous single value, and the field should be a list
		MobyDataObjectVector HASList = new MobyDataObjectVector("", getDataType().getRegistry());
		// Replaces old single value
		members.put(fieldName, HASList);
		HASList.add(existingMember);
		HASList.add(value);
		return HASList;
	    }
	    // Must be HASA (single value), replace current value
	    else{
		return members.put(fieldName, value);
	    }
	}

	// else case: this is the first occurence of the field
	return members.put(fieldName, value);
    }

    /**
     * Sets a number of object fields at once.
     */
    public void putAll(Map<? extends String,? extends MobyDataObject> map){
	members.putAll(map);
    }

    /**
     * Removes the field with the given name, if present
     *
     * @return the data field removed
     */
    public MobyDataObject remove(Object fieldName){
	//public Object remove(Object fieldName){
	return members.remove(fieldName);
    }

    /**
     * Reports the number of data members in the composite object
     */
    public int size(){
	return members.size();
    }

    /**
     * For the user's convenience, the returned Collection is a MobyDataSetInstance, allowing
     * for MOBY-savvy equivalency testing with another Collection of data instances.
     */
    public Collection<MobyDataObject> values(){
	return new MobyDataObjectSet("", members.values());
    }

    /*
     * If the field with a given name is not yet associated with any value, associate it with the given value.
     * Note that this operation is atomic for data integrity purposes in multi-threaded apps.
     */
    public MobyDataObject putIfAbsent(String key, MobyDataObject value){
	return members.putIfAbsent(key, value);
    }

    /*
     * Removes the field with the given name only if it currently has the given value.
     * Note that this operation is atomic for data integrity purposes in multi-threaded apps.
     *
     * @return true if the replacement operation caused a change in the object value
     */
    public boolean remove(Object key, Object value){
	return members.remove(key, value);
    }

    /*
     * Replaces the field with the given name only if the field already exists in the composite.
     * Note that this operation is atomic for data integrity purposes in multi-threaded apps.
     */
    public MobyDataObject replace(String fieldName, MobyDataObject value){
	return members.replace(fieldName, value);
    }

    /*
     * Replaces the field with the given name only if it currently has a given value
     * Note that this operation is atomic for data integrity purposes in multi-threaded apps.
     *
     * @return true if the replacement operation caused a change in the object value
     */
    public boolean replace(String fieldName, MobyDataObject oldValue, MobyDataObject newValue){
	return members.replace(fieldName, oldValue, newValue);
    }
    
}
