/**
 *
 * 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.openejb.server.axis.assembler;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.openejb.OpenEJBException;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaAttribute;
import org.apache.ws.commons.schema.XmlSchemaChoice;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.apache.ws.commons.schema.XmlSchemaComplexContentRestriction;
import org.apache.ws.commons.schema.XmlSchemaComplexType;
import org.apache.ws.commons.schema.XmlSchemaContent;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaEnumerationFacet;
import org.apache.ws.commons.schema.XmlSchemaGroupBase;
import org.apache.ws.commons.schema.XmlSchemaObject;
import org.apache.ws.commons.schema.XmlSchemaObjectCollection;
import org.apache.ws.commons.schema.XmlSchemaParticle;
import org.apache.ws.commons.schema.XmlSchemaSequence;
import org.apache.ws.commons.schema.XmlSchemaSimpleType;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeContent;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeList;
import org.apache.ws.commons.schema.XmlSchemaSimpleTypeRestriction;
import org.apache.ws.commons.schema.XmlSchemaType;
import org.apache.ws.commons.schema.XmlSchemaContentModel;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import javax.xml.namespace.QName;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;

public class CommonsSchemaInfoBuilder {
    private static final Log LOG = LogFactory.getLog(CommonsSchemaInfoBuilder.class);
    private static final String XML_SCHEMA_NS = "http://www.w3.org/2001/XMLSchema";
    private static final String XML_NS_NS = "http://www.w3.org/2000/xmlns/";
    private static final String SOAP_ENCODING_NS = "http://schemas.xmlsoap.org/soap/encoding/";
    private static final QName SOAP_ARRAY = new QName(SOAP_ENCODING_NS, "Array");
    private static final QName SOAP_ARRAY_TYPE = new QName(SOAP_ENCODING_NS, "arrayType");
    private static final QName WSDL_ARRAY_TYPE = new QName("http://schemas.xmlsoap.org/wsdl/", "arrayType");

    private final XmlSchemaCollection xmlSchemaCollection;

    private final Map<QName, XmlTypeInfo> xmlTypes = new HashMap<>();
    private final Map<QName, XmlElementInfo> xmlElements = new HashMap<>();

    public CommonsSchemaInfoBuilder(JarFile moduleFile, URI wsdlUri) throws OpenEJBException {
        if (moduleFile == null) throw new NullPointerException("moduleFile is null");
        if (wsdlUri == null) throw new NullPointerException("wsdlUri is null");

        CommonsSchemaLoader schemaLoader = new CommonsSchemaLoader(wsdlUri, moduleFile);
        xmlSchemaCollection = schemaLoader.loadSchema();
    }

    public CommonsSchemaInfoBuilder(XmlSchemaCollection xmlSchemaCollection) {
        if (xmlSchemaCollection == null) throw new NullPointerException("schemaTypeSystem is null");
        this.xmlSchemaCollection = xmlSchemaCollection;
    }

    public XmlSchemaInfo createSchemaInfo() throws OpenEJBException {

        buildXmlTypeInfos();

        XmlSchemaInfo schemaInfo = new XmlSchemaInfo();
        schemaInfo.types.putAll(xmlTypes);
        schemaInfo.elements.putAll(xmlElements);
        return schemaInfo;
    }


    private void buildXmlTypeInfos() {
        for (XmlSchema schema : xmlSchemaCollection.getXmlSchemas()) {
            // Global Elements
            for (Iterator iterator = schema.getElements().getValues(); iterator.hasNext(); ) {
                XmlSchemaElement globalElement = (XmlSchemaElement) iterator.next();
                addGlobalElement(globalElement);
            }

            // Global Types
            for (Iterator iterator = schema.getSchemaTypes().getValues(); iterator.hasNext(); ) {
                XmlSchemaType globalType = (XmlSchemaType) iterator.next();
                addType(globalType.getQName(), globalType);
            }
        }
    }

    private void addGlobalElement(XmlSchemaElement element) {
        // Nested anonymous type
        QName xmlType = element.getSchemaTypeName();
        if (xmlType == null) {
            // Rule 1.b: Anonymous type inside an element ">E"
            xmlType = new QName(element.getQName().getNamespaceURI(), ">" + element.getQName().getLocalPart());
            addType(xmlType, element.getSchemaType());
        }

        // create the XmlElementInfo
        XmlElementInfo elementInfo = createXmlElementInfo(element.getQName(), xmlType, element);
        xmlElements.put(element.getQName(), elementInfo);

    }

    private static XmlElementInfo createXmlElementInfo(QName qname, QName xmlType, XmlSchemaElement element) {
        XmlElementInfo elementInfo = new XmlElementInfo();

        elementInfo.qname = qname;
        elementInfo.xmlType = xmlType;
        elementInfo.minOccurs = element.getMinOccurs();
        elementInfo.maxOccurs = element.getMaxOccurs();
        elementInfo.nillable = element.isNillable();

        return elementInfo;
    }

    private void addType(QName typeQName, XmlSchemaType type) {
        // skip built in xml schema types
        if (XML_SCHEMA_NS.equals(typeQName.getNamespaceURI())) {
            return;
        }

        XmlTypeInfo typeInfo = createXmlTypeInfo(typeQName, type);
        xmlTypes.put(typeQName, typeInfo);

        if (type instanceof XmlSchemaComplexType) {
            XmlSchemaComplexType complexType = (XmlSchemaComplexType) type;

            // process elements nested inside of this element
            List<XmlSchemaElement> elements = getNestedElements(complexType);
            for (XmlSchemaElement element : elements) {
                addNestedElement(element, typeInfo);
            }
        }
    }

    private void addNestedElement(XmlSchemaElement element, XmlTypeInfo enclosingType) {
        QName elementQName;
        QName typeQName;
        if (element.getRefName() == null) {
            //
            // Normal element in a type
            //

            // Element Name with namespace
            String elementNamespace = element.getQName().getNamespaceURI();
            if (elementNamespace == null || elementNamespace.equals("")) {
                elementNamespace = enclosingType.qname.getNamespaceURI();
            }
            elementQName = new QName(elementNamespace, element.getQName().getLocalPart());

            // Type name
            if (element.getSchemaTypeName() != null) {
                // Global type
                typeQName = element.getSchemaTypeName();
            } else {
                // Anonymous type, so we need to declare it

                // Rule 2.b: Anonymous element absolute name "T>N"
                String anonymoustName = enclosingType.qname.getLocalPart() + ">" + elementQName.getLocalPart();
                QName anonymousQName = new QName(elementNamespace, anonymoustName);

                // Rule 1.b: Anonymous type name ">E"
                typeQName = new QName(elementNamespace, ">" + anonymousQName.getLocalPart());
                addType(typeQName, element.getSchemaType());
            }
        } else {
            //
            // Referenced global element
            //

            // Local the referenced global element
            XmlSchemaElement refElement = xmlSchemaCollection.getElementByQName(element.getRefName());

            // The name and type of the nested element are determined by the referenced element
            elementQName = refElement.getQName();
            typeQName = refElement.getSchemaTypeName();
        }

        // Add element to enclosing type
        XmlElementInfo nestedElement = createXmlElementInfo(elementQName, typeQName, element);
        enclosingType.elements.put(nestedElement.qname, nestedElement);
    }

    public static XmlTypeInfo createXmlTypeInfo(QName qname, XmlSchemaType type) {
        if (qname == null) throw new NullPointerException("qname is null");
        if (type == null) throw new NullPointerException("type is null");

        XmlTypeInfo typeInfo = new XmlTypeInfo();
        typeInfo.qname = qname;
        typeInfo.anonymous = qname.getLocalPart().indexOf('>') >= 0;

        if (type instanceof XmlSchemaSimpleType) {
            XmlSchemaSimpleType simpleType = (XmlSchemaSimpleType) type;
            XmlSchemaSimpleTypeContent content = simpleType.getContent();
            if (content instanceof XmlSchemaSimpleTypeList) {
                XmlSchemaSimpleTypeList list = (XmlSchemaSimpleTypeList) content;
                typeInfo.simpleBaseType = list.getItemType().getQName();

                // this is a list
                typeInfo.listType = true;
            } else if (content instanceof XmlSchemaSimpleTypeRestriction) {
                XmlSchemaSimpleTypeRestriction restriction = (XmlSchemaSimpleTypeRestriction) content;
                typeInfo.simpleBaseType = restriction.getBaseTypeName();

                // is this an enumeration?
                for (Iterator iterator = restriction.getFacets().getIterator(); iterator.hasNext(); ) {
                    if (iterator.next() instanceof XmlSchemaEnumerationFacet) {
                        typeInfo.enumType = true;
                        break;
                    }

                }
            }
        } else if (type instanceof XmlSchemaComplexType) {
            XmlSchemaComplexType complexType = (XmlSchemaComplexType) type;

            // SOAP array component type
            typeInfo.arrayComponentType = extractSoapArrayComponentType(complexType);

            // process attributes (skip soap arrays which have non-mappable attributes)
            if (!isSoapArray(complexType)) {
                XmlSchemaObjectCollection attributes = complexType.getAttributes();
                for (Iterator iterator = attributes.getIterator(); iterator.hasNext(); ) {
                    Object item = iterator.next();
                    if (item instanceof XmlSchemaAttribute) {
                        XmlSchemaAttribute attribute = (XmlSchemaAttribute) item;
                        Object old = typeInfo.attributes.put(attribute.getQName().getLocalPart(), attribute.getSchemaTypeName());
                        if (old != null) {
                            throw new IllegalArgumentException("Complain to your expert group member, spec does not support attributes with the same local name and differing namespaces: original: " + old + ", duplicate local name: " + attribute);
                        }
                    }
                }
            }
        } else {
            LOG.warn("Unknown schema type class " + typeInfo.getClass().getName());
        }

        return typeInfo;
    }

    private static boolean isSoapArray(XmlSchemaComplexType complexType) {
        // Soap arrays are based on complex content restriction
        XmlSchemaContentModel contentModel = complexType.getContentModel();
        if (contentModel == null) {
            return false;
        }
        XmlSchemaContent content = contentModel.getContent();
        if (!(content instanceof XmlSchemaComplexContentRestriction)) {
            return false;
        }

        XmlSchemaComplexContentRestriction restriction = (XmlSchemaComplexContentRestriction) content;
        return SOAP_ARRAY.equals(restriction.getBaseTypeName());
    }

    /**
     * Extract the nested component type of an Array from the XML Schema Type.
     *
     * @return the QName of the nested component type or null if the schema type can not be determined
     * @throws org.apache.openejb.OpenEJBException if the XML Schema Type can not represent an Array @param complexType
     */
    private static QName extractSoapArrayComponentType(XmlSchemaComplexType complexType) {
        // Soap arrays are based on complex content restriction
        if (!isSoapArray(complexType)) {
            return null;
        }

        XmlSchemaComplexContentRestriction restriction = (XmlSchemaComplexContentRestriction) complexType.getContentModel().getContent();

        //First, handle case that looks like this:
        // <complexType name="ArrayOfstring">
        //     <complexContent>
        //         <restriction base="soapenc:Array">
        //             <attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:string[]"/>
        //         </restriction>
        //     </complexContent>
        // </complexType>
        XmlSchemaObjectCollection attributes = restriction.getAttributes();
        for (Iterator iterator = attributes.getIterator(); iterator.hasNext(); ) {
            Object item = iterator.next();
            if (item instanceof XmlSchemaAttribute) {
                XmlSchemaAttribute attribute = (XmlSchemaAttribute) item;
                if (attribute.getRefName().equals(SOAP_ARRAY_TYPE)) {
                    for (Attr attr : attribute.getUnhandledAttributes()) {
                        QName attQName = new QName(attr.getNamespaceURI(), attr.getLocalName());
                        if (WSDL_ARRAY_TYPE.equals(attQName)) {
                            // value is a namespace prefixed xsd type
                            String value = attr.getValue();

                            // extract local part
                            int pos = value.lastIndexOf(":");
                            QName componentType;
                            if (pos < 0) {
                                componentType = new QName("", value);
                            } else {
                                String localPart = value.substring(pos + 1);

                                // resolve the namespace prefix
                                String prefix = value.substring(0, pos);
                                String namespace = getNamespaceForPrefix(prefix, attr.getOwnerElement());

                                componentType = new QName(namespace, localPart);
                            }
                            LOG.debug("determined component type from element type");
                            return componentType;
                        }
                    }
                }
            }
        }

        // If that didn't work, try to handle case like this:
        // <complexType name="ArrayOfstring1">
        //     <complexContent>
        //         <restriction base="soapenc:Array">
        //             <sequence>
        //                 <element name="string1" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
        //             </sequence>
        //         </restriction>
        //     </complexContent>
        // </complexType>
        XmlSchemaParticle particle = restriction.getParticle();
        if (particle instanceof XmlSchemaSequence) {
            XmlSchemaSequence sequence = (XmlSchemaSequence) particle;
            if (sequence.getItems().getCount() != 1) {
                throw new IllegalArgumentException("more than one element inside array definition: " + complexType);
            }
            XmlSchemaObject item = sequence.getItems().getItem(0);
            if (item instanceof XmlSchemaElement) {
                XmlSchemaElement element = (XmlSchemaElement) item;
                QName componentType = element.getSchemaTypeName();
                LOG.debug("determined component type from element type");
                return componentType;
            }
        }

        return null;
    }

    private static String getNamespaceForPrefix(String prefix, Element element) {
        NamedNodeMap attributes = element.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
            Node node = attributes.item(i);
            if (node instanceof Attr) {
                Attr attr = (Attr) node;
                if (XML_NS_NS.equals(attr.getNamespaceURI())) {
                    // this is a namespace declaration, is it the one we are looking for?
                    if (attr.getLocalName().equals(prefix)) {
                        return attr.getValue();
                    }
                }
            }
        }

        // try parent
        if (element.getParentNode() instanceof Element) {
            return getNamespaceForPrefix(prefix, (Element) element.getParentNode());
        }

        // didn't find it - just use prefix as the namespace
        return prefix;
    }


    private static List<XmlSchemaElement> getNestedElements(XmlSchemaComplexType complexType) {
        List<XmlSchemaElement> elements = new ArrayList<>();
        XmlSchemaParticle particle = complexType.getParticle();
        if (particle instanceof XmlSchemaElement) {
            XmlSchemaElement element = (XmlSchemaElement) particle;
            elements.add(element);
        } else if (particle instanceof XmlSchemaGroupBase && !(particle instanceof XmlSchemaChoice)) {
            XmlSchemaGroupBase groupBase = (XmlSchemaGroupBase) particle;
            for (Iterator iterator = groupBase.getItems().getIterator(); iterator.hasNext(); ) {
                XmlSchemaParticle child = (XmlSchemaParticle) iterator.next();
                if (child instanceof XmlSchemaElement) {
                    XmlSchemaElement element = (XmlSchemaElement) child;
                    elements.add(element);
                }
            }
        } else {
            // ignore all other types... you can have these other types, but JAX-RPC doesn't support them
        }
        return elements;
    }
}