/*******************************************************************************
 * Copyright (c) 2004, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.internal.module;

import java.util.*;
import org.eclipse.osgi.internal.resolver.ExportPackageDescriptionImpl;
import org.eclipse.osgi.service.resolver.*;
import org.osgi.framework.Constants;

/*
 * A companion to BundleDescription from the state used while resolving.
 */
public class ResolverBundle extends VersionSupplier implements Comparable{
	public static final int UNRESOLVED = 0;
	public static final int RESOLVING = 1;
	public static final int RESOLVED = 2;

	private Long bundleID;
	private BundleConstraint host;
	private ResolverImport[] imports;
	private ResolverExport[] exports;
	private BundleConstraint[] requires;
	private GenericCapability[] capabilities;
	private GenericConstraint[] genericReqiures;
	// Fragment support
	private ArrayList fragments;
	private HashMap fragmentExports;
	private HashMap fragmentImports;
	private HashMap fragmentRequires;
	private HashMap fragmentGenericRequires;
	// Flag specifying whether this bundle is resolvable
	private boolean resolvable = true;
	// Internal resolver state for this bundle
	private int state = UNRESOLVED;

	private ResolverImpl resolver;
	private boolean newFragmentExports;
	private ArrayList refs;

	ResolverBundle(BundleDescription bundle, ResolverImpl resolver) {
		super(bundle);
		this.bundleID = new Long(bundle.getBundleId());
		this.resolver = resolver;
		initialize(bundle.isResolved());
	}

	void initialize(boolean useSelectedExports) {
		if (getBundle().isSingleton())
			refs = new ArrayList();
		// always add generic capabilities
		GenericDescription[] actualCapabilities = getBundle().getGenericCapabilities();
		capabilities = new GenericCapability[actualCapabilities.length];
		for (int i = 0; i < capabilities.length; i++)
			capabilities[i] = new GenericCapability(this, actualCapabilities[i]);
		if (getBundle().getHost() != null) {
			host = new BundleConstraint(this, getBundle().getHost());
			exports = new ResolverExport[0];
			imports = new ResolverImport[0];
			requires = new BundleConstraint[0];
			genericReqiures = new GenericConstraint[0];
			return;
		}

		ImportPackageSpecification[] actualImports = getBundle().getImportPackages();
		// Reorder imports so that optionals are at the end so that we wire statics before optionals
		ArrayList importList = new ArrayList(actualImports.length);
		for (int i = actualImports.length - 1; i >= 0; i--)
			if (ImportPackageSpecification.RESOLUTION_OPTIONAL.equals(actualImports[i].getDirective(Constants.RESOLUTION_DIRECTIVE)))
				importList.add(new ResolverImport(this, actualImports[i]));
			else
				importList.add(0, new ResolverImport(this, actualImports[i]));
		imports = (ResolverImport[]) importList.toArray(new ResolverImport[importList.size()]);

		ExportPackageDescription[] actualExports = useSelectedExports ? getBundle().getSelectedExports() : getBundle().getExportPackages();
		exports = new ResolverExport[actualExports.length];
		for (int i = 0; i < actualExports.length; i++)
			exports[i] = new ResolverExport(this, actualExports[i]);

		BundleSpecification[] actualRequires = getBundle().getRequiredBundles();
		requires = new BundleConstraint[actualRequires.length];
		for (int i = 0; i < requires.length; i++)
			requires[i] = new BundleConstraint(this, actualRequires[i]);

		GenericSpecification[] actualGenericRequires = getBundle().getGenericRequires();
		genericReqiures = new GenericConstraint[actualGenericRequires.length];
		for (int i = 0; i < genericReqiures.length; i++)
			genericReqiures[i] = new GenericConstraint(this, actualGenericRequires[i]);

		fragments = null;
		fragmentExports = null;
		fragmentImports = null;
		fragmentRequires = null;
		fragmentGenericRequires = null;
	}

	ResolverExport getExport(String name) {
		ResolverExport[] allExports = getExports(name);
		return allExports.length == 0 ? null : allExports[0];
	}

