/*******************************************************************************
 * Copyright (c) 2011, 2015 VMware Inc. 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:
 *    SpringSource, a division of VMware - initial API and implementation and/or initial documentation
 *******************************************************************************/

package org.eclipse.equinox.internal.region;

import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.equinox.internal.region.hook.*;
import org.eclipse.equinox.region.*;
import org.osgi.framework.*;
import org.osgi.framework.hooks.bundle.*;
import org.osgi.framework.hooks.resolver.ResolverHookFactory;

/**
 * {@link StandardRegionDigraph} is the default implementation of {@link RegionDigraph}.
 * <p />
 * 
 * <strong>Concurrent Semantics</strong><br />
 * 
 * Thread safe.
 * 
 */
public final class StandardRegionDigraph implements BundleIdToRegionMapping, RegionDigraph {

	private static final Set<FilteredRegion> EMPTY_EDGE_SET = Collections.unmodifiableSet(new HashSet<FilteredRegion>());

	// This monitor guards the modifications and read operations on the digraph as well as 
	// bundle id modifications of all regions in this digraph
	private final Object monitor = new Object();

	private final Map<String, Region> regions = new HashMap<>();

	// Alien calls may be made to the following object while this.monitor is locked
	// as this.monitor is higher in the lock hierarchy than this object's own monitor.
	private final BundleIdToRegionMapping bundleIdToRegionMapping;

	/* edges maps a given region to an immutable set of edges with their tail at the given region. To update
	 * the edges for a region, the corresponding immutable set is replaced atomically. */
	private final Map<Region, Set<FilteredRegion>> edges = new HashMap<>();

	private final BundleContext bundleContext;

	private final ThreadLocal<Region> threadLocal;

	private final SubgraphTraverser subgraphTraverser;

	private final org.osgi.framework.hooks.bundle.CollisionHook bundleCollisionHook;
	private final org.osgi.framework.hooks.bundle.EventHook bundleEventHook;
	private final org.osgi.framework.hooks.bundle.FindHook bundleFindHook;
	@SuppressWarnings("deprecation")
	private final org.osgi.framework.hooks.service.EventHook serviceEventHook;
	private final org.osgi.framework.hooks.service.FindHook serviceFindHook;
	private final ResolverHookFactory resolverHookFactory;
	private final StandardRegionDigraph origin;
	// Guarded by the origin monitor
	private long originUpdateCount;
	private final AtomicLong updateCount = new AtomicLong();

	private volatile Region defaultRegion;

	public StandardRegionDigraph(StandardRegionDigraph origin) throws BundleException {
		this(null, null, origin);

	}

	public StandardRegionDigraph(BundleContext bundleContext, ThreadLocal<Region> threadLocal) throws BundleException {
		this(bundleContext, threadLocal, null);
	}

