/******************************************************************************
 * 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.io.internal.resolver;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.gemini.blueprint.io.internal.OsgiHeaderUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.packageadmin.ExportedPackage;
import org.osgi.service.packageadmin.PackageAdmin;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * {@link PackageAdmin} based dependency resolver.
 * 
 * <p/>
 * This implementation uses the OSGi PackageAdmin service to determine
 * dependencies between bundles. Since it's highly dependent on an external
 * service, it might be better to use a listener based implementation for poor
 * performing environments.
 * 
 * <p/>
 * This implementation does consider required bundles.
 * 
 * @author Costin Leau
 * 
 */
public class PackageAdminResolver implements DependencyResolver {

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

	private final BundleContext bundleContext;


	public PackageAdminResolver(BundleContext bundleContext) {
		Assert.notNull(bundleContext);
		this.bundleContext = bundleContext;
	}

	public ImportedBundle[] getImportedBundles(Bundle bundle) {
		boolean trace = log.isTraceEnabled();

		PackageAdmin pa = getPackageAdmin();

		// create map with bundles as keys and a list of packages as value
		Map<Bundle, List<String>> importedBundles = new LinkedHashMap<Bundle, List<String>>(8);

		// 1. consider required bundles first

		// see if there are required bundle(s) defined
		String[] entries = OsgiHeaderUtils.getRequireBundle(bundle);

		// 1. if so, locate the bundles
		for (int i = 0; i < entries.length; i++) {
			String[] parsed = OsgiHeaderUtils.parseRequiredBundleString(entries[i]);
			// trim the strings just to be on the safe side (some implementations allows whitespaces, some don't)
			String symName = parsed[0].trim();
			String versionRange = parsed[1].trim();
			Bundle[] foundBundles = pa.getBundles(symName, versionRange);

			if (!ObjectUtils.isEmpty(foundBundles)) {
				Bundle requiredBundle = foundBundles[0];

				// find exported packages
				ExportedPackage[] exportedPackages = pa.getExportedPackages(requiredBundle);
				if (exportedPackages != null)
					addExportedPackages(importedBundles, requiredBundle, exportedPackages);
			}
			else {
				if (trace) {
					log.trace("Cannot find required bundle " + symName + "|" + versionRange);
				}
			}
		}

		// 2. determine imported bundles 
		// get all bundles
		Bundle[] bundles = bundleContext.getBundles();

		for (int i = 0; i < bundles.length; i++) {
			Bundle analyzedBundle = bundles[i];
			// if the bundle is already included (it's a required one), there's no need to look at it again
			if (!importedBundles.containsKey(analyzedBundle)) {
				ExportedPackage[] epa = pa.getExportedPackages(analyzedBundle);
				if (epa != null)
					for (int j = 0; j < epa.length; j++) {
						ExportedPackage exportedPackage = epa[j];
						Bundle[] importingBundles = exportedPackage.getImportingBundles();
						if (importingBundles != null)
							for (int k = 0; k < importingBundles.length; k++) {
								if (bundle.equals(importingBundles[k])) {
									addImportedBundle(importedBundles, exportedPackage);
								}
							}
					}
			}
		}

		List<ImportedBundle> importedBundlesList = new ArrayList<ImportedBundle>(importedBundles.size());

		for (Map.Entry<Bundle, List<String>> entry : importedBundles.entrySet()) {
			Bundle importedBundle = entry.getKey();
			List<String> packages = entry.getValue();
			importedBundlesList.add(new ImportedBundle(importedBundle,
				(String[]) packages.toArray(new String[packages.size()])));
		}

		return (ImportedBundle[]) importedBundlesList.toArray(new ImportedBundle[importedBundlesList.size()]);
	}

	/**
	 * Adds the imported bundle to the map of packages.
	 * 
	 * @param map
	 * @param bundle
	 * @param packageName
	 */
	private void addImportedBundle(Map<Bundle, List<String>> map, ExportedPackage expPackage) {
		Bundle bnd = expPackage.getExportingBundle();
		List<String> packages = map.get(bnd);
		if (packages == null) {
			packages = new ArrayList<String>(4);
			map.put(bnd, packages);
		}
		packages.add(new String(expPackage.getName()));
	}

	/**
	 * Adds the bundle exporting the given packages which are then imported by
	 * the owning bundle. This applies to special imports (such as
	 * Require-Bundle).
	 * 
	 * @param map
	 * @param bundle
	 * @param pkgs
	 */
	private void addExportedPackages(Map<Bundle, List<String>> map, Bundle bundle, ExportedPackage[] pkgs) {
		List<String> packages = map.get(bundle);
		if (packages == null) {
			packages = new ArrayList<String>(pkgs.length);
			map.put(bundle, packages);
		}
		for (int i = 0; i < pkgs.length; i++) {
			packages.add(pkgs[i].getName());
		}
	}

	private PackageAdmin getPackageAdmin() {

		return AccessController.doPrivileged(new PrivilegedAction<PackageAdmin>() {

			public PackageAdmin run() {
				ServiceReference ref = bundleContext.getServiceReference(PackageAdmin.class.getName());
				if (ref == null)
					throw new IllegalStateException(PackageAdmin.class.getName() + " service is required");
				// don't do any proxying since PackageAdmin is normally a framework service
				// we can assume for now that it will always be available
				return (PackageAdmin) bundleContext.getService(ref);
			}
		});
	}
}