/******************************************************************************
 * 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.util.Collections;
import java.util.Dictionary;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.gemini.blueprint.util.internal.MapBasedDictionary;
import org.eclipse.gemini.blueprint.util.internal.ServiceReferenceBasedMap;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * Utility class for retrieving OSGi service references. This class offers a unified filter-based access for OSGi
 * services as well as translation of checked exceptions {@link InvalidSyntaxException} into unchecked ones.
 * 
 * <p/>
 * 
 * This classes uses {@link OsgiFilterUtils} underneath to allow multiple class names to be used for service reference
 * lookup.
 * 
 * @see OsgiFilterUtils
 * @author Costin Leau
 * 
 */
public abstract class OsgiServiceReferenceUtils {

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

	/**
	 * Returns a reference to the <em>best matching</em> service for the given class names.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param classes array of fully qualified class names
	 * @return reference to the <em>best matching</em> service
	 */
	public static ServiceReference getServiceReference(BundleContext bundleContext, String[] classes) {
		return getServiceReference(bundleContext, classes, null);
	}

	/**
	 * Returns a reference to the <em>best matching</em> service for the given class and OSGi filter.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param clazz fully qualified class name (can be <code>null</code>)
	 * @param filter valid OSGi filter (can be <code>null</code>)
	 * @return reference to the <em>best matching</em> service
	 */
	public static ServiceReference getServiceReference(BundleContext bundleContext, String clazz, String filter) {
		ServiceReference[] refs = getServiceReferences(bundleContext, clazz, filter);

		// pick the best service
		return getServiceReference(refs);
	}

	public static ServiceReference getServiceReference(ServiceReference... references) {
		if (ObjectUtils.isEmpty(references)) {
			return null;
		}

		ServiceReference winningReference = references[0];

		if (references.length > 1) {
			long winningId = getServiceId(winningReference);
			int winningRanking = getServiceRanking(winningReference);

			// start iterating in order to find the best match
			for (int i = 1; i < references.length; i++) {
				ServiceReference reference = references[i];
				int serviceRanking = getServiceRanking(reference);
				long serviceId = getServiceId(reference);

				if ((serviceRanking > winningRanking) || (serviceRanking == winningRanking && winningId > serviceId)) {
					winningReference = reference;
					winningId = serviceId;
					winningRanking = serviceRanking;
				}
			}
		}
		return winningReference;
	}

	/**
	 * Returns a reference to the <em>best matching</em> service for the given classes and OSGi filter.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param classes array of fully qualified class names
	 * @param filter valid OSGi filter (can be <code>null</code>)
	 * @return reference to the <em>best matching</em> service
	 */
	public static ServiceReference getServiceReference(BundleContext bundleContext, String[] classes, String filter) {
		// use #getServiceReference(BundleContext, String, String) method to
		// speed the service lookup process by
		// giving one class as a hint to the OSGi implementation

		String clazz = (ObjectUtils.isEmpty(classes) ? null : classes[0]);

		return getServiceReference(bundleContext, clazz, OsgiFilterUtils.unifyFilter(classes, filter));
	}

	/**
	 * Returns a reference to the <em>best matching</em> service for the given OSGi filter.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param filter valid OSGi filter (can be <code>null</code>)
	 * @return reference to the <em>best matching</em> service
	 */
	public static ServiceReference getServiceReference(BundleContext bundleContext, String filter) {
		return getServiceReference(bundleContext, (String) null, filter);
	}

	/**
	 * Returns references to <em>all</em> services matching the given class names.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param classes array of fully qualified class names
	 * @return non-<code>null</code> array of references to matching services
	 */
	public static ServiceReference[] getServiceReferences(BundleContext bundleContext, String[] classes) {
		return getServiceReferences(bundleContext, classes, null);
	}

	/**
	 * Returns references to <em>all</em> services matching the given class name and OSGi filter.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param clazz fully qualified class name (can be <code>null</code>)
	 * @param filter valid OSGi filter (can be <code>null</code>)
	 * @return non-<code>null</code> array of references to matching services
	 */
	public static ServiceReference[] getServiceReferences(BundleContext bundleContext, String clazz, String filter) {
		Assert.notNull(bundleContext, "bundleContext should be not null");

		try {
			ServiceReference[] refs = bundleContext.getServiceReferences(clazz, filter);
			return (refs == null ? new ServiceReference[0] : refs);
		} catch (InvalidSyntaxException ise) {
			throw (RuntimeException) new IllegalArgumentException("invalid filter: " + ise.getFilter()).initCause(ise);
		}
	}

	/**
	 * Returns references to <em>all</em> services matching the given class names and OSGi filter.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param classes array of fully qualified class names
	 * @param filter valid OSGi filter (can be <code>null</code>)
	 * @return non-<code>null</code> array of references to matching services
	 */
	public static ServiceReference[] getServiceReferences(BundleContext bundleContext, String[] classes, String filter) {
		// use #getServiceReferences(BundleContext, String, String) method to
		// speed the service lookup process by
		// giving one class as a hint to the OSGi implementation
		// additionally this allows type filtering

		String clazz = (ObjectUtils.isEmpty(classes) ? null : classes[0]);
		return getServiceReferences(bundleContext, clazz, OsgiFilterUtils.unifyFilter(classes, filter));
	}

