/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.webservices.webserviceref;

import static org.jboss.as.ee.utils.InjectionUtils.getInjectionTarget;
import static org.jboss.as.webservices.WSMessages.MESSAGES;
import static org.jboss.as.webservices.util.ASHelper.getAnnotations;
import static org.jboss.as.webservices.util.ASHelper.getWSRefRegistry;
import static org.jboss.as.webservices.util.DotNames.WEB_SERVICE_REFS_ANNOTATION;
import static org.jboss.as.webservices.util.DotNames.WEB_SERVICE_REF_ANNOTATION;
import static org.jboss.as.webservices.webserviceref.WSRefUtils.processAnnotatedElement;
import static org.jboss.as.webservices.webserviceref.WSRefUtils.processType;

import java.lang.reflect.AccessibleObject;
import java.util.List;
import java.util.Locale;

import javax.xml.ws.Service;

import org.jboss.as.ee.component.Attachments;
import org.jboss.as.ee.component.BindingConfiguration;
import org.jboss.as.ee.component.ComponentDescription;
import org.jboss.as.ee.component.EEModuleClassDescription;
import org.jboss.as.ee.component.EEModuleDescription;
import org.jboss.as.ee.component.FieldInjectionTarget;
import org.jboss.as.ee.component.InjectionSource;
import org.jboss.as.ee.component.InjectionTarget;
import org.jboss.as.ee.component.LookupInjectionSource;
import org.jboss.as.ee.component.MethodInjectionTarget;
import org.jboss.as.ee.component.ResourceInjectionConfiguration;
import org.jboss.as.ejb3.component.session.SessionBeanComponentDescription;
import org.jboss.as.server.deployment.DeploymentPhaseContext;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.as.server.deployment.module.ResourceRoot;
import org.jboss.as.server.deployment.reflect.DeploymentReflectionIndex;
import org.jboss.as.webservices.util.VirtualFileAdaptor;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.MethodInfo;
import org.jboss.modules.Module;
import org.jboss.wsf.spi.deployment.UnifiedVirtualFile;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedServiceRefMetaData;

/**
 * @WebServiceRef annotation processor.
 *
 * @author <a href="mailto:ropalka@redhat.com">Richard Opalka</a>
 */
public class WSRefAnnotationProcessor implements DeploymentUnitProcessor {

    public void deploy(final DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
        final DeploymentUnit unit = phaseContext.getDeploymentUnit();

        // Process @WebServiceRef annotations
        final List<AnnotationInstance> webServiceRefAnnotations = getAnnotations(unit, WEB_SERVICE_REF_ANNOTATION);
        for (final AnnotationInstance annotation : webServiceRefAnnotations) {
            final AnnotationTarget annotationTarget = annotation.target();
            final WSRefAnnotationWrapper annotationWrapper = new WSRefAnnotationWrapper(annotation);

            if (annotationTarget instanceof FieldInfo) {
                processFieldRef(unit, annotationWrapper, (FieldInfo) annotationTarget);
            } else if (annotationTarget instanceof MethodInfo) {
                processMethodRef(unit, annotationWrapper, (MethodInfo) annotationTarget);
            } else if (annotationTarget instanceof ClassInfo) {
                processClassRef(unit, annotationWrapper, (ClassInfo) annotationTarget);
            }
        }

        // Process @WebServiceRefs annotations
        final List<AnnotationInstance> webServiceRefsAnnotations = getAnnotations(unit, WEB_SERVICE_REFS_ANNOTATION);
        for (final AnnotationInstance outerAnnotation : webServiceRefsAnnotations) {
            final AnnotationTarget annotationTarget = outerAnnotation.target();
            if (annotationTarget instanceof ClassInfo) {
                final AnnotationInstance[] values = outerAnnotation.value("value").asNestedArray();
                for (final AnnotationInstance annotation : values) {
                    final WSRefAnnotationWrapper annotationWrapper = new WSRefAnnotationWrapper(annotation);
                    processClassRef(unit, annotationWrapper, (ClassInfo) annotationTarget);
                }
            }
        }
    }

    public void undeploy(final DeploymentUnit unit) {
        // NOOP
    }

    private static void processFieldRef(final DeploymentUnit unit, final WSRefAnnotationWrapper annotation, final FieldInfo fieldInfo) throws DeploymentUnitProcessingException {
        final String fieldName = fieldInfo.name();
        final String injectionType = isEmpty(annotation.type()) || annotation.type().equals(Object.class.getName()) ? fieldInfo.type().name().toString() : annotation.type();
        final InjectionTarget injectionTarget = new FieldInjectionTarget(fieldInfo.declaringClass().name().toString(),  fieldName, injectionType);
        final String bindingName = isEmpty(annotation.name()) ? fieldInfo.declaringClass().name().toString() + "/" + fieldInfo.name() : annotation.name();
        processRef(unit, injectionType, annotation, fieldInfo.declaringClass(), injectionTarget, bindingName);
    }

