/*******************************************************************************
 * Copyright (c) 1998, 2014 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/
package org.eclipse.persistence.internal.oxm;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.persistence.internal.core.helper.CoreClassConstants;
import org.eclipse.persistence.internal.core.queries.CoreContainerPolicy;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.mappings.BinaryDataCollectionMapping;
import org.eclipse.persistence.internal.oxm.mappings.ChoiceCollectionMapping;
import org.eclipse.persistence.internal.oxm.mappings.CollectionReferenceMapping;
import org.eclipse.persistence.internal.oxm.mappings.CompositeCollectionMapping;
import org.eclipse.persistence.internal.oxm.mappings.DirectCollectionMapping;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.internal.oxm.mappings.Mapping;
import org.eclipse.persistence.internal.oxm.record.MarshalContext;
import org.eclipse.persistence.internal.oxm.record.MarshalRecord;
import org.eclipse.persistence.internal.oxm.record.ObjectMarshalContext;
import org.eclipse.persistence.oxm.mappings.nullpolicy.AbstractNullPolicy;
import org.eclipse.persistence.oxm.mappings.nullpolicy.XMLNullRepresentationType;

public class XMLChoiceCollectionMappingMarshalNodeValue extends MappingNodeValue implements ContainerValue {
    private ChoiceCollectionMapping xmlChoiceCollectionMapping;
    private Map<Field, NodeValue> fieldToNodeValues;
    private Map<Class, List<FieldNodeValue>> classToNodeValues;
    private NodeValue choiceElementNodeValue;
    private Field xmlField;
    private boolean isMixedNodeValue;
    private boolean isAny;
    private NodeValue anyNodeValue;
    private int index = -1;

    /**
     * This class is needed to hold field and nodeValue relationship.
     * If we have choice with different fields with the same (java class) type (e.g. via XmlAdapter),
     * there is need to know to which field we are holding the nodeValue.
     *
     * It is used in getNodeValueForValue method. If we knew only class relationship to nodeValue,
     * there is no way how to say that this nodeValue is related to the first or second field (or any other field with given java class).
     *
     * @see AdapterWithElementsTestCases
     *
     *
     * @author Martin Vojtek (martin.vojtek@oracle.com)
     *
     */
    private static class FieldNodeValue {
        private final Field field;
        private final NodeValue nodeValue;
        public FieldNodeValue(Field field, NodeValue nodeValue) {
            super();
            this.field = field;
            this.nodeValue = nodeValue;
        }
        public Field getField() {
            return field;
        }
        public NodeValue getNodeValue() {
            return nodeValue;
        }

    }

    public XMLChoiceCollectionMappingMarshalNodeValue(ChoiceCollectionMapping mapping, Field xmlField) {
        this.xmlChoiceCollectionMapping = mapping;
        this.xmlField = xmlField;
        isAny = mapping.isAny();
        initializeNodeValue();
    }
    
    public boolean isOwningNode(XPathFragment xPathFragment) {
        if(isMixedNodeValue) {
            if(xPathFragment.nameIsText()) {
                return true;
            } else {
                return false;
            }
        }
        return choiceElementNodeValue.isOwningNode(xPathFragment);
    }


    public void setFieldToNodeValues(Map<Field, NodeValue> fieldToNodeValues) {
        this.fieldToNodeValues = fieldToNodeValues;
        this.classToNodeValues = new HashMap<Class, List<FieldNodeValue>>();
        for(Field nextField:fieldToNodeValues.keySet()) {
            Class associatedClass = ((Map<Field, Class>)this.xmlChoiceCollectionMapping.getFieldToClassMappings()).get(nextField);

            if (classToNodeValues.containsKey(associatedClass)) {
                classToNodeValues.get(associatedClass).add(new FieldNodeValue(nextField, fieldToNodeValues.get(nextField)));
            } else {
                List<FieldNodeValue> newFieldToNodeValuesList = new ArrayList<FieldNodeValue>();
                newFieldToNodeValuesList.add(new FieldNodeValue(nextField, fieldToNodeValues.get(nextField)));
                this.classToNodeValues.put(associatedClass, newFieldToNodeValuesList);
            }


        }

        Collection classes = this.classToNodeValues.keySet();
        for(Class nextClass:((Map<Class, Mapping>)this.xmlChoiceCollectionMapping.getChoiceElementMappingsByClass()).keySet()) {
            //Create node values for any classes that aren't already processed
            if(!(classes.contains(nextClass))) {
		Field field = (Field) xmlChoiceCollectionMapping.getClassToFieldMappings().get(nextClass);
                NodeValue nodeValue = new XMLChoiceCollectionMappingUnmarshalNodeValue(xmlChoiceCollectionMapping, xmlField, (Mapping) xmlChoiceCollectionMapping.getChoiceElementMappingsByClass().get(nextClass));
                List<FieldNodeValue> newFieldToNodeValuesList = new ArrayList<FieldNodeValue>();
                newFieldToNodeValuesList.add(new FieldNodeValue(field, nodeValue));
                this.classToNodeValues.put(nextClass, newFieldToNodeValuesList);
                NodeValue nodeValueForField = fieldToNodeValues.get(field);
                nodeValue.setXPathNode(nodeValueForField.getXPathNode());
            }
        }
    }

    private void initializeNodeValue() {
        Mapping xmlMapping = (Mapping) xmlChoiceCollectionMapping.getChoiceElementMappings().get(xmlField);
        if(xmlMapping instanceof BinaryDataCollectionMapping) {
            choiceElementNodeValue = new XMLBinaryDataCollectionMappingNodeValue((BinaryDataCollectionMapping)xmlMapping);
        } else if(xmlMapping instanceof DirectCollectionMapping) {
            choiceElementNodeValue = new XMLCompositeDirectCollectionMappingNodeValue((DirectCollectionMapping)xmlMapping);
        } else if(xmlMapping instanceof CompositeCollectionMapping) {
            choiceElementNodeValue = new XMLCompositeCollectionMappingNodeValue((CompositeCollectionMapping)xmlMapping);
        } else {
            CollectionReferenceMapping refMapping = ((CollectionReferenceMapping)xmlMapping);
            if(refMapping.usesSingleNode() || refMapping.getFields().size() == 1) {
                choiceElementNodeValue = new XMLCollectionReferenceMappingNodeValue(refMapping, xmlField);
            } else {
                choiceElementNodeValue = new XMLCollectionReferenceMappingMarshalNodeValue((CollectionReferenceMapping)xmlMapping);
            }
        }
        if(isAny){
        	anyNodeValue = new XMLChoiceCollectionMappingUnmarshalNodeValue(xmlChoiceCollectionMapping, null, xmlChoiceCollectionMapping.getAnyMapping());
        }
        	
    }

    public boolean marshal(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, CoreAbstractSession session, NamespaceResolver namespaceResolver) {
        if(xmlChoiceCollectionMapping.isReadOnly()) {
            return false;
        }
        
        Object value = xmlChoiceCollectionMapping.getAttributeValueFromObject(object);
        if(value == null) {
            AbstractNullPolicy wrapperNP = xmlChoiceCollectionMapping.getWrapperNullPolicy();
            if (wrapperNP != null && wrapperNP.getMarshalNullRepresentation() == XMLNullRepresentationType.XSI_NIL) {
                marshalRecord.nilSimple(namespaceResolver);
                return true;
            } else {
                return false;
            }
        }
        CoreContainerPolicy cp = getContainerPolicy();
        Object iterator = cp.iteratorFor(value);
        if (null != iterator && cp.hasNext(iterator)) {
            if(xPathFragment != null) {
                XPathFragment groupingFragment = marshalRecord.openStartGroupingElements(namespaceResolver);
                marshalRecord.closeStartGroupingElements(groupingFragment);
            }
        } else {
        	return marshalRecord.emptyCollection(xPathFragment, namespaceResolver, xmlChoiceCollectionMapping.getWrapperNullPolicy() != null);
        }
        
        if(marshalRecord.getMarshaller().isApplicationJSON()){
        	List<NodeValue> nodeValues = new ArrayList();
            List<List> values = new ArrayList<List>();
            
            NodeValue mixedNodeValue = null;
            List mixedValues = null;
            
            //sort the elements. Results will be a list of nodevalues and a corresponding list of 
            //collections associated with those nodevalues
            while(cp.hasNext(iterator)) {        	    
        	    Object nextValue = xmlChoiceCollectionMapping.convertObjectValueToDataValue(cp.next(iterator, session), session, marshalRecord.getMarshaller());
		        NodeValue nodeValue = getNodeValueForValue(nextValue);
		        
		        if(nodeValue != null){
		        	if(nodeValue == this){
		        		mixedNodeValue = this;
		        		if(mixedValues == null){
		        			mixedValues = new ArrayList();
		        		}
		        		mixedValues.add(nextValue);
		        	}else{
			            int index = nodeValues.indexOf(nodeValue);
	        	        if(index > -1){
	        	    	    values.get(index).add(nextValue);
	        	        }else{        	        	
		        	    	nodeValues.add(nodeValue);
		        	    	List valuesList = new ArrayList();
		        	    	valuesList.add(nextValue);
		        	    	values.add(valuesList);        	        	
	        	        }
		        	}
		        }        	  
            }
            //always write out mixed values last so we can determine if the textWrapper key needs to be written.
            if(mixedNodeValue != null){
            	nodeValues.add(mixedNodeValue);
            	values.add(mixedValues);
            }
            
            for(int i =0;i < nodeValues.size(); i++){
            	NodeValue associatedNodeValue = nodeValues.get(i);            	
            	List listValue = values.get(i);            	
            	
            	XPathFragment frag = null;
                if(associatedNodeValue == this){
                	frag = marshalRecord.getTextWrapperFragment();
                }else{            	
            	   frag = associatedNodeValue.getXPathNode().getXPathFragment();
            	   if(frag != null){
            		   frag = getOwningFragment(associatedNodeValue, frag);
            		   associatedNodeValue = ((XMLChoiceCollectionMappingUnmarshalNodeValue)associatedNodeValue).getChoiceElementMarshalNodeValue(); 
            	   }
                }
                if(frag != null || associatedNodeValue.isAnyMappingNodeValue()){
                    int valueSize = listValue.size();
                    marshalRecord.startCollection();
 
                    for(int j=0;j<valueSize; j++){              	          
                    	marshalSingleValueWithNodeValue(frag, marshalRecord, object, listValue.get(j), session, namespaceResolver, ObjectMarshalContext.getInstance(), associatedNodeValue);
                    }
                    marshalRecord.endCollection();                    
                }    
            }                   
        }        
        else{        
            while(cp.hasNext(iterator)) {
                Object nextValue = cp.next(iterator, session);
                marshalSingleValue(xPathFragment, marshalRecord, object, nextValue, session, namespaceResolver, ObjectMarshalContext.getInstance());
            }
        }
        return true;
    }

    public boolean marshalSingleValue(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, Object value, CoreAbstractSession session, NamespaceResolver namespaceResolver, MarshalContext marshalContext) {        
        value = xmlChoiceCollectionMapping.convertObjectValueToDataValue(value, session, marshalRecord.getMarshaller());
        if(value !=null && value.getClass() == CoreClassConstants.STRING && this.xmlChoiceCollectionMapping.isMixedContent()) {
    		marshalMixedContent(marshalRecord, (String)value);
	        return true;
    	}
        NodeValue associatedNodeValue = getNodeValueForValue(value);     
        if(associatedNodeValue != null) {
        	if(associatedNodeValue.isAnyMappingNodeValue()){
        		//NodeValue unwrappedNodeValue = ((XMLChoiceCollectionMappingUnmarshalNodeValue)associatedNodeValue).getChoiceElementMarshalNodeValue();
        		return marshalSingleValueWithNodeValue(null, marshalRecord, object, value, session, namespaceResolver, marshalContext, associatedNodeValue); 
        	}
        	else{
        	//Find the correct fragment
        	XPathFragment frag = associatedNodeValue.getXPathNode().getXPathFragment();
    		if(frag != null){
    		    frag = getOwningFragment(associatedNodeValue, frag);
        	    NodeValue unwrappedNodeValue = ((XMLChoiceCollectionMappingUnmarshalNodeValue)associatedNodeValue).getChoiceElementMarshalNodeValue();
        	    return marshalSingleValueWithNodeValue(frag, marshalRecord, object, value, session, namespaceResolver, marshalContext, unwrappedNodeValue);
    		}
        	}
        }
        return true;
    }

    private boolean marshalSingleValueWithNodeValue(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, Object value, CoreAbstractSession session, NamespaceResolver namespaceResolver, MarshalContext marshalContext, NodeValue unwrappedNodeValue) {        
    	
    	if(unwrappedNodeValue != null){
    	    unwrappedNodeValue.marshalSingleValue(xPathFragment, marshalRecord, object, value, session, namespaceResolver, marshalContext);
    	}    	   
    	return true;
    }

    private NodeValue getNodeValueForValue(Object value){
    	if(value == null){
    		Iterator<NodeValue> nodeValues= fieldToNodeValues.values().iterator();
    		while(nodeValues.hasNext()) {
    			
    		    XMLChoiceCollectionMappingUnmarshalNodeValue unmarshalNodeValue = (XMLChoiceCollectionMappingUnmarshalNodeValue)nodeValues.next();    		    
    		    NodeValue nextNodeValue = unmarshalNodeValue.getChoiceElementMarshalNodeValue();    		    
    		    
    		    if(nextNodeValue instanceof MappingNodeValue){
	        		Mapping nextMapping = ((MappingNodeValue)nextNodeValue).getMapping();
	        		if(nextMapping.isAbstractCompositeCollectionMapping()){
	        			if(((CompositeCollectionMapping)nextMapping).getNullPolicy().isNullRepresentedByXsiNil()){
	        				return unmarshalNodeValue;	        				
	        			}
	        		}else if(nextMapping.isAbstractCompositeDirectCollectionMapping()){
	        			if(((DirectCollectionMapping)nextMapping).getNullPolicy().isNullRepresentedByXsiNil()){
	        				return unmarshalNodeValue;
	        			}
	        		}else if(nextMapping instanceof BinaryDataCollectionMapping){
	        			if(((BinaryDataCollectionMapping)nextMapping).getNullPolicy().isNullRepresentedByXsiNil()){
	        				return unmarshalNodeValue;
	        			}
	        		}
    		    }
        		
        	}
    		return null;
    	}
    	
    	Field associatedField = null;
    	NodeValue nodeValue = null;
    	if(value instanceof Root) {
    		Root rootValue = (Root)value;
    		String localName = rootValue.getLocalName();
    		String namespaceUri = rootValue.getNamespaceURI();
    		Object fieldValue = rootValue.getObject();
    		associatedField = getFieldForName(localName, namespaceUri);
    		if(associatedField == null) {
    		    if(xmlChoiceCollectionMapping.isAny()) {
    		        return this.anyNodeValue;
    		    }
    		    Class theClass = fieldValue.getClass();
    		    while(associatedField == null) {
                    associatedField = (Field) xmlChoiceCollectionMapping.getClassToFieldMappings().get(theClass);
                    if(theClass.getSuperclass() != null) {
                        theClass = theClass.getSuperclass();
                    } else {
                        break;
                    }
    		    }
    		}
    		if(associatedField != null) {
    		    nodeValue = this.fieldToNodeValues.get(associatedField);
    		}
    	} else {
            Class theClass = value.getClass();
            while(associatedField == null) {
                associatedField = (Field) xmlChoiceCollectionMapping.getClassToFieldMappings().get(theClass);
                List<FieldNodeValue> fieldNodes = classToNodeValues.get(theClass);

                nodeValue = null;
                if (null != fieldNodes) {

                    //match also field
                    if (null != associatedField && fieldNodes.size() > 1) {
                        for (FieldNodeValue fieldNode : fieldNodes) {
                            if (fieldNode.getField().equals(associatedField)) {
                                nodeValue = fieldNode.getNodeValue();
                            }
                        }
                    }

                    if (null == nodeValue && fieldNodes.size() > 0) {
                        nodeValue = fieldNodes.get(0).getNodeValue();
                    }
                }

                if(theClass.getSuperclass() != null) {
                    theClass = theClass.getSuperclass();
                } else {
                    break;
                }
            }
    	}
    	if(associatedField == null) {
    	    //check the field associations
    	    List<Field> sourceFields = null;
    	    Class theClass = value.getClass();
    	    while(theClass != null) {
    	        sourceFields = (List<Field>) xmlChoiceCollectionMapping.getClassToSourceFieldsMappings().get(theClass);
    	        if(sourceFields != null) {
    	            break;
    	        }
    	        theClass = theClass.getSuperclass();
    	    }
    	    if(sourceFields != null) {
    	        associatedField = sourceFields.get(0);
    	        nodeValue = fieldToNodeValues.get(associatedField);
    	    }
    	}
    	if(nodeValue != null){
    		return nodeValue;
    	}
    	if(associatedField != null) {
    	    return fieldToNodeValues.get(associatedField);
    	}
    	if (xmlChoiceCollectionMapping.isMixedContent() && value instanceof String){
    		//use this as a placeholder for the nodevalue for mixedcontent
    		return this;
    	}
    	if (xmlChoiceCollectionMapping.isAny()){
    		return anyNodeValue;
    	}
    	return null;
    }
    
    
    private XPathFragment getOwningFragment(NodeValue nodeValue, XPathFragment frag){
    	while(frag != null) {
   	        if(nodeValue.isOwningNode(frag)) {
   	        	return frag;
   	        }
   	        frag = frag.getNextFragment();            
   		}   
    	return null;
    }   
    
    private void marshalMixedContent(MarshalRecord record, String value) {
        record.characters(value);
    }

    private Field getFieldForName(String localName, String namespaceUri) {
    	Iterator<Field> fields = fieldToNodeValues.keySet().iterator(); 
    	while(fields.hasNext()) {
    		Field nextField = fields.next();
    		if(nextField != null){
    		XPathFragment fragment = nextField.getXPathFragment();
    		while(fragment != null && (!fragment.nameIsText())) {
    			if(fragment.getNextFragment() == null || fragment.getHasText()) {
    				if(fragment.getLocalName().equals(localName)) {
    					String fragUri = fragment.getNamespaceURI();
    					if((namespaceUri == null && fragUri == null) || (namespaceUri != null && fragUri != null && namespaceUri.equals(fragUri))) {
    						return nextField;
    					}
    				}
    			}
    			fragment = fragment.getNextFragment();
    		}
    		}
    	}
    	return null;
    }
    
    public Collection<NodeValue> getAllNodeValues() {
        return this.fieldToNodeValues.values();
    }

    public boolean isMarshalNodeValue() {
        return true;
    }
    
    public boolean isUnmarshalNodeValue() {
        return false;
    }

    @Override
    public boolean isWrapperAllowedAsCollectionName() {
        return false;
    }

    public Object getContainerInstance() {
        return getContainerPolicy().containerInstance();
    }

    public void setContainerInstance(Object object, Object containerInstance) {
        xmlChoiceCollectionMapping.setAttributeValueInObject(object, containerInstance);
    }

    public CoreContainerPolicy getContainerPolicy() {
        return xmlChoiceCollectionMapping.getContainerPolicy();
    }
    
    public boolean isContainerValue() {
        return true;
    }  
    
    public ChoiceCollectionMapping getMapping() {
        return xmlChoiceCollectionMapping;
    }    

    public boolean getReuseContainer() {
        return getMapping().getReuseContainer();
    }

    /**
     * INTERNAL:
     * Indicates that this is the choice mapping node value that represents the mixed content.
     */
    public void setIsMixedNodeValue(boolean b) {
        this.isMixedNodeValue = b;
    }

    /**
     * INTERNAL:
     * Return true if this is the node value representing mixed content.
     */    
    public boolean isMixedContentNodeValue() {
        return this.isMixedNodeValue;
    }
    
    /**
     *  INTERNAL:
     *  Used to track the index of the corresponding containerInstance in the containerInstances Object[] on UnmarshalRecord 
     */  
    public void setIndex(int index){
    	this.index = index;
    }
    
    /**
     * INTERNAL:
     * Set to track the index of the corresponding containerInstance in the containerInstances Object[] on UnmarshalRecord
     * Set during TreeObjectBuilder initialization 
     */
    public int getIndex(){
    	return index;
    }

    /**
     * INTERNAL
     * Return true if an empty container should be set on the object if there
     * is no presence of the collection in the XML document.
     * @since EclipseLink 2.3.3
     */
    public boolean isDefaultEmptyContainer() {
        return getMapping().isDefaultEmptyContainer();
    }
    
    @Override
    public void setXPathNode(XPathNode xPathNode) {
        super.setXPathNode(xPathNode);
        if(this.anyNodeValue != null) {
            this.anyNodeValue.setXPathNode(xPathNode);
        }
    }

}