	/**
	 * Returns references to <em>all</em> services matching the OSGi filter.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param filter valid OSGi filter (can be <code>null</code>)
	 * @return non-<code>null</code> array of references to matching services
	 */
	public static ServiceReference[] getServiceReferences(BundleContext bundleContext, String filter) {
		return getServiceReferences(bundleContext, (String) null, filter);
	}

	/**
	 * Returns the service id ({@link Constants#SERVICE_ID}) of the given service reference.
	 * 
	 * @param reference OSGi service reference
	 * @return service id
	 */
	public static long getServiceId(ServiceReference reference) {
		Assert.notNull(reference);
		return ((Long) reference.getProperty(Constants.SERVICE_ID)).longValue();
	}

	/**
	 * Returns the service ranking ({@link Constants#SERVICE_RANKING}) of the given service reference.
	 * 
	 * @param reference OSGi service reference
	 * @return service ranking
	 */
	public static int getServiceRanking(ServiceReference reference) {
		Assert.notNull(reference);

		Object ranking = reference.getProperty(Constants.SERVICE_RANKING);
		// if the property is not supplied or of incorrect type, use a
		// default
		return ((ranking != null && ranking instanceof Integer) ? ((Integer) ranking).intValue() : 0);
	}

	/**
	 * Returns the advertised class names ({@link Constants#OBJECTCLASS}) of the given service reference.
	 * 
	 * @param reference OSGi service reference
	 * @return service advertised class names
	 */
	public static String[] getServiceObjectClasses(ServiceReference reference) {
		Assert.notNull(reference);
		return (String[]) reference.getProperty(Constants.OBJECTCLASS);
	}

	/**
	 * Returns a {@link Map} containing the properties available for the given service reference. This method takes a
	 * snapshot of the properties; future changes to the service properties will not be reflected in the returned
	 * dictionary.
	 * 
	 * <p/> Note that the returned type implements the {@link java.util.Map} interface also.
	 * 
	 * @param reference OSGi service reference
	 * @return a <code>Dictionary</code> containing the service reference properties taken as a snapshot
	 */
	public static Dictionary getServicePropertiesSnapshot(ServiceReference reference) {
		return new MapBasedDictionary(getServicePropertiesSnapshotAsMap(reference));
	}

	/**
	 * Returns a {@link Map} containing the properties available for the given service reference. This method takes a
	 * snapshot of the properties; future changes to the service properties will not be reflected in the returned
	 * dictionary.
	 * 
	 * @param reference OSGi service reference
	 * @return a <code>Map</code> containing the service reference properties taken as a snapshot
	 */
	public static Map getServicePropertiesSnapshotAsMap(ServiceReference reference) {
		Assert.notNull(reference);
		String[] keys = reference.getPropertyKeys();

		Map map = new LinkedHashMap(keys.length);

		for (int i = 0; i < keys.length; i++) {
			map.put(keys[i], reference.getProperty(keys[i]));
		}

		// mark it as read-only
		map = Collections.unmodifiableMap(map);
		return map;
	}

	/**
	 * Returns a {@link Dictionary} containing the properties available for the given service reference. The returned
	 * object will reflect any updates made to to the <code>ServiceReference</code> through the owning
	 * <code>ServiceRegistration</code>.
	 * 
	 * 
	 * <p/> Note that the returned type implements the {@link java.util.Map} interface also.
	 * 
	 * @param reference OSGi service reference
	 * @return a <code>Dictionary</code> containing the latest service reference properties
	 */
	public static Dictionary getServiceProperties(ServiceReference reference) {
		return new MapBasedDictionary(getServicePropertiesAsMap(reference));
	}

	/**
	 * Returns a {@link Map} containing the properties available for the given service reference. The returned object
	 * will reflect any updates made to to the <code>ServiceReference</code> through the owning
	 * <code>ServiceRegistration</code>. Consider using {@link #getServiceProperties(ServiceReference)} which returns an
	 * object that extends {@link Dictionary} as well as implements the {@link Map} interface.
	 * 
	 * @param reference OSGi service reference
	 * @return a <code>Map</code> containing the latest service reference properties
	 * @see #getServiceProperties(ServiceReference)
	 */
	public static Map getServicePropertiesAsMap(ServiceReference reference) {
		Assert.notNull(reference);
		return new ServiceReferenceBasedMap(reference);

	}

	/**
	 * Checks if the given filter matches at least one OSGi service or not.
	 * 
	 * @param bundleContext OSGi bundle context
	 * @param filter valid OSGi filter (can be <code>null</code>)
	 * @return true if the filter matches at least one OSGi service, false otherwise
	 */
	public static boolean isServicePresent(BundleContext bundleContext, String filter) {
		return !ObjectUtils.isEmpty(getServiceReferences(bundleContext, filter));
	}
}