/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     Alexander Kurtakov <akurtako@redhat.com> - Bug 459343
 *******************************************************************************/
package org.eclipse.core.tests.resources;

import java.util.*;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.junit.Assert;

/**
 * Verifies the state of an <code>IResourceDelta</code> by comparing
 * it with a client's expectations.  The delta is considered valid
 * if it contains exactly the set of changes expected by the client,
 * and parents of those changes.
 *
 * <p>Example usage:
 * <code>
 * ResourceDeltaVerifier verifier = new ResourceDeltaComparer();
 * IResourceChangeListener listener = (IResourceChangeListener)verifier;
 * IWorkspace workspace = ResourcesPlugin.getWorkspace();
 * IProject proj = workspace.getRoot().getProject("MyProject");
 * // Assume the project is accessible
 * workspace.addResourceChangeListener(listener);
 * verifier.addExpectedChange(proj, REMOVED, 0);
 * try {
 * 		proj.delete(true, true, null);
 * } catch(CoreException e){
 *     fail("1.0", e);
 * }
 * assert("2.0 "+verifier.getMessage(), verifier.isDeltaValid());
 * </code>
 */
public class ResourceDeltaVerifier extends Assert implements IResourceChangeListener {
	private class ExpectedChange {
		IResource fResource;
		IPath movedFromPath;
		IPath movedToPath;
		int fKind;
		int fChangeFlags;

		public ExpectedChange(IResource resource, int kind, int changeFlags, IPath movedFromPath, IPath movedToPath) {
			fResource = resource;
			fKind = kind;
			fChangeFlags = changeFlags;
			this.movedFromPath = movedFromPath;
			this.movedToPath = movedToPath;
		}

		public int getChangeFlags() {
			return fChangeFlags;
		}

		public IPath getMovedFromPath() {
			if ((fChangeFlags & IResourceDelta.MOVED_FROM) != 0) {
				return movedFromPath;
			}
			return null;
		}

		public IPath getMovedToPath() {
			if ((fChangeFlags & IResourceDelta.MOVED_TO) != 0) {
				return movedToPath;
			}
			return null;
		}

		public int getKind() {
			return fKind;
		}

		@Override
		public String toString() {
			StringBuilder buf = new StringBuilder("ExpectedChange(");
			buf.append(fResource);
			buf.append(", ");
			buf.append(convertKind(fKind));
			buf.append(", ");
			buf.append(convertChangeFlags(fChangeFlags));
			buf.append(")");
			return buf.toString();
		}

	}

	/**
	 * Table of IPath -&gt; ExpectedChange
	 */
	private Hashtable<IPath, ExpectedChange> fExpectedChanges = new Hashtable<>();
	boolean fIsDeltaValid = true;
	private StringBuilder fMessage = new StringBuilder();
	/**
	 * The verifier can be in one of three states.  In the initial
	 * state, the verifier is still receiving inputs via the
	 * addExpectedChange() methods, and the state is RECEIVING_INPUTS.
	 * After a call to verifyDelta(), the state becomes DELTA_VERIFIED
	 * The verifier remains in the second state for any number of delta
	 * verifications.  When a getMessage() or isDeltaValid() method is
	 * called, the verification completes, and the state becomes
	 * VERIFICATION_COMPLETE.  While in this state, any number of
	 * getMessage() and isDeltaValid() methods can be called.
	 * While in the third state, any call to addExpectedChange()
	 * resets the verifier and puts it back in its RECEIVING_INPUTS state.
	 */
	private static final int RECEIVING_INPUTS = 0;
	private static final int DELTA_VERIFIED = 1;
	private static final int VERIFICATION_COMPLETE = 2;

	private int fState = RECEIVING_INPUTS;

	/**
	 * @see #addExpectedChange
	 */
	public void addExpectedChange(IResource[] resources, int status, int changeFlags) {
		for (IResource resource : resources) {
			addExpectedChange(resource, null, status, changeFlags, null, null);
		}
	}

