/*
 * 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 javax.activation.DataHandler;
import javax.xml.namespace.QName;

import org.eclipse.persistence.core.sessions.CoreSession;
import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.core.helper.CoreClassConstants;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.mappings.BinaryDataMapping;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.internal.oxm.record.BinaryDataUnmarshalRecord;
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.deferred.BinaryMappingContentHandler;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * INTERNAL:
 * <p><b>Purpose</b>: This is how the XML Binary Data Mapping is handled when
 * used with the TreeObjectBuilder.</p>
 * @author  mmacivor
 */

public class XMLBinaryDataMappingNodeValue extends NodeValue implements NullCapableValue {
    private BinaryDataMapping xmlBinaryDataMapping;

    protected String getValueToWrite(QName schemaType, Object value, CoreAbstractSession session) {
        return (String) ((ConversionManager) session.getDatasourcePlatform().getConversionManager()).convertObject(value, CoreClassConstants.STRING, schemaType);
    }

    @Override
    public boolean isOwningNode(XPathFragment xPathFragment) {
        return (xPathFragment.getNextFragment() == null) || xPathFragment.isAttribute();
    }

    public XMLBinaryDataMappingNodeValue(BinaryDataMapping mapping) {
        this.xmlBinaryDataMapping = mapping;
    }

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

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

    @Override
    public boolean marshalSingleValue(XPathFragment xPathFragment, MarshalRecord marshalRecord, Object object, Object objectValue, CoreAbstractSession session, NamespaceResolver namespaceResolver, MarshalContext marshalContext) {
        return marshalSingleValue(xPathFragment, marshalRecord, object, objectValue, session, namespaceResolver, marshalContext, null);
    }

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

        if (objectValue instanceof Root) {
            Root xmlRoot = (Root) objectValue;
            xmlRootFrag = new XPathFragment();
            if (xmlRoot.getNamespaceURI() != null && !xmlRoot.getNamespaceURI().equals(namespaceResolver.getDefaultNamespaceURI())) {
                String prefix = namespaceResolver.resolveNamespaceURI(xmlRoot.getNamespaceURI());
                xmlRootFrag.setXPath(prefix + Constants.COLON + xmlRoot.getLocalName());
                xmlRootFrag.setNamespaceURI(xmlRoot.getNamespaceURI());
            }else{
                xmlRootFrag.setXPath(xmlRoot.getLocalName());
                if(xmlRoot.getNamespaceURI() != null && xmlRoot.getNamespaceURI().length() > 0) {
                    xmlRootFrag.setNamespaceURI(xmlRoot.getNamespaceURI());
                }
            }
        }

        Marshaller marshaller = marshalRecord.getMarshaller();
        objectValue = xmlBinaryDataMapping.convertObjectValueToDataValue(objectValue, session, marshaller);
        XPathFragment groupingFragment = marshalRecord.openStartGroupingElements(namespaceResolver);
        if(xPathFragment.isAttribute()){
            if (objectValue == null) {
                marshalRecord.closeStartGroupingElements(groupingFragment);
                return true;
            }
        }else {
            marshalRecord.closeStartGroupingElements(groupingFragment);
            XPathFragment elementFragment = xPathFragment;
            if(xmlRootFrag != null) {
                elementFragment = xmlRootFrag;
            }
            if (objectValue == null) {
                XPathNode holderXPathNode = new XPathNode();
                holderXPathNode.setXPathFragment(elementFragment);
                marshalRecord.addGroupingElement(holderXPathNode);
                boolean returnVal = xmlBinaryDataMapping.getNullPolicy().directMarshal(xPathFragment, marshalRecord, object, session, namespaceResolver);
                if(returnVal){
                    marshalRecord.endElement(elementFragment, namespaceResolver);
                }
                marshalRecord.removeGroupingElement(holderXPathNode);
                return returnVal;
              }else if(!xPathFragment.isSelfFragment){
                marshalRecord.openStartElement(elementFragment, namespaceResolver);
                marshalRecord.closeStartElement();
            }
        }