    private static void processMethodRef(final DeploymentUnit unit, final WSRefAnnotationWrapper annotation, final MethodInfo methodInfo) throws DeploymentUnitProcessingException {
        final String methodName = methodInfo.name();
        if (!methodName.startsWith("set") || methodInfo.args().length != 1) {
            throw MESSAGES.invalidServiceRefSetterMethodName(methodInfo);
        }
        final String injectionType = isEmpty(annotation.type()) || annotation.type().equals(Object.class.getName()) ? methodInfo.args()[0].name().toString() : annotation.type();
        final InjectionTarget injectionTarget = new MethodInjectionTarget(methodInfo.declaringClass().name().toString(), methodName, injectionType);
        final String bindingName = isEmpty(annotation.name()) ? methodInfo.declaringClass().name().toString() + "/" + methodName.substring(3, 4).toLowerCase(Locale.ENGLISH) + methodName.substring(4) : annotation.name();
        processRef(unit, injectionType, annotation, methodInfo.declaringClass(), injectionTarget, bindingName);
    }

    private static void processClassRef(final DeploymentUnit unit, final WSRefAnnotationWrapper annotation, final ClassInfo classInfo) throws DeploymentUnitProcessingException {
        if (isEmpty(annotation.name())) {
            throw MESSAGES.requiredServiceRefName();
        }
        if (isEmpty(annotation.type())) {
            throw MESSAGES.requiredServiceRefType();
        }
        processRef(unit, annotation.type(), annotation, classInfo, null, annotation.name());
    }

    private static void processRef(final DeploymentUnit unit, final String type, final WSRefAnnotationWrapper annotation, final ClassInfo classInfo, final InjectionTarget injectionTarget, final String bindingName) throws DeploymentUnitProcessingException {
        boolean isEJB = false;
        final EEModuleDescription moduleDescription = unit.getAttachment(Attachments.EE_MODULE_DESCRIPTION);
        final String componentClassName = classInfo.name().toString();
        final Module module = unit.getAttachment(org.jboss.as.server.deployment.Attachments.MODULE);
        for (final ComponentDescription componentDescription : moduleDescription.getComponentsByClassName(componentClassName)) {
            if (componentDescription instanceof SessionBeanComponentDescription) {
                isEJB = true;

                final UnifiedServiceRefMetaData serviceRefUMDM = getServiceRef(unit, componentDescription, bindingName);
                initServiceRef(unit, serviceRefUMDM, type, annotation);
                processWSFeatures(unit, serviceRefUMDM, injectionTarget, classInfo);

                // Create the binding from whence our injection comes.
                final InjectionSource serviceRefSource = new WSRefValueSource(serviceRefUMDM, module.getClassLoader());
                final BindingConfiguration bindingConfiguration = new BindingConfiguration(bindingName, serviceRefSource);
                componentDescription.getBindingConfigurations().add(bindingConfiguration);
                // our injection comes from the local lookup, no matter what.
                final ResourceInjectionConfiguration injectionConfiguration = injectionTarget != null ? new ResourceInjectionConfiguration(injectionTarget, new LookupInjectionSource(bindingName)) : null;
                if (injectionConfiguration != null) {
                    componentDescription.addResourceInjection(injectionConfiguration);
                }
            }
        }
        if (!isEJB) {
            final UnifiedServiceRefMetaData serviceRefUMDM = getServiceRef(unit, null, bindingName);
            initServiceRef(unit, serviceRefUMDM, type, annotation);
            processWSFeatures(unit, serviceRefUMDM, injectionTarget, classInfo);

            // TODO: class hierarchies? shared bindings?
            final EEModuleClassDescription classDescription = moduleDescription.addOrGetLocalClassDescription(classInfo.name().toString());
            // Create the binding from whence our injection comes.
            final InjectionSource serviceRefSource = new WSRefValueSource(serviceRefUMDM, module.getClassLoader());
            final BindingConfiguration bindingConfiguration = new BindingConfiguration(bindingName, serviceRefSource);
            classDescription.getBindingConfigurations().add(bindingConfiguration);
            // our injection comes from the local lookup, no matter what.
            final ResourceInjectionConfiguration injectionConfiguration = injectionTarget != null ?
                new ResourceInjectionConfiguration(injectionTarget, new LookupInjectionSource(bindingName)) : null;
            if (injectionConfiguration != null) {
                classDescription.addResourceInjection(injectionConfiguration);
            }
        }
    }

    private static void processWSFeatures(final DeploymentUnit unit, final UnifiedServiceRefMetaData serviceRefUMDM, final InjectionTarget injectionTarget, final ClassInfo classInfo) throws DeploymentUnitProcessingException {
        if (injectionTarget != null) {
            // @WebServiceRef specified on field or method
            processInjectionTarget(unit, serviceRefUMDM, injectionTarget);
        } else {
            // @WebServiceRef specified on class
            processInjectionTarget(unit, serviceRefUMDM, classInfo);
        }
    }

