/*******************************************************************************
 * Copyright (c) 2012, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.container;

import java.net.URL;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.osgi.container.ModuleRevisionBuilder.GenericInfo;
import org.eclipse.osgi.internal.container.AtomicLazyInitializer;
import org.eclipse.osgi.internal.container.InternalUtils;
import org.osgi.framework.AdminPermission;
import org.osgi.framework.Bundle;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.*;
import org.osgi.resource.*;

/**
 * An implementation of {@link BundleWiring}.
 * @since 3.10
 */
public final class ModuleWiring implements BundleWiring {
	class LoaderInitializer implements Callable<ModuleLoader> {
		@Override
		public ModuleLoader call() throws Exception {
			if (!isValid) {
				return null;
			}
			return getRevision().getRevisions().getContainer().adaptor.createModuleLoader(ModuleWiring.this);
		}
	}

	private static final RuntimePermission GET_CLASSLOADER_PERM = new RuntimePermission("getClassLoader"); //$NON-NLS-1$
	private static final String DYNAMICALLY_ADDED_IMPORT_DIRECTIVE = "x.dynamically.added"; //$NON-NLS-1$
	private final ModuleRevision revision;
	private volatile List<ModuleCapability> capabilities;
	private volatile List<ModuleRequirement> requirements;
	private final Collection<String> substitutedPkgNames;
	private final AtomicLazyInitializer<ModuleLoader> loader = new AtomicLazyInitializer<>();
	private final LoaderInitializer loaderInitializer = new LoaderInitializer();
	private volatile List<ModuleWire> providedWires;
	private volatile List<ModuleWire> requiredWires;
	volatile boolean isValid = true;
	private final AtomicReference<Set<String>> dynamicMissRef = new AtomicReference<>();

	ModuleWiring(ModuleRevision revision, List<ModuleCapability> capabilities, List<ModuleRequirement> requirements, List<ModuleWire> providedWires, List<ModuleWire> requiredWires, Collection<String> substitutedPkgNames) {
		super();
		this.revision = revision;
		this.capabilities = capabilities;
		this.requirements = requirements;
		this.providedWires = providedWires;
		this.requiredWires = requiredWires;
		this.substitutedPkgNames = substitutedPkgNames.isEmpty() ? Collections.<String> emptyList() : substitutedPkgNames;
	}

	@Override
	public Bundle getBundle() {
		return revision.getBundle();
	}

	@Override
	public boolean isCurrent() {
		return isValid && revision.isCurrent();
	}

	@Override
	public boolean isInUse() {
		return isCurrent() || !providedWires.isEmpty() || isFragmentInUse();
	}

	private boolean isFragmentInUse() {
		// A fragment is considered in use if it has any required host wires
		if ((BundleRevision.TYPE_FRAGMENT & revision.getTypes()) != 0) {
			List<ModuleWire> hostWires = getRequiredModuleWires(HostNamespace.HOST_NAMESPACE);
			// hostWires may be null if the fragment wiring is no longer valid
			return hostWires == null ? false : !hostWires.isEmpty();
		}
		return false;
	}

	/**
	 * Returns the same result as {@link #getCapabilities(String)} except
	 * uses type ModuleCapability.
	 * @param namespace the namespace
	 * @return the capabilities
	 * @see #getCapabilities(String)
	 */
	public List<ModuleCapability> getModuleCapabilities(String namespace) {
		return getModuleCapabilities(namespace, capabilities);
	}

	private List<ModuleCapability> getModuleCapabilities(String namespace, List<ModuleCapability> allCapabilities) {
		if (!isValid)
			return null;
		if (namespace == null)
			return new ArrayList<>(allCapabilities);
		List<ModuleCapability> result = new ArrayList<>();
		for (ModuleCapability capability : allCapabilities) {
			if (namespace.equals(capability.getNamespace())) {
				result.add(capability);
			}
		}
		return result;
	}

	/**
	 * Returns the same result as {@link #getRequirements(String)} except
	 * uses type ModuleRequirement.
	 * @param namespace the namespace
	 * @return the requirements
	 * @see #getRequirements(String)
	 */
	public List<ModuleRequirement> getModuleRequirements(String namespace) {
		return getModuleRequirements(namespace, requirements);
	}