	ResolverExport[] getExports(String name) {
		ArrayList results = new ArrayList(1); // rare to have more than one
		// it is faster to ask the VersionHashMap for this package name and then compare the exporter to this
		Object[] resolverExports = resolver.getResolverExports().get(name);
		for (int i = 0; i < resolverExports.length; i++)
			if (((ResolverExport)resolverExports[i]).getExporter() == this)
				results.add(resolverExports[i]);
		return (ResolverExport[]) results.toArray(new ResolverExport[results.size()]);
	}

	void clearWires() {
		ResolverImport[] allImports = getImportPackages();
		for (int i = 0; i < allImports.length; i++)
			allImports[i].clearPossibleSuppliers();

		if (host != null)
			host.clearPossibleSuppliers();

		BundleConstraint[] allRequires = getRequires();
		for (int i = 0; i < allRequires.length; i++)
			allRequires[i].clearPossibleSuppliers();

		GenericConstraint[] allGenericRequires = getGenericRequires();
		for (int i = 0; i < allGenericRequires.length; i++)
			allGenericRequires[i].setMatchingCapability(null);
	}

	boolean isResolved() {
		return getState() == ResolverBundle.RESOLVED;
	}

	boolean isFragment() {
		return host != null;
	}

	int getState() {
		return state;
	}

	void setState(int state) {
		this.state = state;
	}

	ResolverImport[] getImportPackages() {
		if (isFragment())
			return new ResolverImport[0];
		if (fragments == null || fragments.size() == 0)
			return imports;
		ArrayList resultList = new ArrayList(imports.length);
		for (int i = 0; i < imports.length; i++)
			resultList.add(imports[i]);
		for (Iterator iter = fragments.iterator(); iter.hasNext();) {
			ResolverBundle fragment = (ResolverBundle) iter.next();
			ArrayList fragImports = (ArrayList) fragmentImports.get(fragment.bundleID);
			if (fragImports != null)
				resultList.addAll(fragImports);
		}
		return (ResolverImport[]) resultList.toArray(new ResolverImport[resultList.size()]);
	}

	ResolverExport[] getExportPackages() {
		if (isFragment())
			return new ResolverExport[0];
		if (fragments == null || fragments.size() == 0)
			return exports;
		ArrayList resultList = new ArrayList(exports.length);
		for (int i = 0; i < exports.length; i++)
			resultList.add(exports[i]);
		for (Iterator iter = fragments.iterator(); iter.hasNext();) {
			ResolverBundle fragment = (ResolverBundle) iter.next();
			ArrayList fragExports = (ArrayList) fragmentExports.get(fragment.bundleID);
			if (fragExports != null)
				resultList.addAll(fragExports);
		}
		return (ResolverExport[]) resultList.toArray(new ResolverExport[resultList.size()]);
	}

	ResolverExport[] getSelectedExports() {
		ResolverExport[] allExports = getExportPackages();
		int removedExports = 0;
		for (int i = 0; i < allExports.length; i++)
			if (allExports[i].isDropped())
				removedExports++;
		if (removedExports == 0)
			return allExports;
		ResolverExport[] selectedExports = new ResolverExport[allExports.length - removedExports];
		int index = 0;
		for (int i = 0; i < allExports.length; i++) {
			if (allExports[i].isDropped())
				continue;
			selectedExports[index] = allExports[i];
			index++;
		}
		return selectedExports;
	}

	BundleConstraint getHost() {
		return host;
	}

	GenericCapability[] getGenericCapabilities() {
		return capabilities;
	}

	BundleConstraint[] getRequires() {
		if (isFragment())
			return new BundleConstraint[0];
		if (fragments == null || fragments.size() == 0)
			return requires;
		ArrayList resultList = new ArrayList(requires.length);
		for (int i = 0; i < requires.length; i++)
			resultList.add(requires[i]);
		for (Iterator iter = fragments.iterator(); iter.hasNext();) {
			ResolverBundle fragment = (ResolverBundle) iter.next();
			ArrayList fragRequires = (ArrayList) fragmentRequires.get(fragment.bundleID);
			if (fragRequires != null)
				resultList.addAll(fragRequires);
		}
		return (BundleConstraint[]) resultList.toArray(new BundleConstraint[resultList.size()]);
	}