    private static UnifiedServiceRefMetaData getServiceRef(final DeploymentUnit unit, final ComponentDescription componentDescription, final String serviceRefName) {
        final WSReferences wsRefRegistry = getWSRefRegistry(unit);
        final String cacheKey = getCacheKey(componentDescription, serviceRefName);
        UnifiedServiceRefMetaData serviceRefUMDM = wsRefRegistry.get(cacheKey);
        if (serviceRefUMDM == null) {
            serviceRefUMDM = new UnifiedServiceRefMetaData(getUnifiedVirtualFile(unit));
            serviceRefUMDM.setServiceRefName(serviceRefName);
            wsRefRegistry.add(cacheKey, serviceRefUMDM);
        }
        return serviceRefUMDM;
    }

    private static String getCacheKey(final ComponentDescription componentDescription, final String serviceRefName) {
        if (componentDescription == null) {
            return serviceRefName;
        } else {
            return componentDescription.getComponentName() + "/" + serviceRefName;
        }
    }

    private static void processInjectionTarget(final DeploymentUnit unit, final UnifiedServiceRefMetaData serviceRefUMDM, final ClassInfo classInfo) throws DeploymentUnitProcessingException {
        final Module module = unit.getAttachment(org.jboss.as.server.deployment.Attachments.MODULE);
        final Class<?> target = getClass(module.getClassLoader(), classInfo.name().toString());
        processAnnotatedElement(target, serviceRefUMDM);
    }

    private static void processInjectionTarget(final DeploymentUnit unit, final UnifiedServiceRefMetaData serviceRefUMDM, final InjectionTarget injectionTarget) throws DeploymentUnitProcessingException {
        final Module module = unit.getAttachment(org.jboss.as.server.deployment.Attachments.MODULE);
        final DeploymentReflectionIndex deploymentReflectionIndex = unit.getAttachment(org.jboss.as.server.deployment.Attachments.REFLECTION_INDEX);
        final String injectionTargetClassName = injectionTarget.getClassName();
        final String injectionTargetName = getInjectionTargetName(injectionTarget);
        final AccessibleObject fieldOrMethod = getInjectionTarget(injectionTargetClassName, injectionTargetName, module.getClassLoader(), deploymentReflectionIndex);
        processAnnotatedElement(fieldOrMethod, serviceRefUMDM);
    }

    private static String getInjectionTargetName(final InjectionTarget injectionTarget) {
        final String name = injectionTarget.getName();
        if (injectionTarget instanceof FieldInjectionTarget) {
            return name;
        } else if (injectionTarget instanceof MethodInjectionTarget) {
            return name.substring(3, 4).toUpperCase(Locale.ENGLISH) + name.substring(4);
        }
        throw new UnsupportedOperationException();
    }

    private static UnifiedServiceRefMetaData initServiceRef(final DeploymentUnit unit, final UnifiedServiceRefMetaData serviceRefUMDM, final String type, final WSRefAnnotationWrapper annotation) throws DeploymentUnitProcessingException  {
        // wsdl location
        if (!isEmpty(annotation.wsdlLocation())) {
            serviceRefUMDM.setWsdlFile(annotation.wsdlLocation());
        }
        // ref class type
        final Module module = unit.getAttachment(org.jboss.as.server.deployment.Attachments.MODULE);
        final Class<?> typeClass = getClass(module.getClassLoader(), type);
        serviceRefUMDM.setServiceRefType(typeClass.getName());
        // ref service interface
        if (!isEmpty(annotation.value())) {
            serviceRefUMDM.setServiceInterface(annotation.value());
        } else if (Service.class.isAssignableFrom(typeClass)) {
            serviceRefUMDM.setServiceInterface(typeClass.getName());
        } else {
            serviceRefUMDM.setServiceInterface(Service.class.getName());
        }
        // ref type
        processType(serviceRefUMDM);

        return serviceRefUMDM;
    }

    private static Class<?> getClass(final ClassLoader classLoader, final String className) throws DeploymentUnitProcessingException { // TODO: refactor to common code
        if (!isEmpty(className)) {
            try {
                return classLoader.loadClass(className);
            } catch (ClassNotFoundException e) {
                throw new DeploymentUnitProcessingException(e);
            }
        }

        return null;
    }

    private static UnifiedVirtualFile getUnifiedVirtualFile(final DeploymentUnit unit) { // TODO: refactor to common code
        final ResourceRoot resourceRoot = unit.getAttachment(org.jboss.as.server.deployment.Attachments.DEPLOYMENT_ROOT);
        return new VirtualFileAdaptor(resourceRoot.getRoot());
    }

    private static boolean isEmpty(final String string) { // TODO: some common class - StringUtils ?
        return string == null || string.isEmpty();
    }

}
