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

import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jws.WebService;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingType;
import javax.xml.ws.Provider;
import javax.xml.ws.Service;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.WebServiceProvider;
import javax.xml.ws.soap.SOAPBinding;

import org.apache.cxf.common.classloader.ClassLoaderUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PackageUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.jaxb.JAXBEncoderDecoder;

public class JaxWsImplementorInfo {

    private static final Logger LOG = LogUtils.getL7dLogger(JaxWsImplementorInfo.class);
    private static final ResourceBundle BUNDLE = LOG.getResourceBundle();

    private Class<?> implementorClass;
    private Class<?> seiClass;
    private ParameterizedType seiType;
    private List<WebService> wsAnnotations = new ArrayList<>(2);
    private WebServiceProvider wsProviderAnnotation;

    public JaxWsImplementorInfo(Class<?> ic) {
        implementorClass = ic;
        initialize();
    }

    public Class<?> getSEIClass() {
        return seiClass;
    }
    public ParameterizedType getSEIType() {
        return seiType;
    }

    public Class<?> getImplementorClass() {
        return implementorClass;
    }

    public Class<?> getEndpointClass() {
        Class<?> endpointInterface = getSEIClass();
        if (null == endpointInterface) {
            endpointInterface = getImplementorClass();
        }
        return endpointInterface;
    }

    public String getWsdlLocation() {
        for (WebService service : wsAnnotations) {
            if (!StringUtils.isEmpty(service.wsdlLocation())) {
                return service.wsdlLocation();
            }
        }

        if (null != wsProviderAnnotation
            && !StringUtils.isEmpty(wsProviderAnnotation.wsdlLocation())) {
            return wsProviderAnnotation.wsdlLocation();
        }
        return null;
    }

    /**
     * See use of targetNamespace in {@link WebService}.
     *
     * @return the qualified name of the service.
     */
    public QName getServiceName() {
        String serviceName = null;
        String namespace = null;

        // serviceName cannot be specified on SEI so check impl class only
        if (!wsAnnotations.isEmpty()) {
            int offset = 1;
            if (seiClass == null) {
                offset = 0;
            }
            //traverse up the parent impl classes for this info as well, but
            //not the last one which would be the sei annotation
            for (int x = 0; x < wsAnnotations.size() - offset; x++) {
                if (StringUtils.isEmpty(serviceName)) {
                    serviceName = wsAnnotations.get(x).serviceName();
                }
                if (StringUtils.isEmpty(namespace)) {
                    namespace = wsAnnotations.get(x).targetNamespace();
                }
            }
        }

        if ((serviceName == null || namespace == null)
            && wsProviderAnnotation != null) {
            serviceName = wsProviderAnnotation.serviceName();
            namespace = wsProviderAnnotation.targetNamespace();
        }

        if (StringUtils.isEmpty(serviceName)) {
            serviceName = implementorClass.getSimpleName() + "Service";
        }

        if (StringUtils.isEmpty(namespace)) {
            namespace = getDefaultNamespace(implementorClass);
        }

        return new QName(namespace, serviceName);
    }

    /**
     * See use of targetNamespace in {@link WebService}.
     *
     * @return the qualified name of the endpoint.
     */
    public QName getEndpointName() {
        String portName = null;
        String namespace = null;
        String name = null;

        // portName cannot be specified on SEI so check impl class only
        if (!wsAnnotations.isEmpty()) {
            int offset = 1;
            if (seiClass == null) {
                offset = 0;
            }
            //traverse up the parent impl classes for this info as well, but
            //not the last one which would be the sei annotation
            for (int x = 0; x < wsAnnotations.size() - offset; x++) {
                if (StringUtils.isEmpty(portName)) {
                    portName = wsAnnotations.get(x).portName();
                }
                if (StringUtils.isEmpty(namespace)) {
                    namespace = wsAnnotations.get(x).targetNamespace();
                }
                if (StringUtils.isEmpty(name)) {
                    name = wsAnnotations.get(x).name();
                }
            }
        }

        if ((portName == null || namespace == null)
            && wsProviderAnnotation != null) {
            portName = wsProviderAnnotation.portName();
            namespace = wsProviderAnnotation.targetNamespace();
        }
        if (StringUtils.isEmpty(portName)
            && !StringUtils.isEmpty(name)) {
            portName = name + "Port";
        }
        if (StringUtils.isEmpty(portName)) {
            portName = implementorClass.getSimpleName() + "Port";
        }

        if (StringUtils.isEmpty(namespace)) {
            namespace = getDefaultNamespace(implementorClass);
        }

        return new QName(namespace, portName);
    }