	/**
	 * Adds an expected deletion for the given resource and all children.
	 */
	public void addExpectedDeletion(IResource resource) {
		addExpectedChange(resource, IResourceDelta.REMOVED, 0);
		if (resource instanceof IContainer) {
			try {
				IResource[] children = ((IContainer) resource).members(IContainer.INCLUDE_PHANTOMS | IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
				for (IResource element : children) {
					addExpectedDeletion(element);
				}
			} catch (CoreException e) {
				e.printStackTrace();
				fail("Failed to get children in addExpectedDeletion");
			}
		}
	}

	/**
	 * Signals to the comparer that the given resource is expected to
	 * change in the specified way.  The change flags should be set to
	 * zero if no change is expected.
	 * @param resource the resource that is expected to change
	 * @param status the type of change (ADDED, REMOVED, CHANGED)
	 * @param changeFlags the type of change (CONTENT, SYNC, etc)
	 * @see IResourceConstants
	 */
	public void addExpectedChange(IResource resource, int status, int changeFlags) {
		addExpectedChange(resource, null, status, changeFlags, null, null);
	}

	/**
	 * Signals to the comparer that the given resource is expected to
	 * change in the specified way.  The change flags should be set to
	 * zero if no change is expected.
	 * @param resource the resource that is expected to change
	 * @param status the type of change (ADDED, REMOVED, CHANGED)
	 * @param changeFlags the type of change (CONTENT, SYNC, etc)
	 * @param movedPath or null
	 * @see IResourceConstants
	 */
	public void addExpectedChange(IResource resource, int status, int changeFlags, IPath movedFromPath, IPath movedToPath) {
		addExpectedChange(resource, null, status, changeFlags, movedFromPath, movedToPath);
	}

	/**
	 * Signals to the comparer that the given resource is expected to
	 * change in the specified way.  The change flags should be set to
	 * zero if no change is expected.
	 * @param resource the resource that is expected to change
	 * @param topLevelParent Do not added expected changes above this parent
	 * @param status the type of change (ADDED, REMOVED, CHANGED)
	 * @param changeFlags the type of change (CONTENT, SYNC, etc)
	 * @param movedPath or null
	 * @see IResourceConstants
	 */
	public void addExpectedChange(IResource resource, IResource topLevelParent, int status, int changeFlags) {
		addExpectedChange(resource, topLevelParent, status, changeFlags, null, null);
	}

	/**
	 * Signals to the comparer that the given resource is expected to
	 * change in the specified way.  The change flags should be set to
	 * zero if no change is expected.
	 * @param resource the resource that is expected to change
	 * @param topLevelParent Do not added expected changes above this parent
	 * @param status the type of change (ADDED, REMOVED, CHANGED)
	 * @param changeFlags the type of change (CONTENT, SYNC, etc)
	 * @param movedPath or null
	 * @see IResourceConstants
	 */
	public void addExpectedChange(IResource resource, IResource topLevelParent, int status, int changeFlags, IPath movedFromPath, IPath movedToPath) {
		resetIfNecessary();

		ExpectedChange expectedChange = new ExpectedChange(resource, status, changeFlags, movedFromPath, movedToPath);
		fExpectedChanges.put(resource.getFullPath(), expectedChange);

		// Add changes for all resources above this one and limited by the topLevelParent
		IResource parentResource = resource.getParent();
		IResource limit = (topLevelParent == null) ? null : topLevelParent.getParent();
		while (parentResource != null && !parentResource.equals(limit)) {
			//change table is keyed by resource path
			IPath key = parentResource.getFullPath();
			if (fExpectedChanges.get(key) == null) {
				ExpectedChange parentExpectedChange = new ExpectedChange(parentResource, IResourceDelta.CHANGED, 0, null, null);
				fExpectedChanges.put(key, parentExpectedChange);
			}
			parentResource = parentResource.getParent();
		}
	}

	private void checkChanges(IResourceDelta delta) {
		IResource resource = delta.getResource();

		ExpectedChange expectedChange = fExpectedChanges.remove(resource.getFullPath());

		int status = delta.getKind();
		int changeFlags = delta.getFlags();

		if (status == IResourceDelta.NO_CHANGE) {
			return;
		}

		if (expectedChange == null) {
			recordMissingExpectedChange(status, changeFlags);
		} else {
			int expectedStatus = expectedChange.getKind();
			int expectedChangeFlags = expectedChange.getChangeFlags();
			if (status != expectedStatus || changeFlags != expectedChangeFlags) {
				recordConflictingChange(expectedStatus, status, expectedChangeFlags, changeFlags);
			}
		}
	}

	private void checkChildren(IResourceDelta delta) {
		IResourceDelta[] affectedChildren = delta.getAffectedChildren(IResourceDelta.ALL_WITH_PHANTOMS, IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
		IResourceDelta[] addedChildren = delta.getAffectedChildren(IResourceDelta.ADDED, IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
		IResourceDelta[] changedChildren = delta.getAffectedChildren(IResourceDelta.CHANGED, IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
		IResourceDelta[] removedChildren = delta.getAffectedChildren(IResourceDelta.REMOVED, IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);

		Hashtable<IResource, IResourceDelta> h = new Hashtable<>(affectedChildren.length + 1);

		for (IResourceDelta childDelta1 : addedChildren) {
			IResource childResource = childDelta1.getResource();
			IResourceDelta childDelta2 = h.get(childResource);
			if (childDelta2 != null) {
				recordDuplicateChild(childResource.getFullPath(), childDelta2.getKind(), childDelta1.getKind(), IResourceDelta.ADDED);
			} else {
				h.put(childResource, childDelta1);
			}
			if (childDelta1.getKind() != IResourceDelta.ADDED) {
				recordIllegalChild(childResource.getFullPath(), IResourceDelta.ADDED, childDelta1.getKind());
			}
		}

		for (IResourceDelta childDelta1 : changedChildren) {
			IResource childResource = childDelta1.getResource();
			IResourceDelta childDelta2 = h.get(childResource);
			if (childDelta2 != null) {
				recordDuplicateChild(childResource.getFullPath(), childDelta2.getKind(), childDelta1.getKind(), IResourceDelta.CHANGED);
			} else {
				h.put(childResource, childDelta1);
			}
			if (childDelta1.getKind() != IResourceDelta.CHANGED) {
				recordIllegalChild(childResource.getFullPath(), IResourceDelta.CHANGED, childDelta1.getKind());
			}
		}

		for (IResourceDelta childDelta1 : removedChildren) {
			IResource childResource = childDelta1.getResource();
			IResourceDelta childDelta2 = h.get(childResource);
			if (childDelta2 != null) {
				recordDuplicateChild(childResource.getFullPath(), childDelta2.getKind(), childDelta1.getKind(), IResourceDelta.REMOVED);
			} else {
				h.put(childResource, childDelta1);
			}
			if (childDelta1.getKind() != IResourceDelta.REMOVED) {
				recordIllegalChild(childResource.getFullPath(), IResourceDelta.REMOVED, childDelta1.getKind());
			}
		}

		for (IResourceDelta childDelta1 : affectedChildren) {
			IResource childResource = childDelta1.getResource();
			IResourceDelta childDelta2 = h.remove(childResource);
			if (childDelta2 == null) {
				int kind = childDelta1.getKind();
				//these kinds should have been added to h earlier
				if (kind == IResourceDelta.ADDED || kind == IResourceDelta.REMOVED || kind == IResourceDelta.CHANGED) {
					recordMissingChild(childResource.getFullPath(), childDelta1.getKind(), false);
				}
			}
		}

		for (Map.Entry<IResource, IResourceDelta> entry : h.entrySet()) {
			IResource childResource = entry.getKey();
			IResourceDelta childDelta = entry.getValue();
			recordMissingChild(childResource.getFullPath(), childDelta.getKind(), true);
		}

		for (IResourceDelta element : affectedChildren) {
			internalVerifyDelta(element);
		}

		for (IResourceDelta childDelta : h.values()) {
			internalVerifyDelta(childDelta);
		}
	}

	private void checkPaths(IResourceDelta delta) {
		IResource resource = delta.getResource();

		IPath expectedFullPath = resource.getFullPath();
		IPath actualFullPath = delta.getFullPath();
		if (!expectedFullPath.equals(actualFullPath)) {
			recordConflictingFullPaths(expectedFullPath, actualFullPath);
		}

		IPath expectedProjectRelativePath = resource.getProjectRelativePath();
		IPath actualProjectRelativePath = delta.getProjectRelativePath();
		if (expectedProjectRelativePath != actualProjectRelativePath) {
			if (expectedProjectRelativePath == null || !expectedProjectRelativePath.equals(actualProjectRelativePath)) {
				recordConflictingProjectRelativePaths(expectedProjectRelativePath, actualProjectRelativePath);
			}
		}

		ExpectedChange expectedChange = fExpectedChanges.get(resource.getFullPath());

		if (expectedChange != null) {
			IPath expectedMovedFromPath = expectedChange.getMovedFromPath();
			IPath actualMovedFromPath = delta.getMovedFromPath();
			if (expectedMovedFromPath != actualMovedFromPath) {
				if (expectedMovedFromPath == null || !expectedMovedFromPath.equals(actualMovedFromPath)) {
					recordConflictingMovedFromPaths(expectedMovedFromPath, actualMovedFromPath);
				}
			}

			IPath expectedMovedToPath = expectedChange.getMovedToPath();
			IPath actualMovedToPath = delta.getMovedToPath();
			if (expectedMovedToPath != actualMovedToPath) {
				if (expectedMovedToPath == null || !expectedMovedToPath.equals(actualMovedToPath)) {
					recordConflictingMovedToPaths(expectedMovedToPath, actualMovedToPath);
				}
			}
		}
	}

	String convertChangeFlags(int changeFlags) {
		if (changeFlags == 0) {
			return "0";
		}
		StringBuilder buf = new StringBuilder();

		if ((changeFlags & IResourceDelta.CONTENT) != 0) {
			changeFlags ^= IResourceDelta.CONTENT;
			buf.append("CONTENT | ");
		}
		if ((changeFlags & IResourceDelta.MOVED_FROM) != 0) {
			changeFlags ^= IResourceDelta.MOVED_FROM;
			buf.append("MOVED_FROM | ");
		}
		if ((changeFlags & IResourceDelta.MOVED_TO) != 0) {
			changeFlags ^= IResourceDelta.MOVED_TO;
			buf.append("MOVED_TO | ");
		}
		if ((changeFlags & IResourceDelta.OPEN) != 0) {
			changeFlags ^= IResourceDelta.OPEN;
			buf.append("OPEN | ");
		}
		if ((changeFlags & IResourceDelta.TYPE) != 0) {
			changeFlags ^= IResourceDelta.TYPE;
			buf.append("TYPE | ");
		}
		if ((changeFlags & IResourceDelta.MARKERS) != 0) {
			changeFlags ^= IResourceDelta.MARKERS;
			buf.append("MARKERS | ");
		}
		if ((changeFlags & IResourceDelta.REPLACED) != 0) {
			changeFlags ^= IResourceDelta.REPLACED;
			buf.append("REPLACED | ");
		}
		if ((changeFlags & IResourceDelta.ENCODING) != 0) {
			changeFlags ^= IResourceDelta.ENCODING;
			buf.append("ENCODING | ");
		}
		if ((changeFlags & IResourceDelta.DERIVED_CHANGED) != 0) {
			changeFlags ^= IResourceDelta.DERIVED_CHANGED;
			buf.append("DERIVED_CHANGED | ");
		}
		if ((changeFlags & IResourceDelta.DESCRIPTION) != 0) {
			changeFlags ^= IResourceDelta.DESCRIPTION;
			buf.append("DESCRIPTION | ");
		}
		if ((changeFlags & IResourceDelta.SYNC) != 0) {
			changeFlags ^= IResourceDelta.SYNC;
			buf.append("SYNC | ");
		}

		if (changeFlags != 0) {
			buf.append(changeFlags);
			buf.append(" | ");
		}

		String result = buf.toString();

		if (result.length() != 0) {
			result = result.substring(0, result.length() - 3);
		}

		return result;
	}

	String convertKind(int kind) {
		switch (kind) {
			case IResourceDelta.ADDED :
				return "ADDED";
			case IResourceDelta.CHANGED :
				return "CHANGED";
			case IResourceDelta.REMOVED :
				return "REMOVED";
			case IResourceDelta.ADDED_PHANTOM :
				return "ADDED_PHANTOM";
			case IResourceDelta.REMOVED_PHANTOM :
				return "REMOVED_PHANTOM";
			default :
				return "Unknown(" + kind + ")";
		}
	}

	/**
	 * Called to cleanup internal state and make sure expectations
	 * are met after iterating over a resource delta.
	 */
	private void finishVerification() {
		HashSet<IPath> resourcePaths = new HashSet<>(fExpectedChanges.keySet());

		for (IPath resourcePath : resourcePaths) {
			fMessage.append("Checking expectations for ");
			fMessage.append(resourcePath);
			fMessage.append("\n");

			ExpectedChange expectedChange = fExpectedChanges.remove(resourcePath);
			if (expectedChange != null) {
				recordMissingActualChange(expectedChange.getKind(), expectedChange.getChangeFlags());
			}
		}
	}

	/**
	 * Returns a message that describes the result of the resource
	 * delta verification checks.
	 */
	public String getMessage() {
		if (fState == RECEIVING_INPUTS) {
			if (hasExpectedChanges()) {
				fail("Verifier has not yet been given a resource delta");
			} else {
				fState = DELTA_VERIFIED;
			}
		}
		if (fState == DELTA_VERIFIED) {
			finishVerification();
			fState = VERIFICATION_COMPLETE;
		}
		return fMessage.toString();
	}

	/**
	 * Returns true if this verifier has received a delta notification
	 * since the last reset, and false otherwise.
	 */
	public boolean hasBeenNotified() {
		return fState == DELTA_VERIFIED;
	}

	/**
	 * Returns true if this verifier currently has an expected
	 * changes, and false otherwise.
	 */
	public boolean hasExpectedChanges() {
		return !fExpectedChanges.isEmpty();
	}

	/**
	 * Compares the given delta with the expected changes.  Recursively
	 * compares child deltas.
	 */
	void internalVerifyDelta(IResourceDelta delta) {
		try {
			// FIXME: bogus
			if (delta == null) {
				return;
			}
			fMessage.append("Verifying delta for ");
			fMessage.append(delta.getFullPath());
			fMessage.append("\n");

			/* don't check changes for the workspace */
			if (delta.getResource() != null) {
				checkPaths(delta);
				checkChanges(delta);
			}
			checkChildren(delta);
		} catch (Exception e) {
			e.printStackTrace();
			fMessage.append("Exception during event notification:" + e.getMessage());
			fIsDeltaValid = false;
		}
	}

	/**
	 * Returns whether the resource delta passed all verification
	 * checks.
	 */
	public boolean isDeltaValid() {
		if (fState == RECEIVING_INPUTS) {
			if (hasExpectedChanges()) {
				fail("Verifier has not yet been given a resource delta");
			} else {
				fState = DELTA_VERIFIED;
			}
		}
		if (fState == DELTA_VERIFIED) {
			finishVerification();
			fState = VERIFICATION_COMPLETE;
		}
		return fIsDeltaValid;
	}

	/**
	 * Tests message formatting.  This main method does not represent the
	 * intended use of the ResourceDeltaVerifier.  See the class comment
	 * for instructions on using the verifier.
	 */
	public static void main(String[] args) {
		ResourceDeltaVerifier comparer = new ResourceDeltaVerifier();

		int status = IResourceDelta.CHANGED;
		int changeFlags = IResourceDelta.CONTENT;
		int expectedStatus = IResourceDelta.CHANGED;
		int actualStatus = IResourceDelta.REMOVED;
		int expectedChangeFlags = IResourceDelta.OPEN;
		int actualChangeFlags = 0;
		int formerChildStatus = expectedStatus;
		int latterChildStatus = actualStatus;

		IPath path = new Path("/a/b/c");
		IPath path2 = new Path("/a/b/d");
		IPath expectedFullPath = path;
		IPath actualFullPath = path2;
		IPath expectedMovedFromPath = path;
		IPath actualMovedFromPath = path2;
		IPath expectedMovedToPath = path;
		IPath actualMovedToPath = path2;
		IPath expectedProjectRelativePath = new Path("b/c");
		IPath actualProjectRelativePath = new Path("b/d");

		comparer.fMessage.append("Checking delta for ");
		comparer.fMessage.append(path);
		comparer.fMessage.append("\n");

		comparer.recordConflictingChange(expectedStatus, actualStatus, expectedChangeFlags, actualChangeFlags);
		comparer.recordConflictingFullPaths(expectedFullPath, actualFullPath);
		comparer.recordConflictingMovedFromPaths(expectedMovedFromPath, actualMovedFromPath);
		comparer.recordConflictingMovedToPaths(expectedMovedToPath, actualMovedToPath);
		comparer.recordConflictingProjectRelativePaths(expectedProjectRelativePath, actualProjectRelativePath);
		comparer.recordDuplicateChild(path, formerChildStatus, latterChildStatus, expectedStatus);
		comparer.recordIllegalChild(path, expectedStatus, actualStatus);
		comparer.recordMissingActualChange(status, changeFlags);
		comparer.recordMissingChild(path, status, true);
		comparer.recordMissingChild(path, status, false);
		comparer.recordMissingExpectedChange(status, changeFlags);

		System.out.print(comparer.fMessage.toString());
	}

	private void recordConflictingChange(int expectedKind, int kind, int expectedChangeFlags, int changeFlags) {
		fIsDeltaValid = false;

		fMessage.append("\tConflicting change\n");

		if (expectedKind != kind) {
			fMessage.append("\t\tExpected kind: <");
			fMessage.append(convertKind(expectedKind));
			fMessage.append("> actual kind: <");
			fMessage.append(convertKind(kind));
			fMessage.append(">\n");
		}

		if (expectedChangeFlags != changeFlags) {
			fMessage.append("\t\tExpected change flags: <");
			fMessage.append(convertChangeFlags(expectedChangeFlags));
			fMessage.append("> actual change flags: <");
			fMessage.append(convertChangeFlags(changeFlags));
			fMessage.append(">\n");
		}
	}

	private void recordConflictingFullPaths(IPath expectedFullPath, IPath actualFullPath) {
		fIsDeltaValid = false;

		fMessage.append("\tConflicting full paths\n");

		fMessage.append("\t\tExpected full path: ");
		fMessage.append(expectedFullPath);
		fMessage.append("\n");

		fMessage.append("\t\tActual full path: ");
		fMessage.append(actualFullPath);
		fMessage.append("\n");
	}

	private void recordConflictingMovedFromPaths(IPath expectedMovedFromPath, IPath actualMovedFromPath) {
		fIsDeltaValid = false;

		fMessage.append("\tConflicting moved from paths\n");

		fMessage.append("\t\tExpected moved from path: ");
		fMessage.append(expectedMovedFromPath);
		fMessage.append("\n");

		fMessage.append("\t\tActual moved from path: ");
		fMessage.append(actualMovedFromPath);
		fMessage.append("\n");
	}

	private void recordConflictingMovedToPaths(IPath expectedMovedToPath, IPath actualMovedToPath) {
		fIsDeltaValid = false;

		fMessage.append("\tConflicting moved to paths\n");

		fMessage.append("\t\tExpected moved to path: ");
		fMessage.append(expectedMovedToPath);
		fMessage.append("\n");

		fMessage.append("\t\tActual moved to path: ");
		fMessage.append(actualMovedToPath);
		fMessage.append("\n");
	}

	private void recordConflictingProjectRelativePaths(IPath expectedProjectRelativePath, IPath actualProjectRelativePath) {
		fIsDeltaValid = false;

		fMessage.append("\tConflicting project relative paths\n");

		fMessage.append("\t\tExpected project relative path: ");
		fMessage.append(expectedProjectRelativePath);
		fMessage.append("\n");

		fMessage.append("\t\tActual project relative path: ");
		fMessage.append(actualProjectRelativePath);
		fMessage.append("\n");
	}

	private void recordDuplicateChild(IPath path, int formerChildKind, int latterChildKind, int expectedKind) {
		fIsDeltaValid = false;

		fMessage.append("\tDuplicate child: ");
		fMessage.append(path);
		fMessage.append("\n");

		fMessage.append("\t\tProduced by IResourceDelta.get");

		switch (expectedKind) {
			case IResourceDelta.ADDED :
				fMessage.append("Added");
				break;
			case IResourceDelta.CHANGED :
				fMessage.append("Changed");
				break;
			case IResourceDelta.REMOVED :
				fMessage.append("Removed");
				break;
		}

		fMessage.append("Children()\n");

		fMessage.append("\t\tFormer child's status: ");
		fMessage.append(convertKind(formerChildKind));
		fMessage.append("\n");

		fMessage.append("\t\tLatter child's status: ");
		fMessage.append(convertKind(latterChildKind));
		fMessage.append("\n");
	}

	private void recordIllegalChild(IPath path, int expectedKind, int actualKind) {
		fIsDeltaValid = false;

		fMessage.append("\tIllegal child: ");
		fMessage.append(path);
		fMessage.append("\n");

		fMessage.append("\t\tProduced by IResourceDelta.get");

		switch (expectedKind) {
			case IResourceDelta.ADDED :
				fMessage.append("Added");
				break;
			case IResourceDelta.CHANGED :
				fMessage.append("Changed");
				break;
			case IResourceDelta.REMOVED :
				fMessage.append("Removed");
				break;
		}

		fMessage.append("Children()\n");

		fMessage.append("\t\tIlleagal child's status: ");
		fMessage.append(convertKind(actualKind));
		fMessage.append("\n");
	}

	private void recordMissingActualChange(int kind, int changeFlags) {
		fIsDeltaValid = false;

		fMessage.append("\tMissing actual change\n");
		fMessage.append("\t\tExpected kind: <");
		fMessage.append(convertKind(kind));
		fMessage.append(">\n");
		fMessage.append("\t\tExpected change flags: <");
		fMessage.append(convertChangeFlags(changeFlags));
		fMessage.append(">\n");
	}

	private void recordMissingChild(IPath path, int kind, boolean isMissingFromAffectedChildren) {
		fIsDeltaValid = false;

		fMessage.append("\tMissing child: ");
		fMessage.append(path);
		fMessage.append("\n");

		fMessage.append("\t\tfrom IResourceDelta.getAffectedChildren(");

		if (!isMissingFromAffectedChildren) {
			switch (kind) {
				case IResourceDelta.ADDED :
					fMessage.append("ADDED");
					break;
				case IResourceDelta.CHANGED :
					fMessage.append("CHANGED");
					break;
				case IResourceDelta.REMOVED :
					fMessage.append("REMOVED");
					break;
				default :
					fMessage.append(kind);
			}
		}

		fMessage.append(")\n");
	}

	private void recordMissingExpectedChange(int kind, int changeFlags) {
		fIsDeltaValid = false;

		fMessage.append("\tMissing expected change\n");
		fMessage.append("\t\tActual kind: <");
		fMessage.append(convertKind(kind));
		fMessage.append(">\n");
		fMessage.append("\t\tActual change flags: <");
		fMessage.append(convertChangeFlags(changeFlags));
		fMessage.append(">\n");
	}

	/**
	 * Resets the listener to its initial state.
	 */
	public void reset() {
		fExpectedChanges.clear();
		fIsDeltaValid = true;
		fMessage.setLength(0);
		fState = RECEIVING_INPUTS;
	}

	private void resetIfNecessary() {
		if (fState == DELTA_VERIFIED) {
			reset();
		}
	}

	/**
	 * Part of the <code>IResourceChangedListener</code> interface.
	 * @see IResourceChangedListener
	 */
	@Override
	public void resourceChanged(IResourceChangeEvent e) {
		verifyDelta(e.getDelta());
	}

	/**
	 * Compares the given delta with the expected changes.  Recursively
	 * compares child deltas.
	 */
	public void verifyDelta(IResourceDelta delta) {
		internalVerifyDelta(delta);
		fState = DELTA_VERIFIED;
	}

	/**
	 * Appends additional information to the current validation error message.
	 *
	 * @param message
	 *            the additional information to append
	 */
	protected void appendToMessage(String message) {
		fMessage.append(message);
		fMessage.append(System.lineSeparator());
	}
}
