/**
 *
 * 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.openejb.OpenEJBException;
import org.apache.openejb.jee.JavaWsdlMapping;
import org.apache.openejb.jee.PortComponent;
import org.apache.openejb.jee.ServiceEndpointInterfaceMapping;
import org.apache.openejb.jee.ServiceEndpointMethodMapping;

import javax.wsdl.Binding;
import javax.wsdl.BindingInput;
import javax.wsdl.BindingOperation;
import javax.wsdl.Port;
import javax.wsdl.extensions.soap.SOAPAddress;
import javax.wsdl.extensions.soap.SOAPBinding;
import javax.wsdl.extensions.soap.SOAPBody;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.xml.namespace.QName;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class JaxRpcServiceInfoBuilder {
    private final JavaWsdlMapping javaWsdlMapping;
    private final XmlSchemaInfo schemaInfo;
    private final PortComponent portComponent;
    private final Port port;
    private final String wsdlFile;
    private final ClassLoader classLoader;
    private JaxRpcServiceInfo serviceInfo;

    public JaxRpcServiceInfoBuilder(JavaWsdlMapping javaWsdlMapping, XmlSchemaInfo schemaInfo, PortComponent portComponent, Port port, String wsdlFile, ClassLoader classLoader) {
        this.javaWsdlMapping = javaWsdlMapping;
        this.schemaInfo = schemaInfo;
        this.portComponent = portComponent;
        this.port = port;
        this.wsdlFile = wsdlFile;
        this.classLoader = classLoader;
    }

    public JaxRpcServiceInfo createServiceInfo() throws OpenEJBException {
        Class serviceEndpointInterface;
        try {
            serviceEndpointInterface = classLoader.loadClass(portComponent.getServiceEndpointInterface());
        } catch (ClassNotFoundException e) {
            throw new OpenEJBException("Unable to load the service-endpoint interface for port-component " + portComponent.getPortComponentName(), e);
        }

        serviceInfo = new JaxRpcServiceInfo();
        serviceInfo.name = portComponent.getWsdlPort().toString();
        serviceInfo.serviceEndpointInterface = portComponent.getServiceEndpointInterface();
        serviceInfo.endpointURL = getAddressLocation(port.getExtensibilityElements());
        serviceInfo.wsdlFile = wsdlFile;
        serviceInfo.defaultBindingStyle = getStyle(port.getBinding());

        // The service is using lightweight mappings if there are no Java to XML service mappings
        boolean isLightweight = javaWsdlMapping.getServiceEndpointInterfaceMapping().isEmpty();

        //
        // Map Operations
        //
        Set wrapperElementQNames = buildOperations(port.getBinding(), serviceEndpointInterface, isLightweight);

        //
        // Map Types
        //
        Collection<JaxRpcTypeInfo> types;
        if (isLightweight) {
            LightweightTypeInfoBuilder builder = new LightweightTypeInfoBuilder(javaWsdlMapping, schemaInfo, classLoader);
            types = builder.buildTypeInfo();
        } else {
            HeavyweightTypeInfoBuilder builder = new HeavyweightTypeInfoBuilder(javaWsdlMapping, schemaInfo, classLoader, wrapperElementQNames, serviceInfo.operations, serviceInfo.defaultBindingStyle.isEncoded());
            types = builder.buildTypeInfo();
        }
        serviceInfo.types.addAll(types);

        return serviceInfo;
    }

    private Set<QName> buildOperations(Binding binding, Class serviceEndpointInterface, boolean lightweight) throws OpenEJBException {
        Set<QName> wrappedElementQNames = new HashSet<>();


        for (Object op : binding.getBindingOperations()) {
            BindingOperation bindingOperation = (BindingOperation) op;
            String operationName = bindingOperation.getOperation().getName();

            if (lightweight) {
                // Lightweight mappings are solely based on the Java method
                Method method = getMethodForOperation(operationName, serviceEndpointInterface);

                // Build the operation info using the method
                LightweightOperationInfoBuilder operationInfoBuilder = new LightweightOperationInfoBuilder(bindingOperation, method);
                JaxRpcOperationInfo operationInfo = operationInfoBuilder.buildOperationInfo();
                serviceInfo.operations.add(operationInfo);
            } else {
                // Heavyweight mappings are solely based on the Java to XML mapping declarations
                ServiceEndpointMethodMapping methodMapping = getMethodMappingForOperation(operationName, serviceEndpointInterface);

                // Build the operation info using the Java to XML method mapping
                HeavyweightOperationInfoBuilder operationInfoBuilder = new HeavyweightOperationInfoBuilder(bindingOperation, methodMapping, javaWsdlMapping, schemaInfo);
                JaxRpcOperationInfo operationInfo = operationInfoBuilder.buildOperationInfo();
                serviceInfo.operations.add(operationInfo);

                // remember wrapped elements for type mapping
                Set<QName> wrappedElementQNamesForOper = operationInfoBuilder.getWrapperElementQNames();
                wrappedElementQNames.addAll(wrappedElementQNamesForOper);
            }
        }

        return wrappedElementQNames;
    }

    private BindingStyle getStyle(Binding binding) throws OpenEJBException {
        SOAPBinding soapBinding = getExtensibilityElement(SOAPBinding.class, binding.getExtensibilityElements());
        String styleString = soapBinding.getStyle();

        BindingInput bindingInput = ((BindingOperation) binding.getBindingOperations().get(0)).getBindingInput();
        SOAPBody soapBody = getExtensibilityElement(SOAPBody.class, bindingInput.getExtensibilityElements());
        String useString = soapBody.getUse();

        BindingStyle bindingStyle = BindingStyle.getBindingStyle(styleString, useString);
        return bindingStyle;
    }

    private String getAddressLocation(List extensibilityElements) throws OpenEJBException {
        SOAPAddress soapAddress = getExtensibilityElement(SOAPAddress.class, extensibilityElements);
        String locationURIString = soapAddress.getLocationURI();
        return locationURIString;
    }

    private Method getMethodForOperation(String operationName, Class serviceEndpointInterface) throws OpenEJBException {
        Method found = null;
        for (Method method : serviceEndpointInterface.getMethods()) {
            if (method.getName().equals(operationName)) {
                if (found != null) {
                    throw new OpenEJBException("Overloaded methods are not allowed in lightweight mappings");
                }
                found = method;
            }
        }
        if (found == null) {
            throw new OpenEJBException("No method found for operation named " + operationName);
        }
        return found;
    }

    private ServiceEndpointMethodMapping getMethodMappingForOperation(String operationName, Class serviceEndpointInterface) throws OpenEJBException {
        // get mapping for service endpoint interface
        ServiceEndpointInterfaceMapping interfaceMapping = javaWsdlMapping.getServiceEndpointInterfaceMappingMap().get(serviceEndpointInterface.getName());
        if (interfaceMapping == null) {
            throw new OpenEJBException("No java-wsdl mapping found for the service interface " + serviceEndpointInterface);
        }

        // match by operation name
        for (ServiceEndpointMethodMapping methodMapping : interfaceMapping.getServiceEndpointMethodMapping()) {
            if (operationName.equals(methodMapping.getWsdlOperation())) {
                return methodMapping;
            }
        }

        // failed - throw nice exception message
        StringBuffer availOps = new StringBuffer(128);
        for (ServiceEndpointMethodMapping methodMapping : interfaceMapping.getServiceEndpointMethodMapping()) {
            if (availOps.length() > 0) availOps.append(",");
            availOps.append(methodMapping.getWsdlOperation());
        }
        throw new OpenEJBException("No method found for operation named '" + operationName + "'. Available operations: " + availOps);
    }

    public static <T extends ExtensibilityElement> T getExtensibilityElement(Class<T> clazz, List extensibilityElements) throws OpenEJBException {
        for (Object o : extensibilityElements) {
            ExtensibilityElement extensibilityElement = (ExtensibilityElement) o;
            if (clazz.isAssignableFrom(extensibilityElement.getClass())) {
                return clazz.cast(extensibilityElement);
            }
        }
        throw new OpenEJBException("No element of class " + clazz.getName() + " found");
    }
}