/*******************************************************************************
 * Copyright (c) 2004, 2016 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.internal.module;

import java.util.*;
import org.eclipse.osgi.service.resolver.BundleSpecification;
import org.eclipse.osgi.service.resolver.ExportPackageDescription;

/*
 * The GroupingChecker checks the 'uses' directive on exported packages for consistency
 */
public class GroupingChecker {
	final PackageRoots nullPackageRoots = new PackageRoots(null);
	// a mapping of bundles to their package roots; keyed by
	// ResolverBundle -> HashMap of packages; keyed by
	// package name -> PackageRoots
	private Map<ResolverBundle, Map<String, PackageRoots>> bundles = new HashMap<>();

	/*
	 * This method fully populates a bundles package roots for the purpose of resolving
	 * a dynamic import.  Package roots must be fully populated because we need all the
	 * roots to do proper uses constraint verification on a dynamic import supplier.
	 */
	public void populateRoots(ResolverBundle bundle) {
		if (bundles.containsKey(bundle))
			// only do the full populate the first time (bug 337272)
			return;
		// process all requires
		BundleConstraint[] requires = bundle.getRequires();
		for (int j = 0; j < requires.length; j++) {
			ResolverBundle selectedSupplier = (ResolverBundle) requires[j].getSelectedSupplier();
			if (selectedSupplier != null)
				isConsistentInternal(bundle, selectedSupplier, new ArrayList<ResolverBundle>(1), true, null);
		}
		// process all imports
		// must check resolved imports to get any dynamically resolved imports
		ExportPackageDescription[] imports = bundle.getBundleDescription().getResolvedImports();
		for (ExportPackageDescription importPkg : imports) {
			List<ResolverExport> exports = bundle.getResolver().getResolverExports().get(importPkg.getName());
			for (ResolverExport export : exports) {
				if (export.getExportPackageDescription() == importPkg)
					isConsistentInternal(bundle, export, true, null);
			}
		}
	}

	/*
	 * Re-populates the package roots or an importing bundle with the given export
	 * This is done after wiring a package from a dynamic import (bug 337272)
	 */
	public void populateRoots(ResolverBundle importingBundle, ResolverExport export) {
		Map<String, PackageRoots> packageRoots = bundles.get(importingBundle);
		if (packageRoots != null)
			packageRoots.remove(export.getName());
		PackageRoots roots = getPackageRoots(export.getExporter(), export.getName(), null);
		packageRoots.put(export.getName(), roots);
	}

	/*
	 * Verifies the uses constraint consistency for the requiringBundle with the possible matching bundle.
	 * If an inconsistency is found the export inconsistency is returned; otherwise null is returned
	 */
	public PackageRoots[][] isConsistent(ResolverBundle requiringBundle, ResolverBundle matchingBundle) {
		List<PackageRoots[]> results = isConsistentInternal(requiringBundle, matchingBundle, new ArrayList<ResolverBundle>(1), false, null);
		return results == null ? null : results.toArray(new PackageRoots[results.size()][]);
	}

	private List<PackageRoots[]> isConsistentInternal(ResolverBundle requiringBundle, ResolverBundle matchingBundle, List<ResolverBundle> visited, boolean dynamicImport, List<PackageRoots[]> results) {
		// needed to prevent endless cycles
		if (visited.contains(matchingBundle))
			return results;
		visited.add(matchingBundle);
		// check that the packages exported by the matching bundle are consistent
		ResolverExport[] matchingExports = matchingBundle.getExportPackages();
		for (int i = 0; i < matchingExports.length; i++) {
			ResolverExport matchingExport = matchingExports[i];
			if (matchingExports[i].getSubstitute() != null)
				matchingExport = (ResolverExport) matchingExports[i].getSubstitute();
			results = isConsistentInternal(requiringBundle, matchingExport, dynamicImport, results);
		}
		// check that the packages from reexported bundles are consistent
		BundleConstraint[] supplierRequires = matchingBundle.getRequires();
		for (int j = 0; j < supplierRequires.length; j++) {
			ResolverBundle reexported = (ResolverBundle) supplierRequires[j].getSelectedSupplier();
			if (reexported == null || !((BundleSpecification) supplierRequires[j].getVersionConstraint()).isExported())
				continue;
			results = isConsistentInternal(requiringBundle, reexported, visited, dynamicImport, results);
		}
		return results;
	}

	/*
	 * Verifies the uses constraint consistency for the importingBundle with the possible matching export.
	 * If an inconsistency is found the export returned; otherwise null is returned
	 */
	public PackageRoots[][] isConsistent(ResolverBundle importingBundle, ResolverExport matchingExport) {
		List<PackageRoots[]> results = isConsistentInternal(importingBundle, matchingExport, false, null);
		return results == null ? null : results.toArray(new PackageRoots[results.size()][]);
	}

