/******************************************************************************
 * 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.service.dependency.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.service.exporter.support.internal.controller.ExporterControllerUtils;
import org.eclipse.gemini.blueprint.service.exporter.support.internal.controller.ExporterInternalActions;
import org.eclipse.gemini.blueprint.service.importer.OsgiServiceDependency;
import org.eclipse.gemini.blueprint.service.importer.support.AbstractOsgiServiceImportFactoryBean;
import org.eclipse.gemini.blueprint.service.importer.support.Availability;
import org.eclipse.gemini.blueprint.service.importer.support.OsgiServiceCollectionProxyFactoryBean;
import org.eclipse.gemini.blueprint.service.importer.support.OsgiServiceProxyFactoryBean;
import org.eclipse.gemini.blueprint.service.importer.support.internal.controller.ImporterControllerUtils;
import org.eclipse.gemini.blueprint.service.importer.support.internal.controller.ImporterInternalActions;
import org.eclipse.gemini.blueprint.service.importer.support.internal.dependency.ImporterStateListener;
import org.eclipse.gemini.blueprint.util.internal.BeanFactoryUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Default implementation of {@link MandatoryServiceDependencyManager} which determines the relationship between
 * importers and exporters and unpublishes exported service if they dependent, transitively, on imported OSGi services
 * that are mandatory and cannot be satisfied.
 * 
 * <strong>Note:</strong> aimed for singleton beans only
 * 
 * @author Costin Leau
 * 
 */
