/******************************************************************************
 * 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.activator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.eclipse.gemini.blueprint.context.ConfigurableOsgiBundleApplicationContext;
import org.eclipse.gemini.blueprint.context.DelegatedExecutionOsgiBundleApplicationContext;
import org.eclipse.gemini.blueprint.context.event.OsgiBundleApplicationContextEventMulticaster;
import org.eclipse.gemini.blueprint.extender.OsgiApplicationContextCreator;
import org.eclipse.gemini.blueprint.extender.OsgiBeanFactoryPostProcessor;
import org.eclipse.gemini.blueprint.extender.internal.dependencies.shutdown.BundleDependencyComparator;
import org.eclipse.gemini.blueprint.extender.internal.dependencies.shutdown.ComparatorServiceDependencySorter;
import org.eclipse.gemini.blueprint.extender.internal.dependencies.shutdown.ServiceDependencySorter;
import org.eclipse.gemini.blueprint.extender.internal.dependencies.shutdown.ShutdownSorter;
import org.eclipse.gemini.blueprint.extender.internal.dependencies.startup.DependencyWaiterApplicationContextExecutor;
import org.eclipse.gemini.blueprint.extender.internal.support.ExtenderConfiguration;
import org.eclipse.gemini.blueprint.extender.internal.support.OsgiBeanFactoryPostProcessorAdapter;
import org.eclipse.gemini.blueprint.extender.internal.util.concurrent.Counter;
import org.eclipse.gemini.blueprint.extender.internal.util.concurrent.RunnableTimedExecution;
import org.eclipse.gemini.blueprint.extender.support.ApplicationContextConfiguration;
import org.eclipse.gemini.blueprint.util.OsgiBundleUtils;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;

/**
 * Manager handling the startup/shutdown threading issues regarding OSGi contexts. Used by {@link ContextLoaderListener}
 * .
 * 
 * @author Costin Leau
 */
class LifecycleManager implements DisposableBean {

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

	/**
	 * The contexts we are currently managing. Keys are bundle ids, values are ServiceDependentOsgiApplicationContexts
	 * for the application context
	 */
	private final Map<Long, ConfigurableOsgiBundleApplicationContext> managedContexts =
			new ConcurrentHashMap<Long, ConfigurableOsgiBundleApplicationContext>(16);

	/** listener counter - used to properly synchronize shutdown */
	private Counter contextsStarted = new Counter("contextsStarted");

	// "Spring Application Context Creation Timer"
	private final Timer timer = new Timer("Spring DM Context Creation Timer", true);

	/** Task executor used for bootstraping the Spring contexts in async mode */
	private final TaskExecutor taskExecutor;

	/** ApplicationContext Creator */
	private final OsgiApplicationContextCreator contextCreator;

	/** BFPP list */
	private final List<OsgiBeanFactoryPostProcessor> postProcessors;

	/** shutdown task executor */
	private final TaskExecutor shutdownTaskExecutor;

	/**
	 * Task executor which uses the same thread for running tasks. Used when doing a synchronous wait-for-dependencies.
	 */
	private final TaskExecutor sameThreadTaskExecutor = new SyncTaskExecutor();

	/** Service-based dependency sorter for shutdown */
	private final ServiceDependencySorter shutdownDependencySorter = new ComparatorServiceDependencySorter();

	private final OsgiBundleApplicationContextEventMulticaster multicaster;

	private final ExtenderConfiguration extenderConfiguration;

	private final BundleContext bundleContext;

	private final OsgiContextProcessor processor;

	private final ApplicationContextConfigurationFactory contextConfigurationFactory;

	private final VersionMatcher versionMatcher;
	private final TypeCompatibilityChecker typeChecker;

	LifecycleManager(ExtenderConfiguration extenderConfiguration, VersionMatcher versionMatcher,
			ApplicationContextConfigurationFactory appCtxCfgFactory, OsgiContextProcessor processor,
			TypeCompatibilityChecker checker, BundleContext context) {

		this.versionMatcher = versionMatcher;
		this.extenderConfiguration = extenderConfiguration;
		this.contextConfigurationFactory = appCtxCfgFactory;

		this.processor = processor;

		this.taskExecutor = extenderConfiguration.getTaskExecutor();
		this.shutdownTaskExecutor = extenderConfiguration.getShutdownTaskExecutor();

		this.multicaster = extenderConfiguration.getEventMulticaster();

		this.contextCreator = extenderConfiguration.getContextCreator();
		this.postProcessors = extenderConfiguration.getPostProcessors();
		this.typeChecker = checker;

		this.bundleContext = context;
	}

