/******************************************************************************
 * 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.extender.internal.support;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.Timer;

import org.apache.commons.logging.Log;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.eclipse.gemini.blueprint.context.ConfigurableOsgiBundleApplicationContext;
import org.eclipse.gemini.blueprint.context.event.OsgiBundleApplicationContextEventMulticaster;
import org.eclipse.gemini.blueprint.context.event.OsgiBundleApplicationContextEventMulticasterAdapter;
import org.eclipse.gemini.blueprint.context.event.OsgiBundleApplicationContextListener;
import org.eclipse.gemini.blueprint.context.support.OsgiBundleXmlApplicationContext;
import org.eclipse.gemini.blueprint.extender.OsgiApplicationContextCreator;
import org.eclipse.gemini.blueprint.extender.OsgiBeanFactoryPostProcessor;
import org.eclipse.gemini.blueprint.extender.OsgiServiceDependencyFactory;
import org.eclipse.gemini.blueprint.extender.internal.dependencies.startup.MandatoryImporterDependencyFactory;
import org.eclipse.gemini.blueprint.extender.support.DefaultOsgiApplicationContextCreator;
import org.eclipse.gemini.blueprint.extender.support.internal.ConfigUtils;
import org.eclipse.gemini.blueprint.util.BundleDelegatingClassLoader;
import org.springframework.scheduling.timer.TimerTaskExecutor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * Configuration class for the extender. Takes care of locating the extender specific configurations and merging the
 * results with the defaults.
 * 
 * @author Costin Leau
 */
public class ExtenderConfiguration implements DisposableBean {

	/** logger */
	private final Log log;

	private static final String TASK_EXECUTOR_NAME = "taskExecutor";

	private static final String SHUTDOWN_TASK_EXECUTOR_NAME = "shutdownTaskExecutor";

	private static final String CONTEXT_CREATOR_NAME = "applicationContextCreator";

	private static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "osgiApplicationEventMulticaster";

	private static final String CONTEXT_LISTENER_NAME = "osgiApplicationContextListener";

	private static final String PROPERTIES_NAME = "extenderProperties";

	private static final String SHUTDOWN_WAIT_KEY = "shutdown.wait.time";

	private static final String PROCESS_ANNOTATIONS_KEY = "process.annotations";

	private static final String WAIT_FOR_DEPS_TIMEOUT_KEY = "dependencies.wait.time";

	private static final String EXTENDER_CFG_LOCATION = "META-INF/spring/extender";

	private static final String XML_PATTERN = "*.xml";

	private static final String ANNOTATION_DEPENDENCY_FACTORY =
			"org.eclipse.gemini.blueprint.extensions.annotation.ServiceReferenceDependencyBeanFactoryPostProcessor";

	/** annotation processing system property (kept for backwards compatibility) */
	private static final String AUTO_ANNOTATION_PROCESSING =
			"org.eclipse.gemini.blueprint.extender.annotation.auto.processing";

	//
	// defaults
	//

	// default dependency wait time (in milliseconds)
	private static final long DEFAULT_DEP_WAIT = ConfigUtils.DIRECTIVE_TIMEOUT_DEFAULT * 1000;
	private static final boolean DEFAULT_NS_BUNDLE_STATE = true;
	private static final long DEFAULT_SHUTDOWN_WAIT = 10 * 1000;
	private static final boolean DEFAULT_PROCESS_ANNOTATION = false;

	private ConfigurableOsgiBundleApplicationContext extenderConfiguration;

	private TaskExecutor taskExecutor, shutdownTaskExecutor;

	private boolean isTaskExecutorManagedInternally;

	private boolean isShutdownTaskExecutorManagedInternally;

	private boolean isMulticasterManagedInternally;

	private long shutdownWaitTime, dependencyWaitTime;

	private boolean processAnnotation, nsBundledResolved;

	private OsgiBundleApplicationContextEventMulticaster eventMulticaster;

	private OsgiBundleApplicationContextListener contextEventListener;

	private boolean forceThreadShutdown;

	private OsgiApplicationContextCreator contextCreator;

	/** bundle wrapped class loader */
	private final ClassLoader classLoader;
	/** List of context post processors */
	private final List<OsgiBeanFactoryPostProcessor> postProcessors =
			Collections.synchronizedList(new ArrayList<OsgiBeanFactoryPostProcessor>(0));
	/** List of service dependency factories */
	private final List<OsgiServiceDependencyFactory> dependencyFactories =
			Collections.synchronizedList(new ArrayList<OsgiServiceDependencyFactory>(0));

	// fields reading/writing lock
	private final Object lock = new Object();

