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

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.wsdl.Binding;
import javax.wsdl.Definition;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Namespace;

import org.apache.cxf.binding.corba.utils.CorbaObjectReferenceHelper;
import org.apache.cxf.binding.corba.utils.CorbaUtils;
import org.apache.cxf.binding.corba.utils.EprMetaData;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.service.model.ServiceInfo;
import org.apache.cxf.wsdl11.WSDLServiceBuilder;
import org.omg.CORBA.ORB;

public final class CorbaObjectReferenceEventProducer extends AbstractStartEndEventProducer {

    static final String WSDLI_NAMESPACE_URI = "http://www.w3.org/2006/01/wsdl-instance";
    static final String XSI_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance";
    static final QName WSA_ADDRESS = new QName(CorbaObjectReferenceHelper.ADDRESSING_NAMESPACE_URI,
                                               "Address");
    static final List<Attribute> IS_NIL_OBJ_REF_ATTRS;
    static {
        XMLEventFactory factory = XMLEventFactory.newInstance();
        IS_NIL_OBJ_REF_ATTRS = new ArrayList<>();
        IS_NIL_OBJ_REF_ATTRS.add(factory.createAttribute(new QName(XSI_NAMESPACE_URI, "nil", "xsi"), "true"));
    }
    private static final String INFER_FROM_TYPE_ID = "InferFromTypeId";
    private static final Logger LOG = LogUtils.getL7dLogger(CorbaObjectReferenceEventProducer.class);

    List<Attribute> refAttrs;

    public CorbaObjectReferenceEventProducer(CorbaObjectHandler h, ServiceInfo service, ORB orbRef) {
        CorbaObjectReferenceHandler handler = (CorbaObjectReferenceHandler) h;
        name = CorbaUtils.processQName(handler.getName(), service);
        orb = orbRef;
        serviceInfo = service;
        refAttrs = null;

        if (handler.getReference() == null) {
            refAttrs = IS_NIL_OBJ_REF_ATTRS;
            return;
        }

        List<CorbaTypeEventProducer> objRefProducers = new ArrayList<>();

        String address = orb.object_to_string(handler.getReference());
        objRefProducers.add(new CorbaAddressEventProducer(address));

        Definition wsdlDef = (Definition)serviceInfo.getProperty(WSDLServiceBuilder.WSDL_DEFINITION);

        // Get the TypeImpl of the object reference so that we can determine the binding
        // needed for this object reference
        org.apache.cxf.binding.corba.wsdl.Object objType =
            (org.apache.cxf.binding.corba.wsdl.Object)handler.getType();
        QName bindingName = objType.getBinding();
        if (bindingName != null) {
            EprMetaData eprInfo = null;
            if (INFER_FROM_TYPE_ID.equalsIgnoreCase(bindingName.getLocalPart())) {
                String typeId = CorbaObjectReferenceHelper.extractTypeIdFromIOR(address);
                if (!StringUtils.isEmpty(typeId)) {
                    eprInfo = getEprMetadataForTypeId(wsdlDef, typeId);
                } else {
                    LOG.log(Level.SEVERE, "For binding with value \"" + INFER_FROM_TYPE_ID
                                    + "\" the type_id of the object reference IOR must be set to its most"
                                    + " derived type. It is currently null indicating CORBA:Object."
                                    + " Address Url=" + address);
                }
            } else {
                eprInfo = getEprMetadataForBindingName(wsdlDef, bindingName);
            }

            if (eprInfo.isValid()) {
                LOG.log(Level.FINE, "Epr metadata " + eprInfo);
                // Create the meta data producer and add its child producers.
                String wsdlLoc = CorbaObjectReferenceHelper.getWSDLLocation(eprInfo.getCandidateWsdlDef());
                CorbaServiceNameEventProducer nameProducer =
                    new CorbaServiceNameEventProducer(eprInfo.getServiceQName(), eprInfo.getPortName());
                QName interfaceName = eprInfo.getBinding().getPortType().getQName();
                CorbaInterfaceNameEventProducer interfaceProducer =
                    new CorbaInterfaceNameEventProducer(interfaceName);
                CorbaMetaDataEventProducer metaProducer =
                    new CorbaMetaDataEventProducer(wsdlLoc, nameProducer, interfaceProducer);
                objRefProducers.add(metaProducer);
            }
        }
        producers = objRefProducers.iterator();
    }

    private EprMetaData getEprMetadataForBindingName(Definition wsdlDef, QName bindingName) {
        EprMetaData info = getObjectReferenceBinding(wsdlDef, bindingName);
        CorbaObjectReferenceHelper.populateEprInfo(info);
        return info;
    }

    private EprMetaData getEprMetadataForTypeId(Definition wsdlDef, String typeId) {
        EprMetaData info = CorbaObjectReferenceHelper.getBindingForTypeId(typeId, wsdlDef);
        CorbaObjectReferenceHelper.populateEprInfo(info);
        return info;
    }

    public List<Attribute> getAttributes() {
        if (currentEventProducer != null) {
            return currentEventProducer.getAttributes();
        }
        return refAttrs;
    }

    protected EprMetaData getObjectReferenceBinding(Definition wsdlDef, QName bindingName) {
        EprMetaData info = new EprMetaData();
        Binding wsdlBinding = wsdlDef.getBinding(bindingName);

        // If the binding name does not have a namespace associated with it, then we'll need to
        // get the list of all bindings and compare their local parts against our name.
        if (wsdlBinding == null && bindingName.getNamespaceURI().isEmpty()
            && !bindingName.getLocalPart().isEmpty()) {
            Collection<Binding> bindingsCollection = CastUtils.cast(wsdlDef.getBindings().values());
            for (Binding b : bindingsCollection) {
                if (b.getQName().getLocalPart().equals(bindingName.getLocalPart())) {
                    wsdlBinding = b;
                    break;
                }
            }
        }

        if (wsdlBinding != null) {
            info.setBinding(wsdlBinding);
            info.setCandidateWsdlDef(wsdlDef);
        }

        return info;
    }

