/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.cxf.binding.corba.runtime;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.cxf.binding.corba.CorbaBindingException;
import org.apache.cxf.binding.corba.CorbaTypeMap;
import org.apache.cxf.binding.corba.types.CorbaHandlerUtils;
import org.apache.cxf.binding.corba.types.CorbaObjectHandler;
import org.apache.cxf.binding.corba.types.CorbaStructListener;
import org.apache.cxf.binding.corba.types.CorbaTypeListener;
import org.apache.cxf.binding.corba.wsdl.ArgType;
import org.apache.cxf.service.model.ServiceInfo;
import org.omg.CORBA.ORB;

public class CorbaStreamWriter implements XMLStreamWriter {

    protected String defaultNS = "";
    protected CorbaTypeListener[] listeners;
    protected ServiceInfo serviceInfo;
    protected CorbaTypeMap typeMap;
    protected ORB orb;
    protected CorbaTypeListener currentTypeListener;
    protected CorbaNamespaceContext ctx = new CorbaNamespaceContext();

    private List<ArgType> params;
    private int paramCounter;

    private final Deque<QName> elements = new ArrayDeque<>();
    private QName paramElement;
    private QName wrapElementName;

    private boolean skipWrap;


    public CorbaStreamWriter(ORB orbRef,
                             CorbaTypeMap map,
                             ServiceInfo sinfo) {
        orb = orbRef;
        typeMap = map;
        serviceInfo = sinfo;
    }

    public CorbaStreamWriter(ORB orbRef,
                             List<ArgType> paramTypes,
                             CorbaTypeMap map,
                             ServiceInfo sinfo,
                             boolean wrap) {
        this(orbRef, map, sinfo);
        params = paramTypes;
        skipWrap = wrap;
        listeners = new CorbaTypeListener[paramTypes.size()];
    }

    public CorbaObjectHandler[] getCorbaObjects() {
        //REVISIT, we can make the CorbaTypeListener & CorbaObjectHandler the same object.
        CorbaObjectHandler[] handlers = new CorbaObjectHandler[listeners.length];
        for (int i = 0; i < listeners.length; i++) {
            if (listeners[i] == null) {
                throw new CorbaBindingException("Uninitalized object for parameter "
                                                + params.get(i).getName());
            }
            handlers[i] = listeners[i].getCorbaObject();
        }
        return handlers;
    }

    public void writeStartElement(String namespaceURI, String localName)
        throws XMLStreamException {
        writeStartElement("", localName, namespaceURI);
    }

    public void writeEndElement() throws XMLStreamException {
        if (elements.isEmpty()) {
            currentTypeListener = null;
        } else {
            QName name = elements.pop();
            if ((name.equals(paramElement)) || (name.equals(wrapElementName))) {
                // let the struct check if all members wer processed
                // it is necessary for empty sequences with minOccurs=0 as last elements of struct
                if (currentTypeListener instanceof CorbaStructListener) {
                    currentTypeListener.processEndElement(name);
                }
                currentTypeListener = null;
            } else {
                currentTypeListener.processEndElement(name);
            }
        }
        ctx.pop();
    }

    public void writeStartElement(java.lang.String localName)
        throws XMLStreamException {
        writeStartElement("", localName, defaultNS);
    }

    public void writeStartElement(java.lang.String prefix,
                                  java.lang.String localName,
                                  java.lang.String namespaceURI)
        throws XMLStreamException {
        ctx.push();
        if (prefix != null) {
            setPrefix(prefix, namespaceURI);
        }
        QName name = new QName(namespaceURI, localName, prefix);
        elements.push(name);
        if (!skipWrap) {
            if (currentTypeListener == null) {
                paramElement = name;
                setCurrentTypeListener(name);
            } else {
                currentTypeListener.processStartElement(name);
            }
        } else {
            wrapElementName = name;
            skipWrap = false;
        }
    }

    protected void setCurrentTypeListener(QName name) throws XMLStreamException {
        ArgType param = params.get(paramCounter);
        QName idlType = param.getIdltype();
        if (!skipWrap || (name.getLocalPart().equals(param.getName()))) {
            currentTypeListener = CorbaHandlerUtils.getTypeListener(name, idlType, typeMap, orb, serviceInfo);
            currentTypeListener.setNamespaceContext(ctx);
            listeners[paramCounter] = currentTypeListener;
            paramCounter++;
        } else {
            throw new XMLStreamException("Expected element not found: " + param.getName()
                                         + " (Found " + name.getLocalPart() + ")");
        }
    }

    public void writeEmptyElement(java.lang.String namespaceURI,
                                  java.lang.String localName)
        throws XMLStreamException {
        writeStartElement(namespaceURI, localName);
        writeEndElement();
    }

    public void writeEmptyElement(java.lang.String prefix,
                                  java.lang.String localName,
                                  java.lang.String namespaceURI)
        throws XMLStreamException {
        writeStartElement(namespaceURI, localName, prefix);
        writeEndElement();
    }