	/**
	 * Constructs a new <code>ExtenderConfiguration</code> instance. Locates the extender configuration, creates an
	 * application context which will returned the extender items.
	 * 
	 * @param bundleContext extender OSGi bundle context
	 */
	public ExtenderConfiguration(BundleContext bundleContext, Log log) {
		this.log = log;
		Bundle bundle = bundleContext.getBundle();
		Properties properties = new Properties(createDefaultProperties());

		Enumeration<?> enm = bundle.findEntries(EXTENDER_CFG_LOCATION, XML_PATTERN, false);

		if (enm == null) {
			log.info("No custom extender configuration detected; using defaults...");

			synchronized (lock) {
				taskExecutor = createDefaultTaskExecutor();
				shutdownTaskExecutor = createDefaultShutdownTaskExecutor();
				eventMulticaster = createDefaultEventMulticaster();
				contextCreator = createDefaultApplicationContextCreator();
				contextEventListener = createDefaultApplicationContextListener();

			}
			classLoader = BundleDelegatingClassLoader.createBundleClassLoaderFor(bundle);
		} else {
			String[] configs = copyEnumerationToList(enm);

			log.info("Detected extender custom configurations at " + ObjectUtils.nullSafeToString(configs));
			// create OSGi specific XML context
			ConfigurableOsgiBundleApplicationContext extenderAppCtx = new OsgiBundleXmlApplicationContext(configs);
			extenderAppCtx.setBundleContext(bundleContext);
			extenderAppCtx.refresh();

			synchronized (lock) {
				extenderConfiguration = extenderAppCtx;
				// initialize beans
				taskExecutor =
						extenderConfiguration.containsBean(TASK_EXECUTOR_NAME) ? (TaskExecutor) extenderConfiguration
								.getBean(TASK_EXECUTOR_NAME, TaskExecutor.class) : createDefaultTaskExecutor();

				shutdownTaskExecutor =
						extenderConfiguration.containsBean(SHUTDOWN_TASK_EXECUTOR_NAME) ? (TaskExecutor) extenderConfiguration
								.getBean(SHUTDOWN_TASK_EXECUTOR_NAME, TaskExecutor.class)
								: createDefaultShutdownTaskExecutor();

				eventMulticaster =
						extenderConfiguration.containsBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME) ? (OsgiBundleApplicationContextEventMulticaster) extenderConfiguration
								.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME,
										OsgiBundleApplicationContextEventMulticaster.class)
								: createDefaultEventMulticaster();

				contextCreator =
						extenderConfiguration.containsBean(CONTEXT_CREATOR_NAME) ? (OsgiApplicationContextCreator) extenderConfiguration
								.getBean(CONTEXT_CREATOR_NAME, OsgiApplicationContextCreator.class)
								: createDefaultApplicationContextCreator();

				contextEventListener =
						extenderConfiguration.containsBean(CONTEXT_LISTENER_NAME) ? (OsgiBundleApplicationContextListener) extenderConfiguration
								.getBean(CONTEXT_LISTENER_NAME, OsgiBundleApplicationContextListener.class)
								: createDefaultApplicationContextListener();
			}

			// get post processors
			postProcessors.addAll(extenderConfiguration.getBeansOfType(OsgiBeanFactoryPostProcessor.class).values());

			// get dependency factories
			dependencyFactories.addAll(extenderConfiguration.getBeansOfType(OsgiServiceDependencyFactory.class)
					.values());

