/*
 * 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.List;

import javax.xml.namespace.QName;

import org.eclipse.persistence.core.queries.CoreAttributeGroup;
import org.eclipse.persistence.core.queries.CoreAttributeItem;
import org.eclipse.persistence.core.sessions.CoreSession;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.mappings.AnyObjectMapping;
import org.eclipse.persistence.internal.oxm.mappings.Descriptor;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.internal.oxm.mappings.Mapping;
import org.eclipse.persistence.internal.oxm.mappings.UnmarshalKeepAsElementPolicy;
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.internal.oxm.record.XMLReader;
import org.eclipse.persistence.internal.oxm.record.XMLRecord;
import org.eclipse.persistence.internal.oxm.record.deferred.AnyMappingContentHandler;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * INTERNAL:
 * <p><b>Purpose</b>: This is how the XML Any Object Mapping is handled when
 * used with the TreeObjectBuilder.</p>
 */
public class XMLAnyObjectMappingNodeValue extends XMLRelationshipMappingNodeValue {
    private AnyObjectMapping xmlAnyObjectMapping;

    public XMLAnyObjectMappingNodeValue(AnyObjectMapping xmlAnyObjectMapping) {
        super();
        this.xmlAnyObjectMapping = xmlAnyObjectMapping;
    }

    public boolean isOwningNode(XPathFragment xPathFragment) {
        return null == xPathFragment;
    }

    public boolean marshal(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, CoreAbstractSession session, NamespaceResolver namespaceResolver) {
        return marshal(xPathFragment, marshalRecord, object, session, namespaceResolver, ObjectMarshalContext.getInstance());
    }

    public boolean marshal(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, CoreAbstractSession session, NamespaceResolver namespaceResolver, MarshalContext marshalContext) {
        if (xmlAnyObjectMapping.isReadOnly()) {
            return false;
        }
        Object objectValue = marshalContext.getAttributeValue(object, xmlAnyObjectMapping);
        return this.marshalSingleValue(xPathFragment, marshalRecord, object, objectValue, session, namespaceResolver, marshalContext);
    }

    public boolean marshalSingleValue(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, Object objectValue, CoreAbstractSession session, NamespaceResolver namespaceResolver, MarshalContext marshalContext) {
        XPathFragment rootFragment = null;

        Marshaller marshaller = marshalRecord.getMarshaller();
        objectValue = xmlAnyObjectMapping.convertObjectValueToDataValue(objectValue, session, marshalRecord.getMarshaller());

        if (null == objectValue) {
            return false;
        }
        XPathFragment groupingFragment = marshalRecord.openStartGroupingElements(namespaceResolver);
        marshalRecord.closeStartGroupingElements(groupingFragment);

        boolean wasXMLRoot = false;
        XPathFragment xmlRootFragment = null;
        Object originalValue = objectValue;
        if (xmlAnyObjectMapping.usesXMLRoot() && (objectValue instanceof Root)) {
            xmlRootFragment = new XPathFragment();
            xmlRootFragment.setNamespaceAware(marshalRecord.isNamespaceAware());
            wasXMLRoot = true;
            objectValue = ((Root) objectValue).getObject();
            if(objectValue == null){
                setupFragment(((Root) originalValue), xmlRootFragment, marshalRecord);
                marshalRecord.nilComplex(xmlRootFragment, namespaceResolver);
                return true;
            }
        }

        if (objectValue instanceof String) {
            marshalSimpleValue(xmlRootFragment, marshalRecord, originalValue, object, objectValue, session, namespaceResolver);
        } else {
            CoreSession childSession = null;
            try {
                childSession = marshaller.getContext().getSession(objectValue);
            } catch (XMLMarshalException e) {
                marshalSimpleValue(xmlRootFragment, marshalRecord, originalValue, object, objectValue, session, namespaceResolver);
                return true;
            }

            Descriptor descriptor = (Descriptor) childSession.getDescriptor(objectValue);
            CoreAttributeGroup group = marshalRecord.getCurrentAttributeGroup();
            CoreAttributeItem item = group.getItem(getMapping().getAttributeName());
            CoreAttributeGroup nestedGroup = XMLRecord.DEFAULT_ATTRIBUTE_GROUP;
            if(item != null) {
                if(item.getGroups() != null) {
                    nestedGroup = item.getGroup(descriptor.getJavaClass());
                }
                if(nestedGroup == null) {
                    nestedGroup = item.getGroup() == null?XMLRecord.DEFAULT_ATTRIBUTE_GROUP:item.getGroup();
                }
            }

            marshalRecord.pushAttributeGroup(nestedGroup);
            ObjectBuilder objectBuilder = (ObjectBuilder) descriptor.getObjectBuilder();

            List extraNamespaces = objectBuilder.addExtraNamespacesToNamespaceResolver(descriptor, marshalRecord, session, true, true);
            if(wasXMLRoot){
                setupFragment(((Root) originalValue), xmlRootFragment, marshalRecord);
            }
            /*
             * B5112171: 25 Apr 2006
             * During marshalling - XML AnyObject and AnyCollection
             * mappings throw a NullPointerException when the
             * "document root element" on child object descriptors are not
             * all defined.  These nodes will be ignored with a warning.
             */
            String defaultRootElementString = descriptor.getDefaultRootElement();
            if (!wasXMLRoot && (defaultRootElementString == null)) {
                throw XMLMarshalException.defaultRootElementNotSpecified(descriptor);
            } else {
                marshalRecord.beforeContainmentMarshal(objectValue);

                if (xmlRootFragment != null) {
                    rootFragment = xmlRootFragment;
                } else {
                    rootFragment = new XPathFragment(defaultRootElementString);
                    //resolve URI
                    if (rootFragment.getNamespaceURI() == null) {
                        if(rootFragment.getPrefix() != null) {
                            String uri = descriptor.getNonNullNamespaceResolver().resolveNamespacePrefix(rootFragment.getPrefix());
                            rootFragment.setNamespaceURI(uri);
                        } else {
                            rootFragment.setNamespaceURI(descriptor.getNonNullNamespaceResolver().getDefaultNamespaceURI());
                        }
                    }
                }

                if (!wasXMLRoot) {
                    marshalRecord.setLeafElementType(descriptor.getDefaultRootElementType());
                }
                getXPathNode().startElement(marshalRecord, rootFragment, object, session, descriptor.getNonNullNamespaceResolver(), objectBuilder, objectValue);

                writeExtraNamespaces(extraNamespaces, marshalRecord, session);
                marshalRecord.addXsiTypeAndClassIndicatorIfRequired(descriptor, descriptor, (Field)xmlAnyObjectMapping.getField(), originalValue, objectValue, wasXMLRoot, false);
                objectBuilder.buildRow(marshalRecord, objectValue, (CoreAbstractSession) childSession, marshaller, null);
                marshalRecord.afterContainmentMarshal(object, objectValue);
                marshalRecord.endElement(rootFragment, namespaceResolver);
                marshalRecord.removeExtraNamespacesFromNamespaceResolver(extraNamespaces, session);
            }
        }

        return true;
    }

