/*
 * 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:
// mmacivor - September 09/2009 - 1.0 - Initial implementation
package org.eclipse.persistence.oxm.record;

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

import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;

import org.eclipse.persistence.exceptions.XMLMarshalException;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.oxm.NamespaceResolver;
import org.eclipse.persistence.internal.oxm.XPathFragment;
import org.eclipse.persistence.internal.oxm.record.DomToXMLEventWriter;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

public class XMLEventWriterRecord extends MarshalRecord {
    private Map<String, String> prefixMapping;
    private NamespaceResolver namespaceResolver;

    private XMLEventWriter xmlEventWriter;
    private XMLEventFactory xmlEventFactory;
    private DomToXMLEventWriter domToXMLEventWriter;
    private boolean isStartElementOpen = false;
    private List attributes;
    private List namespaceDeclarations;
    private XPathFragment xPathFragment;

    public XMLEventWriterRecord(XMLEventWriter xmlEventWriter) {
        this.xmlEventWriter = xmlEventWriter;
        this.xmlEventFactory = XMLEventFactory.newInstance();
        this.domToXMLEventWriter = new DomToXMLEventWriter(xmlEventFactory);
    }

    public XMLEventWriter getXMLEventWriter() {
        return xmlEventWriter;
    }

    public void setXMLEventWriter(XMLEventWriter anXMLEventWriter) {
        this.xmlEventWriter = anXMLEventWriter;
    }

    @Override
    public void attribute(XPathFragment xPathFragment, NamespaceResolver namespaceResolver, String value) {
         String namespaceURI = xPathFragment.getNamespaceURI();
         XMLEvent event;
         if(namespaceURI == null) {
             event = xmlEventFactory.createAttribute(xPathFragment.getLocalName(), value);
         } else {
             String prefix = getPrefixForFragment(xPathFragment);
             if(prefix == null) {
                 event = xmlEventFactory.createAttribute(null, namespaceURI, xPathFragment.getLocalName(), value);
             } else {
                 if(xmlEventWriter.getNamespaceContext().getNamespaceURI(prefix) == null || !xmlEventWriter.getNamespaceContext().getNamespaceURI(prefix).equals(namespaceURI)) {
                     event = xmlEventFactory.createNamespace(prefix, namespaceURI);
                     try {
                         this.xmlEventWriter.setPrefix(prefix, namespaceURI);
                     } catch(XMLStreamException e) {
                         throw XMLMarshalException.marshalException(e);
                     }
                 }
                 event = xmlEventFactory.createAttribute(prefix, namespaceURI, xPathFragment.getLocalName(), value);
             }
         }
         if(event.isNamespace()) {
             if(null == this.namespaceDeclarations) {
                 this.namespaceDeclarations = new ArrayList();
             }
             this.namespaceDeclarations.add(event);
         } else {
             if(null == attributes) {
                 attributes = new ArrayList();
             }
             this.attributes.add(event);
         }
    }

    @Override
    public void attribute(String namespaceURI, String localName, String name, String value) {
         XMLEvent event;
         if(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI)) {
             try {
                 if(javax.xml.XMLConstants.XMLNS_ATTRIBUTE.equals(localName)) {
                     event = xmlEventFactory.createNamespace(value);
                     xmlEventWriter.setDefaultNamespace(value);
                 }  else {
                      event = xmlEventFactory.createNamespace(localName, value);
                      xmlEventWriter.setPrefix(localName, value);
                 }
             } catch(XMLStreamException e) {
                 throw XMLMarshalException.marshalException(e);
             }
         } else {
             if(namespaceURI == null || namespaceURI.length() == 0) {
                 event = xmlEventFactory.createAttribute(localName, value);
             } else {
                 int index = name.indexOf(':');
                 if(index == -1) {
                     event = xmlEventFactory.createAttribute(Constants.EMPTY_STRING, namespaceURI, localName, value);
                 } else {
                     String prefix = name.substring(0, index);
                     event = xmlEventFactory.createAttribute(prefix, namespaceURI, localName, value);
                 }
             }
         }
         if(event.isNamespace()) {
             if(null == this.namespaceDeclarations) {
                 this.namespaceDeclarations = new ArrayList();
             }
             this.namespaceDeclarations.add(event);
         } else {
             if(null == this.attributes) {
                 this.attributes = new ArrayList();
             }
             this.attributes.add(event);
         }
    }

    private void openAndCloseStartElement() {
        try {
            String namespaceURI = xPathFragment.getNamespaceURI();
             if(null == namespaceURI) {
                 Iterator attributesIterator = null;
                 if(null != attributes) {
                     attributesIterator = attributes.iterator();
                 }
                 Iterator namespaceDeclarationsIterator = null;
                 if(null != namespaceDeclarations) {
                     namespaceDeclarationsIterator = namespaceDeclarations.iterator();
                 }
                 xmlEventWriter.add(xmlEventFactory.createStartElement(Constants.EMPTY_STRING, Constants.EMPTY_STRING, xPathFragment.getLocalName(), attributesIterator, namespaceDeclarationsIterator));
                 String defaultNamespace = xmlEventWriter.getNamespaceContext().getNamespaceURI(Constants.EMPTY_STRING);
                 if(defaultNamespace != null && defaultNamespace.length() > 0 ) {
                     xmlEventWriter.setDefaultNamespace(Constants.EMPTY_STRING);
                     xmlEventWriter.add(xmlEventFactory.createNamespace(Constants.EMPTY_STRING));
                 }
             } else {
                 String prefix = getPrefixForFragment(xPathFragment);
                 if(null == prefix) {
                     prefix = Constants.EMPTY_STRING;
                 }
                 Iterator attributesIterator = null;
                 if(null != attributes) {
                     attributesIterator = attributes.iterator();
                 }
                 Iterator namespaceDeclarationsIterator = null;
                 if(null != namespaceDeclarations) {
                     namespaceDeclarationsIterator = namespaceDeclarations.iterator();
                 }
                 xmlEventWriter.add(xmlEventFactory.createStartElement(prefix, namespaceURI, xPathFragment.getLocalName(), attributesIterator, namespaceDeclarationsIterator));
             }
        } catch(XMLStreamException ex) {
            throw XMLMarshalException.marshalException(ex);
        }
    }

    @Override
    public void cdata(String value) {
        try {
            if(isStartElementOpen) {
                openAndCloseStartElement();
                isStartElementOpen = false;
            }
            for(String part : MarshalRecord.splitCData(value)) {
                xmlEventWriter.add(xmlEventFactory.createCData(part));
            }
        } catch(Exception e) {
            throw XMLMarshalException.marshalException(e);
       }
    }

    @Override
    public void characters(String value) {
        try {
            if(isStartElementOpen) {
                openAndCloseStartElement();
                isStartElementOpen = false;
            }
            xmlEventWriter.add(xmlEventFactory.createCharacters(value));
        } catch(Exception e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    @Override
    public void closeStartElement() {
    }

    @Override
    public void openStartElement(XPathFragment xPathFragment, NamespaceResolver namespaceResolver) {
        super.openStartElement(xPathFragment, namespaceResolver);
        if (isStartElementOpen) {
            openAndCloseStartElement();
        }
        isStartElementOpen = true;
        this.xPathFragment = xPathFragment;
        this.attributes = null;
        this.namespaceDeclarations = null;

        if(xPathFragment.isGeneratedPrefix()){
            namespaceDeclaration(xPathFragment.getPrefix(), xPathFragment.getNamespaceURI());
        }

        writePrefixMappings();
    }

    @Override
    public void element(XPathFragment frag) {
        try {
            if(isStartElementOpen) {
                openAndCloseStartElement();
                isStartElementOpen = false;
            }
            String namespaceURI = frag.getNamespaceURI();
            String localName = frag.getLocalName();
            String prefix = getPrefixForFragment(xPathFragment);
            if(null == prefix) {
                prefix = Constants.EMPTY_STRING;
            }
            xmlEventWriter.add(xmlEventFactory.createStartElement(prefix, namespaceURI, localName));
            xmlEventWriter.add(xmlEventFactory.createEndElement(prefix, namespaceURI, localName));
        } catch(Exception e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    @Override
    public void endDocument() {
        try {
            if(isStartElementOpen) {
                openAndCloseStartElement();
                isStartElementOpen = false;
            }
            xmlEventWriter.add(xmlEventFactory.createEndDocument());
            xmlEventWriter.flush();
        } catch(Exception e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    @Override
    public void endElement(XPathFragment pathFragment, NamespaceResolver namespaceResolver) {
        if(isStartElementOpen) {
            openAndCloseStartElement();
            isStartElementOpen = false;
        }
        String prefix = getPrefixForFragment(pathFragment);
        if(null == prefix) {
            prefix = Constants.EMPTY_STRING;
        }
        try {
            xmlEventWriter.add(xmlEventFactory.createEndElement(prefix, xPathFragment.getNamespaceURI(), pathFragment.getLocalName()));
        } catch(Exception e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    @Override
    public void node(Node node, NamespaceResolver resolver, String uri, String name) {
        if(isStartElementOpen) {
            openAndCloseStartElement();
            isStartElementOpen = false;
        }
        try {
            if(node.getNodeType() == Node.DOCUMENT_NODE) {
                node = ((Document)node).getDocumentElement();
            }
            domToXMLEventWriter.writeToEventWriter(node, uri, name, xmlEventWriter);
        } catch(XMLStreamException e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    @Override
    public void startDocument(String encoding, String version) {
        try {
            xmlEventWriter.add(this.xmlEventFactory.createStartDocument(encoding, version, false));
        } catch(Exception e) {
            throw XMLMarshalException.marshalException(e);
        }
    }

    @Override
    public void startPrefixMapping(String prefix, String namespaceUri) {
        if(null == this.prefixMapping) {
            this.prefixMapping = new HashMap<String, String>();
        }
        this.prefixMapping.put(prefix, namespaceUri);
    }

    private void writePrefixMappings() {
        if(null != namespaceResolver) {
            String defaultNamespace = namespaceResolver.getDefaultNamespaceURI();
            if(defaultNamespace != null) {
                XMLEvent namespace = xmlEventFactory.createNamespace(Constants.EMPTY_STRING, defaultNamespace);
                if(null == namespaceDeclarations) {
                    namespaceDeclarations = new ArrayList();
                }
                namespaceDeclarations.add(namespace);
            }
            if(this.namespaceResolver.hasPrefixesToNamespaces()) {
                for(Map.Entry<String, String> entry:this.namespaceResolver.getPrefixesToNamespaces().entrySet()) {
                    XMLEvent namespace = xmlEventFactory.createNamespace(entry.getKey(), entry.getValue());
                    if(null == namespaceDeclarations) {
                        namespaceDeclarations = new ArrayList();
                    }
                    namespaceDeclarations.add(namespace);
                }
            }
            namespaceResolver = null;
        }
        if(null != prefixMapping) {
            for(Map.Entry<String, String> entry:this.prefixMapping.entrySet()) {
                XMLEvent namespace = xmlEventFactory.createNamespace(entry.getKey(), entry.getValue());
                if(null == namespaceDeclarations) {
                    namespaceDeclarations = new ArrayList();
                }
                namespaceDeclarations.add(namespace);
           }
            prefixMapping = null;
        }
    }

    @Override
    public void namespaceDeclarations(NamespaceResolver namespaceResolver) {
    }

    @Override
    public void startPrefixMappings(NamespaceResolver namespaceResolver) {
        this.namespaceResolver = namespaceResolver;
    }

}