    // An event producer to handle the production of the Address XML data.
    class CorbaAddressEventProducer implements CorbaTypeEventProducer {
        int state;

        int[] states = {XMLStreamConstants.START_ELEMENT,
                        XMLStreamConstants.CHARACTERS,
                        XMLStreamConstants.END_ELEMENT};
        final String address;

        CorbaAddressEventProducer(String value) {
            address = value;
        }

        public String getLocalName() {
            return WSA_ADDRESS.getLocalPart();
        }

        public String getText() {
            return address;
        }

        public int next() {
            return states[state++];
        }

        public QName getName() {
            return WSA_ADDRESS;
        }

        public boolean hasNext() {
            return state < states.length;
        }

        public List<Attribute> getAttributes() {
            return null;
        }

        public List<Namespace> getNamespaces() {
            return null;
        }
    }

    // An event producer to handle the production of the Metadata XML data.  This producer will rely
    // on two additional producers to handle the production of sub-events.
    class CorbaMetaDataEventProducer extends AbstractStartEndEventProducer {

        CorbaServiceNameEventProducer svcProducer;
        CorbaInterfaceNameEventProducer intfProducer;
        List<Attribute> metaAttrs;

        CorbaMetaDataEventProducer(CorbaServiceNameEventProducer svc) {
            this(null, svc, null);
        }

        CorbaMetaDataEventProducer(String location,
                                          CorbaServiceNameEventProducer svc,
                                          CorbaInterfaceNameEventProducer intf) {
            name = new QName(CorbaObjectReferenceHelper.ADDRESSING_NAMESPACE_URI, "Metadata");

            List<CorbaTypeEventProducer> metaDataProducers = new ArrayList<>();
            metaDataProducers.add(svc);

            if (intf != null) {
                metaDataProducers.add(intf);
            }

            producers = metaDataProducers.iterator();

            if (location != null) {
                XMLEventFactory factory = XMLEventFactory.newInstance();
                metaAttrs = new ArrayList<>();
                metaAttrs.add(factory.createAttribute(
                        new QName(WSDLI_NAMESPACE_URI, "wsdlLocation", "objrefns1"), location));
            }

        }

        public List<Attribute> getAttributes() {
            if (currentEventProducer != null) {
                return currentEventProducer.getAttributes();
            }
            return metaAttrs;
        }

    }

    // An event producer to handle the production of the ServiceName XML data.
    class CorbaServiceNameEventProducer implements CorbaTypeEventProducer {
        int state;
        int[] states = {XMLStreamConstants.START_ELEMENT,
                        XMLStreamConstants.CHARACTERS,
                        XMLStreamConstants.END_ELEMENT};
        QName serviceName;
        QName name;

        List<Attribute> attributes;
        List<Namespace> namespaces;

        CorbaServiceNameEventProducer(QName svc, String ep) {
            serviceName = svc;

            name = new QName(CorbaObjectReferenceHelper.ADDRESSING_WSDL_NAMESPACE_URI,
                             "ServiceName");

            XMLEventFactory factory = XMLEventFactory.newInstance();

            attributes = new ArrayList<>();
            attributes.add(factory.createAttribute("EndpointName", ep));

            namespaces = new ArrayList<>();
            namespaces.add(factory.createNamespace("objrefns2", svc.getNamespaceURI()));
        }

        public String getLocalName() {
            return name.getLocalPart();
        }

        public String getText() {
            return namespaces.get(0).getPrefix() + ":" + serviceName.getLocalPart();
        }

        public int next() {
            return states[state++];
        }

        public boolean hasNext() {
            return state < states.length;
        }

        public QName getName() {
            return name;
        }

        public List<Attribute> getAttributes() {
            return attributes;
        }

        public List<Namespace> getNamespaces() {
            return namespaces;
        }
    }

    // An event producer to handle the production of the InterfaceName XML data.
    class CorbaInterfaceNameEventProducer implements CorbaTypeEventProducer {
        int state;
        int[] states = {XMLStreamConstants.START_ELEMENT,
                        XMLStreamConstants.CHARACTERS,
                        XMLStreamConstants.END_ELEMENT};
        QName interfaceName;
        QName name;
        List<Namespace> namespaces;

        CorbaInterfaceNameEventProducer(QName intf) {
            interfaceName = intf;

            name = new QName(CorbaObjectReferenceHelper.ADDRESSING_WSDL_NAMESPACE_URI,
                             "InterfaceName");

            XMLEventFactory factory = XMLEventFactory.newInstance();
            namespaces = new ArrayList<>();
            namespaces.add(factory.createNamespace("objrefns2", intf.getNamespaceURI()));
        }

        public String getLocalName() {
            return name.getLocalPart();
        }

        public String getText() {
            return namespaces.get(0).getPrefix() + ":" + interfaceName.getLocalPart();
        }

        public int next() {
            return states[state++];
        }

        public boolean hasNext() {
            return state < states.length;
        }

        public QName getName() {
            return name;
        }

        public List<Attribute> getAttributes() {
            return null;
        }

        public List<Namespace> getNamespaces() {
            return namespaces;
        }
    }
}