	List<ModuleRequirement> getPersistentRequirements() {
		List<ModuleRequirement> persistentRequriements = getModuleRequirements(null);
		if (persistentRequriements == null) {
			return null;
		}
		for (Iterator<ModuleRequirement> iRequirements = persistentRequriements.iterator(); iRequirements.hasNext();) {
			ModuleRequirement requirement = iRequirements.next();
			if (PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace())) {
				if ("true".equals(requirement.getDirectives().get(DYNAMICALLY_ADDED_IMPORT_DIRECTIVE))) { //$NON-NLS-1$
					iRequirements.remove();
				}
			}
		}
		return persistentRequriements;
	}

	private List<ModuleRequirement> getModuleRequirements(String namespace, List<ModuleRequirement> allRequirements) {
		if (!isValid)
			return null;
		if (namespace == null)
			return new ArrayList<>(allRequirements);
		List<ModuleRequirement> result = new ArrayList<>();
		for (ModuleRequirement requirement : allRequirements) {
			if (namespace.equals(requirement.getNamespace())) {
				result.add(requirement);
			}
		}
		return result;
	}

	@Override
	public List<BundleCapability> getCapabilities(String namespace) {
		return InternalUtils.asListBundleCapability(getModuleCapabilities(namespace));

	}

	@Override
	public List<BundleRequirement> getRequirements(String namespace) {
		return InternalUtils.asListBundleRequirement(getModuleRequirements(namespace));
	}

	/**
	 * Returns the same result as {@link #getProvidedWires(String)} except
	 * uses type ModuleWire.
	 * @param namespace the namespace
	 * @return the wires
	 * @see #getProvidedWires(String)
	 */
	public List<ModuleWire> getProvidedModuleWires(String namespace) {
		return getWires(namespace, providedWires);
	}

	List<ModuleWire> getPersistentProvidedWires() {
		return getPersistentWires(providedWires);
	}

	/**
	 * Returns the same result as {@link #getRequiredWires(String)} except
	 * uses type ModuleWire.
	 * @param namespace the namespace
	 * @return the wires
	 * @see #getRequiredWires(String)
	 */
	public List<ModuleWire> getRequiredModuleWires(String namespace) {
		return getWires(namespace, requiredWires);
	}

	List<ModuleWire> getPersistentRequiredWires() {
		return getPersistentWires(requiredWires);
	}

	private List<ModuleWire> getPersistentWires(List<ModuleWire> allWires) {
		List<ModuleWire> persistentWires = getWires(null, allWires);
		if (persistentWires == null) {
			return null;
		}
		for (Iterator<ModuleWire> iWires = persistentWires.iterator(); iWires.hasNext();) {
			ModuleWire wire = iWires.next();
			if (PackageNamespace.PACKAGE_NAMESPACE.equals(wire.getRequirement().getNamespace())) {
				if ("true".equals(wire.getRequirement().getDirectives().get(DYNAMICALLY_ADDED_IMPORT_DIRECTIVE))) { //$NON-NLS-1$
					iWires.remove();
				}
			}
		}
		return persistentWires;
	}

	@Override
	public List<BundleWire> getProvidedWires(String namespace) {
		return InternalUtils.asListBundleWire(getWires(namespace, providedWires));
	}

	@Override
	public List<BundleWire> getRequiredWires(String namespace) {
		return InternalUtils.asListBundleWire(getWires(namespace, requiredWires));
	}

	private List<ModuleWire> getWires(String namespace, List<ModuleWire> allWires) {
		if (!isValid)
			return null;
		if (namespace == null)
			return new ArrayList<>(allWires);
		List<ModuleWire> result = new ArrayList<>();
		for (ModuleWire moduleWire : allWires) {
			if (namespace.equals(moduleWire.getCapability().getNamespace())) {
				result.add(moduleWire);
			}
		}
		return result;
	}

	@Override
	public ModuleRevision getRevision() {
		return revision;
	}

	@Override
	public ClassLoader getClassLoader() {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(GET_CLASSLOADER_PERM);
		}

		if (!isValid) {
			return null;
		}
		ModuleLoader current = getModuleLoader();
		if (current == null) {
			// must not be valid
			return null;
		}
		return current.getClassLoader();
	}

	/**
	 * Returns the module loader for this wiring.  If the module
	 * loader does not exist yet then one will be created
	 * @return the module loader for this wiring.
	 */
	public ModuleLoader getModuleLoader() {
		return loader.getInitialized(loaderInitializer);

	}

	void loadFragments(Collection<ModuleRevision> fragments) {
		ModuleLoader current = loader.get();
		if (current != null) {
			current.loadFragments(fragments);
		}
	}

	@Override
	public List<URL> findEntries(String path, String filePattern, int options) {
		if (!hasResourcePermission())
			return Collections.emptyList();
		if (!isValid) {
			return null;
		}
		ModuleLoader current = getModuleLoader();
		if (current == null) {
			// must not be valid
			return null;
		}
		return current.findEntries(path, filePattern, options);
	}

	@Override
	public Collection<String> listResources(String path, String filePattern, int options) {
		if (!hasResourcePermission())
			return Collections.emptyList();
		if (!isValid) {
			return null;
		}
		ModuleLoader current = getModuleLoader();
		if (current == null) {
			// must not be valid
			return null;
		}
		return current.listResources(path, filePattern, options);
	}

	@Override
	public List<Capability> getResourceCapabilities(String namespace) {
		return InternalUtils.asListCapability(getCapabilities(namespace));
	}

	@Override
	public List<Requirement> getResourceRequirements(String namespace) {
		return InternalUtils.asListRequirement(getRequirements(namespace));
	}

	@Override
	public List<Wire> getProvidedResourceWires(String namespace) {
		return InternalUtils.asListWire(getWires(namespace, providedWires));
	}

	@Override
	public List<Wire> getRequiredResourceWires(String namespace) {
		return InternalUtils.asListWire(getWires(namespace, requiredWires));
	}

	@Override
	public ModuleRevision getResource() {
		return revision;
	}

	void setProvidedWires(List<ModuleWire> providedWires) {
		this.providedWires = providedWires;
	}

	void setRequiredWires(List<ModuleWire> requiredWires) {
		this.requiredWires = requiredWires;
	}

	void setCapabilities(List<ModuleCapability> capabilities) {
		this.capabilities = capabilities;
	}

	void unload() {
		// When unloading a wiring we need to release the loader.
		// This is so that the loaders are not pinned when stopping the framework.
		// Then the framework can be relaunched, at which point new loaders will
		// get created.
		invalidate0(true);
	}

	void invalidate() {
		invalidate0(false);
	}

	private void invalidate0(boolean releaseLoader) {
		// set the isValid to false first
		isValid = false;
		ModuleLoader current = releaseLoader ? loader.getAndClear() : loader.get();
		revision.getRevisions().getContainer().getAdaptor().invalidateWiring(this, current);
	}

	void validate() {
		this.isValid = true;
	}

	boolean isSubtituted(ModuleCapability capability) {
		if (!PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) {
			return false;
		}
		return substitutedPkgNames.contains(capability.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE));
	}

	/**
	 * Returns true if the specified package name has been substituted in this wiring
	 * @param packageName the package name to check
	 * @return true if the specified package name has been substituted in this wiring
	 */
	public boolean isSubstitutedPackage(String packageName) {
		return substitutedPkgNames.contains(packageName);
	}

	/**
	 * Returns an unmodifiable collection of package names for
	 * package capabilities that have been substituted.
	 * @return the substituted package names
	 */
	public Collection<String> getSubstitutedNames() {
		return Collections.unmodifiableCollection(substitutedPkgNames);
	}

	private boolean hasResourcePermission() {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			try {
				sm.checkPermission(new AdminPermission(getBundle(), AdminPermission.RESOURCE));
			} catch (SecurityException e) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Adds the {@link ModuleRevisionBuilder#getRequirements() requirements} from
	 * the specified builder to this wiring.  The new requirements must be in the
	 * {@link PackageNamespace}.  These requirements are transient
	 * and will not exist when loading up persistent wirings.
	 * @param builder the builder that defines the new dynamic imports.
	 */
	public void addDynamicImports(ModuleRevisionBuilder builder) {
		List<GenericInfo> newImports = builder.getRequirements();
		List<ModuleRequirement> newRequirements = new ArrayList<>();
		for (GenericInfo info : newImports) {
			if (!PackageNamespace.PACKAGE_NAMESPACE.equals(info.getNamespace())) {
				throw new IllegalArgumentException("Invalid namespace for package imports: " + info.getNamespace()); //$NON-NLS-1$
			}
			Map<String, Object> attributes = new HashMap<>(info.getAttributes());
			Map<String, String> directives = new HashMap<>(info.getDirectives());
			directives.put(DYNAMICALLY_ADDED_IMPORT_DIRECTIVE, "true"); //$NON-NLS-1$
			directives.put(PackageNamespace.REQUIREMENT_RESOLUTION_DIRECTIVE, PackageNamespace.RESOLUTION_DYNAMIC);
			newRequirements.add(new ModuleRequirement(info.getNamespace(), directives, attributes, revision));
		}
		ModuleDatabase moduleDatabase = revision.getRevisions().getContainer().moduleDatabase;
		moduleDatabase.writeLock();
		try {
			List<ModuleRequirement> updatedRequirements = new ArrayList<>(requirements);
			updatedRequirements.addAll(newRequirements);
			requirements = updatedRequirements;
		} finally {
			moduleDatabase.writeUnlock();
		}
	}

	void addDynamicPackageMiss(String packageName) {
		Set<String> misses = dynamicMissRef.get();
		if (misses == null) {
			dynamicMissRef.compareAndSet(null, Collections.synchronizedSet(new HashSet<String>()));
			misses = dynamicMissRef.get();
		}

		misses.add(packageName);
	}

	boolean isDynamicPackageMiss(String packageName) {
		Set<String> misses = dynamicMissRef.get();
		return misses != null && misses.contains(packageName);
	}

	void removeDynamicPackageMisses(Collection<String> packageNames) {
		Set<String> misses = dynamicMissRef.get();
		if (misses != null) {
			misses.removeAll(packageNames);
		}
	}

	@Override
	public String toString() {
		return revision.toString();
	}

	List<Wire> getSubstitutionWires() {
		if (substitutedPkgNames.isEmpty()) {
			return Collections.emptyList();
		}
		// Could cache this, but seems unnecessary since it will only be used by the resolver
		List<Wire> substitutionWires = new ArrayList<>(substitutedPkgNames.size());
		List<ModuleWire> current = requiredWires;
		for (ModuleWire wire : current) {
			Capability cap = wire.getCapability();
			if (PackageNamespace.PACKAGE_NAMESPACE.equals(cap.getNamespace())) {
				if (substitutedPkgNames.contains(cap.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE))) {
					substitutionWires.add(wire);
				}
			}
		}
		return substitutionWires;
	}
}