	public PackageRoots[][] isConsistent(ResolverBundle requiringBundle, GenericCapability matchingCapability) {
		String[] uses = matchingCapability.getUsesDirective();
		if (uses == null)
			return null;
		ArrayList<PackageRoots[]> results = new ArrayList<>(0);
		for (String usedPackage : uses) {
			PackageRoots providingRoots = getPackageRoots(matchingCapability.getResolverBundle(), usedPackage, null);
			providingRoots.addConflicts(requiringBundle, usedPackage, null, results);
		}
		return results.size() == 0 ? null : results.toArray(new PackageRoots[results.size()][]);
	}

	/*
	 * Verifies the uses constraint consistency for the importingBundle with the possible dynamic matching export.
	 * If an inconsistency is found the export returned; otherwise null is returned.
	 * Dynamic imports must perform extra checks to ensure that existing wires to package roots are 
	 * consistent with the possible matching dynamic export.
	 */
	public PackageRoots[][] isDynamicConsistent(ResolverBundle importingBundle, ResolverExport matchingExport) {
		List<PackageRoots[]> results = isConsistentInternal(importingBundle, matchingExport, true, null);
		return results == null ? null : results.toArray(new PackageRoots[results.size()][]);
	}

	private List<PackageRoots[]> isConsistentInternal(ResolverBundle importingBundle, ResolverExport matchingExport, boolean dyanamicImport, List<PackageRoots[]> results) {
		PackageRoots exportingRoots = getPackageRoots(matchingExport.getExporter(), matchingExport.getName(), null);
		// check that the exports uses packages are consistent with existing package roots
		results = exportingRoots.isConsistentClassSpace(importingBundle, null, results);
		if (!dyanamicImport)
			return results;
		// for dynamic imports we must check that each existing root is consistent with the possible matching export
		PackageRoots importingRoots = getPackageRoots(importingBundle, matchingExport.getName(), null);
		Map<String, PackageRoots> importingPackages = bundles.get(importingBundle);
		if (importingPackages != null)
			for (Iterator<PackageRoots> allImportingPackages = importingPackages.values().iterator(); allImportingPackages.hasNext();) {
				PackageRoots roots = allImportingPackages.next();
				if (roots != importingRoots)
					results = roots.isConsistentClassSpace(exportingRoots, matchingExport.getExporter(), null, results);
			}
		// We also must check any generic capabilities are consistent
		GenericConstraint[] genericRequires = importingBundle.getGenericRequires();
		for (GenericConstraint constraint : genericRequires) {
			VersionSupplier[] suppliers = constraint.getMatchingCapabilities();
			if (suppliers != null) {
				for (VersionSupplier supplier : suppliers) {
					String[] uses = ((GenericCapability) supplier).getUsesDirective();
					if (uses != null)
						for (String usedPackage : uses) {
							if (usedPackage.equals(matchingExport.getName())) {
								results = exportingRoots.addConflicts(supplier.getResolverBundle(), usedPackage, null, results);
							}
						}
				}
			}
		}
		return results;
	}

	/*
	 * returns package roots for a specific package name for a specific bundle
	 */
	PackageRoots getPackageRoots(ResolverBundle bundle, String packageName, List<ResolverBundle> visited) {
		Map<String, PackageRoots> packages = bundles.get(bundle);
		if (packages == null) {
			packages = new HashMap<>(5);
			bundles.put(bundle, packages);
		}
		PackageRoots packageRoots = packages.get(packageName);
		if (packageRoots == null) {
			packageRoots = createPackageRoots(bundle, packageName, visited == null ? new ArrayList<ResolverBundle>(1) : visited);
			packages.put(packageName, packageRoots);
		}
		return packageRoots != null ? packageRoots : nullPackageRoots;
	}

