/*******************************************************************************
 * Copyright (c) 2000, 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.team.internal.core.subscribers;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.team.core.subscribers.ISubscriberChangeEvent;
import org.eclipse.team.core.subscribers.ISubscriberChangeListener;
import org.eclipse.team.core.subscribers.Subscriber;

/**
 * This class acts as a superclass for any class that is collecting subscriber
 * resources. It provides functionality that listens to resource deltas and
 * subscriber change events in order to determine when the state of resources
 * that are supervised by a subscriber may have changed.
 */
public abstract class SubscriberResourceCollector implements IResourceChangeListener, ISubscriberChangeListener {

    Subscriber subscriber;

    /**
     * Create the collector and register it as a listener with the workspace
     * and the subscriber.
     * @param subscriber the subscriber to be associated with this collector
     */
    public SubscriberResourceCollector(Subscriber subscriber) {
        Assert.isNotNull(subscriber);
        this.subscriber = subscriber;
		ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
		subscriber.addListener(this);
    }

	/**
	 * Returns the <code>Subscriber</code> associated with this collector.
	 *
	 * @return the <code>Subscriber</code> associated with this collector.
	 */
	public Subscriber getSubscriber() {
		return subscriber;
	}

    /**
     * De-register the listeners for this collector.
     */
	public void dispose() {
		getSubscriber().removeListener(this);
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
	}

	@Override
	public void subscriberResourceChanged(ISubscriberChangeEvent[] deltas) {
	    try {
		    beginInput();
			IResource[] roots = getRoots();
			for (int i = 0; i < deltas.length; i++) {
				switch (deltas[i].getFlags()) {
					case ISubscriberChangeEvent.SYNC_CHANGED :
						if (isAllRootsIncluded() || isDescendantOfRoot(deltas[i].getResource(), roots)) {
						    change(deltas[i].getResource(), IResource.DEPTH_ZERO);
						}
						break;
					case ISubscriberChangeEvent.ROOT_REMOVED :
						remove(deltas[i].getResource());
						break;
					case ISubscriberChangeEvent.ROOT_ADDED :
						if (isAllRootsIncluded() || isDescendantOfRoot(deltas[i].getResource(), roots)) {
							change(deltas[i].getResource(), IResource.DEPTH_INFINITE);
						}
						break;
				}
			}
	    } finally {
	        endInput();
	    }
	}

    /**
     * This method is invoked at the beginning of a subscriber change event
     * or resource delta event. The endInput method will be invoked at some point
     * following this. There may be several invocations of remove or change
     * in between.
     */
    protected void beginInput() {
        // Do nothing by default
    }

    /**
     * The processing of the resource or subscriber delta has finished.
     * Subclasses can accumulate removals and changes and handle them
     * at this point to allow batched change events.
     */
    protected void endInput() {
        // Do nothing by default
    }


	@Override
	public void resourceChanged(IResourceChangeEvent event) {
	    try {
	        beginInput();
			processDelta(event.getDelta(), getRoots());
	    } finally {
	        endInput();
	    }
	}

    /**
	 * Process the resource delta and posts all necessary events to the background
	 * event handler.
	 *
	 * @param delta the resource delta to analyze
	 */
	protected void processDelta(IResourceDelta delta, IResource[] roots) {
		IResource resource = delta.getResource();
		int kind = delta.getKind();

		if (resource.getType() == IResource.PROJECT) {
			// Handle projects that should be removed from the collector
			if (((kind & IResourceDelta.REMOVED) != 0) /* deleted project */
			        || (delta.getFlags() & IResourceDelta.OPEN) != 0 && !((IProject) resource).isOpen() /* closed project */
			        || !isAncestorOfRoot(resource, roots)) /* not within subscriber roots */ {
				// If the project has any entries in the sync set, remove them
				if (hasMembers(resource)) {
					remove(resource);
				}
			}
		}

		boolean visitChildren = false;
		if (isDescendantOfRoot(resource, roots)) {
			visitChildren = true;
			// If the resource has changed type, remove the old resource handle
			// and add the new one
			if ((delta.getFlags() & IResourceDelta.TYPE) != 0) {
				remove(resource);
				change(resource, IResource.DEPTH_INFINITE);
			}

			// Check the flags for changes the SyncSet cares about.
			// Notice we don't care about MARKERS currently.
			int changeFlags = delta.getFlags();
			if ((changeFlags & (IResourceDelta.OPEN | IResourceDelta.CONTENT)) != 0) {
				change(resource, IResource.DEPTH_ZERO);
			}

			// Check the kind and deal with those we care about
			if ((delta.getKind() & (IResourceDelta.REMOVED | IResourceDelta.ADDED)) != 0) {
				change(resource, IResource.DEPTH_ZERO);
			}
		}

		// Handle changed children
		if (visitChildren || isAncestorOfRoot(resource, roots)) {
			IResourceDelta[] affectedChildren = delta.getAffectedChildren(IResourceDelta.CHANGED | IResourceDelta.REMOVED | IResourceDelta.ADDED);
			for (int i = 0; i < affectedChildren.length; i++) {
				processDelta(affectedChildren[i], roots);
			}
		}
	}

	/**
	 * Return the root resources that are to be considered by this handler.
	 * These may be either the subscriber roots or a set of resources that are
	 * contained by the subscriber's roots.
     * @return the root resources that are to be considered by this handler
     */
	protected IResource[] getRoots() {
		return getSubscriber().roots();
	}

	/**
	 * Return whether the given resource, which is not
	 * within the roots of this handler, has children
	 * that are.
     * @param resource the resource
     * @return whether the resource has children that are being considered
     * by this handler.
     */
    protected abstract boolean hasMembers(IResource resource);

    /**
     * The resource is no longer of concern to the subscriber.
     * Remove the resource and any of it's descendants
     * from the set of resources being collected.
     * @param resource the resource to be removed along with its
     * descendants.
     */
    protected abstract void remove(IResource resource);

    /**
     * The resource sync state has changed to the depth specified.
     * @param resource the resource
     * @param depth the depth
     */
    protected abstract void change(IResource resource, int depth);

	/**
	 * Return whether all roots of a subscriber are included or
	 * if the collector is only consider a subset of the resources.
     * @return whether all roots of a subscriber are included
     */
	protected boolean isAllRootsIncluded() {
		return true;
	}

    private boolean isAncestorOfRoot(IResource parent, IResource[] roots) {
		// Always traverse into projects in case a root was removed
		if (parent.getType() == IResource.ROOT) return true;
		for (int i = 0; i < roots.length; i++) {
			IResource resource = roots[i];
			if (parent.getFullPath().isPrefixOf(resource.getFullPath())) {
				return true;
			}
		}
		return false;
	}

	private boolean isDescendantOfRoot(IResource resource, IResource[] roots) {
		for (int i = 0; i < roots.length; i++) {
			IResource root = roots[i];
			if (root.getFullPath().isPrefixOf(resource.getFullPath())) {
				return true;
			}
		}
		return false;
	}
}