    public boolean startElement(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord, Attributes atts) {
        try {
            Descriptor workingDescriptor = findReferenceDescriptor(xPathFragment, unmarshalRecord, atts, xmlAnyObjectMapping, xmlAnyObjectMapping.getKeepAsElementPolicy());

            UnmarshalKeepAsElementPolicy policy = xmlAnyObjectMapping.getKeepAsElementPolicy();
            if (null != policy && ((workingDescriptor == null && policy.isKeepUnknownAsElement()) || policy.isKeepAllAsElement())) {
                setupHandlerForKeepAsElementPolicy(unmarshalRecord, xPathFragment, atts);
            }else if (workingDescriptor != null) {
                processChild(xPathFragment, unmarshalRecord, atts, workingDescriptor, xmlAnyObjectMapping);
            }else{
                AnyMappingContentHandler handler = new AnyMappingContentHandler(unmarshalRecord, xmlAnyObjectMapping.usesXMLRoot());
                String qnameString = xPathFragment.getLocalName();
                if (xPathFragment.getPrefix() != null) {
                    qnameString = xPathFragment.getPrefix() + Constants.COLON + qnameString;
                }
                handler.startElement(xPathFragment.getNamespaceURI(), xPathFragment.getLocalName(), qnameString, atts);
                XMLReader xmlReader = unmarshalRecord.getXMLReader();
                xmlReader.setContentHandler(handler);
                xmlReader.setLexicalHandler(handler);
                return true;
            }

        } catch (SAXException e) {
            throw XMLMarshalException.unmarshalException(e);
        }
        return true;
    }

    public void endElement(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord) {
        UnmarshalRecord childRecord = unmarshalRecord.getChildRecord();
        if (null != childRecord) {
            Object childObject = childRecord.getCurrentObject();
            // OBJECT VALUE
            if (xmlAnyObjectMapping.usesXMLRoot()) {
                Descriptor workingDescriptor = childRecord.getDescriptor();
                if (workingDescriptor != null) {
                    String prefix = xPathFragment.getPrefix();
                    if ((prefix == null) && (xPathFragment.getNamespaceURI() != null)) {
                        prefix = unmarshalRecord.resolveNamespaceUri(xPathFragment.getNamespaceURI());
                    }
                    childObject = workingDescriptor.wrapObjectInXMLRoot(childObject, xPathFragment.getNamespaceURI(), xPathFragment.getLocalName(), prefix, false, unmarshalRecord.isNamespaceAware(), unmarshalRecord.getUnmarshaller());
                    workingDescriptor = null;
                }
            }
            childObject = xmlAnyObjectMapping.convertDataValueToObjectValue(childObject, unmarshalRecord.getSession(), unmarshalRecord.getUnmarshaller());
            unmarshalRecord.setAttributeValue(childObject, xmlAnyObjectMapping);
        } else {
            SAXFragmentBuilder builder = unmarshalRecord.getFragmentBuilder();

            UnmarshalKeepAsElementPolicy keepAsElementPolicy = xmlAnyObjectMapping.getKeepAsElementPolicy();

            if (null != keepAsElementPolicy && (keepAsElementPolicy.isKeepUnknownAsElement() || keepAsElementPolicy.isKeepAllAsElement()) && builder.getNodes().size() > 1) {
                setOrAddAttributeValueForKeepAsElement(builder, xmlAnyObjectMapping, xmlAnyObjectMapping, unmarshalRecord, false, null);
            } else {
                // TEXT VALUE
                if(xmlAnyObjectMapping.isMixedContent()) {
                    endElementProcessText(unmarshalRecord, xmlAnyObjectMapping, xPathFragment, null);
                } else {
                    unmarshalRecord.resetStringBuffer();
                }
            }
        }
    }