        // figure out CID or bytes
        String c_id = null;
        byte[] bytes = null;
        String mimeType = this.xmlBinaryDataMapping.getMimeType(object);
        String attachmentType = mimeType;
        if(mimeType == null) {
            mimeType = "";
            attachmentType = "application/octet-stream";
        }
        if (xmlBinaryDataMapping.isSwaRef() && (marshaller.getAttachmentMarshaller() != null)) {
            //object value should be a DataHandler
            if (xmlBinaryDataMapping.getAttributeClassification() == XMLBinaryDataHelper.getXMLBinaryDataHelper().DATA_HANDLER) {
                c_id = marshaller.getAttachmentMarshaller().addSwaRefAttachment((DataHandler) objectValue);
            } else {
                XMLBinaryDataHelper.EncodedData data = XMLBinaryDataHelper.getXMLBinaryDataHelper().getBytesForBinaryValue(//
                        objectValue, marshaller, xmlBinaryDataMapping.getMimeType(object));
                bytes = data.getData();
                c_id = marshaller.getAttachmentMarshaller().addSwaRefAttachment(bytes, 0, bytes.length);
            }
        } else if (marshalRecord.isXOPPackage() && !xmlBinaryDataMapping.shouldInlineBinaryData()) {
            XPathFragment lastFrag = ((Field) xmlBinaryDataMapping.getField()).getLastXPathFragment();
            if(xmlRootFrag != null) {
                lastFrag = xmlRootFrag;
            }
            String localName = null;
            String namespaceUri = null;
            if(rootFragment != null) {
                localName = rootFragment.getLocalName();
                namespaceUri = rootFragment.getNamespaceURI();
            }
            if(!lastFrag.isSelfFragment) {
                localName = lastFrag.getLocalName();
                namespaceUri = lastFrag.getNamespaceURI();
            }
            if (objectValue.getClass() == CoreClassConstants.APBYTE) {
                bytes = (byte[]) objectValue;
                c_id = marshaller.getAttachmentMarshaller().addMtomAttachment(bytes, 0, bytes.length, attachmentType, localName, namespaceUri);
            } else if (xmlBinaryDataMapping.getAttributeClassification() == XMLBinaryDataHelper.getXMLBinaryDataHelper().DATA_HANDLER) {
                c_id = marshaller.getAttachmentMarshaller().addMtomAttachment((DataHandler) objectValue, localName, namespaceUri);
            } else {
                XMLBinaryDataHelper.EncodedData data = XMLBinaryDataHelper.getXMLBinaryDataHelper().getBytesForBinaryValue(//
                        objectValue, marshaller, xmlBinaryDataMapping.getMimeType(object));
                bytes = data.getData();
                c_id = marshaller.getAttachmentMarshaller().addMtomAttachment(bytes, 0, bytes.length, data.getMimeType(), localName, namespaceUri);
            }
        }

        // handle attributes
        if (xPathFragment.isAttribute()) {
            // if the CID is null there's nothing to write out
            if (c_id != null) {
                marshalRecord.attribute(xPathFragment, namespaceResolver, c_id);
            }else {
                String value = getValueToWrite(((Field) xmlBinaryDataMapping.getField()).getSchemaType(), objectValue, session);
                marshalRecord.attribute(xPathFragment, namespaceResolver, value);
            }
            marshalRecord.closeStartGroupingElements(groupingFragment);
            return true;
        }

        if (xmlBinaryDataMapping.isSwaRef() && (marshaller.getAttachmentMarshaller() != null)) {
            if(c_id != null) {
                marshalRecord.characters(c_id);
            } else {
                marshalRecord.characters(((Field) xmlBinaryDataMapping.getField()).getSchemaType(), objectValue, mimeType, false);
            }
        } else {
            if (marshalRecord.isXOPPackage() && !xmlBinaryDataMapping.shouldInlineBinaryData()) {
                if(c_id == null) {
                    marshalRecord.characters(((Field) xmlBinaryDataMapping.getField()).getSchemaType(), objectValue, mimeType, false);
                } else {
                    String xopPrefix = null;
                    // If the field's resolver is non-null and has an entry for XOP,
                    // use it - otherwise, create a new resolver, set the XOP entry,
                    // on it, and use it instead.
                    // We do this to avoid setting the XOP namespace declaration on
                    // a given field or descriptor's resolver, as it is only required
                    // on the current element

                    // 20061023: handle NPE on null NSR
                    if (namespaceResolver != null) {
                        xopPrefix = namespaceResolver.resolveNamespaceURI(Constants.XOP_URL);
                    }
                    boolean addDeclaration = false;
                    if (xopPrefix == null || namespaceResolver == null) {
                        addDeclaration = true;
                        xopPrefix = Constants.XOP_PREFIX;
                    }
                    XPathFragment xopInclude = new XPathFragment();
                    xopInclude.setLocalName("Include");
                    xopInclude.setPrefix(xopPrefix);
                    xopInclude.setNamespaceURI(Constants.XOP_URL);
                    marshalRecord.openStartElement(xopInclude, namespaceResolver);
                    marshalRecord.attribute(Constants.EMPTY_STRING, "href", "href", c_id);
                    if (addDeclaration) {
                        marshalRecord.namespaceDeclaration(xopPrefix,  Constants.XOP_URL);
                    }
                    marshalRecord.closeStartElement();
                    marshalRecord.endElement(xopInclude, namespaceResolver);
                    //marshal as an attachment
                }
            } else {
                marshalRecord.characters(((Field)xmlBinaryDataMapping.getField()).getSchemaType(), objectValue, mimeType, false);
            }
        }