	GenericConstraint[] getGenericRequires() {
		if (isFragment() || fragments == null || fragments.size() == 0)
			return genericReqiures;
		ArrayList resultList = new ArrayList(genericReqiures.length);
		for (int i = 0; i < genericReqiures.length; i++)
			resultList.add(genericReqiures[i]);
		for (Iterator iter = fragments.iterator(); iter.hasNext();) {
			ResolverBundle fragment = (ResolverBundle) iter.next();
			ArrayList fragGenericRegs = (ArrayList) fragmentGenericRequires.get(fragment.bundleID);
			if (fragGenericRegs != null)
				resultList.addAll(fragGenericRegs);
		}
		return (GenericConstraint[]) resultList.toArray(new GenericConstraint[resultList.size()]);
	}

	BundleConstraint getRequire(String name) {
		BundleConstraint[] allRequires = getRequires();
		for (int i = 0; i < allRequires.length; i++)
			if (allRequires[i].getVersionConstraint().getName().equals(name))
				return allRequires[i];
		return null;
	}

	public BundleDescription getBundle() {
		return (BundleDescription) getBaseDescription();
	}

	ResolverImport getImport(String name) {
		ResolverImport[] allImports = getImportPackages();
		for (int i = 0; i < allImports.length; i++) {
			if (allImports[i].getName().equals(name)) {
				return allImports[i];
			}
		}
		return null;
	}

	public String toString() {
		return "[" + getBundle() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
	}

	private void initFragments() {
		if (fragments == null)
			fragments = new ArrayList(1);
		if (fragmentExports == null)
			fragmentExports = new HashMap(1);
		if (fragmentImports == null)
			fragmentImports = new HashMap(1);
		if (fragmentRequires == null)
			fragmentRequires = new HashMap(1);
		if (fragmentGenericRequires == null)
			fragmentGenericRequires = new HashMap(1);
	}

	private boolean isImported(String packageName) {
		ResolverImport[] allImports = getImportPackages();
		for (int i = 0; i < allImports.length; i++)
			if (packageName.equals(allImports[i].getName()))
				return true;

		return false;
	}

	private boolean isExported(String packageName) {
		ResolverExport export = getExport(packageName);
		if (export == null)
			return false;
		// let exports from a bundle manifest be exported in addition to the ones from the vm profile
		return 0 > ((Integer) export.getExportPackageDescription().getDirective(ExportPackageDescriptionImpl.EQUINOX_EE)).intValue();
	}

	private boolean isRequired(String bundleName) {
		return getRequire(bundleName) != null;
	}

	ResolverExport[] attachFragment(ResolverBundle fragment, boolean dynamicAttach) {
		if (isFragment())
			return new ResolverExport[0]; // cannot attach to fragments;
		if (!getBundle().attachFragments() || (isResolved() && !getBundle().dynamicFragments()))
			return new ResolverExport[0]; // host is restricting attachment
		if (fragment.getHost().getNumPossibleSuppliers() > 0 && !((HostSpecification) fragment.getHost().getVersionConstraint()).isMultiHost())
			return new ResolverExport[0]; // fragment is restricting attachment

		ImportPackageSpecification[] newImports = fragment.getBundle().getImportPackages();
		BundleSpecification[] newRequires = fragment.getBundle().getRequiredBundles();
		ExportPackageDescription[] newExports = fragment.getBundle().getExportPackages();
		GenericSpecification[] newGenericRequires = fragment.getBundle().getGenericRequires();

		// if this is not during initialization then check if constraints conflict
		if (dynamicAttach && constraintsConflict(fragment.getBundle(), newImports, newRequires, newGenericRequires))
			return new ResolverExport[0]; // do not allow fragments with conflicting constraints
		if (isResolved() && newExports.length > 0)
			fragment.setNewFragmentExports(true);

		initFragments();
		if (fragments.contains(fragment))
			return new ResolverExport[0];
		fragments.add(fragment);
		fragment.getHost().addPossibleSupplier(this);

		if (newImports.length > 0) {
			ArrayList hostImports = new ArrayList(newImports.length);
			for (int i = 0; i < newImports.length; i++)
				if (!isImported(newImports[i].getName()))
					hostImports.add(new ResolverImport(this, newImports[i]));
			fragmentImports.put(fragment.bundleID, hostImports);
		}

		if (newRequires.length > 0) {
			ArrayList hostRequires = new ArrayList(newRequires.length);
			for (int i = 0; i < newRequires.length; i++)
				if (!isRequired(newRequires[i].getName()))
					hostRequires.add(new BundleConstraint(this, newRequires[i]));
			fragmentRequires.put(fragment.bundleID, hostRequires);
		}

		if (newGenericRequires.length > 0) {
			ArrayList hostGenericRequires = new ArrayList(newGenericRequires.length);
			for (int i = 0; i < newGenericRequires.length; i++)
				hostGenericRequires.add(new GenericConstraint(this, newGenericRequires[i]));
			fragmentGenericRequires.put(fragment.bundleID, hostGenericRequires);
		}

		ArrayList hostExports = new ArrayList(newExports.length);
		if (newExports.length > 0 && dynamicAttach) {
			StateObjectFactory factory = resolver.getState().getFactory();
			for (int i = 0; i < newExports.length; i++) {
				if (!isExported(newExports[i].getName())) {
					ExportPackageDescription hostExport = factory.createExportPackageDescription(newExports[i].getName(), newExports[i].getVersion(), newExports[i].getDirectives(), newExports[i].getAttributes(), newExports[i].isRoot(), getBundle());
					hostExports.add(new ResolverExport(this, hostExport));
				}
			}
			fragmentExports.put(fragment.bundleID, hostExports);
		}
		return (ResolverExport[]) hostExports.toArray(new ResolverExport[hostExports.size()]);
	}

