/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.xml.ws.db.toplink;

import java.awt.Image;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
import java.util.Map;

import javax.activation.DataHandler;
import javax.mail.internet.MimeMultipart;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.attachment.AttachmentMarshaller;
import javax.xml.bind.attachment.AttachmentUnmarshaller;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.WebServiceException;

import org.eclipse.persistence.jaxb.JAXBContext;
import org.eclipse.persistence.jaxb.JAXBMarshaller;
import org.eclipse.persistence.jaxb.JAXBTypeElement;
import org.eclipse.persistence.jaxb.JAXBUnmarshaller;
import org.eclipse.persistence.jaxb.TypeMappingInfo;
import org.eclipse.persistence.oxm.record.XMLStreamWriterRecord;

import org.eclipse.persistence.internal.oxm.record.XMLStreamReaderInputSource;
import org.eclipse.persistence.internal.oxm.record.XMLStreamReaderReader;
import org.jvnet.staxex.Base64Data;
import org.jvnet.staxex.XMLStreamReaderEx;
import org.jvnet.staxex.XMLStreamWriterEx;

import org.w3c.dom.Node;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;


import com.sun.xml.ws.spi.db.BindingContext;
import com.sun.xml.ws.spi.db.XMLBridge;
import com.sun.xml.ws.spi.db.TypeInfo;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class JAXBBond<T> implements XMLBridge<T> {

    JAXBContextWrapper parent;
    TypeInfo typeInfo;
    TypeMappingInfo mappingInfo;
    boolean isParameterizedType = false;

    public JAXBBond(JAXBContextWrapper p, TypeInfo ti) {
        this.parent = p;
        this.typeInfo = ti;
        if (parent.infoMap != null) {
            mappingInfo = parent.infoMap.get(ti);
        }
        if (mappingInfo != null) {
            isParameterizedType = (mappingInfo.getType() instanceof ParameterizedType);
        }
    }

    @Override
    public BindingContext context() {
        return parent;
    }

    @Override
    public TypeInfo getTypeInfo() {
        return typeInfo;
    }

    @Override
    public void marshal(T object, XMLStreamWriter output,
            AttachmentMarshaller am) throws JAXBException {
        JAXBMarshaller marshaller = null;
        try {
            marshaller = parent.mpool.allocate();
            marshaller.setAttachmentMarshaller(am);
            marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FRAGMENT,
                    true);
            boolean isEx = (output instanceof XMLStreamWriterEx);
            if (mappingInfo != null) {
                if (isParameterizedType) {
                    JAXBTypeElement jte = new JAXBTypeElement(
                            mappingInfo.getXmlTagName(), object,
                            (ParameterizedType) mappingInfo.getType());
                    if (isEx) {
                        marshaller.marshal(jte, new NewStreamWriterRecord(
                                (XMLStreamWriterEx) output), mappingInfo);
                    } else {
                        marshaller.marshal(jte, output, mappingInfo);
                    }
                } else {
                    JAXBElement<T> elt = new JAXBElement<T>(
                            mappingInfo.getXmlTagName(),
                            (Class<T>) typeInfo.type, object);
                    if (isEx) {
                        marshaller.marshal(elt, new NewStreamWriterRecord(
                                (XMLStreamWriterEx) output), mappingInfo);
                    } else {
                        marshaller.marshal(elt, output, mappingInfo);
                    }
                }
            } else {
                if (isEx) {
                    marshaller.marshal(object, new NewStreamWriterRecord(
                            (XMLStreamWriterEx) output));
                } else {
                    marshaller.marshal(object, output);
                }
            }
        } finally {
            if (marshaller != null) {
                marshaller.setAttachmentMarshaller(null);
                parent.mpool.replace(marshaller);
            }
        }
    }

    // TODO NamespaceContext nsContext
    @Override
    public void marshal(T object, OutputStream output,
            NamespaceContext nsContext, AttachmentMarshaller am)
            throws JAXBException {
        JAXBMarshaller marshaller = null;

        try {
            marshaller = parent.mpool.allocate();
            marshaller.setAttachmentMarshaller(am);
            marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FRAGMENT,
                    true);
            if (mappingInfo != null) {
                if (isParameterizedType) {
                    JAXBTypeElement jte = new JAXBTypeElement(
                            mappingInfo.getXmlTagName(), object,
                            (ParameterizedType) mappingInfo.getType());
                    marshaller.marshal(jte, new StreamResult(output),
                            mappingInfo);
                } else {
                    JAXBElement<T> elt = new JAXBElement<T>(
                            mappingInfo.getXmlTagName(),
                            (Class<T>) mappingInfo.getType(), object);
                    // marshaller.marshal(elt, output);
                    // GAG missing
                    marshaller.marshal(elt, new StreamResult(output),
                            mappingInfo);
                }
            } else {
                marshaller.marshal(object, output);
            }
        } finally {
            if (marshaller != null) {
                marshaller.setAttachmentMarshaller(null);
                parent.mpool.replace(marshaller);
            }
        }
    }

    @Override
    public void marshal(T object, Node output) throws JAXBException {
        JAXBMarshaller marshaller = null;
        try {
            marshaller = parent.mpool.allocate();
            // marshaller.setAttachmentMarshaller(am);
            marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FRAGMENT,
                    true);
            if (mappingInfo != null) {
                if (isParameterizedType) {
                    JAXBTypeElement jte = new JAXBTypeElement(
                            mappingInfo.getXmlTagName(), object,
                            (ParameterizedType) mappingInfo.getType());
                    marshaller.marshal(jte, new DOMResult(output), mappingInfo);
                } else {
                    JAXBElement<T> elt = new JAXBElement<T>(
                            mappingInfo.getXmlTagName(),
                            (Class<T>) mappingInfo.getType(), object);
                    // marshaller.marshal(elt, output);
                    marshaller.marshal(elt, new DOMResult(output), mappingInfo);
                }
            } else {
                marshaller.marshal(object, output);
            }
        } finally {
            if (marshaller != null) {
                marshaller.setAttachmentMarshaller(null);
                parent.mpool.replace(marshaller);
            }
        }
    }

    @Override
    public void marshal(T object, ContentHandler contentHandler,
            AttachmentMarshaller am) throws JAXBException {
        JAXBMarshaller marshaller = null;
        try {
            marshaller = parent.mpool.allocate();
            marshaller.setAttachmentMarshaller(am);
            marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FRAGMENT,
                    true);
            if (mappingInfo != null) {
                if (isParameterizedType) {
                    JAXBTypeElement jte = new JAXBTypeElement(
                            mappingInfo.getXmlTagName(), object,
                            (ParameterizedType) mappingInfo.getType());
                    marshaller.marshal(jte, new SAXResult(contentHandler),
                            mappingInfo);
                } else {
                    JAXBElement<T> elt = new JAXBElement<T>(
                            mappingInfo.getXmlTagName(),
                            (Class<T>) mappingInfo.getType(), object);
                    // marshaller.marshal(elt, contentHandler);

                    // GAG missing
                    marshaller.marshal(elt, new SAXResult(contentHandler),
                            mappingInfo);

                    // marshaller.marshal(elt, contentHandler, mappingInfo);
                }
            } else {
                marshaller.marshal(object, contentHandler);
            }
        } finally {
            if (marshaller != null) {
                marshaller.setAttachmentMarshaller(null);
                parent.mpool.replace(marshaller);
            }
        }
    }

    @Override
    public void marshal(T object, Result result) throws JAXBException {
        JAXBMarshaller marshaller = null;
        try {
            marshaller = parent.mpool.allocate();
            marshaller.setAttachmentMarshaller(null);
            marshaller.setProperty(javax.xml.bind.Marshaller.JAXB_FRAGMENT,
                    true);
            if (mappingInfo != null) {
                if (isParameterizedType) {
                    JAXBTypeElement jte = new JAXBTypeElement(
                            mappingInfo.getXmlTagName(), object,
                            (ParameterizedType) mappingInfo.getType());
                    marshaller.marshal(jte, result, mappingInfo);
                } else {
                    JAXBElement<T> elt = new JAXBElement<T>(
                            mappingInfo.getXmlTagName(),
                            (Class<T>) mappingInfo.getType(), object);
                    // marshaller.marshal(elt, result);
                    marshaller.marshal(elt, result, mappingInfo);
                }
            } else {
                TypeMappingInfo tmi = null;
                if (object instanceof JAXBElement) {
                    QName q = ((JAXBElement) object).getName();
                    JAXBContext ctx = (JAXBContext) parent.getJAXBContext();

                    Map<TypeMappingInfo, QName> mtq = ctx
                            .getTypeMappingInfoToSchemaType();
                    for (Map.Entry<TypeMappingInfo, QName> es : mtq.entrySet()) {
                        if (q.equals(es.getValue())) {
                            tmi = es.getKey();
                            break;
                        }

                    }
                }
                if (tmi != null) {
                    marshaller.marshal(object, result, tmi);
                } else {
                    marshaller.marshal(object, result);
                }
            }
        } finally {
            if (marshaller != null) {
                marshaller.setAttachmentMarshaller(null);
                parent.mpool.replace(marshaller);
            }
        }
    }

    // This is used in RPC
    @Override
    public T unmarshal(XMLStreamReader in, AttachmentUnmarshaller au)
            throws JAXBException {
        JAXBUnmarshaller unmarshaller = null;
        try {
            QName tagName = null;
            if (in.getEventType() == XMLStreamConstants.START_ELEMENT) {
                tagName = in.getName();
            }

            unmarshaller = parent.upool.allocate();
            unmarshaller.setAttachmentUnmarshaller(au);

            Object o;
            if (in instanceof XMLStreamReaderEx) {
                CustomXMLStreamReaderReader cr = new CustomXMLStreamReaderReader();
                XMLStreamReaderInputSource is = new XMLStreamReaderInputSource(
                        in);
                SAXSource saxSource = new SAXSource(cr, is);
                o = ((mappingInfo != null) ? unmarshaller.unmarshal(saxSource,
                        mappingInfo) : unmarshaller.unmarshal(saxSource));
            } else {
                o = ((mappingInfo != null) ? unmarshaller.unmarshal(in,
                        mappingInfo) : unmarshaller.unmarshal(in));
            }

            if (o instanceof JAXBElement) {
                o = ((JAXBElement) o).getValue();
            }
            // TODO recycle to pool

            // Workaround for Eclipselink JAXB not consuming END_ELEMENT.
            try {
                if (in.getEventType() == XMLStreamConstants.END_ELEMENT && in.getName().equals(tagName)) {
                    in.next();
                }
            } catch (XMLStreamException e) {
                throw new WebServiceException(e);
            }
            return (T) o;
        } finally {
            if (unmarshaller != null) {
                unmarshaller.setAttachmentUnmarshaller(null);
                parent.upool.replace(unmarshaller);
            }
        }
    }

    @Override
    public T unmarshal(Source in, AttachmentUnmarshaller au)
            throws JAXBException {
        JAXBUnmarshaller unmarshaller = null;
        try {
            unmarshaller = parent.upool.allocate();
            unmarshaller.setAttachmentUnmarshaller(au);
            Object o;
            if (mappingInfo != null) {
                o = unmarshaller.unmarshal(in, mappingInfo);
            } else {
                o = unmarshaller.unmarshal(in);
            }
            if (o instanceof JAXBElement) {
                o = ((JAXBElement) o).getValue();
            }
            // TODO recycle to pool
            return (T) o;
        } finally {
            if (unmarshaller != null) {
                unmarshaller.setAttachmentUnmarshaller(null);
                parent.upool.replace(unmarshaller);
            }
        }
    }

    @Override
    public T unmarshal(InputStream in) throws JAXBException {
        JAXBUnmarshaller unmarshaller = null;
        try {
            unmarshaller = parent.upool.allocate();
            // GAG missing
            Object o = ((mappingInfo != null) ? unmarshaller.unmarshal(
                    new StreamSource(in), mappingInfo) : unmarshaller
                    .unmarshal(in));
            // Object o = unmarshaller.unmarshal(in);
            if (o instanceof JAXBElement) {
                o = ((JAXBElement) o).getValue();
            }
            // TODO recycle to pool
            return (T) o;
        } finally {
            if (unmarshaller != null) {
                unmarshaller.setAttachmentUnmarshaller(null);
                parent.upool.replace(unmarshaller);
            }
        }
    }

    @Override
    public T unmarshal(Node in, AttachmentUnmarshaller au) throws JAXBException {
        JAXBUnmarshaller unmarshaller = null;
        try {
            unmarshaller = parent.upool.allocate();
            unmarshaller.setAttachmentUnmarshaller(au);
            Object o = ((mappingInfo != null) ? unmarshaller.unmarshal(
                    new DOMSource(in), mappingInfo) : unmarshaller
                    .unmarshal(in));
            // Object o = unmarshaller.unmarshal(in);
            if (o instanceof JAXBElement) {
                o = ((JAXBElement) o).getValue();
            }
            // TODO recycle to pool
            return (T) o;
        } finally {
            if (unmarshaller != null) {
                unmarshaller.setAttachmentUnmarshaller(null);
                parent.upool.replace(unmarshaller);
            }
        }
    }

    /**
     * The combination of jaxws-ri and eclipselink-jaxb may perform better with
     * XMLStreamWriter
     */
    @Override
    public boolean supportOutputStream() {
        return false;
    }

    public static class CustomXMLStreamReaderReader extends XMLStreamReaderReader {

        @Override
        protected void parseCharactersEvent(XMLStreamReader xmlStreamReader)
                throws SAXException {
            // super.parseCharactersEvent(xmlStreamReader);
            XMLStreamReaderEx xrex = (XMLStreamReaderEx) xmlStreamReader;
            try {
                CharSequence characters = xrex.getPCDATA();
                if (characters instanceof Base64Data) {
                    contentHandler.characters(characters);
                } else {
                    String value = characters.toString();
                    contentHandler.characters(value.toCharArray(), 0,
                            value.length());
                }
            } catch (Exception ex) {
                throw new WebServiceException(ex);
            }
        }

        @Override
        public Object getValue(CharSequence characters, Class<?> dataType) {
            if (Base64Data.class.equals(characters.getClass())) {
                if (DataHandler.class == dataType || Image.class == dataType
                        || Source.class == dataType
                        || MimeMultipart.class == dataType) {
                    return ((Base64Data) characters).getDataHandler();
                } else if (byte[].class == dataType) {
                    return ((Base64Data) characters).getExact();
                }
            }
            return null;
        }
    }

    public static class NewStreamWriterRecord extends XMLStreamWriterRecord {

        private transient XMLStreamWriterEx xsw;

        public NewStreamWriterRecord(XMLStreamWriterEx xsw) {
            super(xsw);
            this.xsw = xsw;
        }

        @Override
        public void characters(QName schemaType, Object value, String mimeType, boolean isCData) {
            if (mimeType != null) {
                if (value instanceof DataHandler) {
                    try {
                        xsw.writeBinary((DataHandler) value);
                    } catch (XMLStreamException e) {
                        throw new WebServiceException(e);
                    }
                    return;
                } else if (value instanceof byte[]) {
                    try {
                        byte[] b = (byte[])value;
                        xsw.writeBinary(b,0,b.length, null);
                    } catch (XMLStreamException e) {
                        throw new WebServiceException(e);
                    }
                    return;
                }
            }
            super.characters(schemaType, value, mimeType, isCData);
        }
 
        private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException {
            throw new UnsupportedOperationException();
        }
        
        private void writeObject(ObjectOutputStream oos) throws IOException {
            throw new UnsupportedOperationException();
        }    
    }
}
