/*******************************************************************************
 * Copyright (c) 2003, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.osgi.internal.serviceregistry;

import java.util.*;
import org.eclipse.osgi.framework.debug.Debug;
import org.eclipse.osgi.framework.internal.core.*;
import org.osgi.framework.*;
import org.osgi.framework.Constants;

/**
 * A registered service.
 *
 * The framework returns a ServiceRegistration object when a
 * {@link BundleContextImpl#registerService(String, Object, Dictionary) BundleContext.registerService}
 * method is successful. This object is for the private use of
 * the registering bundle and should not be shared with other bundles.
 * <p>The ServiceRegistration object may be used to update the properties
 * for the service or to unregister the service.
 *
 * <p>If the ServiceRegistration is garbage collected the framework may remove
 * the service. This implies that if a
 * bundle wants to keep its service registered, it should keep the
 * ServiceRegistration object referenced.
 * 
 * @ThreadSafe
 */
public class ServiceRegistrationImpl<S> implements ServiceRegistration<S>, Comparable<ServiceRegistrationImpl<?>> {
	/** Internal framework object. */
	private final Framework framework;

	private final ServiceRegistry registry;

	/** context which registered this service. */
	private final BundleContextImpl context;

	/** bundle which registered this service. */
	private final Bundle bundle;

	/** service classes for this registration. */
	private final String[] clazzes;

	/** service object for this registration. */
	private final S service;

	/** Reference to this registration. */
	/* @GuardedBy("registrationLock") */
	private ServiceReferenceImpl<S> reference;

	/** List of contexts using the service. 
	 * List&lt;BundleContextImpl&gt;.
	 * */
	/* @GuardedBy("registrationLock") */
	private final List<BundleContextImpl> contextsUsing;

	/** properties for this registration. */
	/* @GuardedBy("registrationLock") */
	private ServiceProperties properties;

	/** service id. */
	private final long serviceid;

	/** service ranking. */
	/* @GuardedBy("registrationLock") */
	private int serviceranking;

	/* internal object to use for synchronization */
	private final Object registrationLock = new Object();

	/** The registration state */
	/* @GuardedBy("registrationLock") */
	private int state;
	private static final int REGISTERED = 0x00;
	private static final int UNREGISTERING = 0x01;
	private static final int UNREGISTERED = 0x02;

	/**
	 * Construct a ServiceRegistration and register the service
	 * in the framework's service registry.
	 *
	 */
	ServiceRegistrationImpl(ServiceRegistry registry, BundleContextImpl context, String[] clazzes, S service) {
		this.registry = registry;
		this.context = context;
		this.bundle = context.getBundleImpl();
		this.framework = context.getFramework();
		this.clazzes = clazzes; /* must be set before calling createProperties. */
		this.service = service;
		this.serviceid = registry.getNextServiceId(); /* must be set before calling createProperties. */
		this.contextsUsing = new ArrayList<BundleContextImpl>(10);

		synchronized (registrationLock) {
			this.state = REGISTERED;
			/* We leak this from the constructor here, but it is ok
			 * because the ServiceReferenceImpl constructor only
			 * stores the value in a final field without
			 * otherwise using it.
			 */
			this.reference = new ServiceReferenceImpl<S>(this);
		}
	}

