/*
 * 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.axis2.jaxws.description.builder.converter;

import org.apache.axis2.jaxws.description.builder.BindingTypeAnnot;
import org.apache.axis2.jaxws.description.builder.DescriptionBuilderComposite;
import org.apache.axis2.jaxws.description.builder.FieldDescriptionComposite;
import org.apache.axis2.jaxws.description.builder.MethodDescriptionComposite;
import org.apache.axis2.jaxws.description.builder.ServiceModeAnnot;
import org.apache.axis2.jaxws.description.builder.WebFaultAnnot;
import org.apache.axis2.jaxws.description.builder.WebServiceAnnot;
import org.apache.axis2.jaxws.description.builder.WebServiceProviderAnnot;
import org.apache.axis2.jaxws.description.builder.WebServiceRefAnnot;
import org.apache.axis2.jaxws.util.ClassLoaderUtils;
import org.apache.axis2.java.security.AccessController;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.jws.WebService;
import javax.xml.ws.BindingType;
import javax.xml.ws.ServiceMode;
import javax.xml.ws.WebFault;
import javax.xml.ws.WebServiceClient;
import javax.xml.ws.WebServiceProvider;
import javax.xml.ws.WebServiceRef;
import javax.xml.ws.WebServiceRefs;
import javax.xml.ws.spi.WebServiceFeatureAnnotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;

public class JavaClassToDBCConverter {
    private static final Log log = LogFactory.getLog(JavaClassToDBCConverter.class);

    private Class serviceClass;

    private String seiClassName;

    private List<Class> classes;
    
    private static final Map<Class, Object> annotationProcessors;
    
    static {
        annotationProcessors = new HashMap<Class, Object>();
    }
    
    public JavaClassToDBCConverter(Class serviceClass) {
        this.serviceClass = serviceClass;
        classes = new ArrayList<Class>();
        establishClassHierarchy(serviceClass);
        establishInterfaceHierarchy(serviceClass.getInterfaces());
        establishExceptionClasses(serviceClass);
    }

    /**
     * The only method we will expose to users of this class. It will trigger the creation of the
     * <code>DescriptionBuilderComposite</code> based on our service class. It will also handle the
     * case of an impl class that references an SEI.
     *
     * @return - <code>DescriptionBuilderComposite</code>
     */
    public HashMap<String, DescriptionBuilderComposite> produceDBC() {
        if (log.isDebugEnabled()) {
            log.debug("Creating DescriptionBuilderComposite map from Java Class.");
        }
        
        HashMap<String, DescriptionBuilderComposite> dbcMap = new HashMap<String,
                DescriptionBuilderComposite>();
        for (int i = 0; i < classes.size(); i++) {
            buildDBC(dbcMap, classes.get(i));
            if (seiClassName != null && !seiClassName.equals("")) {
                try {
                    final ClassLoader contextClassLoader = (ClassLoader) AccessController.doPrivileged(
                            new PrivilegedAction() {
                                public Object run() {
                                    return Thread.currentThread().getContextClassLoader();
                                }
                            }
                    );
                    Class seiClass =
                            null;
                    try {
                        seiClass = (Class) AccessController.doPrivileged(
                                new PrivilegedExceptionAction() {
                                    public Object run() throws ClassNotFoundException {
                                        return contextClassLoader.loadClass(seiClassName);
                                    }
                                }
                        );
                    } catch (PrivilegedActionException e) {
                        throw (ClassNotFoundException) e.getException();
                    }
                    buildDBC(dbcMap, seiClass);
                    
                    // Also try to see if the SEI has any super interfaces  
                    Class[] interfaces = seiClass.getInterfaces();
                    for (int j = 0; j < interfaces.length; j++) {
                        buildDBC(dbcMap, interfaces[j]);
                    }
                }
                catch (ClassNotFoundException e) {
                    if (log.isDebugEnabled()) {
                        log.debug("Class not found exception caught for class: " + seiClassName, e);
                    }
                }
            }
        }
        return dbcMap;
    }

    private void buildDBC(HashMap<String, DescriptionBuilderComposite> dbcMap, Class clazz) {
        serviceClass = clazz;
        DescriptionBuilderComposite composite = new DescriptionBuilderComposite();
        introspectClass(composite);
        dbcMap.put(composite.getClassName(), composite);        
    }
    
    /**
     * This method will drive the introspection of the class-level information. It will store the
     * gathered information in the pertinent data members of the <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuilderComposite</code>
     */
    private void introspectClass(DescriptionBuilderComposite composite) {
        // Need to investigate this, probably want to specify
        composite.setClassLoader(ClassLoaderUtils.getClassLoader(serviceClass));
        composite.setIsInterface(serviceClass.isInterface());
        composite.setSuperClassName(serviceClass.getSuperclass() != null ? serviceClass.
                getSuperclass().getName() : null);
        composite.setClassName(serviceClass.getName());
        setInterfaces(composite);
        setTypeTargettedAnnotations(composite);
        Field[] fields = (Field[]) AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        return serviceClass.getFields();
                    }
                }
        );
        if (fields.length > 0) {
            JavaFieldsToFDCConverter fieldConverter = new JavaFieldsToFDCConverter(
                    fields);
            List<FieldDescriptionComposite> fdcList = fieldConverter.convertFields();
            ConverterUtils.attachFieldDescriptionComposites(composite, fdcList);
        }
        if (serviceClass.getMethods().length > 0) {
            // Inherited methods and constructors for superclasses will be in a seperate DBC for
            // the superclass.  We only need the ones actually declared in this class.
            Method[] methods = (Method[]) AccessController.doPrivileged(
                    new PrivilegedAction() {
                        public Object run() {
                            return serviceClass.getDeclaredMethods();
                        }
                    }
            );
            Constructor[] declaredConstructors = (Constructor[]) AccessController.doPrivileged(
                    new PrivilegedAction() {
                        public Object run() {
                            return serviceClass.getDeclaredConstructors();
                        }
                    }
            );
            JavaMethodsToMDCConverter methodConverter = new JavaMethodsToMDCConverter(
                    methods, declaredConstructors,
                    serviceClass.getName());
            List<MethodDescriptionComposite> mdcList = methodConverter.convertMethods();
            ConverterUtils.attachMethodDescriptionComposites(composite, mdcList);
        }
    }

    /**
     * This method is responsible for finding any interfaces implemented by the service class. We will
     * then set these as a list of fully qualified class names on the <code>DescriptionBuilderComposite</code>
     *
     * @param composite <code>DescriptionBuilderComposite</code>
     */
    private void setInterfaces(DescriptionBuilderComposite composite) {
        Type[] interfaces = (Type[]) AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        return serviceClass.getGenericInterfaces();
                    }
                }
        );
        List<String> interfaceList = interfaces.length > 0 ? new ArrayList<String>()
                : null;
        for (int i = 0; i < interfaces.length; i++) {
            interfaceList.add(getNameFromType(interfaces[i]));
        }
        // We only want to set this list if we found interfaces b/c the
        // DBC news up an interface list as part of its static initialization.
        // Thus, if we set this list to null we may cause an NPE
        if (interfaceList != null) {
            composite.setInterfacesList(interfaceList);
        }
    }

    private String getNameFromType(Type type) {
        String returnName = null;
        if (type instanceof Class) {
            returnName = ((Class)type).getName();
        } else if (type instanceof ParameterizedType) {
            returnName = ((ParameterizedType)type).toString();
        }
        return returnName;
    }


    /**
     * This method will drive the attachment of Type targetted annotations to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuilderComposite</code>
     */
    private void setTypeTargettedAnnotations(DescriptionBuilderComposite composite) {
        attachBindingTypeAnnotation(composite);
        attachHandlerChainAnnotation(composite);
        attachServiceModeAnnotation(composite);
        attachSoapBindingAnnotation(composite);
        attachWebFaultAnnotation(composite);
        attachWebServiceAnnotation(composite);
        attachWebServiceClientAnnotation(composite);
        attachWebServiceProviderAnnotation(composite);
        attachWebServiceRefsAnnotation(composite);
        attachWebServiceRefAnnotation(composite);
        attachWebServiceFeatureAnnotations(composite);
    }

    /**
     * This method will be used to attach @WebService annotation data to the
     * <code>DescriptionBuildercomposite</code>
     *
     * @param composite - <code>DescriptionBuilderComposite</code>
     */
    private void attachWebServiceAnnotation(DescriptionBuilderComposite composite) {
        WebService webService = (WebService)ConverterUtils.getAnnotation(
                WebService.class, serviceClass);
        if (webService != null) {
            // Attach @WebService annotated data
            WebServiceAnnot wsAnnot = WebServiceAnnot.createWebServiceAnnotImpl();
            wsAnnot.setEndpointInterface(webService.endpointInterface());
            // check for SEI and save name if necessary
            seiClassName = webService.endpointInterface();
            wsAnnot.setName(webService.name());
            wsAnnot.setPortName(webService.portName());
            wsAnnot.setServiceName(webService.serviceName());
            wsAnnot.setTargetNamespace(webService.targetNamespace());
            wsAnnot.setWsdlLocation(webService.wsdlLocation());
            composite.setWebServiceAnnot(wsAnnot);
        }
    }

    /**
     * This method will be used to attach @WebServiceClient annotation data to the
     * <code>DescriptionBuildercomposite</code>
     *
     * @param composite - <code>DescriptionBuilderComposite</code>
     */
    private void attachWebServiceClientAnnotation(DescriptionBuilderComposite composite) {
        WebServiceClient webServiceClient = (WebServiceClient)ConverterUtils.
                getAnnotation(WebServiceClient.class, serviceClass);
        if (webServiceClient != null) {

        }

    }

    /**
     * This method will be used to attach @WebServiceProvider annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuildercomposite</code>
     */
    private void attachWebServiceProviderAnnotation(DescriptionBuilderComposite composite) {
        WebServiceProvider webServiceProvider = (WebServiceProvider)ConverterUtils.
                getAnnotation(WebServiceProvider.class, serviceClass);
        if (webServiceProvider != null) {
            // Attach @WebServiceProvider annotation data
            WebServiceProviderAnnot wspAnnot = WebServiceProviderAnnot.
                    createWebServiceAnnotImpl();
            wspAnnot.setPortName(webServiceProvider.portName());
            wspAnnot.setServiceName(webServiceProvider.serviceName());
            wspAnnot.setTargetNamespace(webServiceProvider.targetNamespace());
            wspAnnot.setWsdlLocation(webServiceProvider.wsdlLocation());
            composite.setWebServiceProviderAnnot(wspAnnot);
        }
    }

    /**
     * This method will be used to attach @BindingType annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuildercomposite</code>
     */
    private void attachBindingTypeAnnotation(DescriptionBuilderComposite composite) {
        BindingType bindingType = (BindingType)ConverterUtils.getAnnotation(
                BindingType.class, serviceClass);
        if (bindingType != null) {
            // Attach @BindingType annotation data
            BindingTypeAnnot btAnnot = BindingTypeAnnot.createBindingTypeAnnotImpl();
            btAnnot.setValue(bindingType.value());
            composite.setBindingTypeAnnot(btAnnot);
        }
    }

    /**
     * This method will be used to attach @HandlerChain annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuildercomposite</code>
     */
    private void attachHandlerChainAnnotation(DescriptionBuilderComposite composite) {
        ConverterUtils.attachHandlerChainAnnotation(composite, serviceClass);
    }

    /**
     * This method will be used to attach @ServiceMode annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuildercomposite</code>
     */
    private void attachServiceModeAnnotation(DescriptionBuilderComposite composite) {
        ServiceMode serviceMode = (ServiceMode)ConverterUtils.getAnnotation(
                ServiceMode.class, serviceClass);
        if (serviceMode != null) {
            // Attach @ServiceMode annotated data
            ServiceModeAnnot smAnnot = ServiceModeAnnot.createWebServiceAnnotImpl();
            smAnnot.setValue(serviceMode.value());
            composite.setServiceModeAnnot(smAnnot);
        }
    }

    /**
     * This method will be used to drive the setting of @SOAPBinding annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuildercomposite</code>
     */
    private void attachSoapBindingAnnotation(DescriptionBuilderComposite composite) {
        ConverterUtils.attachSoapBindingAnnotation(composite, serviceClass);
    }

    /**
     * This method will be used to attach @WebFault annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuilderComposite</code>
     */
    private void attachWebFaultAnnotation(DescriptionBuilderComposite composite) {
        WebFault webFault = (WebFault)ConverterUtils.getAnnotation(
                WebFault.class, serviceClass);
        if (webFault != null) {
            WebFaultAnnot webFaultAnnot = WebFaultAnnot.createWebFaultAnnotImpl();
            webFaultAnnot.setFaultBean(webFault.faultBean());
            webFaultAnnot.setName(webFault.name());
            webFaultAnnot.setTargetNamespace(webFault.targetNamespace());
            try {
                webFaultAnnot.setMessageName(webFault.messageName());
            } catch (NoSuchMethodError ex) {
                // Ignore: we are running on Java 1.6 and the JAX-WS 2.2 libs have not been endorsed
            }
            composite.setWebFaultAnnot(webFaultAnnot);
        }
    }

    /**
     * This method will be used to attach @WebServiceRefs annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuilderComposite</code>
     */
    private void attachWebServiceRefsAnnotation(DescriptionBuilderComposite composite) {
        WebServiceRefs webServiceRefs = (WebServiceRefs)ConverterUtils.getAnnotation(
                WebServiceRefs.class, serviceClass);
        if (webServiceRefs != null) {
            WebServiceRef[] refs = webServiceRefs.value();
            for (WebServiceRef ref : refs) {
                WebServiceRefAnnot wsrAnnot = ConverterUtils.createWebServiceRefAnnot(
                        ref);
                composite.setWebServiceRefAnnot(wsrAnnot);
            }
        }
    }

    /**
     * This method will be used to drive the setting of @WebServiceRef annotation data to the
     * <code>DescriptionBuilderComposite</code>
     *
     * @param composite - <code>DescriptionBuilderComposite</code>
     */
    private void attachWebServiceRefAnnotation(DescriptionBuilderComposite composite) {
        ConverterUtils.attachWebServiceRefAnnotation(composite, serviceClass);

    }
    
    /**
     * Finds the list of WebServiceFeatureAnnotation instances, and set them on the composite.
     * 
     * @param composite
     */
    private void attachWebServiceFeatureAnnotations(DescriptionBuilderComposite composite) {
        List<Annotation> features = ConverterUtils.getAnnotations(
            WebServiceFeatureAnnotation.class, serviceClass); 
        
        if (features.size() > 0) {
            if (log.isDebugEnabled()) {
                log.debug("There were [" + features.size() + "] WebServiceFeature annotations found.");
            }
            
            composite.setWebServiceFeatures(features);
        }
    }

    private void establishClassHierarchy(Class rootClass) {
        classes.add(rootClass);
        if (rootClass.getSuperclass() != null && !rootClass.getSuperclass().getName().
                equals("java.lang.Object")) {
            classes.add(rootClass.getSuperclass());
            establishInterfaceHierarchy(rootClass.getSuperclass().getInterfaces());
            establishClassHierarchy(rootClass.getSuperclass());
        }
    }

    private void establishInterfaceHierarchy(Class[] interfaces) {
        if (interfaces.length > 0) {
            for (Class inter : interfaces) {
                classes.add(inter);
                establishInterfaceHierarchy(inter.getInterfaces());
            }
        }
    }

    /**
     * Adds any checked exceptions (i.e. declared on a method via a throws clause)
     * to the list of classes for which a DBC needs to be built.
     * @param rootClass
     */
    private void establishExceptionClasses(final Class rootClass) {
        Method[] methods = (Method[]) AccessController.doPrivileged(
                new PrivilegedAction() {
                    public Object run() {
                        return rootClass.getMethods();
                    }
                }
        );
        for (Method method : methods) {
            Class[] exceptionClasses = method.getExceptionTypes();
            if (exceptionClasses.length > 0) {
                for (Class checkedException : exceptionClasses) {
                    classes.add(checkedException);
                }
            }
        }
    }


}
