/*
 * 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.io.IOException;
import java.net.URISyntaxException;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.aries.subsystem.core.archive.ProvisionPolicyDirective;
import org.apache.aries.subsystem.core.archive.SubsystemContentHeader;
import org.apache.aries.subsystem.core.archive.SubsystemManifest;
import org.apache.aries.subsystem.core.archive.SubsystemTypeHeader;
import org.apache.aries.subsystem.core.internal.BundleResourceInstaller.BundleConstituent;
import org.apache.aries.subsystem.core.internal.DependencyCalculator.MissingCapability;
import org.apache.aries.subsystem.core.internal.StartAction.Restriction;
import org.apache.aries.subsystem.core.repository.Repository;
import org.eclipse.equinox.region.Region;
import org.osgi.framework.BundleException;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.framework.namespace.NativeNamespace;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.resource.Capability;
import org.osgi.resource.Namespace;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
import org.osgi.resource.Wire;
import org.osgi.resource.Wiring;
import org.osgi.service.resolver.HostedCapability;
import org.osgi.service.subsystem.Subsystem.State;

public class ResolveContext extends org.osgi.service.resolver.ResolveContext {
	private final Repository contentRepository;
	private final Repository localRepository;
	private final Repository preferredProviderRepository;
	private final Repository repositoryServiceRepository;
	private final SubsystemResource resource;
	private final Repository systemRepository;
	private final Map<Resource, Wiring> wirings = computeWirings();

	public ResolveContext(SubsystemResource resource) {
		this.resource = resource;
		contentRepository = new ContentRepository(resource.getInstallableContent(), resource.getSharedContent());
		localRepository = resource.getLocalRepository();
		preferredProviderRepository = new PreferredProviderRepository(resource);
		repositoryServiceRepository = new RepositoryServiceRepository();
		systemRepository = Activator.getInstance().getSystemRepository();
	}
	
	private void installDependenciesOfRequirerIfNecessary(Requirement requirement) {
		if (requirement == null) {
			return;
		}
		Resource requirer = requirement.getResource();
		if (resource.equals(requirer)) {
			return;
		}
		Collection<BasicSubsystem> subsystems;
		if (requirer instanceof BasicSubsystem) {
			BasicSubsystem subsystem = (BasicSubsystem)requirer;
			subsystems = Collections.singletonList(subsystem);
		}
		else if (requirer instanceof BundleRevision) {
			BundleRevision revision = (BundleRevision)requirer;
			BundleConstituent constituent = new BundleConstituent(null, revision);
			subsystems = Activator.getInstance().getSubsystems().getSubsystemsByConstituent(constituent);
		}
		else {
			return;
		}
		for (BasicSubsystem subsystem : subsystems) {
			if (Utils.isProvisionDependenciesInstall(subsystem) 
					|| !State.INSTALLING.equals(subsystem.getState())) {
				continue;
			}
			AccessController.doPrivileged(new StartAction(subsystem, subsystem, subsystem, Restriction.INSTALL_ONLY));
		}
	}
	
	private boolean isResolved(Resource resource) {
		return wirings.containsKey(resource);
	}
	
	private boolean isProcessableAsFragment(Requirement requirement) {
		Resource resource = requirement.getResource();
		String namespace = requirement.getNamespace();
		return Utils.isFragment(resource)
				&& !(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE.equals(namespace)
						|| HostNamespace.HOST_NAMESPACE.equals(namespace));
	}
	
	private void processAsFragment(Requirement requirement, List<Capability> capabilities) {
		String namespace = requirement.getNamespace();
		Resource fragment = requirement.getResource();
		Wiring fragmentWiring = wirings.get(fragment);
		List<Wire> fragmentWires = fragmentWiring.getRequiredResourceWires(HostNamespace.HOST_NAMESPACE);
		for (Wire fragmentWire : fragmentWires) {
			Resource host = fragmentWire.getProvider();
			Wiring hostWiring = wirings.get(host);
			List<Wire> hostWires = hostWiring.getRequiredResourceWires(namespace);
			processWires(hostWires, requirement, capabilities);
		}
	}
	
	private void processWires(Collection<Wire> wires, Requirement requirement, List<Capability> capabilities) {
		for (Wire wire : wires) {
			processWire(wire, requirement, capabilities);
		}
	}
	
	private void processWire(Wire wire, Requirement requirement, List<Capability> capabilities) {
		if (requirement.equals(wire.getRequirement())) {
			capabilities.add(wire.getCapability());
		}
	}
	
	private void processCapability(Capability capability, Requirement requirement, List<Capability> capabilities) {
		if (ResourceHelper.matches(requirement, capability)) {
			capabilities.add(capability);
		}
	}
	
	private void processResourceCapabilities(Collection<Capability> resourceCapabilities, Requirement requirement, List<Capability> capabilities) {
		for (Capability resourceCapability : resourceCapabilities) {
			processCapability(resourceCapability, requirement, capabilities);
		}
	}
	
	private void processAsBundle(Requirement requirement, List<Capability> capabilities) {
		String namespace = requirement.getNamespace();
		Resource bundle = requirement.getResource();
		Wiring wiring = wirings.get(bundle);
		List<Wire> wires = wiring.getRequiredResourceWires(namespace);
		processWires(wires, requirement, capabilities);
	}
	
	private void processAsSubstitutableExport(boolean isFragment, Requirement requirement, List<Capability> capabilities) {
		String namespace = requirement.getNamespace();
		if (!PackageNamespace.PACKAGE_NAMESPACE.equals(namespace)) {
			return;
		}
		Resource resource = requirement.getResource();
		Wiring wiring = wirings.get(resource);
		if (isFragment) {
			List<Wire> fragmentWires = wiring.getRequiredResourceWires(HostNamespace.HOST_NAMESPACE);
			for (Wire fragmentWire : fragmentWires) {
				Resource host = fragmentWire.getProvider();
				processResourceCapabilities(
						wirings.get(host).getResourceCapabilities(namespace),
						requirement,
						capabilities);
			}
		}
		else {
			List<Capability> resourceCapabilities = wiring.getResourceCapabilities(namespace);
			processResourceCapabilities(resourceCapabilities, requirement, capabilities);
		}
	}
	
	private void processAlreadyResolvedResource(Resource resource, Requirement requirement, List<Capability> capabilities) {
		boolean isFragment = isProcessableAsFragment(requirement);
		if (isFragment) {
			processAsFragment(requirement, capabilities);
		}
		else {
			processAsBundle(requirement, capabilities);
		}
		if (capabilities.isEmpty() && Utils.isMandatory(requirement)) {
			processAsSubstitutableExport(isFragment, requirement, capabilities);
			if (capabilities.isEmpty()) {
				// ARIES-1538. Do not fail subsystem resolution if an already
				// resolved resource has a missing dependency.
				capabilities.add(new MissingCapability(requirement));
			}
		}
	}
	
	private void processNewlyResolvedResource(Resource resource, Requirement requirement, List<Capability> capabilities) {
		try {
			// Only check the system repository for osgi.ee and osgi.native
			if (ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE.equals(requirement.getNamespace())
					|| NativeNamespace.NATIVE_NAMESPACE.equals(requirement.getNamespace())) {
				addDependenciesFromSystemRepository(requirement, capabilities);
			} else {
				addDependenciesFromContentRepository(requirement, capabilities);
				addDependenciesFromPreferredProviderRepository(requirement, capabilities);
				addDependenciesFromSystemRepository(requirement, capabilities);
				addDependenciesFromLocalRepository(requirement, capabilities);
				if (capabilities.isEmpty()) {
					addDependenciesFromRepositoryServiceRepositories(requirement, capabilities);
				}
			}
			if (capabilities.isEmpty()) {
				// Is the requirement optional?
				String resolution = requirement.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE);
				if (Namespace.RESOLUTION_OPTIONAL.equals(resolution)) {
					// Yes, it's optional. Add a missing capability to ensure
					// it gets added to the sharing policy per the specification.
					capabilities.add(new MissingCapability(requirement));
				}
				// Is the requirement resource already resolved? See ARIES-1538.
				else if (isResolved(requirement.getResource())) {
					// Yes, the resource has already been resolved. Do not fail
					// the subsystem resolution due to a missing dependency.
					capabilities.add(new MissingCapability(requirement));
				}
			}
		}
		catch (Throwable t) {
			Utils.handleTrowable(t);
		}
	}

	@Override
	public List<Capability> findProviders(Requirement requirement) {
		ArrayList<Capability> capabilities = new ArrayList<Capability>();
		Resource resource = requirement.getResource();
		if (isResolved(resource)
				&& Utils.isEffectiveResolve(requirement)) {
			processAlreadyResolvedResource(resource, requirement, capabilities);
		}
		else {
			installDependenciesOfRequirerIfNecessary(requirement);
			processNewlyResolvedResource(resource, requirement, capabilities);
		}
		capabilities.trimToSize();
		return capabilities;
	}

	@Override
	public int insertHostedCapability(List<Capability> capabilities, HostedCapability hostedCapability) {
	    // Must specify the location where the capability is to be added. From the ResoveContext javadoc:
	    // "This method must insert the specified HostedCapability in a place that makes the list maintain
	    // the preference order."
	    // The Felix implementation provides a list that requires the index to be specified in the add() call,
	    // otherwise it will throw an exception.
        int sz = capabilities.size();
		capabilities.add(sz, hostedCapability);
        return sz;
	}

	@Override
	public boolean isEffective(Requirement requirement) {
		return true;
	}

	@Override
	public Collection<Resource> getMandatoryResources() {
		return resource.getMandatoryResources();
	}

	@Override
	public Collection<Resource> getOptionalResources() {
		return resource.getOptionalResources();
	}

	@Override
	public Map<Resource, Wiring> getWirings() {
		return Collections.emptyMap();
	}

	private boolean addDependencies(Repository repository, Requirement requirement, List<Capability> capabilities, boolean validate) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		if (repository == null)
			return false;
		Map<Requirement, Collection<Capability>> m = repository.findProviders(Collections.singleton(requirement));
		if (m.containsKey(requirement)) {
			Collection<Capability> cc = m.get(requirement);
			addValidCapabilities(cc, capabilities, requirement, validate);
		}
		return !capabilities.isEmpty();
	}

	private boolean addDependenciesFromContentRepository(Requirement requirement, List<Capability> capabilities) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		return addDependencies(contentRepository, requirement, capabilities, false);
	}

	private boolean addDependenciesFromLocalRepository(Requirement requirement, List<Capability> capabilities) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		return addDependencies(localRepository, requirement, capabilities, true);
	}

	private boolean addDependenciesFromPreferredProviderRepository(Requirement requirement, List<Capability> capabilities) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		return addDependencies(preferredProviderRepository, requirement, capabilities, true);
	}

	private boolean addDependenciesFromRepositoryServiceRepositories(Requirement requirement, List<Capability> capabilities) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		return addDependencies(repositoryServiceRepository, requirement, capabilities, true);
	}

	private boolean addDependenciesFromSystemRepository(Requirement requirement, List<Capability> capabilities) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		boolean result = addDependencies(systemRepository, requirement, capabilities, true);
		return result;
	}

	private void addValidCapabilities(Collection<Capability> from, Collection<Capability> to, Requirement requirement, boolean validate) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		for (Capability c : from) {
			if (!validate || isValid(c, requirement)) {
				// either validation is not requested or the capability is valid.
				to.add(c);
			}
		}
	}

	private void addWiring(Resource resource, Map<Resource, Wiring> wirings) {
		if (resource instanceof BundleConstituent) {
			BundleConstituent bc = (BundleConstituent)resource;
			BundleWiring wiring = bc.getWiring();
			if (wiring != null) {
				wirings.put(bc.getBundle().adapt(BundleRevision.class), wiring);
			}
		}
		else if (resource instanceof BundleRevision) {
			BundleRevision br = (BundleRevision)resource;
			BundleWiring wiring = br.getWiring();
			if (wiring != null) {
				wirings.put(br, wiring);
			}
			
		}
	}

	private Map<Resource, Wiring> computeWirings() {
		Map<Resource, Wiring> wirings = new HashMap<Resource, Wiring>();
		for (BasicSubsystem subsystem : Activator.getInstance().getSubsystems().getSubsystems()) { // NEED
			for (Resource constituent : subsystem.getConstituents()) {
				addWiring(constituent, wirings);
			}
		}
		return Collections.unmodifiableMap(wirings);
	}
	
	private boolean isContent(Resource resource) {
		return this.resource.isContent(resource);
	}

	private boolean isInstallable(Resource resource) {
		return !isShared(resource);
	}

	private boolean isShared(Resource resource) {
		return Utils.isSharedResource(resource);
	}

	private boolean isValid(Capability capability, Requirement requirement) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		if (IdentityNamespace.IDENTITY_NAMESPACE.equals(capability.getNamespace()))
			return true;
		Resource provider = capability.getResource();
		Resource requirer = requirement.getResource();
		SubsystemManifest manifest = resource.getSubsystemManifest();
		SubsystemContentHeader header = manifest.getSubsystemContentHeader();
		if (header.contains(provider) && header.contains(requirer)) {
			// Shortcut. If both the provider and requirer are content then they
			// are in the same region and the capability will be visible.
			return true;
		}
		Region from = findRegionForCapabilityValidation(provider);
		Region to = findRegionForCapabilityValidation(requirer);
		return new SharingPolicyValidator(from, to).isValid(capability);
	}
	
	private boolean isAcceptDependencies() {
		SubsystemManifest manifest = resource.getSubsystemManifest();
		SubsystemTypeHeader header = manifest.getSubsystemTypeHeader();
		ProvisionPolicyDirective directive = header.getProvisionPolicyDirective();
		return directive.isAcceptDependencies();
	}
	
	private Region findRegionForCapabilityValidation(Resource resource) throws BundleException, IOException, InvalidSyntaxException, URISyntaxException {
		if (isInstallable(resource)) {
			// This is an installable resource so we need to figure out where it
			// will be installed.
			if (isContent(resource) // If the resource is content of this subsystem, it will be installed here.
					// Or if this subsystem accepts dependencies, the resource will be installed here.
					|| isAcceptDependencies()) {
				if (this.resource.isComposite()) {
					// Composites define their own sharing policy with which
					// their regions are already configured by the time we get
					// here. We ensure capabilities are visible to this region.
					return this.resource.getRegion();
				}
				// For applications and features, we must ensure capabilities
				// are visible to their scoped parent. Features import
				// everything. Applications have their sharing policies
				// computed, so if capabilities are visible to the parent, we
				// know we can make them visible to the application. 
				BasicSubsystem parent = this.resource.getParents().iterator().next();
				// If the parent accepts dependencies, the resource will 
				// be installed there and all capabilities will be visible.
				if (parent.getSubsystemManifest().getSubsystemTypeHeader().getProvisionPolicyDirective().isAcceptDependencies()) {
					return parent.getRegion();
				}
				// Otherwise, the "parent" is defined as the first scoped 
				// ancestor whose sharing policy has already been set. This 
				// covers the case of multiple subsystems from the same archive 
				// being installed whose regions will form a tree of depth N.
				parent = Utils.findFirstScopedAncestorWithSharingPolicy(this.resource);
				return parent.getRegion();
			}
			return Utils.findFirstSubsystemAcceptingDependenciesStartingFrom(this.resource.getParents().iterator().next()).getRegion();
		}
		else {
			// This is an already installed resource from the system repository.
			if (Utils.isBundle(resource)) {
				if (isContent(resource) 
						&& this.resource.getSubsystemManifest().getSubsystemTypeHeader().getAriesProvisionDependenciesDirective().isResolve()) {
					// If we get here with a subsystem that is 
					// apache-aries-provision-dependencies:=resolve, it means
					// that a restart has occurred with the subsystem in the
					// INSTALLING state. Its content has already been installed.
					// However, because the sharing policy has not yet been set,
					// we must treat it similarly to the installable content case
					// above.
					return Utils.findFirstScopedAncestorWithSharingPolicy(this.resource).getRegion();
				}
			    BundleRevision revision = resource instanceof BundleRevision ? (BundleRevision)resource : ((BundleRevisionResource)resource).getRevision();
				// If it's a bundle, use region digraph to get the region in order
				// to account for bundles in isolated regions outside of the
				// subsystems API.
				return Activator.getInstance().getRegionDigraph().getRegion(revision.getBundle());
			}
			else {
				if (this.resource.getSubsystemManifest().getSubsystemTypeHeader().getAriesProvisionDependenciesDirective().isResolve()) {
					return Utils.findFirstScopedAncestorWithSharingPolicy(this.resource).getRegion();
				}
				// If it's anything else, get the region from one of the
				// subsystems referencing it.
				return Activator.getInstance().getSubsystems().getSubsystemsReferencing(resource).iterator().next().getRegion();
			}
		}
	}
}
