/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.compendium.internal.cm;

import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.gemini.blueprint.compendium.internal.cm.ManagedFactoryDisposableInvoker.DestructionCodes;
import org.eclipse.gemini.blueprint.context.BundleContextAware;
import org.eclipse.gemini.blueprint.service.exporter.OsgiServiceRegistrationListener;
import org.eclipse.gemini.blueprint.service.exporter.support.DefaultInterfaceDetector;
import org.eclipse.gemini.blueprint.service.exporter.support.ExportContextClassLoaderEnum;
import org.eclipse.gemini.blueprint.service.exporter.support.InterfaceDetector;
import org.eclipse.gemini.blueprint.service.exporter.support.OsgiServiceFactoryBean;
import org.eclipse.gemini.blueprint.service.exporter.support.ServicePropertiesChangeListener;
import org.eclipse.gemini.blueprint.service.importer.support.internal.collection.DynamicCollection;
import org.eclipse.gemini.blueprint.util.OsgiServiceUtils;
import org.eclipse.gemini.blueprint.util.internal.MapBasedDictionary;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedServiceFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * {@link FactoryBean Factory} class that automatically manages instances based on the configuration available inside a
 * {@link ManagedServiceFactory}.
 * 
 * The factory returns a list of {@link ServiceRegistration} of all published instances.
 * 
 * @author Costin Leau
 */
