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

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Dictionary;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.FieldFilter;

/**
 * Utility class for OSGi {@link Bundle}s. Provides convenience methods for
 * interacting with a Bundle object.
 * 
 * @author Adrian Colyer
 * @author Costin Leau
 * @see OsgiStringUtils
 */
public abstract class OsgiBundleUtils {

	/**
	 * Whether Bundle#getBundleContext() method is available (even on 4.0
	 * platforms)
	 */
	private static final boolean getBundleContextAvailable;

	private static volatile BundleContextExtractor extractor;

	static {
		getBundleContextAvailable = (ReflectionUtils.findMethod(Bundle.class, "getBundleContext", new Class[0]) != null);
		if (getBundleContextAvailable)
			extractor = new Osgi41BundleContextExtractor();
	}


	private interface BundleContextExtractor {

		BundleContext getBundleContext(Bundle bundle);
	}

	private static class Osgi41BundleContextExtractor implements BundleContextExtractor {

		public BundleContext getBundleContext(Bundle bundle) {
			return bundle.getBundleContext();
		}
	}

	private static class ReflectionMethodInvocation implements BundleContextExtractor {

		private final Method method;


		private ReflectionMethodInvocation(Method method) {
			ReflectionUtils.makeAccessible(method);
			this.method = method;
		}

		public BundleContext getBundleContext(Bundle bundle) {
			return (BundleContext) ReflectionUtils.invokeMethod(method, bundle);
		}
	}

	private static class FieldExtractor implements BundleContextExtractor {

		private final Field field;


		private FieldExtractor(Field field) {
			ReflectionUtils.makeAccessible(field);
			this.field = field;
		}

		public BundleContext getBundleContext(Bundle bundle) {
			return (BundleContext) ReflectionUtils.getField(field, bundle);
		}
	}


	/**
	 * Returns the underlying BundleContext for the given Bundle. This uses
	 * reflection and highly dependent of the OSGi implementation. Should not be
	 * used if OSGi 4.1 is being used.
	 * 
	 * @param bundle OSGi bundle
	 * @return the bundle context for this bundle
	 */
	public static BundleContext getBundleContext(final Bundle bundle) {
		if (bundle == null)
			return null;

		if (extractor == null) {
			// try getBundleContext (for non OSGi 4.1 platforms)
			Method method = ReflectionUtils.findMethod(bundle.getClass(), "getBundleContext", new Class[0]);
			if (method != null) {
				if (Modifier.isPublic(method.getModifiers())) {
					extractor = new Osgi41BundleContextExtractor();
				}
			}
			else {
				if (method == null) {
					// try Equinox getContext
					method = ReflectionUtils.findMethod(bundle.getClass(), "getContext", new Class[0]);
				}
				if (method != null) {
					extractor = new ReflectionMethodInvocation(method);
				}
				else {
					final Field[] fields = new Field[1];
					// fallback to field inspection (KF and Prosyst)
					ReflectionUtils.doWithFields(bundle.getClass(), new FieldCallback() {

						public void doWith(final Field field) throws IllegalArgumentException, IllegalAccessException {
							ReflectionUtils.makeAccessible(field);
							fields[0] = field;
						}
					}, new FieldFilter() {

						public boolean matches(Field field) {
							return fields[0] == null && BundleContext.class.isAssignableFrom(field.getType());
						}
					});

					if (fields[0] != null) {
						extractor = new FieldExtractor(fields[0]);
					}
					else {
						throw new IllegalArgumentException("Cannot extract bundleContext from bundle type "
								+ bundle.getClass());
					}
				}
			}
		}

		return extractor.getBundleContext(bundle);
	}

	/**
	 * Indicates if the given bundle is active or not.
	 * 
	 * @param bundle OSGi bundle
	 * @return true if the given bundle is active, false otherwise
	 * @see Bundle#ACTIVE
	 */
	public static boolean isBundleActive(Bundle bundle) {
		Assert.notNull(bundle, "bundle is required");
		return (bundle.getState() == Bundle.ACTIVE);
	}

	/**
	 * Indicates if the given bundle is resolved or not.
	 * 
	 * @param bundle OSGi bundle
	 * @return true if the given bundle is resolved, false otherwise
	 * @see Bundle#RESOLVED
	 */
	public static boolean isBundleResolved(Bundle bundle) {
		Assert.notNull(bundle, "bundle is required");
		return (bundle.getState() >= Bundle.RESOLVED);
	}

	/**
	 * Indicates if the given bundle is lazily activated or not. That is, the
	 * bundle has a lazy activation policy and a STARTING state. Bundles that
	 * have been lazily started but have been activated will return false.
	 * 
	 * <p/>
	 * On OSGi R4.0 platforms, this method will always return false.
	 * 
	 * @param bundle OSGi bundle
	 * @return true if the bundle is lazily activated, false otherwise.
	 */
	@SuppressWarnings("unchecked")
	public static boolean isBundleLazyActivated(Bundle bundle) {
		Assert.notNull(bundle, "bundle is required");

		if (OsgiPlatformDetector.isR41()) {
			if (bundle.getState() == Bundle.STARTING) {
				Dictionary<Object, Object> headers = bundle.getHeaders();
				if (headers != null) {
					Object val = headers.get(Constants.BUNDLE_ACTIVATIONPOLICY);
					if (val instanceof String) {
						String value = ((String) val).trim();
						return (value.startsWith(Constants.ACTIVATION_LAZY));
					}
				}
			}
		}
		return false;
	}

	/**
	 * Indicates if the given bundle is a fragment or not.
	 * 
	 * @param bundle OSGi bundle
	 * @return true if the given bundle is a fragment, false otherwise
	 * @see Constants#FRAGMENT_HOST
	 */
	public static boolean isFragment(Bundle bundle) {
		Assert.notNull(bundle, "bundle is required");
		return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
	}

	/**
	 * Indicates if the given bundle is the system bundle or not.
	 * 
	 * @param bundle OSGi bundle
	 * @return true if the given bundle is a fragment, false otherwise
	 */
	public static boolean isSystemBundle(Bundle bundle) {
		Assert.notNull(bundle);
		return (bundle.getBundleId() == 0);
	}

	/**
	 * Returns the given bundle version.
	 * 
	 * @param bundle OSGi bundle
	 * @return bundle version
	 * @see Constants#BUNDLE_VERSION
	 */
	public static Version getBundleVersion(Bundle bundle) {
		return getHeaderAsVersion(bundle, Constants.BUNDLE_VERSION);
	}

	/**
	 * Finds an install bundle based by its symbolic name.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param symbolicName bundle symbolic name
	 * @return bundle matching the symbolic name (<code>null</code> if none is
	 *         found)
	 */
	public static Bundle findBundleBySymbolicName(BundleContext bundleContext, String symbolicName) {
		Assert.notNull(bundleContext, "bundleContext is required");
		Assert.hasText(symbolicName, "a not-null/not-empty symbolicName isrequired");

		Bundle[] bundles = bundleContext.getBundles();
		for (int i = 0; i < bundles.length; i++) {
			if (symbolicName.equals(bundles[i].getSymbolicName())) {
				return bundles[i];
			}
		}
		return null;
	}

	/**
	 * Returns the version for a given bundle manifest header.
	 * 
	 * @param bundle OSGi bundle
	 * @param header bundle manifest header
	 * @return the header value
	 */
	public static Version getHeaderAsVersion(Bundle bundle, String header) {
		Assert.notNull(bundle);
		return Version.parseVersion((String) bundle.getHeaders().get(header));
	}
}