	boolean constraintsConflict(BundleDescription fragment, ImportPackageSpecification[] newImports, BundleSpecification[] newRequires, GenericSpecification[] newGenericRequires) {
		// this method iterates over all additional constraints from a fragment
		// if the host is resolved then the fragment is not allowed to add new constraints;
		// if the host is resolved and it already has a constraint of the same name then ensure the supplier satisfies the fragment's constraint
		boolean result = false;
		for (int i = 0; i < newImports.length; i++) {
			ResolverImport hostImport = getImport(newImports[i].getName());
			ResolverExport resolvedExport = (ResolverExport) (hostImport == null ? null : hostImport.getSelectedSupplier());
			if ((resolvedExport == null && isResolved()) || (resolvedExport != null && !newImports[i].isSatisfiedBy(resolvedExport.getExportPackageDescription()))) {
				result = true;
				resolver.getState().addResolverError(fragment, ResolverError.FRAGMENT_CONFLICT, newImports[i].toString(), newImports[i]);
			}
		}
		for (int i = 0; i < newRequires.length; i++) {
			BundleConstraint hostRequire = getRequire(newRequires[i].getName());
			ResolverBundle resolvedRequire = (ResolverBundle) (hostRequire == null ? null : hostRequire.getSelectedSupplier());
			if ((resolvedRequire == null && isResolved()) || (resolvedRequire != null && !newRequires[i].isSatisfiedBy(resolvedRequire.getBundle()))) {
				result = true;
				resolver.getState().addResolverError(fragment, ResolverError.FRAGMENT_CONFLICT, newRequires[i].toString(), newRequires[i]);
			}
		}
		// generic constraints cannot conflict; 
		// only check that a fragment does not add generics constraints to an already resolved host
		if (isResolved() && newGenericRequires != null && newGenericRequires.length > 0)
			result = true;
		return result;
	}

	private void setNewFragmentExports(boolean newFragmentExports) {
		this.newFragmentExports = newFragmentExports;
	}

	boolean isNewFragmentExports() {
		return newFragmentExports;
	}

