/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.security.AccessController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.aries.subsystem.core.archive.DynamicImportPackageHeader;
import org.apache.aries.subsystem.core.archive.DynamicImportPackageRequirement;
import org.apache.aries.subsystem.core.internal.BundleResourceInstaller.BundleConstituent;
import org.apache.aries.subsystem.core.internal.StartAction.Restriction;
import org.eclipse.equinox.region.Region;
import org.eclipse.equinox.region.RegionDigraph.FilteredRegion;
import org.eclipse.equinox.region.RegionDigraphVisitor;
import org.eclipse.equinox.region.RegionFilter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.hooks.weaving.WovenClass;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.service.subsystem.Subsystem;
import org.osgi.service.subsystem.SubsystemException;

public class WovenClassListener implements org.osgi.framework.hooks.weaving.WovenClassListener {
	private static class RegionUpdaterInfo {
		private final Region head;
		private final Collection<DynamicImportPackageRequirement> requirements;
		private final Region tail;
		
		public RegionUpdaterInfo(Region tail, Region head) {
			this.tail = tail;
			this.head = head;
			requirements = new ArrayList<DynamicImportPackageRequirement>();
		}
		
		public Region head() {
			return head;
		}
		
		public void requirement(DynamicImportPackageRequirement requirement) {
			requirements.add(requirement);
		}
		
		public Collection<DynamicImportPackageRequirement> requirements() {
			return requirements;
		}
		
		public Region tail() {
			return tail;
		}
	}
	
	private final BundleContext context;
	private final Subsystems subsystems;
	
	public WovenClassListener(BundleContext context, Subsystems subsystems) {
		this.context = context;
		this.subsystems = subsystems;
	}
	
	@Override
	public void modified(WovenClass wovenClass) {
		if (wovenClass.getState() != WovenClass.TRANSFORMED) {
			// Dynamic package imports must be added when the woven class is in
			// the transformed state in order to ensure the class will load once
			// the defined state is reached.
			return;
		}
		List<String> dynamicImports = wovenClass.getDynamicImports();
		if (dynamicImports.isEmpty()) {
			// Nothing to do if there are no dynamic imports.
			return;
		}
		BundleWiring wiring = wovenClass.getBundleWiring();
		Bundle bundle = wiring.getBundle();
		BundleRevision revision = bundle.adapt(BundleRevision.class);
		BundleConstituent constituent = new BundleConstituent(null, revision);
		Collection<BasicSubsystem> basicSubsystems = subsystems.getSubsystemsByConstituent(constituent);
		BasicSubsystem subsystem = basicSubsystems.iterator().next();
		// Find the scoped subsystem in the region.
		subsystem = scopedSubsystem(subsystem);
		if (subsystem.getSubsystemId() == 0) {
			// The root subsystem needs no sharing policy.
			return;
		}
		if (EnumSet.of(Subsystem.State.INSTALLING, Subsystem.State.INSTALLED).contains(subsystem.getState())) {
			// The scoped subsystem must be resolved before adding dynamic 
			// package imports to the sharing policy in order to minimize 
			// unpredictable wirings. Resolving the scoped subsystem will also
			// resolve all of the unscoped subsystems in the region.
			AccessController.doPrivileged(new StartAction(subsystem, subsystem, subsystem, Restriction.RESOLVE_ONLY));
		}
		Bundle systemBundle = context.getBundle(org.osgi.framework.Constants.SYSTEM_BUNDLE_LOCATION);
		FrameworkWiring frameworkWiring = systemBundle.adapt(FrameworkWiring.class);
		// The following map tracks all of the necessary updates as each dynamic
		// import is processed. The key is the tail region of the connection 
		// whose filter needs updating.
		Map<Region, RegionUpdaterInfo> updates = new HashMap<Region, RegionUpdaterInfo>();
		for (String dynamicImport : dynamicImports) {
			// For each dynamic import, collect the necessary update information.
			DynamicImportPackageHeader header = new DynamicImportPackageHeader(dynamicImport);
			List<DynamicImportPackageRequirement> requirements = header.toRequirements(revision);
			for (DynamicImportPackageRequirement requirement : requirements) {
				Collection<BundleCapability> providers = frameworkWiring.findProviders(requirement);
				if (providers.isEmpty()) {
					// If nothing provides a capability matching the dynamic
					// import, no updates are made.
					continue;
				}
				addSharingPolicyUpdates(requirement, subsystem, providers, updates);
			}
		}
		// Now update each sharing policy only once.
		for (RegionUpdaterInfo update : updates.values()) {
			RegionUpdater updater = new RegionUpdater(update.tail(), update.head());
			try {
				updater.addRequirements(update.requirements());
			}
			catch (IllegalStateException e) {
				// Something outside of the subsystems implementation has
				// deleted the edge between the parent and child subsystems.
				// Assume the dynamic import sharing policy is being handled
				// elsewhere. See ARIES-1429.
			} 
			catch (Exception e) {
				throw new SubsystemException(e);
			} 
		}
	}
	