    public QName getInterfaceName() {
        String name = null;
        String namespace = null;

        if (seiClass != null) {
            WebService service = seiClass.getAnnotation(WebService.class);
            if (!StringUtils.isEmpty(service.name())) {
                name = service.name();
            }
            if (!StringUtils.isEmpty(service.targetNamespace())) {
                namespace = service.targetNamespace();
            }
        } else {
            for (WebService service : wsAnnotations) {
                if (!StringUtils.isEmpty(service.name()) && name == null) {
                    name = service.name();
                }
                if (!StringUtils.isEmpty(service.targetNamespace()) && namespace == null) {
                    namespace = service.targetNamespace();
                }
            }
        }
        if (name == null) {
            if (seiClass != null) {
                name = seiClass.getSimpleName();
            } else if (implementorClass != null) {
                name = implementorClass.getSimpleName();
            }
        }
        if (namespace == null) {
            if (seiClass != null) {
                namespace = getDefaultNamespace(seiClass);
            } else if (implementorClass != null) {
                namespace = getDefaultNamespace(implementorClass);
            }
        }

        return new QName(namespace, name);
    }

    private String getDefaultNamespace(Class<?> clazz) {
        String pkg = PackageUtils.getNamespace(PackageUtils.getPackageName(clazz));
        return StringUtils.isEmpty(pkg) ? "http://unknown.namespace/" : pkg;
    }

    private String getWSInterfaceName(Class<?> implClz) {
        if (implClz.isInterface()
            && implClz.getAnnotation(WebService.class) != null) {
            return implClz.getName();
        }
        Class<?>[] clzs = implClz.getInterfaces();
        for (Class<?> clz : clzs) {
            if (null != clz.getAnnotation(WebService.class)) {
                return clz.getName();
            }
        }
        return null;
    }

    private String getImplementorClassName() {
        for (WebService service : wsAnnotations) {
            if (!StringUtils.isEmpty(service.endpointInterface())) {
                return service.endpointInterface();
            }
        }
        return null;
    }

    protected static boolean ifAnnotationLoadedByOtherClassLoader(Class<?> cls,
        Class<? extends Annotation> annotationClass) {
        for (Annotation an : cls.getAnnotations()) {
            if (an.annotationType() != null
                && annotationClass.getName().equals(an.annotationType().getName())) {
                return true;
            }
        }
        return false;

    }
    private void initialize() {
        Class<?> cls = implementorClass;
        while (cls != null) {
            WebService annotation = cls.getAnnotation(WebService.class);
            if (annotation != null) {
                wsAnnotations.add(annotation);
                if (cls.isInterface()) {
                    cls = null;
                }
            } else {
                // check if there are annotations has the same name with WebServices
                if (ifAnnotationLoadedByOtherClassLoader(cls, WebService.class)) {
                    LOG.log(Level.WARNING,
                            "WEBSERVICE_ANNOTATIONS_IS_LOADED_BY_OTHER_CLASSLOADER",
                            WebService.class.getName());
                }
            }
            if (cls != null) {
                cls = cls.getSuperclass();
            }
        }
        String sei = getImplementorClassName();
        boolean seiFromWsAnnotation = true;
        if (StringUtils.isEmpty(sei)) {
            seiFromWsAnnotation = false;
            sei = getWSInterfaceName(implementorClass);
        }
        if (!StringUtils.isEmpty(sei)) {
            try {
                seiClass = ClassLoaderUtils.loadClass(sei, implementorClass);
            } catch (ClassNotFoundException ex) {
                throw new WebServiceException(BUNDLE.getString("SEI_LOAD_FAILURE_MSG"), ex);
            }
            WebService seiAnnotation = seiClass.getAnnotation(WebService.class);
            if (null == seiAnnotation) {
                throw new WebServiceException(BUNDLE.getString("SEI_WITHOUT_WEBSERVICE_ANNOTATION_EXC"));
            }
            if (seiFromWsAnnotation
                && (!StringUtils.isEmpty(seiAnnotation.portName())
                || !StringUtils.isEmpty(seiAnnotation.serviceName())
                || !StringUtils.isEmpty(seiAnnotation.endpointInterface()))) {
                String expString = BUNDLE.getString("ILLEGAL_ATTRIBUTE_IN_SEI_ANNOTATION_EXC");
                throw new WebServiceException(expString);
            }
            wsAnnotations.add(seiAnnotation);

            for (int x = implementorClass.getInterfaces().length - 1; x >= 0; x--) {
                if (seiClass.equals(implementorClass.getInterfaces()[x])) {
                    Type type = implementorClass.getGenericInterfaces()[x];
                    if (type instanceof ParameterizedType) {
                        seiType = (ParameterizedType)type;
                    }
                }
            }
        }

        wsProviderAnnotation = getWebServiceProviderAnnotation(implementorClass);
    }

