/*******************************************************************************
 * Copyright (c) 2013, 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.compatibility.state;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.osgi.container.*;
import org.eclipse.osgi.internal.resolver.BaseDescriptionImpl.BaseCapability;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.State;
import org.osgi.framework.*;
import org.osgi.framework.hooks.resolver.ResolverHook;
import org.osgi.framework.hooks.resolver.ResolverHookFactory;
import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.wiring.*;

class PlatformBundleListener implements SynchronousBundleListener, FrameworkListener, ResolverHookFactory {

	private final State systemState;
	private final StateConverter converter;
	private final ModuleDatabase database;
	private final ModuleContainer container;
	private long lastResolveStamp = -1;
	private AtomicBoolean gotUnresolved = new AtomicBoolean(false);

	PlatformBundleListener(State systemState, StateConverter converter, ModuleDatabase database, ModuleContainer container) {
		this.systemState = systemState;
		this.converter = converter;
		this.database = database;
		this.container = container;
	}

	@Override
	public void bundleChanged(BundleEvent event) {
		switch (event.getType()) {
			case BundleEvent.INSTALLED : {
				BundleRevision revision = event.getBundle().adapt(BundleRevision.class);
				if (revision != null) {
					systemState.addBundle(converter.createDescription(revision));
					systemState.setTimeStamp(database.getRevisionsTimestamp());
				}
				break;
			}
			case BundleEvent.UNINSTALLED : {
				systemState.removeBundle(event.getBundle().getBundleId());
				systemState.setTimeStamp(database.getRevisionsTimestamp());
				break;
			}
			case BundleEvent.UPDATED : {
				BundleRevision revision = event.getBundle().adapt(BundleRevision.class);
				if (revision != null) {
					systemState.updateBundle(converter.createDescription(revision));
					systemState.setTimeStamp(database.getRevisionsTimestamp());
				}
				break;
			}
			case BundleEvent.UNRESOLVED : {
				gotUnresolved.set(true);
				break;
			}
			case BundleEvent.RESOLVED : {
				resolve(gotUnresolved.getAndSet(false));
				break;
			}
			default :
				// do nothing
				break;
		}
	}

	private void resolve(boolean uninstalled) {
		database.readLock();
		try {
			if (lastResolveStamp != database.getRevisionsTimestamp()) {
				Collection<ModuleRevision> containerRemovalPending = container.getRemovalPending();
				BundleDescription[] stateRemovalPendingDescs = systemState.getRemovalPending();
				Collection<BundleDescription> stateRemovalPending = new ArrayList<>(stateRemovalPendingDescs.length);
				for (BundleDescription description : stateRemovalPendingDescs) {
					if (!containerRemovalPending.contains(description.getUserObject())) {
						stateRemovalPending.add(description);
					}
				}
				if (!stateRemovalPending.isEmpty()) {
					systemState.resolve(stateRemovalPending.toArray(new BundleDescription[stateRemovalPending.size()]), true);
				} else {
					systemState.resolve(!uninstalled);
				}
				lastResolveStamp = database.getRevisionsTimestamp();
				systemState.setTimeStamp(database.getRevisionsTimestamp());
			}
		} finally {
			database.readUnlock();
		}
	}

	@Override
	public void frameworkEvent(FrameworkEvent event) {
		if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
			resolve(gotUnresolved.getAndSet(false));
		}
	}

	@Override
	public ResolverHook begin(Collection<BundleRevision> triggers) {
		return new ResolverHook() {

			@Override
			public void filterSingletonCollisions(BundleCapability singleton, Collection<BundleCapability> collisionCandidates) {
				BundleDescription desc = (BundleDescription) singleton.getRevision();
				ModuleRevision revision = (ModuleRevision) desc.getUserObject();
				if (revision.getWiring() != null) {
					collisionCandidates.clear();
				} else {
					for (Iterator<BundleCapability> iCandidates = collisionCandidates.iterator(); iCandidates.hasNext();) {
						BundleDescription candDesc = (BundleDescription) iCandidates.next().getRevision();
						ModuleRevision candRevision = (ModuleRevision) candDesc.getUserObject();
						if (candRevision.getWiring() == null) {
							iCandidates.remove();
						}
					}
				}
			}

			@Override
			public void filterResolvable(Collection<BundleRevision> candidates) {
				for (Iterator<BundleRevision> iCandidates = candidates.iterator(); iCandidates.hasNext();) {
					BundleDescription candDesc = (BundleDescription) iCandidates.next();
					ModuleRevision candRevision = (ModuleRevision) candDesc.getUserObject();
					if (candRevision.getWiring() == null) {
						iCandidates.remove();
					}
				}
			}

			@Override
			public void filterMatches(BundleRequirement requirement, Collection<BundleCapability> candidates) {
				String namespace = requirement.getNamespace();
				BundleDescription reqDesc = (BundleDescription) requirement.getRevision();
				ModuleRevision reqRevision = (ModuleRevision) reqDesc.getUserObject();
				ModuleWiring reqWiring = reqRevision.getWiring();
				if (reqWiring == null) {
					candidates.clear();
					return;
				}
				Collection<ModuleWiring> wirings = new ArrayList<>(1);
				if ((reqRevision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) {
					if (ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE.equals(namespace) || HostNamespace.HOST_NAMESPACE.equals(namespace)) {
						wirings.add(reqWiring);
					} else {
						List<ModuleWire> hostWires = reqWiring.getRequiredModuleWires(namespace);
						for (ModuleWire hostWire : hostWires) {
							ModuleWiring hostWiring = hostWire.getProviderWiring();
							if (hostWiring != null) {
								wirings.add(hostWiring);
							}
						}
					}
				} else {
					wirings.add(reqWiring);
				}
				for (Iterator<BundleCapability> iCandidates = candidates.iterator(); iCandidates.hasNext();) {
					BaseCapability baseCapability = (BaseCapability) iCandidates.next();
					Object userObject = baseCapability.getBaseDescription().getUserObject();
					boolean foundCandidate = false;
					wirings: for (ModuleWiring wiring : wirings) {
						List<ModuleWire> wires = wiring.getRequiredModuleWires(namespace);
						for (ModuleWire wire : wires) {
							if (userObject instanceof ModuleRevision && userObject.equals(wire.getProvider())) {
								foundCandidate = true;
							} else if (userObject instanceof ModuleCapability && userObject.equals(wire.getCapability())) {
								foundCandidate = true;
							}
							if (foundCandidate)
								break wirings;
						}
					}
					if (!foundCandidate) {
						iCandidates.remove();
					}
				}
			}

			@Override
			public void end() {
				// nothing
			}
		};
	}

}
