/*
 * Copyright (c) 1998, 2018 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 v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.oxm;

import java.util.StringTokenizer;
import javax.xml.namespace.QName;

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.DirectCollectionMapping;
import org.eclipse.persistence.internal.oxm.mappings.Field;
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.internal.oxm.record.UnmarshalRecord;

import org.eclipse.persistence.oxm.mappings.nullpolicy.AbstractNullPolicy;
import org.eclipse.persistence.oxm.mappings.nullpolicy.XMLNullRepresentationType;
import org.xml.sax.Attributes;

/**
 * INTERNAL:
 * <p><b>Purpose</b>: This is how the XML Composite Direct Collection Mapping is
 * handled when used with the TreeObjectBuilder.</p>
 */

public class XMLCompositeDirectCollectionMappingNodeValue extends MappingNodeValue implements ContainerValue {
    private static final String SPACE = " ";
    private DirectCollectionMapping xmlCompositeDirectCollectionMapping;
    private int index = -1;

    public XMLCompositeDirectCollectionMappingNodeValue(DirectCollectionMapping xmlCompositeDirectCollectionMapping) {
        super();
        this.xmlCompositeDirectCollectionMapping = xmlCompositeDirectCollectionMapping;
    }

    public boolean isOwningNode(XPathFragment xPathFragment) {
         XPathFragment nextFragment = xPathFragment.getNextFragment();
         if (nextFragment == null || xmlCompositeDirectCollectionMapping.usesSingleNode()) {
             return xPathFragment.isAttribute() || xPathFragment.nameIsText();
         } else {
             return  (nextFragment != null) && (nextFragment.nameIsText() || nextFragment.isAttribute());
         }
    }

    /**
     * Override the method in XPathNode such that the marshaller can be set on the
     * marshalRecord - this is required for XMLConverter usage.
     */
    public boolean marshal(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, CoreAbstractSession session, NamespaceResolver namespaceResolver) {
        if (xmlCompositeDirectCollectionMapping.isReadOnly()) {
            return false;
        }

        CoreContainerPolicy cp = getContainerPolicy();
        Object collection = xmlCompositeDirectCollectionMapping.getAttributeAccessor().getAttributeValueFromObject(object);
        if (null == collection) {
            AbstractNullPolicy wrapperNP = xmlCompositeDirectCollectionMapping.getWrapperNullPolicy();
            if (wrapperNP != null && wrapperNP.getMarshalNullRepresentation() == XMLNullRepresentationType.XSI_NIL) {
                marshalRecord.nilSimple(namespaceResolver);
                return true;
            } else {
                return false;
            }
        }

        Object iterator = cp.iteratorFor(collection);
        Field xmlField = (Field) xmlCompositeDirectCollectionMapping.getField();
        if (null != iterator && cp.hasNext(iterator)) {
            XPathFragment groupingFragment = marshalRecord.openStartGroupingElements(namespaceResolver);
            marshalRecord.closeStartGroupingElements(groupingFragment);
        } else {
            if (xmlField.usesSingleNode() && !xmlCompositeDirectCollectionMapping.isDefaultEmptyContainer()) {
                XPathFragment groupingFragment = marshalRecord.openStartGroupingElements(namespaceResolver);
                marshalRecord.closeStartGroupingElements(groupingFragment);
            } else {
                return marshalRecord.emptyCollection(xPathFragment, namespaceResolver, xmlCompositeDirectCollectionMapping.getWrapperNullPolicy() != null);
            }
        }

        Object objectValue;
        if (xmlField.usesSingleNode()) {
            StringBuilder stringValueStringBuilder = new StringBuilder();
            String newValue;
            QName schemaType = null;
            if (xPathFragment != null && !xPathFragment.isAttribute() && !xPathFragment.nameIsText) {
                marshalRecord.openStartElement(xPathFragment, namespaceResolver);
            }

            while (cp.hasNext(iterator)) {
                objectValue = cp.next(iterator, session);
                objectValue = xmlCompositeDirectCollectionMapping.convertObjectValueToDataValue(objectValue, session, marshalRecord.getMarshaller());
                schemaType = xmlField.getSchemaTypeForValue(objectValue, session);

                newValue = marshalRecord.getValueToWrite(schemaType, objectValue, (ConversionManager) session.getDatasourcePlatform().getConversionManager());
                if (null != newValue) {
                    stringValueStringBuilder.append(newValue);
                    if (cp.hasNext(iterator)) {
                        stringValueStringBuilder.append(SPACE);
                    }
                }
            }

            XPathFragment groupingFragment = marshalRecord.openStartGroupingElements(namespaceResolver);
            if (xPathFragment != null && xPathFragment.isAttribute()) {
                marshalRecord.attribute(xPathFragment, namespaceResolver, stringValueStringBuilder.toString());
                marshalRecord.closeStartGroupingElements(groupingFragment);
            } else {
                marshalRecord.closeStartGroupingElements(groupingFragment);
                if (xmlCompositeDirectCollectionMapping.isCDATA()) {
                    marshalRecord.cdata(stringValueStringBuilder.toString());
                } else {
                    marshalRecord.characters(stringValueStringBuilder.toString());
                    if (xPathFragment != null && !xPathFragment.isAttribute() && !xPathFragment.nameIsText) {
                        marshalRecord.endElement(xPathFragment, namespaceResolver);
                    }
                }
            }
        } else {
            int valueSize = cp.sizeFor(collection);
            if(marshalRecord.getMarshaller().isApplicationJSON() && (valueSize > 1 || !marshalRecord.getMarshaller().isReduceAnyArrays())) {
                marshalRecord.startCollection();
            }

            while (cp.hasNext(iterator)) {
                objectValue = cp.next(iterator, session);
                marshalSingleValue(xPathFragment, marshalRecord, object, objectValue, session, namespaceResolver, ObjectMarshalContext.getInstance());
            }
            if(marshalRecord.getMarshaller().isApplicationJSON() && (valueSize > 1 || !marshalRecord.getMarshaller().isReduceAnyArrays())) {
                marshalRecord.endCollection();
            }
        }
        return true;
    }