    private static WebServiceProvider getWebServiceProviderAnnotation(Class<?> cls) {
        if (cls == null) {
            return null;
        }
        WebServiceProvider ann = cls.getAnnotation(WebServiceProvider.class);
        if (null != ann) {
            return ann;
        }
        if (ifAnnotationLoadedByOtherClassLoader(cls, WebServiceProvider.class)) {
            LOG.log(Level.WARNING,
                "WEBSERVICE_ANNOTATIONS_IS_LOADED_BY_OTHER_CLASSLOADER",
                WebServiceProvider.class.getName());
        }
        for (Class<?> inf : cls.getInterfaces()) {
            if (null != inf.getAnnotation(WebServiceProvider.class)) {
                return inf.getAnnotation(WebServiceProvider.class);
            }
            if (ifAnnotationLoadedByOtherClassLoader(cls, WebServiceProvider.class)) {
                LOG.log(Level.WARNING,
                        "WEBSERVICE_ANNOTATIONS_IS_LOADED_BY_OTHER_CLASSLOADER",
                        WebServiceProvider.class.getName());
            }
        }
        return getWebServiceProviderAnnotation(cls.getSuperclass());
    }

    public boolean isWebServiceProvider() {
        return Provider.class.isAssignableFrom(implementorClass);
    }

    public WebServiceProvider getWsProvider() {
        return wsProviderAnnotation;
    }

    public Service.Mode getServiceMode() {
        ServiceMode m = implementorClass.getAnnotation(ServiceMode.class);
        if (m != null && m.value() != null) {
            return m.value();
        }
        return Service.Mode.PAYLOAD;
    }

    public Class<?> getProviderParameterType() {
        return doGetProviderParameterType(implementorClass);
    }

    private static Class<?> doGetProviderParameterType(Class<?> c) {
        while (c != null) {
            Type[] intfTypes = c.getGenericInterfaces();
            for (Type t : intfTypes) {
                Class<?> clazz = JAXBEncoderDecoder.getClassFromType(t);
                if (Provider.class.isAssignableFrom(clazz)) {
                    if (Provider.class == clazz) {
                        Type[] paramTypes = ((ParameterizedType)t).getActualTypeArguments();
                        return JAXBEncoderDecoder.getClassFromType(paramTypes[0]);
                    }
                    return doGetProviderParameterType(clazz);
                }
            }
            c = c.getSuperclass();
        }
        return null;
    }

    public String getBindingType() {
        BindingType bType = implementorClass.getAnnotation(BindingType.class);
        if (bType != null) {
            return bType.value();
        }
        return SOAPBinding.SOAP11HTTP_BINDING;
    }
}