	private void addSharingPolicyUpdates(
			final DynamicImportPackageRequirement requirement, 
			final BasicSubsystem scopedSubsystem,
			final Collection<BundleCapability> providers,
			Map<Region, RegionUpdaterInfo> updates) {
		
		final List<BasicSubsystem> subsystems = new ArrayList<BasicSubsystem>();
		final Map<Region, BasicSubsystem> regionToSubsystem = new HashMap<Region, BasicSubsystem>();
		regionToSubsystem(scopedSubsystem, regionToSubsystem);
		scopedSubsystem.getRegion().visitSubgraph(new RegionDigraphVisitor() {
			private final List<BasicSubsystem> visited = new ArrayList<BasicSubsystem>();
			
			@Override
			public void postEdgeTraverse(RegionFilter filter) {
				// Nothing.
			}

			@Override
			public boolean preEdgeTraverse(RegionFilter filter) {
				return true;
			}

			@Override
			public boolean visit(Region region) {
				BasicSubsystem subsystem = regionToSubsystem.get(region);
				if (subsystem == null || subsystem.isRoot()) {
					// Don't mess with regions not created by the subsystem
					// implementation. Also, the root subsystem never has a
					// sharing policy.
					return false;
				}
				if (!visited.isEmpty() && !subsystem.equals(scopedParent(visited.get(visited.size() - 1)))) {
					// We're only interested in walking up the scoped parent tree.
					return false;
				}
				visited.add(subsystem);
				if (!requirement.getPackageName().contains("*")) {
					for (BundleCapability provider : providers) {
						BundleRevision br = provider.getResource();
						if (region.contains(br.getBundle())) {
							// The region contains a bundle providing a matching
							// capability, and the dynamic import does not contain a
							// wildcard. The requirement is therefore completely
							// satisfied.
							return false;
						}
					}
				}
				boolean allowed = false;
				Set<FilteredRegion> filters = region.getEdges();
				for (FilteredRegion filteredRegion : filters) {
					RegionFilter filter = filteredRegion.getFilter();
					if (filter.isAllowed(providers.iterator().next())) {
						// The region already allows matching capabilities
						// through so there is no need to update the sharing
						// policy.
						allowed = true;
						break;
					}
				}
				if (!allowed) {
					// The subsystem region requires a sharing policy update.
					subsystems.add(subsystem);
				}
				// Visit the next region.
				return true;
			}
		});
		// Collect the information for the necessary sharing policy updates.
		for (BasicSubsystem subsystem : subsystems) {
			Region tail = subsystem.getRegion();
			Region head = scopedParent(subsystem).getRegion();
			RegionUpdaterInfo info = updates.get(tail);
			if (info == null) {
				info = new RegionUpdaterInfo(tail, head);
				updates.put(tail, info);
			}
			info.requirement(requirement);
		}
	}
	
	private void regionToSubsystem(BasicSubsystem subsystem, Map<Region, BasicSubsystem> map) {
		map.put(subsystem.getRegion(), subsystem);
		subsystem = scopedParent(subsystem);
		if (subsystem == null) {
			return;
		}
		regionToSubsystem(subsystem, map);
	}
	
	private BasicSubsystem scopedParent(BasicSubsystem subsystem) {
		Collection<Subsystem> parents = subsystem.getParents();
		if (parents.isEmpty()) {
			return null;
		}
		subsystem = (BasicSubsystem)parents.iterator().next();
		return scopedSubsystem(subsystem);
	}
	
	private BasicSubsystem scopedSubsystem(BasicSubsystem subsystem) {
		while (!subsystem.isScoped()) {
			subsystem = (BasicSubsystem)subsystem.getParents().iterator().next();
		}
		return subsystem;
	}
}