        if(!xPathFragment.isSelfFragment()){
            marshalRecord.endElement(xPathFragment, namespaceResolver);
        }
        return true;
    }

    @Override
    public boolean startElement(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord, Attributes atts) {
        try {

            unmarshalRecord.removeNullCapableValue(this);
            Field xmlField = (Field) xmlBinaryDataMapping.getField();
            BinaryMappingContentHandler handler = new BinaryMappingContentHandler(unmarshalRecord, this, this.xmlBinaryDataMapping);
            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 ex) {
            throw XMLMarshalException.unmarshalException(ex);
        }
    }

    @Override
    public void endElement(XPathFragment xPathFragment, UnmarshalRecord unmarshalRecord) {
        unmarshalRecord.resetStringBuffer();
    }

    /**
     * Handle swaRef and inline attribute cases.
     */
    @Override
    public void attribute(UnmarshalRecord unmarshalRecord, String URI, String localName, String value) {
        unmarshalRecord.removeNullCapableValue(this);

        Object fieldValue = null;
        if (xmlBinaryDataMapping.isSwaRef()) {
            if (unmarshalRecord.getUnmarshaller().getAttachmentUnmarshaller() != null) {
                if (xmlBinaryDataMapping.getAttributeClassification() == XMLBinaryDataHelper.getXMLBinaryDataHelper().DATA_HANDLER) {
                    fieldValue = unmarshalRecord.getUnmarshaller().getAttachmentUnmarshaller().getAttachmentAsDataHandler(value);
                } else {
                    fieldValue = unmarshalRecord.getUnmarshaller().getAttachmentUnmarshaller().getAttachmentAsByteArray(value);
                }
                xmlBinaryDataMapping.setAttributeValueInObject(unmarshalRecord.getCurrentObject(), XMLBinaryDataHelper.getXMLBinaryDataHelper().convertObject(fieldValue, xmlBinaryDataMapping.getAttributeClassification(), unmarshalRecord.getSession(), null));
            }
        } else {
            // value should be base64 binary string
            fieldValue = ((ConversionManager) unmarshalRecord.getSession().getDatasourcePlatform().getConversionManager()).convertSchemaBase64ToByteArray(value);
            xmlBinaryDataMapping.setAttributeValueInObject(unmarshalRecord.getCurrentObject(), XMLBinaryDataHelper.getXMLBinaryDataHelper().convertObject(fieldValue, xmlBinaryDataMapping.getAttributeClassification(), unmarshalRecord.getSession(), null));
        }
    }

    @Override
    public void setNullValue(Object object, CoreSession session) {
        Object value = xmlBinaryDataMapping.getObjectValue(null, session);
        xmlBinaryDataMapping.setAttributeValueInObject(object, value);
    }

    @Override
    public boolean isNullCapableValue() {
        return xmlBinaryDataMapping.getNullPolicy().getIsSetPerformedForAbsentNode();
    }

    public DataHandler getDataHandlerForObjectValue(Object obj, Class classification) {
        if (classification == DataHandler.class) {
            return (DataHandler) obj;
        }
        return null;
    }

    public BinaryDataMapping getMapping() {
        return this.xmlBinaryDataMapping;
    }

    @Override
    public UnmarshalRecord buildSelfRecord(UnmarshalRecord unmarshalRecord, Attributes atts) {
        unmarshalRecord.removeNullCapableValue(this);
        BinaryDataUnmarshalRecord newRecord = new BinaryDataUnmarshalRecord(null, unmarshalRecord, this, xmlBinaryDataMapping);
        return newRecord;
    }

    @Override
    public void endSelfNodeValue(UnmarshalRecord unmarshalRecord, UnmarshalRecord selfRecord, Attributes attributes) {
        unmarshalRecord.resetStringBuffer();
    }

}