    public void attribute(UnmarshalRecord unmarshalRecord, String namespaceURI, String localName, String value) {
        Object collection = unmarshalRecord.getContainerInstance(this);
        if (xmlCompositeDirectCollectionMapping.usesSingleNode()) {
            StringTokenizer stringTokenizer = new StringTokenizer(value);
            while (stringTokenizer.hasMoreTokens()) {
                addUnmarshalValue(unmarshalRecord, stringTokenizer.nextToken(), collection);
            }
        } else {
            addUnmarshalValue(unmarshalRecord, value, collection);
        }
    }

    public boolean startElement(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord, Attributes atts) {
        Field xmlField = (Field) xmlCompositeDirectCollectionMapping.getField();
        XPathFragment lastXPathFragment = xmlField.getLastXPathFragment();
        if (lastXPathFragment.nameIsText()) {
            String type = atts.getValue(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, Constants.SCHEMA_TYPE_ATTRIBUTE);
            if (null != type) {
                String namespaceURI = null;
                int colonIndex = type.indexOf(Constants.COLON);
                if (colonIndex > -1) {
                    String prefix = type.substring(0, colonIndex);
                    namespaceURI = unmarshalRecord.resolveNamespacePrefix(prefix);
                    type = type.substring(colonIndex + 1);
                }
                unmarshalRecord.setTypeQName(new QName(namespaceURI, type));
            }
        } else if (lastXPathFragment.isAttribute()) {
            if (!xmlField.usesSingleNode()) {
                String namespaceURI = lastXPathFragment.getNamespaceURI();
                if (namespaceURI == null) {
                    namespaceURI = Constants.EMPTY_STRING;
                }
                String value = atts.getValue(namespaceURI, lastXPathFragment.getLocalName());
                Object collection = unmarshalRecord.getContainerInstance(this);
                addUnmarshalValue(unmarshalRecord, value, collection);
            }
        }
        return true;
    }