	/**
	 * Call after constructing this object to complete the registration.
	 */
	void register(Dictionary<String, ?> props) {
		final ServiceReferenceImpl<S> ref;
		synchronized (registry) {
			context.checkValid();
			synchronized (registrationLock) {
				ref = reference; /* used to publish event outside sync */
				this.properties = createProperties(props); /* must be valid after unregister is called. */
			}
			if (Debug.DEBUG_SERVICES) {
				Debug.println("registerService[" + bundle + "](" + this + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			}
			registry.addServiceRegistration(context, this);
		}

		/* must not hold the registrations lock when this event is published */
		registry.publishServiceEvent(new ServiceEvent(ServiceEvent.REGISTERED, ref));
	}

	/**
	 * Update the properties associated with this service.
	 *
	 * <p>The key "objectClass" cannot be modified by this method. It's
	 * value is set when the service is registered.
	 *
	 * <p>The following steps are followed to modify a service's properties:
	 * <ol>
	 * <li>The service's properties are replaced with the provided properties.
	 * <li>A {@link ServiceEvent} of type {@link ServiceEvent#MODIFIED}
	 * is synchronously sent.
	 * </ol>
	 *
	 * @param props The properties for this service.
	 *        Changes should not be made to this object after calling this method.
	 *        To update the service's properties this method should be called again.
	 * @exception java.lang.IllegalStateException If
	 * this ServiceRegistration has already been unregistered.
	 *
	 * @exception IllegalArgumentException If the <tt>properties</tt>
	 * parameter contains case variants of the same key name.
	 */
	public void setProperties(Dictionary<String, ?> props) {
		final ServiceReferenceImpl<S> ref;
		final ServiceProperties previousProperties;
		synchronized (registry) {
			synchronized (registrationLock) {
				if (state != REGISTERED) { /* in the process of unregisterING */
					throw new IllegalStateException(Msg.SERVICE_ALREADY_UNREGISTERED_EXCEPTION);
				}

				ref = reference; /* used to publish event outside sync */
				previousProperties = this.properties;
				this.properties = createProperties(props);
			}
			registry.modifyServiceRegistration(context, this);
		}
		/* must not hold the registrationLock when this event is published */
		registry.publishServiceEvent(new ModifiedServiceEvent(ref, previousProperties));
	}

	/**
	 * Unregister the service.
	 * Remove a service registration from the framework's service
	 * registry.
	 * All {@link ServiceReferenceImpl} objects for this registration
	 * can no longer be used to interact with the service.
	 *
	 * <p>The following steps are followed to unregister a service:
	 * <ol>
	 * <li>The service is removed from the framework's service
	 * registry so that it may no longer be used.
	 * {@link ServiceReferenceImpl}s for the service may no longer be used
	 * to get a service object for the service.
	 * <li>A {@link ServiceEvent} of type {@link ServiceEvent#UNREGISTERING}
	 * is synchronously sent so that bundles using this service
	 * may release their use of the service.
	 * <li>For each bundle whose use count for this service is greater
	 * than zero:
	 * <ol>
	 * <li>The bundle's use count for this service is set to zero.
	 * <li>If the service was registered with a {@link ServiceFactory},
	 * the {@link ServiceFactory#ungetService ServiceFactory.ungetService} method
	 * is called to release the service object for the bundle.
	 * </ol>
	 * </ol>
	 *
	 * @exception java.lang.IllegalStateException If
	 * this ServiceRegistration has already been unregistered.
	 * @see BundleContextImpl#ungetService
	 */
	public void unregister() {
		final ServiceReferenceImpl<S> ref;
		synchronized (registry) {
			synchronized (registrationLock) {
				if (state != REGISTERED) { /* in the process of unregisterING */
					throw new IllegalStateException(Msg.SERVICE_ALREADY_UNREGISTERED_EXCEPTION);
				}

				/* remove this object from the service registry */
				if (Debug.DEBUG_SERVICES) {
					Debug.println("unregisterService[" + bundle + "](" + this + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				}

				registry.removeServiceRegistration(context, this);

				state = UNREGISTERING; /* mark unregisterING */
				ref = reference; /* used to publish event outside sync */
			}
		}

		/* must not hold the registrationLock when this event is published */
		registry.publishServiceEvent(new ServiceEvent(ServiceEvent.UNREGISTERING, ref));

		int size = 0;
		BundleContextImpl[] users = null;

		synchronized (registrationLock) {
			/* we have published the ServiceEvent, now mark the service fully unregistered */
			state = UNREGISTERED;

			size = contextsUsing.size();
			if (size > 0) {
				if (Debug.DEBUG_SERVICES) {
					Debug.println("unregisterService: releasing users"); //$NON-NLS-1$
				}
				users = contextsUsing.toArray(new BundleContextImpl[size]);
			}
		}

		/* must not hold the registrationLock while releasing services */
		for (int i = 0; i < size; i++) {
			releaseService(users[i]);
		}

		synchronized (registrationLock) {
			contextsUsing.clear();

			reference = null; /* mark registration dead */
		}

		/* The properties field must remain valid after unregister completes. */
	}

	/**
	 * Returns a {@link ServiceReferenceImpl} object for this registration.
	 * The {@link ServiceReferenceImpl} object may be shared with other bundles.
	 *
	 * @exception java.lang.IllegalStateException If
	 * this ServiceRegistration has already been unregistered.
	 * @return A {@link ServiceReferenceImpl} object.
	 */
	public ServiceReference<S> getReference() {
		return getReferenceImpl();
	}

	ServiceReferenceImpl<S> getReferenceImpl() {
		/* use reference instead of unregistered so that ServiceFactorys, called
		 * by releaseService after the registration is unregistered, can
		 * get the ServiceReference. Note this technically may violate the spec
		 * but makes more sense.
		 */
		synchronized (registrationLock) {
			if (reference == null) {
				throw new IllegalStateException(Msg.SERVICE_ALREADY_UNREGISTERED_EXCEPTION);
			}

			return reference;
		}
	}

	/**
	 * Construct a properties object from the dictionary for this
	 * ServiceRegistration.
	 *
	 * @param p The properties for this service.
	 * @return A Properties object for this ServiceRegistration.
	 */
	/* @GuardedBy("registrationLock") */
	private ServiceProperties createProperties(Dictionary<String, ?> p) {
		assert Thread.holdsLock(registrationLock);
		ServiceProperties props = new ServiceProperties(p);

		props.set(Constants.OBJECTCLASS, clazzes, true);
		props.set(Constants.SERVICE_ID, new Long(serviceid), true);
		props.setReadOnly();
		Object ranking = props.getProperty(Constants.SERVICE_RANKING);

		serviceranking = (ranking instanceof Integer) ? ((Integer) ranking).intValue() : 0;

		return props;
	}

	/**
	 * Return the properties object. This is for framework internal use only.
	 * @return The service registration's properties.
	 */
	public ServiceProperties getProperties() {
		synchronized (registrationLock) {
			return properties;
		}
	}

	/**
	 * Get the value of a service's property.
	 *
	 * <p>This method will continue to return property values after the
	 * service has been unregistered. This is so that references to
	 * unregistered service can be interrogated.
	 * (For example: ServiceReference objects stored in the log.)
	 *
	 * @param key Name of the property.
	 * @return Value of the property or <code>null</code> if there is
	 * no property by that name.
	 */
	Object getProperty(String key) {
		synchronized (registrationLock) {
			return properties.getProperty(key);
		}
	}

	/**
	 * Get the list of key names for the service's properties.
	 *
	 * <p>This method will continue to return the keys after the
	 * service has been unregistered. This is so that references to
	 * unregistered service can be interrogated.
	 * (For example: ServiceReference objects stored in the log.)
	 *
	 * @return The list of property key names.
	 */
	String[] getPropertyKeys() {
		synchronized (registrationLock) {
			return properties.getPropertyKeys();
		}
	}

	/**
	 * Return the service id for this service.
	 * @return The service id for this service.
	 */
	long getId() {
		return serviceid;
	}

	/**
	 * Return the service ranking for this service.
	 * @return The service ranking for this service.
	 */
	int getRanking() {
		synchronized (registrationLock) {
			return serviceranking;
		}
	}

	String[] getClasses() {
		return clazzes;
	}

	S getServiceObject() {
		return service;
	}

	/**
	 * Return the bundle which registered the service.
	 *
	 * <p>This method will always return <code>null</code> when the
	 * service has been unregistered. This can be used to
	 * determine if the service has been unregistered.
	 *
	 * @return The bundle which registered the service.
	 */
	Bundle getBundle() {
		synchronized (registrationLock) {
			if (reference == null) {
				return null;
			}

			return bundle;
		}
	}

	/**
	 * This method returns the bundle which registered the 
	 * service regardless of the registration status of this
	 * service registration.  This is not an OSGi specified
	 * method.
	 * @return The bundle which registered the service.
	 */
	public Bundle getRegisteringBundle() {
		return bundle;
	}

	Object getSafeService(BundleContextImpl user) {
		try {
			return getService(user);
		} catch (IllegalStateException e) {
			// can happen if the user is stopped on another thread
			return null;
		}
	}

	/**
	 * Get a service object for the using BundleContext.
	 *
	 * @param user BundleContext using service.
	 * @return Service object
	 */
	Object getService(BundleContextImpl user) {
		synchronized (registrationLock) {
			if (state == UNREGISTERED) { /* service unregistered */
				return null;
			}
		}
		if (Debug.DEBUG_SERVICES) {
			Debug.println("getService[" + user.getBundleImpl() + "](" + this + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}

		Map<ServiceRegistrationImpl<?>, ServiceUse<?>> servicesInUse = user.getServicesInUseMap();
		if (servicesInUse == null) { /* user is closed */
			user.checkValid(); /* throw exception */
		}
		/* Use a while loop to support retry if a call to a ServiceFactory fails */
		while (true) {
			ServiceUse<?> use;
			boolean added = false;
			/* Obtain the ServiceUse object for this service by bundle user */
			synchronized (servicesInUse) {
				user.checkValid();
				use = servicesInUse.get(this);
				if (use == null) {
					/* if this is the first use of the service
					 * optimistically record this service is being used. */
					use = new ServiceUse<S>(user, this);
					added = true;
					synchronized (registrationLock) {
						if (state == UNREGISTERED) { /* service unregistered */
							return null;
						}
						servicesInUse.put(this, use);
						contextsUsing.add(user);
					}
				}
			}

			/* Obtain and return the service object */
			synchronized (use) {
				/* if another thread removed the ServiceUse, then
				 * go back to the top and start again */
				synchronized (servicesInUse) {
					user.checkValid();
					if (servicesInUse.get(this) != use) {
						continue;
					}
				}
				Object serviceObject = use.getService();
				/* if the service factory failed to return an object and
				 * we created the service use, then remove the 
				 * optimistically added ServiceUse. */
				if ((serviceObject == null) && added) {
					synchronized (servicesInUse) {
						synchronized (registrationLock) {
							servicesInUse.remove(this);
							contextsUsing.remove(user);
						}
					}
				}
				return serviceObject;
			}
		}
	}

	/**
	 * Unget a service for the using BundleContext.
	 *
	 * @param user BundleContext using service.
	 * @return <code>false</code> if the context bundle's use count for the service
	 *         is zero or if the service has been unregistered,
	 *         otherwise <code>true</code>.
	 */
	boolean ungetService(BundleContextImpl user) {
		synchronized (registrationLock) {
			if (state == UNREGISTERED) {
				return false;
			}
		}

		if (Debug.DEBUG_SERVICES) {
			Debug.println("ungetService[" + user.getBundleImpl() + "](" + this + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}

		Map<ServiceRegistrationImpl<?>, ServiceUse<?>> servicesInUse = user.getServicesInUseMap();
		if (servicesInUse == null) {
			return false;
		}

		ServiceUse<?> use;
		synchronized (servicesInUse) {
			use = servicesInUse.get(this);
			if (use == null) {
				return false;
			}
		}

		synchronized (use) {
			if (use.ungetService()) {
				/* use count is now zero */
				synchronized (servicesInUse) {
					synchronized (registrationLock) {
						servicesInUse.remove(this);
						contextsUsing.remove(user);
					}
				}
			}
		}
		return true;
	}

	/**
	 * Release the service for the using BundleContext.
	 *
	 * @param user BundleContext using service.
	 */
	void releaseService(BundleContextImpl user) {
		synchronized (registrationLock) {
			if (reference == null) { /* registration dead */
				return;
			}
		}

		if (Debug.DEBUG_SERVICES) {
			Debug.println("releaseService[" + user.getBundleImpl() + "](" + this + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}

		Map<ServiceRegistrationImpl<?>, ServiceUse<?>> servicesInUse = user.getServicesInUseMap();
		if (servicesInUse == null) {
			return;
		}
		ServiceUse<?> use;
		synchronized (servicesInUse) {
			synchronized (registrationLock) {
				use = servicesInUse.remove(this);
				if (use == null) {
					return;
				}
				contextsUsing.remove(user);
			}
		}
		synchronized (use) {
			use.releaseService();
		}
	}

	/**
	 * Return the list of bundle which are using this service.
	 *
	 * @return Array of Bundles using this service.
	 */
	Bundle[] getUsingBundles() {
		synchronized (registrationLock) {
			if (state == UNREGISTERED) /* service unregistered */
				return null;

			int size = contextsUsing.size();
			if (size == 0)
				return null;

			/* Copy list of BundleContext into an array of Bundle. */
			Bundle[] bundles = new Bundle[size];
			for (int i = 0; i < size; i++)
				bundles[i] = contextsUsing.get(i).getBundleImpl();

			return bundles;
		}
	}

	boolean isAssignableTo(Bundle client, String className) {
		return framework.isServiceAssignableTo(bundle, client, className, service.getClass());
	}

	/**
	 * Return a String representation of this object.
	 *
	 * @return String representation of this object.
	 */
	public String toString() {
		int size = clazzes.length;
		StringBuffer sb = new StringBuffer(50 * size);

		sb.append('{');

		for (int i = 0; i < size; i++) {
			if (i > 0) {
				sb.append(", "); //$NON-NLS-1$
			}
			sb.append(clazzes[i]);
		}

		sb.append("}="); //$NON-NLS-1$
		sb.append(getProperties().toString());

		return sb.toString();
	}

	/**
	 * Compares this <code>ServiceRegistrationImpl</code> with the specified
	 * <code>ServiceRegistrationImpl</code> for order.
	 * 
	 * <p>
	 * This does a reverse comparison so that the highest item is sorted to the left.
	 * We keep ServiceRegistationImpls in sorted lists such that the highest
	 * ranked service is at element 0 for quick retrieval.
	 * 
	 * @param other The <code>ServiceRegistrationImpl</code> to be compared.
	 * @return Returns a negative integer, zero, or a positive integer if this
	 *         <code>ServiceRegistrationImpl</code> is greater than, equal to, or
	 *         less than the specified <code>ServiceRegistrationImpl</code>.
	 */
	public int compareTo(ServiceRegistrationImpl<?> other) {
		final int thisRanking = this.getRanking();
		final int otherRanking = other.getRanking();
		if (thisRanking != otherRanking) {
			if (thisRanking < otherRanking) {
				return 1;
			}
			return -1;
		}
		final long thisId = this.getId();
		final long otherId = other.getId();
		if (thisId == otherId) {
			return 0;
		}
		if (thisId < otherId) {
			return -1;
		}
		return 1;
	}
}