	/**
	 * Context creation is a potentially long-running activity (certainly more than we want to do on the synchronous
	 * event callback).
	 * 
	 * <p/> Based on our configuration, the context can be started on the same thread or on a different one.
	 * 
	 * <p/> Kick off a background activity to create an application context for the given bundle if needed.
	 * 
	 * <b>Note:</b> Make sure to do the fastest filtering first to avoid slow-downs on platforms with a big number of
	 * plugins and wiring (i.e. Eclipse platform).
	 * 
	 * @param bundle
	 */
	protected void maybeCreateApplicationContextFor(Bundle bundle) {

		boolean debug = log.isDebugEnabled();
		String bundleString = "[" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "]";

		final Long bundleId = new Long(bundle.getBundleId());

		if (managedContexts.containsKey(bundleId)) {
			if (debug) {
				log.debug("Bundle " + bundleString + " is already managed; ignoring...");
			}
			return;
		}

		if (!versionMatcher.matchVersion(bundle)) {
			return;
		}

		BundleContext localBundleContext = OsgiBundleUtils.getBundleContext(bundle);

		// initialize context
		final DelegatedExecutionOsgiBundleApplicationContext localApplicationContext;

		if (debug)
			log.debug("Inspecting bundle " + bundleString);

		try {
			localApplicationContext = contextCreator.createApplicationContext(localBundleContext);
		} catch (Exception ex) {
			log.error("Cannot create application context for bundle " + bundleString, ex);
			return;
		}

		if (localApplicationContext == null) {
			log.debug("No application context created for bundle " + bundleString);
			return;
		}

		if (typeChecker != null) {
			if (!typeChecker.isTypeCompatible(localBundleContext)) {
				log.info("Bundle " + OsgiStringUtils.nullSafeName(bundle) + " is not type compatible with extender "
						+ OsgiStringUtils.nullSafeName(bundleContext.getBundle()) + "; ignoring bundle...");
				return;
			}
		}

		log.debug("Bundle " + OsgiStringUtils.nullSafeName(bundle) + " is type compatible with extender "
				+ OsgiStringUtils.nullSafeName(bundleContext.getBundle()) + "; processing bundle...");

		// create a dedicated hook for this application context
		BeanFactoryPostProcessor processingHook =
				new OsgiBeanFactoryPostProcessorAdapter(localBundleContext, postProcessors);

		// add in the post processors
		localApplicationContext.addBeanFactoryPostProcessor(processingHook);

		// add the context to the tracker
		managedContexts.put(bundleId, localApplicationContext);

		localApplicationContext.setDelegatedEventMulticaster(multicaster);

		ApplicationContextConfiguration config = contextConfigurationFactory.createConfiguration(bundle);

		final boolean asynch = config.isCreateAsynchronously();

		// create refresh runnable
		Runnable contextRefresh = new Runnable() {

			public void run() {
				// post refresh events are caught through events
				if (log.isTraceEnabled()) {
					log.trace("Calling pre-refresh on processor " + processor);
				}
				processor.preProcessRefresh(localApplicationContext);
				localApplicationContext.refresh();
			}
		};

		// executor used for creating the appCtx
		// chosen based on the sync/async configuration
		TaskExecutor executor = null;

		String creationType;

		// synch/asynch context creation
		if (asynch) {
			// for the async stuff use the executor
			executor = taskExecutor;
			creationType = "Asynchronous";
		} else {
			// for the sync stuff, use this thread
			executor = sameThreadTaskExecutor;
			creationType = "Synchronous";
		}

		if (debug) {
			log.debug(creationType + " context creation for bundle " + bundleString);
		}

		// wait/no wait for dependencies behaviour
		if (config.isWaitForDependencies()) {
			DependencyWaiterApplicationContextExecutor appCtxExecutor =
					new DependencyWaiterApplicationContextExecutor(localApplicationContext, !asynch,
							extenderConfiguration.getDependencyFactories());

			long timeout;
			// check whether a timeout has been defined

			if (config.isTimeoutDeclared()) {
				timeout = config.getTimeout();
				if (debug)
					log.debug("Setting bundle-defined, wait-for-dependencies/graceperiod timeout value=" + timeout
							+ " ms, for bundle " + bundleString);

			} else {
				timeout = extenderConfiguration.getDependencyWaitTime();
				if (debug)
					log.debug("Setting globally defined wait-for-dependencies/graceperiod timeout value=" + timeout
							+ " ms, for bundle " + bundleString);
			}

			appCtxExecutor.setTimeout(config.getTimeout());
			appCtxExecutor.setWatchdog(timer);
			appCtxExecutor.setTaskExecutor(executor);
			appCtxExecutor.setMonitoringCounter(contextsStarted);
			// set events publisher
			appCtxExecutor.setDelegatedMulticaster(this.multicaster);

			contextsStarted.increment();
		} else {
			// do nothing; by default contexts do not wait for services.
		}

		executor.execute(contextRefresh);
	}