public class DefaultMandatoryDependencyManager implements MandatoryServiceDependencyManager, BeanFactoryAware,
		DisposableBean {

	/**
	 * Importer state listener that gets associated with each exporter.
	 * 
	 * @author Costin Leau
	 */
	private class ImporterDependencyListener implements ImporterStateListener {

		private final Object exporter;
		private final String exporterName;

		private ImporterDependencyListener(Object exporter) {
			this.exporter = exporter;
			this.exporterName = (String) exporterToName.get(exporter);

		}

		public void importerSatisfied(Object importer, OsgiServiceDependency dependency) {

			boolean trace = log.isTraceEnabled();
			boolean exporterRemoved = false;

			// update importer status
			synchronized (exporter) {
				Map<Object, Boolean> importers = exporterToImporterDeps.get(exporter);
				exporterRemoved = !(importers != null);

				// if the list is not present (exporter was removed), bail out
				if (!exporterRemoved) {
					importers.put(importer, Boolean.TRUE);
					if (trace)
						log.trace("Importer [" + importerToName.get(importer)
								+ "] is satisfied; checking the rest of the dependencies for exporter "
								+ exporterToName.get(exporter));

					checkIfExporterShouldStart(exporter, importers);
				}
			}

			if (exporterRemoved && trace)
				log.trace("Exporter [" + exporterName + "] removed; ignoring dependency [" + dependency.getBeanName()
						+ "] update");
		}

		public void importerUnsatisfied(Object importer, OsgiServiceDependency dependency) {

			boolean exporterRemoved = false;

			synchronized (exporter) {
				Map<Object, Boolean> importers = exporterToImporterDeps.get(exporter);
				exporterRemoved = !(importers != null);
				if (!exporterRemoved) {
					// record the importer status
					importers.put(importer, Boolean.FALSE);
				}
			}

			boolean trace = log.isTraceEnabled();

			if (!exporterRemoved) {
				if (trace)
					log.trace("Exporter [" + exporterName + "] stopped; transitive OSGi dependency ["
							+ dependency.getBeanName() + "] is unsatifised");

				// if the importer goes down, simply shut down the exporter
				stopExporter(exporter);
			} else {
				if (trace) {
					log.trace("Exporter [" + exporterName + "] removed; ignoring dependency ["
							+ dependency.getBeanName() + "] update");
				}
			}
		}
	}

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

	/** cache map - useful for avoiding double registration */
	private final ConcurrentMap<String, Object> exportersSeen = new ConcurrentHashMap<String, Object>(4);

	private static final Object VALUE = new Object();

	/**
	 * Importers on which an exporter depends. The exporter instance is used as a key, while the value is represented by
	 * a list of importers name and their status (up or down).
	 */
	private final Map<Object, Map<Object, Boolean>> exporterToImporterDeps =
			new ConcurrentHashMap<Object, Map<Object, Boolean>>(8);

	/** exporter -> importer listener map */
	private final Map<Object, ImporterStateListener> exporterListener =
			new ConcurrentHashMap<Object, ImporterStateListener>(8);

	/** importer -> name map */
	private final ConcurrentMap<Object, String> importerToName = new ConcurrentHashMap<Object, String>(8);

	/** exporter name map */
	private final Map<Object, String> exporterToName = new ConcurrentHashMap<Object, String>(8);

	/** owning bean factory */
	private ConfigurableListableBeanFactory beanFactory;

	public void addServiceExporter(Object exporter, String exporterBeanName) {
		Assert.hasText(exporterBeanName);

		if (exportersSeen.putIfAbsent(exporterBeanName, VALUE) == null) {

			String beanName = exporterBeanName;

			if (beanFactory.isFactoryBean(exporterBeanName))
				beanName = BeanFactory.FACTORY_BEAN_PREFIX + exporterBeanName;

			// check if it's factory bean (no need to check for abstract
			// definition since we're called by a BPP)
			if (!beanFactory.isSingleton(beanName)) {
				log.info("Exporter [" + beanName + "] is not singleton and will not be tracked");
			}

			else {
				if (log.isDebugEnabled())
					log.debug("Exporter [" + beanName + "] is being tracked for dependencies");

				exporterToName.put(exporter, exporterBeanName);
				// retrieve associated controller
				ExporterInternalActions controller = ExporterControllerUtils.getControllerFor(exporter);

				// disable publication at startup
				controller.registerServiceAtStartup(false);

				// populate the dependency maps
				discoverDependentImporterFor(exporterBeanName, exporter);
			}
		}
	}

	/**
	 * Discover all the importers for the given exporter. Since the importers are already created before the exporter
	 * instance is created, this method only does filtering based on the mandatory imports.
	 */
	protected void discoverDependentImporterFor(String exporterBeanName, Object exporter) {

		boolean trace = log.isTraceEnabled();

		// determine exporters
		String[] importerA =
				BeanFactoryUtils.getTransitiveDependenciesForBean(beanFactory, exporterBeanName, true,
						OsgiServiceProxyFactoryBean.class);

		String[] importerB =
				BeanFactoryUtils.getTransitiveDependenciesForBean(beanFactory, exporterBeanName, true,
						OsgiServiceCollectionProxyFactoryBean.class);

		String[] importerNames = StringUtils.concatenateStringArrays(importerA, importerB);

		// create map of associated importers
		Map<Object, String> dependingImporters = new LinkedHashMap<Object, String>(importerNames.length);

		if (trace)
			log.trace("Exporter [" + exporterBeanName + "] depends (transitively) on the following importers:"
					+ ObjectUtils.nullSafeToString(importerNames));

		// first create a listener for the exporter
		ImporterStateListener listener = new ImporterDependencyListener(exporter);
		exporterListener.put(exporter, listener);

		// exclude non-mandatory importers
		// non-singletons get added only once (as one instance is enough)
		for (int i = 0; i < importerNames.length; i++) {
			if (beanFactory.isSingleton(importerNames[i])) {
				Object importer = beanFactory.getBean(importerNames[i]);

				// create an importer -> exporter association
				if (isMandatory(importer)) {
					dependingImporters.put(importer, importerNames[i]);
					importerToName.putIfAbsent(importer, importerNames[i]);
				}

				else if (trace)
					log.trace("Importer [" + importerNames[i] + "] is optional; skipping it");
			} else if (trace)
				log.trace("Importer [" + importerNames[i] + "] is a non-singleton; ignoring it");
		}

		if (trace)
			log.trace("After filtering, exporter [" + exporterBeanName + "] depends on importers:"
					+ dependingImporters.values());

		Collection<Object> filteredImporters = dependingImporters.keySet();

		// add the importers and their status to the collection
		synchronized (exporter) {
			Map<Object, Boolean> importerStatuses = new LinkedHashMap<Object, Boolean>(filteredImporters.size());

			for (Iterator<Object> iter = filteredImporters.iterator(); iter.hasNext();) {
				Object importer = iter.next();
				importerStatuses.put(importer, Boolean.valueOf(isSatisfied(importer)));
				// add the listener after the importer status has been recorded
				addListener(importer, listener);
			}
			exporterToImporterDeps.put(exporter, importerStatuses);
			if (!checkIfExporterShouldStart(exporter, importerStatuses)) {
				callUnregisterOnStartup(exporter);
			}
		}
	}

	private boolean checkIfExporterShouldStart(Object exporter, Map<Object, Boolean> importers) {

		if (!importers.containsValue(Boolean.FALSE)) {
			startExporter(exporter);

			if (log.isDebugEnabled())
				log.trace("Exporter [" + exporterToName.get(exporter) + "] started; "
						+ "all its dependencies are satisfied");
			return true;

		} else {
			List<String> unsatisfiedDependencies = new ArrayList<String>(importers.size());

			for (Iterator<Map.Entry<Object, Boolean>> iterator = importers.entrySet().iterator(); iterator.hasNext();) {
				Map.Entry<Object, Boolean> entry = iterator.next();
				if (Boolean.FALSE.equals(entry.getValue()))
					unsatisfiedDependencies.add(importerToName.get(entry.getKey()));
			}

			if (log.isTraceEnabled()) {
				log.trace("Exporter [" + exporterToName.get(exporter)
						+ "] not started; there are still unsatisfied dependencies " + unsatisfiedDependencies);
			}

			return false;
		}
	}

	public void removeServiceExporter(Object bean, String beanName) {
		if (log.isTraceEnabled()) {
			log.trace("Removing exporter [" + beanName + "]");
		}

		// remove the exporter and its listeners from the map
		ImporterStateListener stateListener = (ImporterStateListener) exporterListener.remove(bean);

		Map<Object, Boolean> importers;

		synchronized (bean) {
			importers = exporterToImporterDeps.remove(bean);
		}

		// no need to do synchronization anymore since no other threads will find the collection
		if (importers != null)
			for (Iterator<Object> iterator = importers.keySet().iterator(); iterator.hasNext();) {
				Object importer = iterator.next();
				// get associated controller
				removeListener(importer, stateListener);
			}
	}

	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory);
		this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
	}

	public void destroy() {
		exportersSeen.clear();
		exporterListener.clear();
		exporterToImporterDeps.clear();
		exporterToName.clear();
		importerToName.clear();
	}

	private void startExporter(Object exporter) {
		ExporterControllerUtils.getControllerFor(exporter).registerService();
	}

	private void stopExporter(Object exporter) {
		ExporterControllerUtils.getControllerFor(exporter).unregisterService();
	}

	private void callUnregisterOnStartup(Object exporter) {
		ExporterControllerUtils.getControllerFor(exporter).callUnregisterOnStartup();
	}

	private void addListener(Object importer, ImporterStateListener stateListener) {
		ImporterInternalActions controller = ImporterControllerUtils.getControllerFor(importer);
		controller.addStateListener(stateListener);
	}

	private void removeListener(Object importer, ImporterStateListener stateListener) {
		ImporterInternalActions controller = ImporterControllerUtils.getControllerFor(importer);
		controller.removeStateListener(stateListener);
	}

	private boolean isSatisfied(Object importer) {
		return ImporterControllerUtils.getControllerFor(importer).isSatisfied();
	}

	private boolean isMandatory(Object importer) {
		if (importer instanceof AbstractOsgiServiceImportFactoryBean) {
			return Availability.MANDATORY.equals(((AbstractOsgiServiceImportFactoryBean) importer).getAvailability());
		}
		return false;
	}
}