/*
 * 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 java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedElement;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

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.FixedInjectionSource;
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.naming.ManagedReferenceFactory;
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.reflect.DeploymentClassIndex;
import org.jboss.as.server.deployment.reflect.DeploymentReflectionIndex;
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 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.DotNames.WEB_SERVICE_REFS_ANNOTATION;
import static org.jboss.as.webservices.util.DotNames.WEB_SERVICE_REF_ANNOTATION;

/**
 * @author <a href="mailto:ropalka@redhat.com">Richard Opalka</a>
 * @WebServiceRef annotation processor.
 */
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 {

        final EEModuleDescription moduleDescription = unit.getAttachment(Attachments.EE_MODULE_DESCRIPTION);
        final AnnotatedElement target = createAnnotatedElement(unit, classInfo, injectionTarget);
        final String componentClassName = classInfo.name().toString();
        final Map<String, String> bindingMap = new HashMap<String, String>();
        boolean isEJB = false;
        for (final ComponentDescription componentDescription : moduleDescription.getComponentsByClassName(componentClassName)) {
            if (componentDescription instanceof SessionBeanComponentDescription) {
                isEJB = true;
                bindingMap.put(componentDescription.getComponentName() + "/" + bindingName, bindingName);
            }
        }
        if (!isEJB) {
            bindingMap.put(bindingName, bindingName);
        }

        for (String refKey : bindingMap.keySet()) {
            String refName = bindingMap.get(refKey);
            ManagedReferenceFactory factory = WebServiceReferences.createWebServiceFactory(unit, type, annotation, target, refName, refKey);
            final EEModuleClassDescription classDescription = moduleDescription.addOrGetLocalClassDescription(classInfo.name().toString());
            // Create the binding from whence our injection comes.
            final InjectionSource serviceRefSource = new FixedInjectionSource(factory, factory);
            final BindingConfiguration bindingConfiguration = new BindingConfiguration(refName, 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(refName)) : null;
            if (injectionConfiguration != null) {
                classDescription.addResourceInjection(injectionConfiguration);
            }
        }

    }

    private static AnnotatedElement createAnnotatedElement(final DeploymentUnit unit, final ClassInfo classInfo, final InjectionTarget injectionTarget) throws DeploymentUnitProcessingException {
        if (injectionTarget == null) {
            return elementForInjectionTarget(unit, classInfo);
        } else {
            return elementForInjectionTarget(unit, injectionTarget);
        }
    }


    private static AnnotatedElement elementForInjectionTarget(final DeploymentUnit unit, final ClassInfo classInfo) throws DeploymentUnitProcessingException {
        final DeploymentClassIndex classIndex = unit.getAttachment(org.jboss.as.server.deployment.Attachments.CLASS_INDEX);
        final Class<?> target = getClass(classIndex, classInfo.name().toString());
        return target;
    }

    private static AnnotatedElement elementForInjectionTarget(final DeploymentUnit unit, 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);
        return fieldOrMethod;
    }

    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 Class<?> getClass(final DeploymentClassIndex classIndex, final String className) throws DeploymentUnitProcessingException { // TODO: refactor to common code
        if (!isEmpty(className)) {
            try {
                return classIndex.classIndex(className).getModuleClass();
            } catch (ClassNotFoundException e) {
                throw new DeploymentUnitProcessingException(e);
            }
        }

        return null;
    }

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

}
