/*
 * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors.
 *
 * Licensed 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.switchyard.component.bean;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeBeanDiscovery;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.enterprise.inject.spi.ProcessBean;

import org.jboss.logging.Logger;
import org.switchyard.common.type.Classes;
import org.switchyard.component.bean.deploy.BeanDeploymentMetaData;
import org.switchyard.component.bean.deploy.BeanDeploymentMetaDataCDIBean;
import org.switchyard.component.bean.deploy.CDIBean;
import org.switchyard.component.bean.deploy.CDIBeanServiceDescriptor;
import org.switchyard.component.bean.internal.ReferenceInvokerBean;
import org.switchyard.component.bean.internal.context.ContextBean;
import org.switchyard.component.bean.internal.exchange.ExchangeBean;
import org.switchyard.component.bean.internal.message.MessageBean;

/**
 * Portable CDI extension for SwitchYard.
 *
 * @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
 */
@ApplicationScoped
public class SwitchYardCDIServiceDiscovery implements Extension {

    /**
     * Logger
     */
    private static Logger _logger = Logger.getLogger(SwitchYardCDIServiceDiscovery.class);
    /**
     * Bean deployment metadata.
     */
    private BeanDeploymentMetaData _beanDeploymentMetaData;
    /**
     * List of created {@link ClientProxyBean} instances.
     */
    private List<ClientProxyBean> _createdProxyBeans = new ArrayList<ClientProxyBean>();
    
    /**
     * List of created ReferenceInvokerBean instances.
     */
    private List<ReferenceInvokerBean> _createdInvokerBeans = new ArrayList<ReferenceInvokerBean>();

    /**
     * {@link javax.enterprise.inject.spi.BeforeBeanDiscovery} CDI event observer.
     *
     * @param beforeEvent CDI Event instance.
     * @param beanManager CDI Bean Manager instance.
     */
    public void beforeBeanDiscovery(@Observes BeforeBeanDiscovery beforeEvent, BeanManager beanManager) {
        _logger.debug("CDI Bean discovery process started.");

        _beanDeploymentMetaData = new BeanDeploymentMetaData();
        _beanDeploymentMetaData.setBeanManager(beanManager);
        _beanDeploymentMetaData.setDeploymentClassLoader(Classes.getTCCL());
    }

    /**
     * {@link javax.enterprise.inject.spi.ProcessBean} CDI event observer.
     *
     * @param processBean CDI Event instance.
     * @param beanManager CDI Bean Manager instance.
     */
    public void processBean(@Observes ProcessBean processBean, BeanManager beanManager) {
        Bean<?> bean = processBean.getBean();
        Set<InjectionPoint> injectionPoints = bean.getInjectionPoints();

        // Create proxies for the relevant injection points...
        for (InjectionPoint injectionPoint : injectionPoints) {
            for (Annotation qualifier : injectionPoint.getQualifiers()) {
                if (Reference.class.isAssignableFrom(qualifier.annotationType())) {
                    Member member = injectionPoint.getMember();
                    if (member instanceof Field) {
                        Class<?> memberType = ((Field) member).getType();
                        if (memberType.isInterface()) {
                            if (memberType.equals(ReferenceInvoker.class)) {
                                addInvokerBean((Reference)qualifier, injectionPoint.getQualifiers());
                            } else {
                                addInjectableClientProxyBean((Field) member, (Reference) qualifier, injectionPoint.getQualifiers(), beanManager);
                            }
                        }
                    }
                }
            }
        }

        CDIBean cdiBean = new CDIBean(bean, beanManager);

        // Create Service Proxy ExchangeHandlers and register them as Services, for all @Service beans...
        if (isServiceBean(bean)) {
            _logger.debug("Adding ServiceDescriptor for bean " + bean.getBeanClass().getName());
            _beanDeploymentMetaData.addServiceDescriptor(new CDIBeanServiceDescriptor(cdiBean, _beanDeploymentMetaData));
        }

        // Register all beans in the deployment...
        _beanDeploymentMetaData.addDeploymentBean(cdiBean);
    }

    /**
     * {@link javax.enterprise.inject.spi.ProcessBean} CDI event observer.
     *
     * @param afterEvent  CDI Event instance.
     */
    public void afterBeanDiscovery(@Observes AfterBeanDiscovery afterEvent) {
        for (ClientProxyBean proxyBean : _createdProxyBeans) {
            _logger.debug("Adding ClientProxyBean for bean Service " + proxyBean.getServiceName() + ".  Service Interface type is " + proxyBean.getServiceInterface().getName());
            afterEvent.addBean(proxyBean);
            _beanDeploymentMetaData.addClientProxy(proxyBean);
        }
        
        for (ReferenceInvokerBean invokerBean : _createdInvokerBeans) {
            _logger.debug("Adding ReferenceInvokerBean for bean Service " + invokerBean.getServiceName());
            afterEvent.addBean(invokerBean);
            _beanDeploymentMetaData.addReferenceInvoker(invokerBean);
        }

        afterEvent.addBean(new BeanDeploymentMetaDataCDIBean(_beanDeploymentMetaData));
        afterEvent.addBean(new ContextBean());
        afterEvent.addBean(new MessageBean());
        afterEvent.addBean(new ExchangeBean());

        _logger.debug("CDI Bean discovery process completed.");
    }

    private void addInjectableClientProxyBean(Field injectionPointField, Reference serviceReference, Set<Annotation> qualifiers, BeanManager beanManager) {
        final String serviceName;

        if (serviceReference.value().length() > 0) {
            serviceName = serviceReference.value();
        } else {
            serviceName = injectionPointField.getType().getSimpleName();
        }

        addClientProxyBean(serviceName, injectionPointField.getType(), qualifiers);
    }

    private void addClientProxyBean(String serviceName, Class<?> beanClass, Set<Annotation> qualifiers) {
        // Check do we already have a proxy for this service interface...
        for (ClientProxyBean clientProxyBean : _createdProxyBeans) {
            if (serviceName.equals(clientProxyBean.getServiceName()) && beanClass == clientProxyBean.getBeanClass()) {
                // ignore... we already have a proxy ...
                return;
            }
        }

        ClientProxyBean clientProxyBean = new ClientProxyBean(serviceName, beanClass, qualifiers, _beanDeploymentMetaData);
        _createdProxyBeans.add(clientProxyBean);
    }
    
    private void addInvokerBean(Reference serviceReference, Set<Annotation> qualifiers) {
        // Value of Reference annotation is required for ReferenceInvoker
        if (serviceReference.value().length() == 0) {
            _logger.debug("Unable to create reference invoker for @Reference with missing value");
        }
        
        String serviceName = serviceReference.value();
        // Check do we already have an invoker for this service reference ...
        for (ReferenceInvokerBean invokerBean : _createdInvokerBeans) {
            if (serviceName.equals(invokerBean.getServiceName())) {
                // ignore... we already have a proxy ...
                return;
            }
        }

        ReferenceInvokerBean invokerBean = new ReferenceInvokerBean(serviceName, qualifiers);
        _createdInvokerBeans.add(invokerBean);
    }

    private boolean isServiceBean(Bean<?> bean) {
        Class<?> beanClass = bean.getBeanClass();
        return (Modifier.isPublic(beanClass.getModifiers()) && beanClass.isAnnotationPresent(Service.class));
    }

}
