/**
 *
 * 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.openejb.jee.JavaWsdlMapping;
import org.apache.openejb.jee.JavaXmlTypeMapping;
import org.apache.openejb.jee.VariableMapping;

import static org.apache.openejb.server.axis.assembler.JaxRpcTypeInfo.SerializerType;

import javax.xml.namespace.QName;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class HeavyweightTypeInfoBuilder {
    private static final String SOAP_ENCODING_NS = "http://schemas.xmlsoap.org/soap/encoding/";
    private static final String XML_SCHEMA_NS = "http://www.w3.org/2001/XMLSchema";

    private static final Log LOG = LogFactory.getLog(HeavyweightTypeInfoBuilder.class);

    private final JavaWsdlMapping mapping;
    private final ClassLoader classLoader;
    private final XmlSchemaInfo schemaInfo;
    private final Set wrapperElementQNames;
    private final Collection<JaxRpcOperationInfo> operations;
    private final boolean hasEncoded;

    public HeavyweightTypeInfoBuilder(JavaWsdlMapping mapping, XmlSchemaInfo schemaInfo, ClassLoader classLoader, Set wrapperElementQNames, Collection<JaxRpcOperationInfo> operations, boolean hasEncoded) {
        this.mapping = mapping;
        this.classLoader = classLoader;
        this.schemaInfo = schemaInfo;
        this.wrapperElementQNames = wrapperElementQNames;
        this.operations = operations;
        this.hasEncoded = hasEncoded;
    }

    public List<JaxRpcTypeInfo> buildTypeInfo() throws OpenEJBException {
        List<JaxRpcTypeInfo> typeInfos = new ArrayList<>();

        Set<QName> mappedTypeQNames = new HashSet<>();

        //
        // Map types with explicity Java to XML mappings
        //
        for (JavaXmlTypeMapping javaXmlTypeMapping : mapping.getJavaXmlTypeMapping()) {
            // get the QName for this mapping
            QName qname;
            if (javaXmlTypeMapping.getRootTypeQname() != null) {
                qname = javaXmlTypeMapping.getRootTypeQname();

                // Skip the wrapper elements.
                if (wrapperElementQNames.contains(qname)) {
                    continue;
                }
            } else if (javaXmlTypeMapping.getAnonymousTypeQname() != null) {
                String anonTypeQNameString = javaXmlTypeMapping.getAnonymousTypeQname();

                // this appears to be ignored...
                int pos = anonTypeQNameString.lastIndexOf(":");
                if (pos == -1) {
                    throw new OpenEJBException("anon QName is invalid, no final ':' " + anonTypeQNameString);
                }
                String namespace = anonTypeQNameString.substring(0, pos);
                String localPart = anonTypeQNameString.substring(pos + 1);
                qname = new QName(namespace, localPart);

                // Skip the wrapper elements.
                // todo why is this +2
                if (wrapperElementQNames.contains(new QName(namespace, anonTypeQNameString.substring(pos + 2)))) {
                    continue;
                }
            } else {
                throw new OpenEJBException("either root type qname or anonymous type qname must be set");
            }

            // get the xml type qname of this mapping
            QName xmlTypeQName;
            if ("element".equals(javaXmlTypeMapping.getQNameScope())) {
                XmlElementInfo elementInfo = schemaInfo.elements.get(qname);
                if (elementInfo == null) {
                    LOG.warn("Element [" + qname + "] not been found in schema, known elements: " + schemaInfo.elements.keySet());
                }
                xmlTypeQName = elementInfo.xmlType;
            } else {
                xmlTypeQName = qname;
            }

            // finally, get the xml type info for the mapping
            XmlTypeInfo xmlTypeInfo = schemaInfo.types.get(xmlTypeQName);
            if (xmlTypeInfo == null) {
                // if this is a built in type then assume this is a redundant mapping
                if (WEBSERVICE_NAMESPACES.contains(xmlTypeInfo.qname.getNamespaceURI())) {
                    continue;
                }
                LOG.warn("Schema type QName [" + qname + "] not been found in schema: " + schemaInfo.types.keySet());
                continue;
            }

            // mark this type as mapped
            mappedTypeQNames.add(xmlTypeInfo.qname);

            // load the java class
            Class clazz;
            try {
                clazz = Class.forName(javaXmlTypeMapping.getJavaType(), false, classLoader);
            } catch (ClassNotFoundException e) {
                throw new OpenEJBException("Could not load java type " + javaXmlTypeMapping.getJavaType(), e);
            }

            // create the jax-rpc type mapping
            JaxRpcTypeInfo typeInfo = createTypeInfo(qname, xmlTypeInfo, clazz);
            mapFields(clazz, xmlTypeInfo, javaXmlTypeMapping, typeInfo);

            typeInfos.add(typeInfo);
        }

        //
        // Map types used in operations
        //
        for (JaxRpcOperationInfo operationInfo : operations) {
            List<JaxRpcParameterInfo> parameters = new ArrayList<>(operationInfo.parameters);

            // add the return type to the parameters so it is processed below
            if (operationInfo.returnXmlType != null) {
                JaxRpcParameterInfo returnParameter = new JaxRpcParameterInfo();
                returnParameter.xmlType = operationInfo.returnXmlType;
                returnParameter.javaType = operationInfo.returnJavaType;
                parameters.add(returnParameter);
            }

            // add type mappings for each parameter (including the return type)
            for (JaxRpcParameterInfo parameterInfo : parameters) {
                QName xmlType = parameterInfo.xmlType;

                // skip types that have already been mapped or are built in types
                if (xmlType == null ||
                    mappedTypeQNames.contains(xmlType) ||
                    xmlType.getNamespaceURI().equals(XML_SCHEMA_NS) ||
                    xmlType.getNamespaceURI().equals(SOAP_ENCODING_NS)) {
                    continue;
                }

                // get the xml type info
                XmlTypeInfo xmlTypeInfo = schemaInfo.types.get(xmlType);
                if (xmlTypeInfo == null) {
                    LOG.warn("Type QName [" + xmlType + "] defined by operation [" + operationInfo + "] has not been found in schema: " + schemaInfo.types.keySet());
                    continue;
                }
                mappedTypeQNames.add(xmlTypeInfo.qname);

                // load the java class
                Class<?> clazz;
                try {
                    clazz = classLoader.loadClass(parameterInfo.javaType);
                } catch (ClassNotFoundException e) {
                    throw new OpenEJBException("Could not load paramter");
                }

                // we only process simpleTypes and arrays (not normal complex types)
                if (xmlTypeInfo.simpleBaseType == null && !clazz.isArray()) {
                    if (!mappedTypeQNames.contains(xmlTypeInfo.qname)) {
                        // TODO: this lookup is not enough: the jaxrpc mapping file may define an anonymous mapping
                        LOG.warn("Operation " + operationInfo.name + "] uses XML type [" + xmlTypeInfo + "], whose mapping is not declared by the jaxrpc mapping file.\n Continuing deployment; " + "yet, the deployment is not-portable.");
                    }
                    continue;
                }

                // create the jax-rpc type mapping
                JaxRpcTypeInfo typeInfo = createTypeInfo(parameterInfo.qname, xmlTypeInfo, clazz);
                typeInfos.add(typeInfo);
            }
        }

        return typeInfos;
    }

    /**
     * Creates a JaxRpcTypeInfo based on the information contained in the XML Schema Type and Java Class.
     *
     * @param xmlTypeInfo the xml schema for the type
     * @param clazz       the java class for the type
     * @return the JaxRpcTypeInfo object
     * @throws OpenEJBException if the schema is invalid
     */
    private JaxRpcTypeInfo createTypeInfo(QName qname, XmlTypeInfo xmlTypeInfo, Class clazz) throws OpenEJBException {
        SerializerType serializerType;
        if (xmlTypeInfo.listType) {
            serializerType = SerializerType.LIST;
        } else if (clazz.isArray()) {
            serializerType = SerializerType.ARRAY;
        } else if (xmlTypeInfo.enumType) {
            serializerType = SerializerType.ENUM;
        } else {
            serializerType = SerializerType.OTHER;
        }

        JaxRpcTypeInfo typeInfo = new JaxRpcTypeInfo();
        typeInfo.qname = qname;
        typeInfo.javaType = clazz.getName();
        typeInfo.serializerType = serializerType;
        typeInfo.simpleBaseType = xmlTypeInfo.simpleBaseType;

        // If we understand the axis comments correctly, componentQName is never set for a webservice.
        if (serializerType == SerializerType.ARRAY) {
            typeInfo.componentType = xmlTypeInfo.arrayComponentType;
        }

        return typeInfo;
    }

    /**
     * Map the (nested) fields of a XML Schema Type to Java Beans properties or public fields of the specified Java Class.
     *
     * @param javaClass          the java class to map
     * @param xmlTypeInfo        the xml schema for the type
     * @param javaXmlTypeMapping the java to xml type mapping metadata
     * @param typeInfo           the JaxRpcTypeInfo for this type
     * @throws OpenEJBException if the XML Schema Type can not be mapped to the Java Class
     */
    private void mapFields(Class javaClass, XmlTypeInfo xmlTypeInfo, JavaXmlTypeMapping javaXmlTypeMapping, JaxRpcTypeInfo typeInfo) throws OpenEJBException {
        // Skip arrays since they can't define a variable-mapping element
        if (!javaClass.isArray()) {
            // if there is a variable-mapping, LOG a warning
            if (!javaXmlTypeMapping.getVariableMapping().isEmpty()) {
                LOG.warn("Ignoring variable-mapping defined for class " + javaClass + " which is an array.");
            }
            return;
        }

        // Index Java bean properties by name
        Map<String, Class> properties = new HashMap<>();
        try {
            PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(javaClass).getPropertyDescriptors();
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                properties.put(propertyDescriptor.getName(), propertyDescriptor.getPropertyType());
            }
        } catch (IntrospectionException e) {
            throw new OpenEJBException("Class " + javaClass + " is not a valid javabean", e);
        }

        for (VariableMapping variableMapping : javaXmlTypeMapping.getVariableMapping()) {
            String fieldName = variableMapping.getJavaVariableName();

            if (variableMapping.getXmlAttributeName() != null) {
                JaxRpcFieldInfo fieldInfo = new JaxRpcFieldInfo();
                fieldInfo.name = fieldName;

                // verify that the property exists on the java class
                Class javaType = properties.get(fieldName);
                if (javaType == null) {
                    throw new OpenEJBException("field name " + fieldName + " not found in " + properties);
                }

                String attributeLocalName = variableMapping.getXmlAttributeName();
                QName xmlName = new QName("", attributeLocalName);
                fieldInfo.xmlName = xmlName;

                fieldInfo.xmlType = xmlTypeInfo.attributes.get(attributeLocalName);
                if (fieldInfo.xmlType == null) {
                    throw new OpenEJBException("attribute " + xmlName + " not found in schema " + xmlTypeInfo.qname);
                }

                typeInfo.fields.add(fieldInfo);
            } else {
                JaxRpcFieldInfo fieldInfo = new JaxRpcFieldInfo();
                fieldInfo.isElement = true;
                fieldInfo.name = fieldName;

                // verify that the property exists on the java class or there is a public field
                Class javaType = properties.get(fieldName);
                if (javaType == null) {
                    //see if it is a public field
                    try {
                        Field field = javaClass.getField(fieldName);
                        javaType = field.getType();
                    } catch (NoSuchFieldException e) {
                        throw new OpenEJBException("field name " + fieldName + " not found in " + properties, e);
                    }
                }


                QName xmlName = new QName("", variableMapping.getXmlElementName());
                XmlElementInfo nestedElement = xmlTypeInfo.elements.get(xmlName);
                if (nestedElement == null) {
                    String ns = xmlTypeInfo.qname.getNamespaceURI();
                    xmlName = new QName(ns, variableMapping.getXmlElementName());
                    nestedElement = xmlTypeInfo.elements.get(xmlName);
                    if (nestedElement == null) {
                        throw new OpenEJBException("element " + xmlName + " not found in schema " + xmlTypeInfo.qname);
                    }
                }
                fieldInfo.isNillable = nestedElement.nillable || hasEncoded;
                fieldInfo.xmlName = xmlName;

                // xml type
                if (nestedElement.xmlType != null) {
                    fieldInfo.xmlType = nestedElement.xmlType;
                } else {
                    QName anonymousName;
                    if (xmlTypeInfo.anonymous) {
                        anonymousName = new QName(xmlTypeInfo.qname.getNamespaceURI(), xmlTypeInfo.qname.getLocalPart() +
                            ">" + nestedElement.qname.getLocalPart());
                    } else {
                        anonymousName = new QName(xmlTypeInfo.qname.getNamespaceURI(),
                            ">" + xmlTypeInfo.qname.getLocalPart() + ">" + nestedElement.qname.getLocalPart());
                    }
                    fieldInfo.xmlType = anonymousName;
                }

                if (javaType.isArray()) {
                    fieldInfo.minOccurs = nestedElement.minOccurs;
                    fieldInfo.maxOccurs = nestedElement.maxOccurs;
                }

                typeInfo.fields.add(fieldInfo);
            }
        }
    }

    /**
     * All of the known built in XML Schemas used by webservices.  This is used to supress unknown type exceptions
     */
    private static final Set<String> WEBSERVICE_NAMESPACES = Collections.unmodifiableSet(new LinkedHashSet<String>(Arrays.asList(
        "http://schemas.xmlsoap.org/soap/encoding/", // SOAP 1.1
        "http://www.w3.org/2003/05/soap-encoding",   // SOAP 1.2
        "http://xml.apache.org/xml-soap",            // Apache XMLSOAP
        "http://www.w3.org/1999/XMLSchema",          // XSD 1999
        "http://www.w3.org/2000/10/XMLSchema",       // XSD 2000
        "http://www.w3.org/2001/XMLSchema",          // XSD 2001
        "http://www.w3.org/XML/1998/namespace"       // XML (for xml-any)
    )));
}