	private PackageRoots createPackageRoots(ResolverBundle bundle, String packageName, List<ResolverBundle> visited) {
		if (visited.contains(bundle))
			return null;
		visited.add(bundle); // prevent endless cycles
		// check imports
		if (bundle.getBundleDescription().isResolved()) {
			// must check resolved imports to get any dynamically resolved imports 
			ExportPackageDescription[] imports = bundle.getBundleDescription().getResolvedImports();
			for (ExportPackageDescription importPkg : imports) {
				if (importPkg.getExporter() == bundle.getBundleDescription() || !importPkg.getName().equals(packageName))
					continue;
				List<ResolverExport> exports = bundle.getResolver().getResolverExports().get(packageName);
				for (ResolverExport export : exports) {
					if (export.getExportPackageDescription() == importPkg)
						return getPackageRoots(export.getExporter(), packageName, visited);
				}
			}
		} else {
			ResolverImport imported = bundle.getImport(packageName);
			if (imported != null && imported.getSelectedSupplier() != null) {
				// make sure we are not resolved to our own import
				ResolverExport selectedExport = (ResolverExport) imported.getSelectedSupplier();
				if (selectedExport.getExporter() != bundle) {
					// found resolved import; get the roots from the resolved exporter;
					// this is all the roots if the package is imported
					return getPackageRoots(selectedExport.getExporter(), packageName, visited);
				}
			}
		}
		// check if the bundle exports the package
		ResolverExport[] exports = bundle.getExports(packageName);
		List<PackageRoots> roots = new ArrayList<>(0);
		// check roots from required bundles
		BundleConstraint[] requires = bundle.getRequires();
		for (int i = 0; i < requires.length; i++) {
			ResolverBundle supplier = (ResolverBundle) requires[i].getSelectedSupplier();
			if (supplier == null)
				continue; // no supplier, probably optional
			if (supplier.getExport(packageName) != null) {
				// the required bundle exports the package; get the package roots from it
				PackageRoots requiredRoots = getPackageRoots(supplier, packageName, visited);
				if (requiredRoots != nullPackageRoots)
					roots.add(requiredRoots);
			} else {
				// the bundle does not export the package; but it may reexport another bundle that does
				BundleConstraint[] supplierRequires = supplier.getRequires();
				for (int j = 0; j < supplierRequires.length; j++) {
					ResolverBundle reexported = (ResolverBundle) supplierRequires[j].getSelectedSupplier();
					if (reexported == null || !((BundleSpecification) supplierRequires[j].getVersionConstraint()).isExported())
						continue;
					if (reexported.getExport(packageName) != null) {
						// the reexported bundle exports the package; get the package roots from it
						PackageRoots reExportedRoots = getPackageRoots(reexported, packageName, visited);
						if (reExportedRoots != nullPackageRoots)
							roots.add(reExportedRoots);
					}
				}
			}
		}
		if (exports.length > 0 || roots.size() > 1) {
			PackageRoots[] requiredRoots = roots.toArray(new PackageRoots[roots.size()]);
			if (exports.length == 0) {
				PackageRoots superSet = requiredRoots[0];
				for (int i = 1; i < requiredRoots.length; i++) {
					if (requiredRoots[i].superSet(superSet)) {
						superSet = requiredRoots[i];
					} else if (!superSet.superSet(requiredRoots[i])) {
						superSet = null;
						break;
					}
				}
				if (superSet != null)
					return superSet;
			}
			// in this case we cannot share the package roots object; must create one specific for this bundle
			PackageRoots result = new PackageRoots(packageName);
			// first merge all the roots from required bundles
			for (int i = 0; i < requiredRoots.length; i++)
				result.merge(requiredRoots[i]);
			// always add this bundles exports to the end if it exports the package
			for (int i = 0; i < exports.length; i++)
				result.addRoot(exports[i]);
			return result;
		}
		return roots.size() == 0 ? nullPackageRoots : roots.get(0);
	}

	public void clear() {
		bundles.clear();
	}

	public void clear(ResolverBundle rb) {
		bundles.remove(rb);
	}

	class PackageRoots {
		private String name;
		private ResolverExport[] roots;

		PackageRoots(String name) {
			this.name = name;
		}

		public boolean hasRoots() {
			return roots != null && roots.length > 0;
		}

		public void addRoot(ResolverExport export) {
			if (roots == null) {
				roots = new ResolverExport[] {export};
				return;
			}
			// need to do an extra check to make sure we are not adding the same package name 
			// from multiple versions of the same bundle
			String exportBSN = export.getExporter().getName();
			if (exportBSN != null) {
				// first one wins
				for (int i = 0; i < roots.length; i++)
					if (export.getExporter() != roots[i].getExporter() && exportBSN.equals(roots[i].getExporter().getName()))
						return;
			}
			if (!contains(export, roots)) {
				ResolverExport[] newRoots = new ResolverExport[roots.length + 1];
				System.arraycopy(roots, 0, newRoots, 0, roots.length);
				newRoots[roots.length] = export;
				roots = newRoots;
			}
		}

		private boolean contains(ResolverExport export, ResolverExport[] exports) {
			for (int i = 0; i < exports.length; i++)
				if (exports[i] == export)
					return true;
			return false;
		}

		public void merge(PackageRoots packageRoots) {
			if (packageRoots == null || packageRoots.roots == null)
				return;
			int size = packageRoots.roots.length;
			for (int i = 0; i < size; i++)
				addRoot(packageRoots.roots[i]);
		}