    public void endElement(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord) {
        Field xmlField = (Field) xmlCompositeDirectCollectionMapping.getField();

        Object value = unmarshalRecord.getCharacters().toString();
        if(((String)value).length() == 0 && !xmlField.usesSingleNode()){
            if( xmlCompositeDirectCollectionMapping.getNullValue() != null){
                value = xmlCompositeDirectCollectionMapping.getNullValue();
            }
        }
        unmarshalRecord.resetStringBuffer();
        XPathFragment lastXPathFragment = xmlField.getLastXPathFragment();
        if (!lastXPathFragment.nameIsText()) {
            return;
        }

        if (xmlField.usesSingleNode() ) {

            Object collection = unmarshalRecord.getContainerInstance(this);
            StringTokenizer stringTokenizer = new StringTokenizer((String)value);
            while (stringTokenizer.hasMoreTokens()) {
                addUnmarshalValue(unmarshalRecord, stringTokenizer.nextToken(), collection);
            }
        } else {
            if(!unmarshalRecord.getXMLReader().isInCollection() && unmarshalRecord.isNil() ){
                unmarshalRecord.setAttributeValueNull(this);
            }else{
                Object collection = unmarshalRecord.getContainerInstance(this);
                addUnmarshalValue(unmarshalRecord, value, collection);
            }
        }
    }

    public void endElement(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord, Object collection) {
        Field xmlField = (Field) xmlCompositeDirectCollectionMapping.getField();

        Object value = unmarshalRecord.getCharacters().toString();
        if(((String)value).length() == 0 && !xmlField.usesSingleNode()){
            if( xmlCompositeDirectCollectionMapping.getNullValue() != null){
                value = xmlCompositeDirectCollectionMapping.getNullValue();
            }
        }
        unmarshalRecord.resetStringBuffer();

        if (xmlField.usesSingleNode() && value instanceof String) {
            StringTokenizer stringTokenizer = new StringTokenizer((String)value);
            while (stringTokenizer.hasMoreTokens()) {
                addUnmarshalValue(unmarshalRecord, stringTokenizer.nextToken(), collection);
            }
        } else {
            if (xmlField.getLastXPathFragment().nameIsText()) {
                if(!unmarshalRecord.getXMLReader().isInCollection() && unmarshalRecord.isNil() ){
                    unmarshalRecord.setAttributeValueNull(this);
                } else{
                    addUnmarshalValue(unmarshalRecord, value, collection);
                }
            }
        }
    }

    private void addUnmarshalValue(UnmarshalRecord unmarshalRecord, Object value, Object collection) {

        if (unmarshalRecord.isNil() && unmarshalRecord.getXMLReader().isNullRepresentedByXsiNil(xmlCompositeDirectCollectionMapping.getNullPolicy())){
            value = null;
        } else if (!isWhitespaceAware() && Constants.EMPTY_STRING.equals(value)) {
            value = null;
        }

        Field xmlField = (Field) xmlCompositeDirectCollectionMapping.getField();

        ConversionManager conversionManager = unmarshalRecord.getConversionManager();
        if (unmarshalRecord.getTypeQName() != null) {
            Class typeClass = xmlField.getJavaClass(unmarshalRecord.getTypeQName(), conversionManager);
            value = conversionManager.convertObject(value, typeClass, unmarshalRecord.getTypeQName());
        } else {
            value = unmarshalRecord.getXMLReader().convertValueBasedOnSchemaType(xmlField, value, conversionManager, unmarshalRecord);
        }

        value = xmlCompositeDirectCollectionMapping.convertDataValueToObjectValue(value, unmarshalRecord.getSession(), unmarshalRecord.getUnmarshaller());

        if (value != null && value.getClass() == CoreClassConstants.STRING) {
            if (xmlCompositeDirectCollectionMapping.isCollapsingStringValues()) {
                value = conversionManager.collapseStringValue((String)value);
            } else if (xmlCompositeDirectCollectionMapping.isNormalizingStringValues()) {
                value = conversionManager.normalizeStringValue((String)value);
            }
        }
        unmarshalRecord.addAttributeValue(this, value, collection);
    }

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

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