			classLoader = extenderConfiguration.getClassLoader();
			// extender properties using the defaults as backup
			if (extenderConfiguration.containsBean(PROPERTIES_NAME)) {
				Properties customProperties =
						(Properties) extenderConfiguration.getBean(PROPERTIES_NAME, Properties.class);
				Enumeration<?> propertyKey = customProperties.propertyNames();
				while (propertyKey.hasMoreElements()) {
					String property = (String) propertyKey.nextElement();
					properties.setProperty(property, customProperties.getProperty(property));
				}
			}
		}

		synchronized (lock) {
			shutdownWaitTime = getShutdownWaitTime(properties);
			dependencyWaitTime = getDependencyWaitTime(properties);
			processAnnotation = getProcessAnnotations(properties);
		}

		// load default dependency factories
		addDefaultDependencyFactories();

		// allow post processing
		contextCreator = postProcess(contextCreator);
	}

	/**
	 * Allows post processing of the context creator.
	 * 
	 * @param contextCreator
	 * @return
	 */
	protected OsgiApplicationContextCreator postProcess(OsgiApplicationContextCreator contextCreator) {
		return contextCreator;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * Cleanup the configuration items.
	 */
	public void destroy() {

		synchronized (lock) {
			if (isMulticasterManagedInternally) {
				eventMulticaster.removeAllListeners();
				eventMulticaster = null;
			}

			if (extenderConfiguration != null) {
				extenderConfiguration.close();
				extenderConfiguration = null;
			}

			// postpone the task executor shutdown
			if (forceThreadShutdown) {

				if (isTaskExecutorManagedInternally) {
					log.warn("Forcing the (internally created) taskExecutor to stop...");
					ThreadGroup th = ((SimpleAsyncTaskExecutor) taskExecutor).getThreadGroup();
					if (!th.isDestroyed()) {
						// ask the threads nicely to stop
						th.interrupt();
					}
				}
				taskExecutor = null;
			}

			if (isShutdownTaskExecutorManagedInternally) {
				try {
					((DisposableBean) shutdownTaskExecutor).destroy();
				} catch (Exception ex) {
					log.debug("Received exception while shutting down shutdown task executor", ex);
				}
				shutdownTaskExecutor = null;
			}
		}
	}

	/**
	 * Copies the URLs returned by the given enumeration and returns them as an array of Strings for consumption by the
	 * application context.
	 * 
	 * @param enm
	 * @return
	 */
	@SuppressWarnings("deprecation")
	private String[] copyEnumerationToList(Enumeration<?> enm) {
		List<String> urls = new ArrayList<String>(4);
		while (enm != null && enm.hasMoreElements()) {
			URL configURL = (URL) enm.nextElement();
			if (configURL != null) {
				String configURLAsString = configURL.toExternalForm();
				try {
					urls.add(URLDecoder.decode(configURLAsString, "UTF8"));
				} catch (UnsupportedEncodingException uee) {
					log.warn("UTF8 encoding not supported, using the platform default");
					urls.add(URLDecoder.decode(configURLAsString));
				}
			}
		}

		return (String[]) urls.toArray(new String[urls.size()]);
	}

	private Properties createDefaultProperties() {
		Properties properties = new Properties();
		properties.setProperty(SHUTDOWN_WAIT_KEY, "" + DEFAULT_SHUTDOWN_WAIT);
		properties.setProperty(PROCESS_ANNOTATIONS_KEY, "" + DEFAULT_PROCESS_ANNOTATION);
		properties.setProperty(WAIT_FOR_DEPS_TIMEOUT_KEY, "" + DEFAULT_DEP_WAIT);

		return properties;
	}

	protected void addDefaultDependencyFactories() {
		boolean debug = log.isDebugEnabled();

		// default JDK 1.4 processor
		dependencyFactories.add(0, new MandatoryImporterDependencyFactory());

		// load through reflection the dependency and injection processors if running on JDK 1.5 and annotation
		// processing is enabled
		if (processAnnotation) {
			// dependency processor
			Class<?> annotationProcessor = null;
			try {
				annotationProcessor =
						Class.forName(ANNOTATION_DEPENDENCY_FACTORY, false, ExtenderConfiguration.class
								.getClassLoader());
			} catch (ClassNotFoundException cnfe) {
				log.warn("Spring DM annotation package not found, annotation processing disabled.");
				log.debug("Spring DM annotation package not found, annotation processing disabled.", cnfe);
				return;
			}
			Object processor = BeanUtils.instantiateClass(annotationProcessor);
			Assert.isInstanceOf(OsgiServiceDependencyFactory.class, processor);
			dependencyFactories.add(1, (OsgiServiceDependencyFactory) processor);

			if (debug)
				log.debug("Succesfully loaded annotation dependency processor [" + ANNOTATION_DEPENDENCY_FACTORY + "]");

			// add injection processor (first in line)
			postProcessors.add(0, new OsgiAnnotationPostProcessor());
			log.info("Spring-DM annotation processing enabled");
		} else {
			if (debug) {
				log.debug("Spring-DM annotation processing disabled; [" + ANNOTATION_DEPENDENCY_FACTORY
						+ "] not loaded");
			}
		}

	}

	private TaskExecutor createDefaultTaskExecutor() {
		// create thread-pool for starting contexts
		ThreadGroup threadGroup =
				new ThreadGroup("eclipse-gemini-blueprint-extender[" + ObjectUtils.getIdentityHexString(this) + "]-threads");
		threadGroup.setDaemon(false);

		SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
		taskExecutor.setThreadGroup(threadGroup);
		taskExecutor.setThreadNamePrefix("EclipseGeminiBlueprintExtenderThread-");

		isTaskExecutorManagedInternally = true;

		return taskExecutor;
	}

	private TaskExecutor createDefaultShutdownTaskExecutor() {
		TimerTaskExecutor taskExecutor = new TimerTaskExecutor() {
			@Override
			protected Timer createTimer() {
				return new Timer("Gemini Blueprint context shutdown thread", true);
			}
		};

		taskExecutor.afterPropertiesSet();
		isShutdownTaskExecutorManagedInternally = true;
		return taskExecutor;
	}

	private OsgiBundleApplicationContextEventMulticaster createDefaultEventMulticaster() {
		isMulticasterManagedInternally = true;
		return new OsgiBundleApplicationContextEventMulticasterAdapter(new SimpleApplicationEventMulticaster());
	}

	private OsgiApplicationContextCreator createDefaultApplicationContextCreator() {
		return new DefaultOsgiApplicationContextCreator();
	}

	private OsgiBundleApplicationContextListener createDefaultApplicationContextListener() {
		return new DefaultOsgiBundleApplicationContextListener(log);
	}

	private long getShutdownWaitTime(Properties properties) {
		return Long.parseLong(properties.getProperty(SHUTDOWN_WAIT_KEY));
	}

	private long getDependencyWaitTime(Properties properties) {
		return Long.parseLong(properties.getProperty(WAIT_FOR_DEPS_TIMEOUT_KEY));
	}

	private boolean getProcessAnnotations(Properties properties) {
		return Boolean.valueOf(properties.getProperty(PROCESS_ANNOTATIONS_KEY)).booleanValue()
				|| Boolean.getBoolean(AUTO_ANNOTATION_PROCESSING);
	}

	/**
	 * Returns the taskExecutor.
	 * 
	 * @return Returns the taskExecutor
	 */
	public TaskExecutor getTaskExecutor() {
		synchronized (lock) {
			return taskExecutor;
		}
	}

	/**
	 * Returns the shutdown task executor.
	 * 
	 * @return Returns the shutdown task executor
	 */
	public TaskExecutor getShutdownTaskExecutor() {
		synchronized (lock) {
			return shutdownTaskExecutor;
		}
	}

	/**
	 * Returns the contextEventListener.
	 * 
	 * @return Returns the contextEventListener
	 */
	public OsgiBundleApplicationContextListener getContextEventListener() {
		synchronized (lock) {
			return contextEventListener;
		}
	}

	/**
	 * Returns the shutdownWaitTime.
	 * 
	 * @return Returns the shutdownWaitTime
	 */
	public long getShutdownWaitTime() {
		synchronized (lock) {
			return shutdownWaitTime;
		}
	}

	/**
	 * Indicates if the process annotation is enabled or not.
	 * 
	 * @return Returns true if the annotation should be processed or not otherwise.
	 */
	public boolean shouldProcessAnnotation() {
		synchronized (lock) {
			return processAnnotation;
		}
	}

	/**
	 * Returns the dependencyWaitTime.
	 * 
	 * @return Returns the dependencyWaitTime
	 */
	public long getDependencyWaitTime() {
		synchronized (lock) {
			return dependencyWaitTime;
		}
	}

	/**
	 * Returns the eventMulticaster.
	 * 
	 * @return Returns the eventMulticaster
	 */
	public OsgiBundleApplicationContextEventMulticaster getEventMulticaster() {
		synchronized (lock) {
			return eventMulticaster;
		}
	}

	/**
	 * Sets the flag to force the taskExtender to close up in case of runaway threads - this applies *only* if the
	 * taskExecutor has been created internally.
	 * 
	 * <p/> The flag will cause a best attempt to shutdown the threads.
	 * 
	 * @param forceThreadShutdown The forceThreadShutdown to set.
	 */
	public void setForceThreadShutdown(boolean forceThreadShutdown) {
		synchronized (lock) {
			this.forceThreadShutdown = forceThreadShutdown;
		}
	}

	/**
	 * Returns the contextCreator.
	 * 
	 * @return Returns the contextCreator
	 */
	public OsgiApplicationContextCreator getContextCreator() {
		synchronized (lock) {
			return contextCreator;
		}
	}

	/**
	 * Returns the postProcessors.
	 * 
	 * @return Returns the postProcessors
	 */
	public List<OsgiBeanFactoryPostProcessor> getPostProcessors() {
		return postProcessors;
	}

	/**
	 * Returns the class loader wrapped around the extender bundle.
	 * 
	 * @return extender bundle class loader
	 */
	public ClassLoader getClassLoader() {
		return classLoader;
	}

	/**
	 * Returns the dependencies factories declared by the extender configuration. The list automatically contains the
	 * default listeners (such as the annotation one).
	 * 
	 * @return list of dependency factories
	 */
	public List<OsgiServiceDependencyFactory> getDependencyFactories() {
		return dependencyFactories;
	}
}