		public List<PackageRoots[]> isConsistentClassSpace(ResolverBundle importingBundle, List<PackageRoots> visited, List<PackageRoots[]> results) {
			if (roots == null)
				return results;
			if (visited == null)
				visited = new ArrayList<>(1);
			if (visited.contains(this))
				return results;
			visited.add(this);
			int size = roots.length;
			for (int i = 0; i < size; i++) {
				ResolverExport root = roots[i];
				String[] uses = root.getUsesDirective();
				if (uses == null)
					continue;
				for (int j = 0; j < uses.length; j++) {
					if (uses[j].equals(root.getName()))
						continue;
					PackageRoots thisUsedRoots = getPackageRoots(root.getExporter(), uses[j], null);
					PackageRoots importingUsedRoots = getPackageRoots(importingBundle, uses[j], null);
					if (thisUsedRoots == importingUsedRoots)
						continue;
					if (thisUsedRoots != nullPackageRoots && importingUsedRoots != nullPackageRoots)
						if (!(subSet(thisUsedRoots.roots, importingUsedRoots.roots) || subSet(importingUsedRoots.roots, thisUsedRoots.roots))) {
							if (results == null)
								results = new ArrayList<>(1);
							results.add(new PackageRoots[] {this, importingUsedRoots});
						}
					// need to check the usedRoots consistency for transitive closure
					results = thisUsedRoots.isConsistentClassSpace(importingBundle, visited, results);
				}
			}
			return results;
		}

		public List<PackageRoots[]> isConsistentClassSpace(PackageRoots exportingRoots, ResolverBundle exporter, List<PackageRoots> visited, List<PackageRoots[]> results) {
			if (roots == null)
				return results;
			int size = roots.length;
			for (int i = 0; i < size; i++) {
				ResolverExport root = roots[i];
				String[] uses = root.getUsesDirective();
				if (uses == null)
					continue;
				if (visited == null)
					visited = new ArrayList<>(1);
				if (visited.contains(this))
					return results;
				visited.add(this);
				for (int j = 0; j < uses.length; j++) {
					if (uses[j].equals(root.getName()) || !uses[j].equals(exportingRoots.name))
						continue;
					PackageRoots thisUsedRoots = getPackageRoots(root.getExporter(), uses[j], null);
					PackageRoots exportingUsedRoots = getPackageRoots(exporter, uses[j], null);
					if (thisUsedRoots == exportingRoots)
						return results;
					if (thisUsedRoots != nullPackageRoots && exportingUsedRoots != nullPackageRoots)
						if (!(subSet(thisUsedRoots.roots, exportingUsedRoots.roots) || subSet(exportingUsedRoots.roots, thisUsedRoots.roots))) {
							if (results == null)
								results = new ArrayList<>(1);
							results.add(new PackageRoots[] {this, exportingUsedRoots});
						}
					// need to check the usedRoots consistency for transitive closure
					results = thisUsedRoots.isConsistentClassSpace(exportingRoots, exporter, visited, results);
				}
			}
			return results;
		}

		List<PackageRoots[]> addConflicts(ResolverBundle bundle, String usedPackage, List<PackageRoots> visited, List<PackageRoots[]> results) {
			PackageRoots bundleUsedRoots = getPackageRoots(bundle, usedPackage, null);
			if (this == bundleUsedRoots)
				return results;
			if (this != nullPackageRoots && bundleUsedRoots != nullPackageRoots)
				if (!(subSet(this.roots, bundleUsedRoots.roots) || subSet(bundleUsedRoots.roots, this.roots))) {
					if (results == null)
						results = new ArrayList<>(1);
					results.add(new PackageRoots[] {this, bundleUsedRoots});
				}
			// need to check the usedRoots consistency for transitive closure
			return this.isConsistentClassSpace(bundleUsedRoots, bundle, visited, results);
		}

		// TODO this is a behavioral change; before we only required 1 supplier to match; now roots must be subsets
		private boolean subSet(ResolverExport[] superSet, ResolverExport[] subSet) {
			for (int i = 0; i < subSet.length; i++) {
				boolean found = false;
				for (int j = 0; j < superSet.length; j++)
					// compare by exporter in case the bundle exports the package multiple times
					if (subSet[i].getExporter() == superSet[j].getExporter()) {
						found = true;
						break;
					}
				if (!found)
					return false;
			}
			return true;
		}

		public boolean superSet(PackageRoots subSet) {
			return subSet(roots, subSet.roots);
		}

		public String getName() {
			return name;
		}

		public ResolverExport[] getRoots() {
			return roots;
		}
	}
}