	ResolverExport[] detachFragment(ResolverBundle fragment, ResolverConstraint reason) {
		if (isFragment())
			return new ResolverExport[0];
		initFragments();

		if (!fragments.remove(fragment))
			return new ResolverExport[0];

		fragment.setNewFragmentExports(false);
		fragment.getHost().removePossibleSupplier(this);
		ArrayList fragImports = (ArrayList) fragmentImports.remove(fragment.bundleID);
		ArrayList fragRequires = (ArrayList) fragmentRequires.remove(fragment.bundleID);
		ArrayList removedExports = (ArrayList) fragmentExports.remove(fragment.bundleID);
		fragmentGenericRequires.remove(fragment.bundleID);
		if (reason != null) {
			ResolverBundle[] remainingFrags = (ResolverBundle[]) fragments.toArray(new ResolverBundle[fragments.size()]);
			for (int i = 0; i < remainingFrags.length; i++) {
				resolver.getResolverExports().remove(detachFragment(remainingFrags[i], null));
				VersionConstraint[] constraints;
				if (reason instanceof ResolverImport)
					constraints = remainingFrags[i].getBundle().getImportPackages();
				else
					constraints = remainingFrags[i].getBundle().getRequiredBundles();
				for (int j = 0; j < constraints.length; j++)
					if (reason.getName().equals(constraints[j].getName()))
						continue; // this fragment should remained unattached.
				resolver.getResolverExports().put(attachFragment(remainingFrags[i], true));
				ArrayList newImports = (ArrayList) fragmentImports.get(remainingFrags[i].bundleID);
				if (newImports != null && fragImports != null)
					for (Iterator iNewImports = newImports.iterator(); iNewImports.hasNext();) {
						ResolverImport newImport = (ResolverImport) iNewImports.next();
						for (Iterator iOldImports = fragImports.iterator(); iOldImports.hasNext();) {
							ResolverImport oldImport = (ResolverImport) iOldImports.next();
							if (newImport.getName().equals(oldImport.getName()))
								newImport.setPossibleSuppliers(oldImport.getPossibleSuppliers());
						}
					}
				ArrayList newRequires = (ArrayList) fragmentRequires.get(remainingFrags[i].bundleID);
				if (newRequires != null && fragRequires != null)
					for (Iterator iNewRequires = newRequires.iterator(); iNewRequires.hasNext();) {
						BundleConstraint newRequire = (BundleConstraint) iNewRequires.next();
						for (Iterator iOldRequires = fragRequires.iterator(); iOldRequires.hasNext();) {
							BundleConstraint oldRequire = (BundleConstraint) iOldRequires.next();
							if (newRequire.getName().equals(oldRequire.getName()))
								newRequire.setPossibleSuppliers(oldRequire.getPossibleSuppliers());
						}
					}
			}
		}
		return removedExports == null ? new ResolverExport[0] : (ResolverExport[]) removedExports.toArray(new ResolverExport[removedExports.size()]);
	}

	void detachAllFragments() {
		if (fragments == null)
			return;
		ResolverBundle[] allFragments = (ResolverBundle[]) fragments.toArray(new ResolverBundle[fragments.size()]);
		for (int i = 0; i < allFragments.length; i++)
			detachFragment(allFragments[i], null);
	}

	boolean isResolvable() {
		return resolvable;
	}

	void setResolvable(boolean resolvable) {
		this.resolvable = resolvable;
	}

	void addExport(ResolverExport re) {
		ResolverExport[] newExports = new ResolverExport[exports.length + 1];
		for (int i = 0; i < exports.length; i++)
			newExports[i] = exports[i];
		newExports[exports.length] = re;
		exports = newExports;
	}

	ResolverImpl getResolver() {
		return resolver;
	}

	void clearRefs() {
		if (refs != null)
			refs.clear();
	}

	void addRef(ResolverBundle ref) {
		if (refs != null && !refs.contains(ref))
			refs.add(ref);
	}

	int getRefs() {
		return refs == null ? 0 : refs.size();
	}

	ResolverBundle[] getFragments() {
		return  fragments == null ? new ResolverBundle[0] : (ResolverBundle[]) fragments.toArray(new ResolverBundle[fragments.size()]);
	}

	/*
	 * This is used to sort bundles by BSN.  This is needed to fix bug 174930
	 * If both BSNs are null then 0 is returned
	 * If this BSN is null the 1 is returned
	 * If the other BSN is null then -1 is returned
	 * otherwise String.compareTo is used
	 */
	public int compareTo(Object o) {
		String bsn = getName();
		String otherBsn = ((ResolverBundle) o).getName();
		if (bsn == null)
			return otherBsn == null ? 0 : 1;
		return otherBsn == null ? -1 : bsn.compareTo(otherBsn);
	}
}
