/*
 * Copyright 2002-2011 the original author or authors.
 * 
 * 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.springframework.flex.config;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.flex.remoting.RemotingDestination;
import org.springframework.flex.remoting.RemotingDestinationExporter;
import org.springframework.flex.remoting.RemotingExclude;
import org.springframework.flex.remoting.RemotingInclude;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * {@link BeanFactoryPostProcessor} implementation that searches the {@link BeanFactory} for beans annotated with
 * {@link RemotingDestination} and adds a corresponding {@link RemotingDestinationExporter} bean definition according to
 * the attributes of the {@link RemotingDestination} annotation and any methods found to be marked with either the
 * {@link RemotingInclude} or {@link RemotingExclude} annotation.
 * 
 * <p>
 * This processor will be enabled automatically when using the message-broker tag of the xml config namespace.
 * 
 * @author Jeremy Grelle
 */
public class RemotingAnnotationPostProcessor implements BeanFactoryPostProcessor {

    private static final Log log = LogFactory.getLog(RemotingAnnotationPostProcessor.class);

    // --------------------------- Bean Configuration Properties -------------//
    private static final String MESSAGE_BROKER_PROPERTY = "messageBroker";

    private static final String SERVICE_PROPERTY = "service";

    private static final String DESTINATION_ID_PROPERTY = "destinationId";

    private static final String CHANNELS_PROPERTY = "channels";

    private static final String INCLUDE_METHODS_PROPERTY = "includeMethods";

    private static final String EXCLUDE_METHODS_PROPERTY = "excludeMethods";

    private static final String SERVICE_ADAPTER_PROPERTY = "serviceAdapter";

    /**
     * 
     * {@inheritDoc}
     */
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        Set<RemotingDestinationMetadata> remoteBeans = findRemotingDestinations(beanFactory);

        if (remoteBeans.size() > 0) {
            Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory,
                "In order for services to be exported via the @RemotingDestination annotation, the current BeanFactory must be a BeanDefinitionRegistry.");
        }

        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;

        for (RemotingDestinationMetadata remotingDestinationConfig : remoteBeans) {

            BeanDefinitionBuilder exporterBuilder = BeanDefinitionBuilder.rootBeanDefinition(RemotingDestinationExporter.class);
            exporterBuilder.getRawBeanDefinition().setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

            RemotingDestination remotingDestination = remotingDestinationConfig.getRemotingDestination();

            String messageBrokerId = StringUtils.hasText(remotingDestination.messageBroker()) ? remotingDestination.messageBroker()
                : BeanIds.MESSAGE_BROKER;
            String destinationId = StringUtils.hasText(remotingDestination.value()) ? remotingDestination.value()
                : remotingDestinationConfig.getBeanName();
            
            String[] channels = null;
            for (String channelValue : remotingDestination.channels()) {
                channelValue = beanFactory.resolveEmbeddedValue(channelValue);
                String[] parsedChannels = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(channelValue));
                channels = StringUtils.mergeStringArrays(channels, parsedChannels);
            }
            
            exporterBuilder.addPropertyReference(MESSAGE_BROKER_PROPERTY, messageBrokerId);
            exporterBuilder.addPropertyValue(SERVICE_PROPERTY, remotingDestinationConfig.getBeanName());
            exporterBuilder.addDependsOn(remotingDestinationConfig.getBeanName());
            exporterBuilder.addPropertyValue(DESTINATION_ID_PROPERTY, destinationId);
            exporterBuilder.addPropertyValue(CHANNELS_PROPERTY, channels);
            exporterBuilder.addPropertyValue(INCLUDE_METHODS_PROPERTY, remotingDestinationConfig.getIncludeMethods());
            exporterBuilder.addPropertyValue(EXCLUDE_METHODS_PROPERTY, remotingDestinationConfig.getExcludeMethods());
            exporterBuilder.addPropertyValue(SERVICE_ADAPTER_PROPERTY, remotingDestination.serviceAdapter());

            BeanDefinitionReaderUtils.registerWithGeneratedName(exporterBuilder.getBeanDefinition(), registry);
        }

    }

    /**
     * Helper that searches the BeanFactory for beans annotated with @RemotingDestination, being careful not to force
     * eager creation of the beans if it can be avoided.
     * 
     * @param beanFactory the BeanFactory to search
     * @return a set of collected RemotingDestinationMetadata
     */
    private Set<RemotingDestinationMetadata> findRemotingDestinations(ConfigurableListableBeanFactory beanFactory) {
        Set<RemotingDestinationMetadata> remotingDestinations = new HashSet<RemotingDestinationMetadata>();
        Set<String> beanNames = new HashSet<String>();
        beanNames.addAll(Arrays.asList(beanFactory.getBeanDefinitionNames()));
        if (beanFactory.getParentBeanFactory() instanceof ListableBeanFactory) {
            beanNames.addAll(Arrays.asList(((ListableBeanFactory)beanFactory.getParentBeanFactory()).getBeanDefinitionNames()));
        }
        for (String beanName : beanNames) {
            if (beanName.startsWith("scopedTarget.")) {
                continue;
            }
            RemotingDestination remotingDestination = null;
            BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName);
            if (bd.isAbstract() || bd.isLazyInit()) {
                continue;
            }
            if (bd instanceof AbstractBeanDefinition) {
                AbstractBeanDefinition abd = (AbstractBeanDefinition) bd;
                if (abd.hasBeanClass()) {
                    Class<?> beanClass = abd.getBeanClass();
                    remotingDestination = AnnotationUtils.findAnnotation(beanClass, RemotingDestination.class);
                    if (remotingDestination != null) {
                        remotingDestinations.add(new RemotingDestinationMetadata(remotingDestination, beanName, beanClass));
                        continue;
                    }
                }
            }
            Class<?> handlerType = beanFactory.getType(beanName);
            if (handlerType != null) {
                remotingDestination = AnnotationUtils.findAnnotation(handlerType, RemotingDestination.class);
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Could not get type of bean '" + beanName + "' from bean factory.");
                }
            }
            if (remotingDestination != null) {
                remotingDestinations.add(new RemotingDestinationMetadata(remotingDestination, beanName, handlerType));
            } 
        }
        return remotingDestinations;
    }

}