    protected void setOrAddAttributeValue(UnmarshalRecord unmarshalRecord, Object value, XPathFragment xPathFragment, Object collection){
        if (!xmlAnyObjectMapping.usesXMLRoot()) {
            unmarshalRecord.setAttributeValue(value, xmlAnyObjectMapping);
        } else {
            Root xmlRoot = unmarshalRecord.createRoot();
            xmlRoot.setNamespaceURI(xPathFragment.getNamespaceURI());
            xmlRoot.setSchemaType(unmarshalRecord.getTypeQName());
            xmlRoot.setLocalName(xPathFragment.getLocalName());
            xmlRoot.setObject(value);
           // xmlRoot.setDeclaredType(type);
            unmarshalRecord.setAttributeValue(xmlRoot, xmlAnyObjectMapping);
        }
    }


    private Namespace setupFragment(Root originalValue, XPathFragment xmlRootFragment, MarshalRecord marshalRecord) {
        Namespace generatedNamespace = null;
        String xpath = originalValue.getLocalName();
        if (originalValue.getNamespaceURI() != null) {
            xmlRootFragment.setNamespaceURI((originalValue).getNamespaceURI());
            String prefix = marshalRecord.getNamespaceResolver().resolveNamespaceURI((originalValue).getNamespaceURI());
            if (prefix == null || prefix.length() == 0) {
                prefix = marshalRecord.getNamespaceResolver().generatePrefix("ns0");
                generatedNamespace = new Namespace(prefix, xmlRootFragment.getNamespaceURI());
                xmlRootFragment.setGeneratedPrefix(true);
            }
            xpath = prefix + Constants.COLON + xpath;

        }
        xmlRootFragment.setXPath(xpath);
        return generatedNamespace;
    }

    private void marshalSimpleValue(XPathFragment xmlRootFragment, MarshalRecord marshalRecord, Object originalValue, Object object, Object value, CoreAbstractSession session, NamespaceResolver namespaceResolver) {
        QName qname = null;
        if (xmlRootFragment != null) {
            qname = ((Root) originalValue).getSchemaType();
            setupFragment(((Root) originalValue), xmlRootFragment, marshalRecord);
            getXPathNode().startElement(marshalRecord, xmlRootFragment, object, session, namespaceResolver, null, null);
            updateNamespaces(qname, marshalRecord, null);
        }

        if (value instanceof org.w3c.dom.Node) {
            marshalRecord.node((org.w3c.dom.Node) value, marshalRecord.getNamespaceResolver());
        } else {
            marshalRecord.characters(qname, value, null, false);
        }

        if (xmlRootFragment != null) {
            marshalRecord.endElement(xmlRootFragment, namespaceResolver);
        }
    }

    public AnyObjectMapping getMapping() {
        return xmlAnyObjectMapping;
    }

    public boolean isWhitespaceAware() {
        return false;
    }

    public boolean isAnyMappingNodeValue() {
        return true;
    }

    @Override
    public boolean isMixedContentNodeValue() {
        return this.xmlAnyObjectMapping.isMixedContent();
    }

    @Override
    protected Descriptor findReferenceDescriptor(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord, Attributes atts, Mapping mapping, UnmarshalKeepAsElementPolicy policy) {
        Descriptor referenceDescriptor = super.findReferenceDescriptor(xPathFragment, unmarshalRecord, atts, mapping, policy);
        if (referenceDescriptor == null) {
            Context xmlContext = unmarshalRecord.getUnmarshaller().getContext();
            XPathQName xpathQName = new XPathQName(xPathFragment.getNamespaceURI(), xPathFragment.getLocalName(), unmarshalRecord.isNamespaceAware());
            referenceDescriptor = (Descriptor) xmlContext.getDescriptor(xpathQName);
            // Check if descriptor is for a wrapper, if it is null it out and let continue
            if (referenceDescriptor != null && referenceDescriptor.isWrapper()) {
                referenceDescriptor = null;
            }
        }
        return referenceDescriptor;
    }

}