	/**
	 * Closing an application context is a potentially long-running activity, however, we *have* to do it synchronously
	 * during the event process as the BundleContext object is not valid once we return from this method.
	 * 
	 * @param bundle
	 */
	protected void maybeCloseApplicationContextFor(Bundle bundle) {
		final ConfigurableOsgiBundleApplicationContext context =
				(ConfigurableOsgiBundleApplicationContext) managedContexts.remove(Long.valueOf(bundle.getBundleId()));
		if (context == null) {
			return;
		}

		RunnableTimedExecution.execute(new Runnable() {

			private final String toString = "Closing runnable for context " + context.getDisplayName();

			public void run() {
				closeApplicationContext(context);
			}

			public String toString() {
				return toString;
			}

		}, extenderConfiguration.getShutdownWaitTime(), shutdownTaskExecutor);
	}

	/**
	 * Closes an application context. This is a convenience methods that invokes the event notification as well.
	 * 
	 * @param ctx
	 */
	private void closeApplicationContext(ConfigurableOsgiBundleApplicationContext ctx) {
		if (log.isDebugEnabled()) {
			log.debug("Closing application context " + ctx.getDisplayName());
		}

		if (log.isTraceEnabled()) {
			log.trace("Calling pre-close on processor " + processor);
		}
		processor.preProcessClose(ctx);
		try {
			ctx.close();
		} finally {
			if (log.isTraceEnabled()) {
				log.trace("Calling post close on processor " + processor);
			}
			processor.postProcessClose(ctx);
		}
	}

	public void destroy() {
		// first stop the watchdog
		stopTimer();

		// get hold of the needed bundles
		List<Bundle> bundles = new ArrayList<Bundle>(managedContexts.size());

		for (ConfigurableOsgiBundleApplicationContext context : managedContexts.values()) {
			bundles.add(context.getBundle());
		}

		boolean debug = log.isDebugEnabled();

		if (debug) {
			log.debug("Starting shutdown procedure for bundles " + bundles);
		}
		while (!bundles.isEmpty()) {
			Collection<Bundle> candidates = ShutdownSorter.getBundles(bundles);
			if (debug)
				log.debug("Staging shutdown for bundles " + candidates);

			final List<Runnable> taskList = new ArrayList<Runnable>(candidates.size());
			final List<ConfigurableOsgiBundleApplicationContext> closedContexts =
					Collections.synchronizedList(new ArrayList<ConfigurableOsgiBundleApplicationContext>());
			final Object[] contextClosingDown = new Object[1];

			for (Bundle shutdownBundle : candidates) {
				Long id = new Long(shutdownBundle.getBundleId());
				final ConfigurableOsgiBundleApplicationContext context =
						(ConfigurableOsgiBundleApplicationContext) managedContexts.get(id);
				if (context != null) {
					closedContexts.add(context);
					// add a new runnable
					taskList.add(new Runnable() {

						private final String toString = "Closing runnable for context " + context.getDisplayName();

						public void run() {
							contextClosingDown[0] = context;
							// eliminate context
							closedContexts.remove(context);
							closeApplicationContext(context);
						}

						public String toString() {
							return toString;
						}
					});
				}
			}

			// tasks
			final Runnable[] tasks = (Runnable[]) taskList.toArray(new Runnable[taskList.size()]);

			// start the ripper >:)
			for (int j = 0; j < tasks.length; j++) {
				if (RunnableTimedExecution.execute(tasks[j], extenderConfiguration.getShutdownWaitTime(),
						shutdownTaskExecutor)) {
					if (debug) {
						log.debug(contextClosingDown[0] + " context did not close successfully; forcing shutdown...");
					}
				}
			}
		}

		this.managedContexts.clear();

		// before bailing out; wait for the threads that might be left by
		// the task executor
		stopTaskExecutor();
	}

	/**
	 * Do some additional waiting so the service dependency listeners detect the shutdown.
	 */
	private void stopTaskExecutor() {
		boolean debug = log.isDebugEnabled();

		if (debug)
			log.debug("Waiting for " + contextsStarted + " service dependency listener(s) to stop...");

		contextsStarted.waitForZero(extenderConfiguration.getShutdownWaitTime());

		if (!contextsStarted.isZero()) {
			if (debug)
				log.debug(contextsStarted.getValue()
						+ " service dependency listener(s) did not responded in time; forcing them to shutdown...");
			extenderConfiguration.setForceThreadShutdown(true);
		}

		else
			log.debug("All listeners closed");
	}

	/**
	 * Cancel any tasks scheduled for the timer.
	 */
	private void stopTimer() {
		if (log.isDebugEnabled())
			log.debug("Canceling timer tasks");
		timer.cancel();
	}
}