/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.aries.subsystem.core.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.aries.subsystem.core.internal.BundleResourceInstaller.BundleConstituent;
import org.eclipse.equinox.region.Region;
import org.eclipse.equinox.region.RegionDigraph;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.hooks.bundle.EventHook;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.Resource;

public class BundleEventHook implements EventHook {
	private final Activator activator;
	private final ConcurrentHashMap<Bundle, BundleRevision> bundleToRevision;
	
	private boolean active;
	private List<BundleEvent> events;
	
	public BundleEventHook() {
		activator = Activator.getInstance();
		bundleToRevision = new ConcurrentHashMap<Bundle, BundleRevision>();
	}
	
	@Override
	public void event(BundleEvent event, Collection<BundleContext> contexts) {
		if ((event.getType() & (BundleEvent.INSTALLED | BundleEvent.UNINSTALLED)) == 0)
			return;
		// Protect against deadlock when the bundle event hook receives an
		// event before subsystems has fully initialized, in which case the
		// events are queued and processed once initialization is complete.
		synchronized (this) {
			if (!active) {
				if (events == null)
					events = new ArrayList<BundleEvent>();
				events.add(event);
				return;
			}
		}
		handleEvent(event);
	}
	
	// Events must be processed in order. Don't allow events to go through
	// synchronously before all pending events have been processed.
	synchronized void activate() {
		active = true;
		processPendingEvents();
	}
	
	synchronized void deactivate() {
		active = false;
	}
	
	synchronized void processPendingEvents() {
		if (events == null)
			return;
		for (BundleEvent event : events)
			handleEvent(event);
		events = null;
	}
	
	private Subsystems getSubsystems() {
		return activator.getSubsystems();
	}
	
	/*
	 * Note that because some events may be processed asynchronously, we can no
	 * longer rely on the guarantees that a synchronous event brings. For
	 * example, bundle revisions adapted from bundles included in events may be
	 * null.
	 */
	private void handleEvent(BundleEvent event) {
		switch (event.getType()) {
			case BundleEvent.INSTALLED:
				handleInstalledEvent(event);
				break;
			// TODO I think updates will play a role here as well. Need to keep
			// track of the most current bundle revision?
			case BundleEvent.UNINSTALLED:
				handleUninstalledEvent(event);
				break;
		}
	}
	
	/*
	 * This method guards against an uninstalled origin bundle. Guards against a
	 * null bundle revision are done elsewhere. It is assumed the bundle
	 * revision is never null once we get here.
	 */
	private void handleExplicitlyInstalledBundleBundleContext(BundleRevision originRevision, BundleRevision bundleRevision) {
		/* 
		 * The newly installed bundle must become a constituent of all the Subsystems of which the bundle
		 * whose context was used to perform the install is a constituent (OSGI.enterprise spec. 134.10.1.1).
		 */
		Collection<BasicSubsystem> subsystems = getSubsystems().getSubsystemsReferencing(originRevision);
		boolean bundleRevisionInstalled=false;
		for (BasicSubsystem s : subsystems) {
			for (Resource constituent : s.getConstituents()) {
				if (constituent instanceof BundleConstituent) {
					BundleRevision rev = ((BundleConstituent) constituent).getRevision();
					if (originRevision.equals(rev)) {
						Utils.installResource(bundleRevision, s);
						bundleRevisionInstalled=true;
					}
				}
			}
		}
		/* if the bundle is not made constituent of any subsystem then make it constituent of root */
		if (!bundleRevisionInstalled) {
			Utils.installResource(bundleRevision, getSubsystems().getRootSubsystem());
		}
	}	
	private void handleExplicitlyInstalledBundleRegionDigraph(Bundle origin, BundleRevision bundleRevision) {
			// The bundle needs to be associated with the scoped subsystem of 
			// the region used to install the bundle.
			RegionDigraph digraph = activator.getRegionDigraph();
			Region region = digraph.getRegion(origin);
			for (BasicSubsystem s : getSubsystems().getSubsystems()) {
				if ((s.isApplication() || s.isComposite())
						&& region.equals(s.getRegion())) {
					Utils.installResource(bundleRevision, s);
					return;
				}
			}
			throw new IllegalStateException("No subsystem found for bundle " + bundleRevision + " in region " + region);
	}
	
	private void handleInstalledEvent(BundleEvent event) {
		Bundle origin = event.getOrigin();
		BundleRevision originRevision = origin.adapt(BundleRevision.class);
		Bundle bundle = event.getBundle();
		BundleRevision bundleRevision = bundle.adapt(BundleRevision.class);
		if (bundleRevision == null) {
			// The event is being processed asynchronously and the installed
			// bundle has been uninstalled. Nothing we can do.
			return;
		}
		bundleToRevision.put(bundle, bundleRevision);
		// Only handle explicitly installed bundles. An explicitly installed
		// bundle is a bundle that was installed using some other bundle's
		// BundleContext or using RegionDigraph.
		if (ThreadLocalSubsystem.get() != null
				// Region context bundles must be treated as explicit installations.
				|| bundleRevision.getSymbolicName().startsWith(Constants.RegionContextBundleSymbolicNamePrefix)) {
			return;
		}
		// Indicate that a bundle is being explicitly installed on this thread.
		// This protects against attempts to resolve the bundle as part of
		// processing the explicit installation.
		ThreadLocalBundleRevision.set(bundleRevision);
		try {
			if ("org.eclipse.equinox.region".equals(origin.getSymbolicName())) {
				// The bundle was installed using RegionDigraph.
				handleExplicitlyInstalledBundleRegionDigraph(origin, bundleRevision);
			}
			else {
				// The bundle was installed using some other bundle's BundleContext.
				handleExplicitlyInstalledBundleBundleContext(originRevision, bundleRevision);
			}
		}
		finally {
			// Always remove the bundle so that it can be resolved no matter
			// what happens here.
			ThreadLocalBundleRevision.remove();
		}
	}
	
	@SuppressWarnings("unchecked")
	private void handleUninstalledEvent(BundleEvent event) {
		Bundle bundle = event.getBundle();
		BundleRevision revision = bundleToRevision.remove(bundle);
		if (ThreadLocalSubsystem.get() != null
				|| (revision == null ? false : 
					// Region context bundles must be treated as explicit installations.
					revision.getSymbolicName().startsWith(Constants.RegionContextBundleSymbolicNamePrefix))) {
			return;
		}
		Collection<BasicSubsystem> subsystems;
		if (revision == null) {
			// The bundle was installed while the bundle event hook was unregistered.
			Object[] o = activator.getSubsystems().getSubsystemsByBundle(bundle);
			if (o == null)
				return;
			revision = (BundleRevision)o[0];
			subsystems = (Collection<BasicSubsystem>)o[1];
		}
		else {
			subsystems = activator.getSubsystems().getSubsystemsByConstituent(new BundleConstituent(null, revision));
		}
		for (BasicSubsystem subsystem : subsystems) {
			ResourceUninstaller.newInstance(revision, subsystem).uninstall();
		}
	}
}