	private StandardRegionDigraph(BundleContext bundleContext, ThreadLocal<Region> threadLocal, StandardRegionDigraph origin) throws BundleException {
		this.subgraphTraverser = new SubgraphTraverser();
		this.bundleIdToRegionMapping = new StandardBundleIdToRegionMapping();
		this.bundleContext = bundleContext;
		this.threadLocal = threadLocal;

		// Note we are safely escaping this only because we know the hook impls
		// do not escape the digraph to other threads on construction.
		this.resolverHookFactory = new RegionResolverHookFactory(this);

		this.bundleFindHook = new RegionBundleFindHook(this, bundleContext == null ? 0 : bundleContext.getBundle().getBundleId());
		this.bundleEventHook = new RegionBundleEventHook(this, this.threadLocal, bundleContext == null ? 0 : bundleContext.getBundle().getBundleId());
		this.bundleCollisionHook = new RegionBundleCollisionHook(this, this.threadLocal);

		this.serviceFindHook = new RegionServiceFindHook(this);
		this.serviceEventHook = new RegionServiceEventHook(this);
		this.origin = origin;
		if (origin != null) {
			synchronized (origin.monitor) {
				this.originUpdateCount = origin.updateCount.get();
				this.replace(origin, false);
			}
		} else {
			this.originUpdateCount = -1;
		}
		this.defaultRegion = null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Region createRegion(String regionName) throws BundleException {
		return createRegion(regionName, true);
	}

	private Region createRegion(String regionName, boolean notify) throws BundleException {
		Region region = new BundleIdBasedRegion(regionName, this, this, this.bundleContext, this.threadLocal);
		synchronized (this.monitor) {
			if (getRegion(regionName) != null) {
				throw new BundleException("Region '" + regionName + "' already exists", BundleException.UNSUPPORTED_OPERATION); //$NON-NLS-1$ //$NON-NLS-2$
			}
			this.regions.put(region.getName(), region);
			this.edges.put(region, EMPTY_EDGE_SET);
			incrementUpdateCount();
		}
		if (notify) {
			notifyAdded(region);
		}
		return region;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void connect(Region tailRegion, RegionFilter filter, Region headRegion) throws BundleException {
		createConnection(tailRegion, filter, headRegion, false);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public RegionFilter replaceConnection(Region tailRegion, RegionFilter filter, Region headRegion) throws BundleException {
		return createConnection(tailRegion, filter, headRegion, true);
	}

	private RegionFilter createConnection(Region tailRegion, RegionFilter filter, Region headRegion, boolean replace) throws BundleException {
		if (tailRegion == null)
			throw new IllegalArgumentException("The tailRegion must not be null."); //$NON-NLS-1$
		if (!replace && filter == null)
			throw new IllegalArgumentException("The filter must not be null."); //$NON-NLS-1$
		if (headRegion == null)
			throw new IllegalArgumentException("The headRegion must not be null."); //$NON-NLS-1$
		if (headRegion.equals(tailRegion)) {
			throw new BundleException("Cannot connect region '" + headRegion + "' to itself", BundleException.UNSUPPORTED_OPERATION); //$NON-NLS-1$ //$NON-NLS-2$
		}
		if (tailRegion.getRegionDigraph() != this)
			throw new IllegalArgumentException("The tailRegion does not belong to this digraph."); //$NON-NLS-1$
		if (headRegion.getRegionDigraph() != this)
			throw new IllegalArgumentException("The headRegion does not belong to this digraph."); //$NON-NLS-1$

		FilteredRegion existing = null;
		boolean tailAdded = false;
		boolean headAdded = false;
		synchronized (this.monitor) {
			Set<FilteredRegion> connections = this.edges.get(tailRegion);
			if (connections == null) {
				connections = new HashSet<>();
			} else {
				connections = new HashSet<>(connections);
				for (FilteredRegion edge : connections) {
					if (headRegion.equals(edge.getRegion())) {
						if (replace) {
							existing = edge;
						} else {
							throw new BundleException("Region '" + tailRegion + "' is already connected to region '" + headRegion, BundleException.UNSUPPORTED_OPERATION); //$NON-NLS-1$ //$NON-NLS-2$
						}
					}
				}
			}

			checkFilterDoesNotAllowExistingBundle(tailRegion, filter);

			tailAdded = this.regions.put(tailRegion.getName(), tailRegion) == null;
			headAdded = this.regions.put(headRegion.getName(), headRegion) == null;

			if (existing != null) {
				connections.remove(existing);
			}

			if (filter != null) {
				connections.add(new StandardFilteredRegion(headRegion, filter));
			}

			this.edges.put(tailRegion, Collections.unmodifiableSet(connections));
			incrementUpdateCount();
		}
		if (tailAdded) {
			notifyAdded(tailRegion);
		}
		if (headAdded) {
			notifyAdded(headRegion);
		}
		return existing == null ? null : existing.getFilter();
	}

	private void checkFilterDoesNotAllowExistingBundle(Region tailRegion, RegionFilter filter) {
		// TODO: enumerate the bundles in the region and check the filter does not allow any of them
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Iterator<Region> iterator() {
		synchronized (this.monitor) {
			Set<Region> snapshot = new HashSet<>(this.regions.size());
			snapshot.addAll(this.regions.values());
			return snapshot.iterator();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Set<FilteredRegion> getEdges(Region tailRegion) {
		synchronized (this.monitor) {
			// Cope with the case where tailRegion is not in the digraph
			Set<FilteredRegion> edgeSet = this.edges.get(tailRegion);
			return edgeSet == null ? EMPTY_EDGE_SET : edgeSet;
		}
	}

	static class StandardFilteredRegion implements FilteredRegion {

		private Region region;

		private RegionFilter regionFilter;

		StandardFilteredRegion(Region region, RegionFilter regionFilter) {
			this.region = region;
			this.regionFilter = regionFilter;
		}

		@Override
		public Region getRegion() {
			return this.region;
		}

		@Override
		public RegionFilter getFilter() {
			return this.regionFilter;
		}

	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Region getRegion(String regionName) {
		synchronized (this.monitor) {
			return regions.get(regionName);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Region getRegion(Bundle bundle) {
		return getRegion(bundle.getBundleId());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Region getRegion(long bundleId) {
		synchronized (this.monitor) {
			return this.bundleIdToRegionMapping.getRegion(bundleId);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void removeRegion(Region region) {
		if (region == null)
			throw new IllegalArgumentException("The region cannot be null."); //$NON-NLS-1$
		notifyRemoving(region);
		synchronized (this.monitor) {
			if (this.defaultRegion != null && this.defaultRegion.equals(region)) {
				this.defaultRegion = null;
			}
			this.regions.remove(region.getName());
			this.edges.remove(region);
			for (Entry<Region, Set<FilteredRegion>> entry : edges.entrySet()) {
				Region r = entry.getKey();
				Set<FilteredRegion> edgeSet = entry.getValue();
				for (FilteredRegion edge : edgeSet) {
					if (region.equals(edge.getRegion())) {
						Set<FilteredRegion> mutableEdgeSet = new HashSet<>(edgeSet);
						mutableEdgeSet.remove(edge);
						this.edges.put(r, Collections.unmodifiableSet(mutableEdgeSet));
						break;
					}
				}
			}
			this.bundleIdToRegionMapping.dissociateRegion(region);
			incrementUpdateCount();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		synchronized (this.monitor) {
			StringBuilder s = new StringBuilder();
			boolean first = true;
			s.append("RegionDigraph{"); //$NON-NLS-1$
			for (Region r : this) {
				if (!first) {
					s.append(", "); //$NON-NLS-1$
				}
				s.append(r);
				first = false;
			}
			s.append("}"); //$NON-NLS-1$

			s.append("["); //$NON-NLS-1$
			first = true;
			for (Region r : this) {
				Set<FilteredRegion> edgeSet = this.edges.get(r);
				if (edgeSet != null) {
					for (FilteredRegion filteredRegion : edgeSet) {
						if (!first) {
							s.append(", "); //$NON-NLS-1$
						}
						s.append(r + "->" + filteredRegion.getRegion()); //$NON-NLS-1$
						first = false;
					}
				}
			}
			s.append("]"); //$NON-NLS-1$
			return s.toString();
		}
	}

	@Override
	public Set<Region> getRegions() {
		Set<Region> result = new HashSet<>();
		synchronized (this.monitor) {
			result.addAll(this.regions.values());
		}
		return result;
	}

	@Override
	public RegionFilterBuilder createRegionFilterBuilder() {
		return new StandardRegionFilterBuilder();
	}

	private void notifyAdded(Region region) {
		Set<RegionLifecycleListener> listeners = getListeners();
		for (RegionLifecycleListener listener : listeners) {
			listener.regionAdded(region);
		}
	}

	private void notifyRemoving(Region region) {
		Set<RegionLifecycleListener> listeners = getListeners();
		for (RegionLifecycleListener listener : listeners) {
			listener.regionRemoving(region);
		}
	}

	private Set<RegionLifecycleListener> getListeners() {
		if (this.bundleContext == null)
			return Collections.emptySet();
		Set<RegionLifecycleListener> listeners = new HashSet<>();
		try {
			Collection<ServiceReference<RegionLifecycleListener>> listenerServiceReferences = this.bundleContext.getServiceReferences(RegionLifecycleListener.class, null);
			for (ServiceReference<RegionLifecycleListener> listenerServiceReference : listenerServiceReferences) {
				RegionLifecycleListener regionLifecycleListener = this.bundleContext.getService(listenerServiceReference);
				if (regionLifecycleListener != null) {
					listeners.add(regionLifecycleListener);
				}
			}
		} catch (InvalidSyntaxException e) {
			e.printStackTrace();
		}
		return listeners;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void visitSubgraph(Region startingRegion, RegionDigraphVisitor visitor) {
		this.subgraphTraverser.visitSubgraph(startingRegion, visitor);
	}

	/**
	 * Returns a snapshot of filtered regions
	 * 
	 * @return a snapshot of filtered regions
	 */
	Map<Region, Set<FilteredRegion>> getFilteredRegions() {
		synchronized (this.monitor) {
			return new HashMap<>(this.edges);
		}
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public RegionDigraphPersistence getRegionDigraphPersistence() {
		return new StandardRegionDigraphPersistence();
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public RegionDigraph copy() throws BundleException {
		return new StandardRegionDigraph(this);
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public void replace(RegionDigraph digraph) throws BundleException {
		replace(digraph, true);
	}

	private void replace(RegionDigraph digraph, boolean check) throws BundleException {
		if (!(digraph instanceof StandardRegionDigraph))
			throw new IllegalArgumentException("Only digraphs of type '" + StandardRegionDigraph.class.getName() + "' are allowed: " + digraph.getClass().getName()); //$NON-NLS-1$ //$NON-NLS-2$
		StandardRegionDigraph replacement = (StandardRegionDigraph) digraph;
		if (check && replacement.origin != this)
			throw new IllegalArgumentException("The replacement digraph is not a copy of this digraph."); //$NON-NLS-1$

		// notify removing first, and outside the monitor lock
		final Set<Region> removed = getRegions();
		removed.removeAll(replacement.getRegions());
		for (Region region : removed) {
			notifyRemoving(region);
		}

		Map<Region, Set<FilteredRegion>> filteredRegions = replacement.getFilteredRegions();
		final Set<Region> added = new HashSet<>();
		synchronized (this.monitor) {
			if (check && this.updateCount.get() != replacement.originUpdateCount) {
				throw new BundleException("The origin update count has changed since the replacement copy was created.", BundleException.INVALID_OPERATION); //$NON-NLS-1$
			}
			Map<String, Region> nameToRegion = new HashMap<>(regions);
			this.regions.clear();
			this.edges.clear();
			this.bundleIdToRegionMapping.clear();
			for (Region original : filteredRegions.keySet()) {
				Region copy = nameToRegion.get(original.getName());
				if (copy != null) {
					// reuse the previous region object
					regions.put(copy.getName(), copy);
					edges.put(copy, EMPTY_EDGE_SET);
				} else {
					// create a new one
					copy = this.createRegion(original.getName(), false);
					// collect added for notifying later ouside the lock
					added.add(copy);
				}
				for (Long id : original.getBundleIds()) {
					copy.addBundle(id);
				}
			}
			for (Map.Entry<Region, Set<FilteredRegion>> connection : filteredRegions.entrySet()) {
				Region tailRegion = this.getRegion(connection.getKey().getName());
				for (FilteredRegion headFilter : connection.getValue()) {
					Region headRegion = this.getRegion(headFilter.getRegion().getName());
					this.connect(tailRegion, headFilter.getFilter(), headRegion);
				}
			}
			incrementUpdateCount();
			if (check) {
				replacement.originUpdateCount = this.updateCount.get();
			}
		}
		// Now notify of additions outside the lock
		for (Region region : added) {
			notifyAdded(region);
		}
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public ResolverHookFactory getResolverHookFactory() {
		return resolverHookFactory;
	}

	CollisionHook getBundleCollisionHook() {
		return bundleCollisionHook;
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public EventHook getBundleEventHook() {
		return bundleEventHook;
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public FindHook getBundleFindHook() {
		return bundleFindHook;
	}

	/** 
	 * {@inheritDoc}
	 */
	@SuppressWarnings("deprecation")
	@Override
	public org.osgi.framework.hooks.service.EventHook getServiceEventHook() {
		return serviceEventHook;
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public org.osgi.framework.hooks.service.FindHook getServiceFindHook() {
		return serviceFindHook;
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public void setDefaultRegion(Region defaultRegion) {
		synchronized (this.monitor) {
			if (defaultRegion != null) {
				checkRegionExists(defaultRegion);
			}
			this.defaultRegion = defaultRegion;
		}
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public Region getDefaultRegion() {
		return this.defaultRegion;
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public void associateBundleWithRegion(long bundleId, Region region) throws BundleException {
		synchronized (this.monitor) {
			checkRegionExists(region);
			this.bundleIdToRegionMapping.associateBundleWithRegion(bundleId, region);
			incrementUpdateCount();
		}
	}

	private void checkRegionExists(Region region) {
		if (this.regions.get(region.getName()) == null) {
			throw new IllegalStateException("Operation not allowed on region " + region.getName() + " which is not part of a digraph"); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private void incrementUpdateCount() {
		synchronized (this.monitor) {
			this.updateCount.incrementAndGet();
		}

	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public void dissociateBundleFromRegion(long bundleId, Region region) {
		synchronized (this.monitor) {
			checkRegionExists(region);
			this.bundleIdToRegionMapping.dissociateBundleFromRegion(bundleId, region);
			incrementUpdateCount();
		}
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public boolean isBundleAssociatedWithRegion(long bundleId, Region region) {
		synchronized (this.monitor) {
			return this.bundleIdToRegionMapping.isBundleAssociatedWithRegion(bundleId, region);
		}
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public Set<Long> getBundleIds(Region region) {
		synchronized (this.monitor) {
			return this.bundleIdToRegionMapping.getBundleIds(region);
		}
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public void clear() {
		synchronized (this.monitor) {
			this.bundleIdToRegionMapping.clear();
			incrementUpdateCount();
		}
	}

	/** 
	 * {@inheritDoc}
	 */
	@Override
	public void dissociateRegion(Region region) {
		synchronized (this.monitor) {
			this.bundleIdToRegionMapping.dissociateRegion(region);
			incrementUpdateCount();
		}
	}

}