    public void writeEmptyElement(java.lang.String localName)
        throws XMLStreamException {
        writeStartElement(localName);
        writeEndElement();
    }

    public void writeEndDocument()
        throws XMLStreamException {
    }

    public void close()
        throws XMLStreamException {
    }

    public void flush()
        throws XMLStreamException {
    }

    public void writeAttribute(java.lang.String localName,
                               java.lang.String value)
        throws XMLStreamException {
        writeAttribute("", defaultNS, localName, value);
    }

    public void writeAttribute(java.lang.String prefix,
                               java.lang.String namespaceURI,
                               java.lang.String localName,
                               java.lang.String value)
        throws XMLStreamException {
        currentTypeListener.processWriteAttribute(prefix, namespaceURI, localName, value);
    }

    public void writeAttribute(java.lang.String namespaceURI,
                               java.lang.String localName,
                               java.lang.String value)
        throws XMLStreamException {
        writeAttribute("", namespaceURI, localName, value);
    }

    public void writeNamespace(java.lang.String prefix,
                               java.lang.String namespaceURI)
        throws XMLStreamException {
        if (currentTypeListener != null) {
            currentTypeListener.processWriteNamespace(prefix, namespaceURI);
        }
        setPrefix(prefix, namespaceURI);
    }

    public void writeDefaultNamespace(java.lang.String namespaceURI)
        throws XMLStreamException {
        defaultNS = namespaceURI;
    }

    public void writeComment(java.lang.String data)
        throws XMLStreamException {
    }

    public void writeProcessingInstruction(java.lang.String target)
        throws XMLStreamException {
    }

    public void writeProcessingInstruction(java.lang.String target,
                                           java.lang.String data)
        throws XMLStreamException {
    }

    public void writeCData(java.lang.String data)
        throws XMLStreamException {
        throw new XMLStreamException("Not yet implemented");
    }

    public void writeDTD(java.lang.String dtd)
        throws XMLStreamException {
    }

    public void writeEntityRef(java.lang.String name)
        throws XMLStreamException {
        throw new XMLStreamException("Not yet implemented");
    }

    public void writeStartDocument()
        throws XMLStreamException {
    }

    public void writeStartDocument(java.lang.String encoding,
                                   java.lang.String version)
        throws XMLStreamException {
    }

    public void writeStartDocument(java.lang.String version)
        throws XMLStreamException {
    }

    public void writeCharacters(java.lang.String text)
        throws XMLStreamException {
        currentTypeListener.processCharacters(text);
    }

    public void writeCharacters(char[] text,
                                int start,
                                int len)
        throws XMLStreamException {
        currentTypeListener.processCharacters(new String(text, start, len));
    }

    public java.lang.String getPrefix(java.lang.String uri)
        throws XMLStreamException {
        return ctx.getPrefix(uri);
    }

    public void setPrefix(java.lang.String prefix,
                          java.lang.String uri)
        throws XMLStreamException {
        ctx.setPrefix(prefix, uri);
    }

    public void setDefaultNamespace(java.lang.String uri)
        throws XMLStreamException {
        defaultNS = uri;
    }

    public void setNamespaceContext(NamespaceContext context)
        throws XMLStreamException {
        //ignore
    }

    public NamespaceContext getNamespaceContext() {
        return ctx;
    }

    public java.lang.Object getProperty(java.lang.String name)
        throws java.lang.IllegalArgumentException {
        return null;
    }



    public class CorbaNamespaceContext implements NamespaceContext {

        private Map<String, String> map;
        private CorbaNamespaceContext parent;

        public CorbaNamespaceContext() {
            this.map = new HashMap<>();
        }

        private CorbaNamespaceContext(Map<String, String> map, CorbaNamespaceContext p) {
            this.map = map;
            this.parent = p;
        }

        public void push() {
            parent = new CorbaNamespaceContext(map, parent);
            map = new HashMap<>();
        }
        public void pop() {
            if (parent != null) {
                map = parent.map;
                parent = parent.parent;
            }
        }

        public void setPrefix(String pfx, String ns) {
            map.put(pfx, ns);
        }
        public String getNamespaceURI(String prefix) {
            String answer = map.get(prefix);
            if (answer == null && parent != null) {
                return parent.getNamespaceURI(prefix);
            }
            return answer;
        }

        public String getPrefix(String namespaceURI) {
            for (Map.Entry<String, String> entry : map.entrySet()) {
                if (namespaceURI.equals(entry.getValue())) {
                    return entry.getKey();
                }
            }
            if (parent != null) {
                return parent.getPrefix(namespaceURI);
            }
            return null;
        }

        public Iterator<String> getPrefixes(String namespaceURI) {
            Set<String> set = new HashSet<>();
            for (Map.Entry<String, String> entry : map.entrySet()) {
                if (namespaceURI.equals(entry.getValue())) {
                    set.add(entry.getKey());
                }
            }
            if (parent != null) {
                Iterator<?> iter = parent.getPrefixes(namespaceURI);
                while (iter.hasNext()) {
                    set.add((String)iter.next());
                }
            }
            return set.iterator();
        }
    }


}