    public CoreContainerPolicy getContainerPolicy() {
        return xmlCompositeDirectCollectionMapping.getContainerPolicy();
    }

    public boolean isContainerValue() {
        return true;
    }

    public boolean marshalSingleValue(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, Object value, CoreAbstractSession session, NamespaceResolver namespaceResolver, MarshalContext marshalContext) {
        value = xmlCompositeDirectCollectionMapping.convertObjectValueToDataValue(value, session, marshalRecord.getMarshaller());

        if (null != value) {
            Field xmlField = (Field) xmlCompositeDirectCollectionMapping.getField();
            QName schemaType = xmlField.getSchemaTypeForValue(value, session);
            boolean isElementOpen = false;
            if (Constants.QNAME_QNAME.equals(schemaType)) {
                QName fieldValue = (QName) value;
                if ((fieldValue.getNamespaceURI() == null || fieldValue.getNamespaceURI().equals("")) && marshalRecord.getNamespaceResolver().getDefaultNamespaceURI() != null) {
                    // In this case, an extra xmlns="" declaration is going to be added. This may
                    // require adjusting the namespace of the current fragment.
                    String defaultNamespaceURI = namespaceResolver.getDefaultNamespaceURI();
                    if (defaultNamespaceURI.equals(xPathFragment.getNamespaceURI()) && xPathFragment.getPrefix() == null) {
                        String prefix = namespaceResolver.generatePrefix();
                        String xPath = prefix + Constants.COLON + xPathFragment.getShortName();
                        XPathFragment newFragment = new XPathFragment(xPath);
                        newFragment.setNamespaceURI(defaultNamespaceURI);
                        newFragment.setNextFragment(xPathFragment.getNextFragment());
                        marshalRecord.openStartElement(newFragment, namespaceResolver);
                        isElementOpen = true;
                        marshalRecord.namespaceDeclaration(prefix, defaultNamespaceURI);
                        marshalRecord.predicateAttribute(xPathFragment, namespaceResolver);
                        xPathFragment = newFragment;
                    }
                }
            }

            if (!isElementOpen) {
                 marshalRecord.openStartElement(xPathFragment, namespaceResolver);
            }

            XPathFragment nextFragment = xPathFragment.getNextFragment();
            if (nextFragment != null && nextFragment.isAttribute()) {
                marshalRecord.predicateAttribute(xPathFragment, namespaceResolver);
                marshalRecord.attribute(nextFragment, namespaceResolver, value,schemaType);
                marshalRecord.closeStartElement();
            } else {
                if (xmlField.isTypedTextField()) {
                    updateNamespaces(schemaType, marshalRecord, xmlField);
                }
                marshalRecord.closeStartElement();
                marshalRecord.predicateAttribute(xPathFragment, namespaceResolver);
                marshalRecord.characters(schemaType, value, null, xmlCompositeDirectCollectionMapping.isCDATA());
            }
            marshalRecord.endElement(xPathFragment, namespaceResolver);
            return true;
        } else {
            AbstractNullPolicy nullPolicy = xmlCompositeDirectCollectionMapping.getNullPolicy();
            if (nullPolicy.getMarshalNullRepresentation() != XMLNullRepresentationType.ABSENT_NODE) {
                marshalRecord.openStartElement(xPathFragment, namespaceResolver);
                XPathFragment nextFragment = xPathFragment.getNextFragment();
                nullPolicy.directMarshal(nextFragment, marshalRecord, object, session, namespaceResolver);
                marshalRecord.endElement(xPathFragment, namespaceResolver);
                return true;
            }
        }
        return false;
    }

    public DirectCollectionMapping getMapping() {
        return xmlCompositeDirectCollectionMapping;
    }

    public boolean isWhitespaceAware() {
        return !xmlCompositeDirectCollectionMapping.getNullPolicy().isNullRepresentedByEmptyNode();
    }

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

    /**
     *  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 boolean isWrapperAllowedAsCollectionName() {
        return true;
    }

}