public class ManagedServiceFactoryFactoryBean implements InitializingBean, BeanClassLoaderAware, BeanFactoryAware,
		BundleContextAware, DisposableBean, FactoryBean<Collection> {

	/**
	 * Configuration Admin whiteboard 'listener'.
	 * 
	 * @author Costin Leau
	 */
	private class ConfigurationWatcher implements ManagedServiceFactory {

		public void deleted(String pid) {
			if (log.isTraceEnabled())
				log.trace("Configuration [" + pid + "] has been deleted");
			destroyInstance(pid);
		}

		public String getName() {
			return "Spring DM managed-service-factory support";
		}

		public void updated(String pid, Dictionary props) throws ConfigurationException {
			if (log.isTraceEnabled())
				log.trace("Configuration [" + pid + "] has been updated with properties " + props);
			createOrUpdate(pid, new MapBasedDictionary(props));
		}
	}

	/**
	 * Simple processor that applies the ConfigurationAdmin configuration before the bean is initialized.
	 * 
	 * @author Costin Leau
	 */
	private class InitialInjectionProcessor implements BeanPostProcessor {

		public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
			return bean;
		}

		public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
			CMUtils.applyMapOntoInstance(bean, initialInjectionProperties, beanFactory);

			if (log.isTraceEnabled()) {
				log.trace("Applying initial injection for managed bean " + beanName);
			}

			return bean;
		}
	}

	/**
	 * Simple associating cache between types and disposable invoker.
	 * 
	 * @author Costin Leau
	 */
	private class DestructionInvokerCache {

		private final String methodName;
		final ConcurrentMap<Class<?>, ManagedFactoryDisposableInvoker> cache =
				new ConcurrentHashMap<Class<?>, ManagedFactoryDisposableInvoker>(4);

		DestructionInvokerCache(String methodName) {
			this.methodName = methodName;
		}

		ManagedFactoryDisposableInvoker getInvoker(Class<?> type) {
			ManagedFactoryDisposableInvoker invoker = cache.get(type);
			// non-atomic check for the destruction invoker (duplicate invokers are okay)
			if (invoker == null) {
				invoker = new ManagedFactoryDisposableInvoker(type, methodName);
				cache.put(type, invoker);
			}
			return invoker;
		}

	}

	/** logger */
	private static final Log log = LogFactory.getLog(ManagedServiceFactoryFactoryBean.class);

	/** visibility monitor */
	private final Object monitor = new Object();
	/** Configuration Admin fpid */
	private String factoryPid;
	/** bundle context */
	private BundleContext bundleContext;
	/** embedded bean factory for instance management */
	private DefaultListableBeanFactory beanFactory;
	/** bean definition template */
	private RootBeanDefinition templateDefinition;
	/** owning bean factory - can be null */
	private BeanFactory owningBeanFactory;
	/** configuration watcher registration */
	private ServiceRegistration configurationWatcher;
	/** inner bean service registrations */
	private final DynamicCollection serviceRegistrations = new DynamicCollection(8);
	/** read-only view of the registration */
	private final Collection<ServiceRegistration> userReturnedCollection =
			Collections.unmodifiableCollection(serviceRegistrations);
	/** lookup map between exporters and associated pids */
	private final Map<String, OsgiServiceFactoryBean> serviceExporters =
			new ConcurrentHashMap<String, OsgiServiceFactoryBean>(8);

	// exporting template
	/** listeners */
	private OsgiServiceRegistrationListener[] listeners = new OsgiServiceRegistrationListener[0];
	/** auto export */
	private InterfaceDetector detector = DefaultInterfaceDetector.DISABLED;
	/** ccl */
	private ExportContextClassLoaderEnum ccl = ExportContextClassLoaderEnum.UNMANAGED;
	/** interfaces */
	private Class<?>[] interfaces;
	/** class loader */
	private ClassLoader classLoader;
	private boolean autowireOnUpdate = false;
	private String updateMethod;
	/** update callback */
	private UpdateCallback updateCallback;

	// this fields gets set whenever a new CM entry is created
	// no synch is needed since the CF is guaranteed to be called sequentially and the callback doesn't return
	// until the bean is fully initialized and published
	public Map initialInjectionProperties;

	/**
	 * destroyed flag - used since some CM implementations still call the service even though it was unregistered
	 */
	private boolean destroyed = false;
	/** service properties */
	private volatile Map serviceProperties;
	/** special destruction invoker for managed-components/template beans */
	private volatile DestructionInvokerCache destructionInvokerFactory;

	public void afterPropertiesSet() throws Exception {

		synchronized (monitor) {
			Assert.notNull(factoryPid, "factoryPid required");
			Assert.notNull(bundleContext, "bundleContext is required");
			Assert.notNull(templateDefinition, "templateDefinition is required");

			Assert.isTrue(!DefaultInterfaceDetector.DISABLED.equals(detector) || !ObjectUtils.isEmpty(interfaces),
					"No service interface(s) specified and auto-export "
							+ "discovery disabled; change at least one of these properties");
		}

		processTemplateDefinition();
		createEmbeddedBeanFactory();

		updateCallback = CMUtils.createCallback(autowireOnUpdate, updateMethod, beanFactory);

		registerService();
	}

	private void processTemplateDefinition() {
		// make sure the scope is singleton
		templateDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
		// let the special invoker handle the destroy method
		String destroyMethod = templateDefinition.getDestroyMethodName();
		templateDefinition.setDestroyMethodName(null);
		// prevent Spring for calling DiposableBean (it's already handled by the invoker)
		templateDefinition.registerExternallyManagedDestroyMethod("destroy");
		destructionInvokerFactory = new DestructionInvokerCache(destroyMethod);
	}

	public void destroy() throws Exception {

		synchronized (monitor) {
			destroyed = true;
			// remove factory service
			OsgiServiceUtils.unregisterService(configurationWatcher);
			configurationWatcher = null;
			// destroy instances
			destroyFactory();
		}

		destructionInvokerFactory.cache.clear();
		destructionInvokerFactory = null;

		synchronized (serviceRegistrations) {
			serviceRegistrations.clear();
		}
	}

	private void createEmbeddedBeanFactory() {
		synchronized (monitor) {
			DefaultListableBeanFactory bf = new DefaultListableBeanFactory(owningBeanFactory);
			if (owningBeanFactory instanceof ConfigurableBeanFactory) {
				bf.copyConfigurationFrom((ConfigurableBeanFactory) owningBeanFactory);
			}
			// just to be on the safe side
			bf.setBeanClassLoader(classLoader);
			// add autowiring processor
			bf.addBeanPostProcessor(new InitialInjectionProcessor());

			beanFactory = bf;
		}
	}

	private void registerService() {
		synchronized (monitor) {
			Dictionary props = new Hashtable(2);
			props.put(Constants.SERVICE_PID, factoryPid);

			configurationWatcher =
					bundleContext.registerService(ManagedServiceFactory.class.getName(), new ConfigurationWatcher(),
							props);
		}
	}

	// no monitor since the calling method already holds it
	private void destroyFactory() {
		if (beanFactory != null) {
			// destroy singletons manually to prevent multiple calls of the destruction contract (DisposableBean)
			// being called by both the factory and the special invoker
			String[] singletonBeans = beanFactory.getSingletonNames();
			for (String sigletonName : singletonBeans) {
				Object singleton = beanFactory.getBean(sigletonName);
				beanFactory.removeBeanDefinition(sigletonName);
				ManagedFactoryDisposableInvoker invoker = destructionInvokerFactory.getInvoker(singleton.getClass());
				invoker.destroy(sigletonName, singleton, DestructionCodes.BUNDLE_STOPPING);
			}
			beanFactory = null;
		}
	}

	private void createOrUpdate(String pid, Map props) {
		synchronized (monitor) {
			if (destroyed)
				return;

			if (beanFactory.containsBean(pid)) {
				updateInstance(pid, props);
			} else {
				createInstance(pid, props);
			}
		}
	}

	private void createInstance(String pid, Map props) {
		synchronized (monitor) {
			if (destroyed)
				return;

			beanFactory.registerBeanDefinition(pid, templateDefinition);
			initialInjectionProperties = props;
			// create instance (causing the injection BPP to be applied)
			Object bean = beanFactory.getBean(pid);
			registerService(pid, bean);
		}
	}

	private void registerService(String pid, Object bean) {
		OsgiServiceFactoryBean exporter = createExporter(pid, bean);
		serviceExporters.put(pid, exporter);
		try {
			serviceRegistrations.add(exporter.getObject());
		} catch (Exception ex) {
			throw new BeanCreationException("Cannot publish bean for pid " + pid, ex);
		}
	}

	private OsgiServiceFactoryBean createExporter(String beanName, Object bean) {
		OsgiServiceFactoryBean exporter = new OsgiServiceFactoryBean();
		exporter.setInterfaceDetector(detector);
		exporter.setBeanClassLoader(classLoader);
		exporter.setBeanName(beanName);
		exporter.setBundleContext(bundleContext);
		exporter.setExportContextClassLoader(ccl);
		exporter.setInterfaces(interfaces);
		exporter.setListeners(listeners);
		exporter.setTarget(bean);

		// add properties
		Properties props = new Properties();
		if (serviceProperties != null) {
			props.putAll(serviceProperties);
		}
		// add the service pid (to be able to identify the bean instance)
		props.put(Constants.SERVICE_PID, beanName);
		exporter.setServiceProperties(props);

		try {
			exporter.afterPropertiesSet();
		} catch (Exception ex) {
			throw new BeanCreationException("Cannot publish bean for pid " + beanName, ex);
		}
		return exporter;
	}

	private void updateInstance(String pid, Map props) {
		if (updateCallback != null) {
			Object instance = beanFactory.getBean(pid);
			updateCallback.update(instance, props);
		}
	}

	private void destroyInstance(String pid) {
		synchronized (monitor) {
			// bail out fast
			if (destroyed)
				return;

			if (beanFactory.containsBeanDefinition(pid)) {
				unregisterService(pid);
				Object singleton = beanFactory.getBean(pid);
				// remove definition and instance
				beanFactory.removeBeanDefinition(pid);
				ManagedFactoryDisposableInvoker invoker = destructionInvokerFactory.getInvoker(singleton.getClass());
				invoker.destroy(pid, singleton, DestructionCodes.CM_ENTRY_DELETED);
			}
		}
	}

	private void unregisterService(String pid) {
		OsgiServiceFactoryBean exporterFactory = serviceExporters.remove(pid);

		if (exporterFactory != null) {

			Object registration = null;
			try {
				registration = exporterFactory.getObject();
			} catch (Exception ex) {
				// log the exception and continue
				log.error("Could not retrieve registration for pid " + pid, ex);
			}

			if (log.isTraceEnabled()) {
				log.trace("Unpublishing bean for pid " + pid + " w/ registration " + registration);
			}
			// remove service registration
			serviceRegistrations.remove(registration);

			exporterFactory.destroy();
		}
	}

	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		synchronized (monitor) {
			this.owningBeanFactory = beanFactory;
		}
	}

	public void setBundleContext(BundleContext bundleContext) {
		synchronized (monitor) {
			this.bundleContext = bundleContext;
		}
	}

	public Collection getObject() throws Exception {
		return userReturnedCollection;
	}

	public Class<Collection> getObjectType() {
		return Collection.class;
	}

	public boolean isSingleton() {
		return true;
	}

	/**
	 * Sets the listeners interested in registration and unregistration events.
	 * 
	 * @param listeners registration/unregistration listeners.
	 */
	public void setListeners(OsgiServiceRegistrationListener[] listeners) {
		if (listeners != null)
			this.listeners = listeners;
	}

	/**
	 * @param factoryPid The factoryPid to set.
	 */
	public void setFactoryPid(String factoryPid) {
		synchronized (monitor) {
			this.factoryPid = factoryPid;
		}
	}

	/**
	 * @param templateDefinition The templateDefinition to set.
	 */
	public void setTemplateDefinition(BeanDefinition[] templateDefinition) {
		if (templateDefinition != null && templateDefinition.length > 0) {
			this.templateDefinition = new RootBeanDefinition();
			this.templateDefinition.overrideFrom(templateDefinition[0]);
		} else {
			this.templateDefinition = null;
		}
	}

	public void setBeanClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	public void setInterfaceDetector(InterfaceDetector detector) {
		this.detector = detector;
	}

	/**
	 * @param ccl The ccl to set.
	 */
	public void setExportContextClassLoader(ExportContextClassLoaderEnum ccl) {
		this.ccl = ccl;
	}

	/**
	 * @param interfaces The interfaces to set.
	 */
	public void setInterfaces(Class<?>[] interfaces) {
		this.interfaces = interfaces;
	}

	/**
	 * Sets whether autowire on update should be performed automatically or not.
	 * 
	 * @param autowireOnUpdate
	 */
	public void setAutowireOnUpdate(boolean autowireOnUpdate) {
		this.autowireOnUpdate = autowireOnUpdate;
	}

	/**
	 * @param updateMethod The updateMethod to set.
	 */
	public void setUpdateMethod(String updateMethod) {
		this.updateMethod = updateMethod;
	}

	/**
	 * Sets the properties used when exposing the target as an OSGi service. If the given argument implements (
	 * {@link ServicePropertiesChangeListener}), any updates to the properties will be reflected by the service
	 * registration.
	 * 
	 * @param serviceProperties properties used for exporting the target as an OSGi service
	 */
	public void setServiceProperties(Map serviceProperties) {
		this.serviceProperties = serviceProperties;
	}
}