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

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;

import net.sourceforge.phpdt.core.IClasspathEntry;
import net.sourceforge.phpdt.core.IElementChangedListener;
import net.sourceforge.phpdt.core.IJavaModel;
import net.sourceforge.phpdt.core.IJavaProject;
import net.sourceforge.phpdt.core.JavaModelException;
import net.sourceforge.phpdt.internal.core.util.Util;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.Platform;

/**
 * Keep the global states used during Java element delta processing.
 */
public class DeltaProcessingState implements IResourceChangeListener {

	/*
	 * Collection of listeners for Java element deltas
	 */
	public IElementChangedListener[] elementChangedListeners = new IElementChangedListener[5];

	public int[] elementChangedListenerMasks = new int[5];

	public int elementChangedListenerCount = 0;

	/*
	 * Collection of pre Java resource change listeners
	 */
	public IResourceChangeListener[] preResourceChangeListeners = new IResourceChangeListener[1];

	public int preResourceChangeListenerCount = 0;

	/*
	 * The delta processor for the current thread.
	 */
	private ThreadLocal deltaProcessors = new ThreadLocal();

	/* A table from IPath (from a classpath entry) to RootInfo */
	public HashMap roots = new HashMap();

	/*
	 * A table from IPath (from a classpath entry) to ArrayList of RootInfo Used
	 * when an IPath corresponds to more than one root
	 */
	public HashMap otherRoots = new HashMap();

	/*
	 * A table from IPath (from a classpath entry) to RootInfo from the last
	 * time the delta processor was invoked.
	 */
	public HashMap oldRoots = new HashMap();

	/*
	 * A table from IPath (from a classpath entry) to ArrayList of RootInfo from
	 * the last time the delta processor was invoked. Used when an IPath
	 * corresponds to more than one root
	 */
	public HashMap oldOtherRoots = new HashMap();

	/*
	 * A table from IPath (a source attachment path from a classpath entry) to
	 * IPath (a root path)
	 */
	public HashMap sourceAttachments = new HashMap();

	/* Whether the roots tables should be recomputed */
	public boolean rootsAreStale = true;

	/* Threads that are currently running initializeRoots() */
	private Set initializingThreads = Collections
			.synchronizedSet(new HashSet());

	public Hashtable externalTimeStamps = new Hashtable();

	public HashMap projectUpdates = new HashMap();

	public static class ProjectUpdateInfo {
		JavaProject project;

		IClasspathEntry[] oldResolvedPath;

		IClasspathEntry[] newResolvedPath;

		IClasspathEntry[] newRawPath;

		/**
		 * Update projects references so that the build order is consistent with
		 * the classpath
		 */
		public void updateProjectReferencesIfNecessary()
				throws JavaModelException {

			String[] oldRequired = this.project
					.projectPrerequisites(this.oldResolvedPath);

			if (this.newResolvedPath == null) {
				this.newResolvedPath = this.project
						.getResolvedClasspath(this.newRawPath, null, true,
								true, null/* no reverse map */);
			}
			String[] newRequired = this.project
					.projectPrerequisites(this.newResolvedPath);
			try {
				IProject projectResource = this.project.getProject();
				IProjectDescription description = projectResource
						.getDescription();

				IProject[] projectReferences = description
						.getDynamicReferences();

				HashSet oldReferences = new HashSet(projectReferences.length);
				for (int i = 0; i < projectReferences.length; i++) {
					String projectName = projectReferences[i].getName();
					oldReferences.add(projectName);
				}
				HashSet newReferences = (HashSet) oldReferences.clone();

				for (int i = 0; i < oldRequired.length; i++) {
					String projectName = oldRequired[i];
					newReferences.remove(projectName);
				}
				for (int i = 0; i < newRequired.length; i++) {
					String projectName = newRequired[i];
					newReferences.add(projectName);
				}

				Iterator iter;
				int newSize = newReferences.size();

				checkIdentity: {
					if (oldReferences.size() == newSize) {
						iter = newReferences.iterator();
						while (iter.hasNext()) {
							if (!oldReferences.contains(iter.next())) {
								break checkIdentity;
							}
						}
						return;
					}
				}
				String[] requiredProjectNames = new String[newSize];
				int index = 0;
				iter = newReferences.iterator();
				while (iter.hasNext()) {
					requiredProjectNames[index++] = (String) iter.next();
				}
				Util.sort(requiredProjectNames); // ensure that if changed,
													// the order is consistent

				IProject[] requiredProjectArray = new IProject[newSize];
				IWorkspaceRoot wksRoot = projectResource.getWorkspace()
						.getRoot();
				for (int i = 0; i < newSize; i++) {
					requiredProjectArray[i] = wksRoot
							.getProject(requiredProjectNames[i]);
				}
				description.setDynamicReferences(requiredProjectArray);
				projectResource.setDescription(description, null);

			} catch (CoreException e) {
				throw new JavaModelException(e);
			}
		}
	}

	/**
	 * Workaround for bug 15168 circular errors not reported This is a cache of
	 * the projects before any project addition/deletion has started.
	 */
	public IJavaProject[] modelProjectsCache;

	/*
	 * Need to clone defensively the listener information, in case some listener
	 * is reacting to some notification iteration by adding/changing/removing
	 * any of the other (for example, if it deregisters itself).
	 */
	public void addElementChangedListener(IElementChangedListener listener,
			int eventMask) {
		for (int i = 0; i < this.elementChangedListenerCount; i++) {
			if (this.elementChangedListeners[i].equals(listener)) {

				// only clone the masks, since we could be in the middle of
				// notifications and one listener decide to change
				// any event mask of another listeners (yet not notified).
				int cloneLength = this.elementChangedListenerMasks.length;
				System
						.arraycopy(
								this.elementChangedListenerMasks,
								0,
								this.elementChangedListenerMasks = new int[cloneLength],
								0, cloneLength);
				this.elementChangedListenerMasks[i] = eventMask; // could be
																	// different
				return;
			}
		}
		// may need to grow, no need to clone, since iterators will have cached
		// original arrays and max boundary and we only add to the end.
		int length;
		if ((length = this.elementChangedListeners.length) == this.elementChangedListenerCount) {
			System
					.arraycopy(
							this.elementChangedListeners,
							0,
							this.elementChangedListeners = new IElementChangedListener[length * 2],
							0, length);
			System.arraycopy(this.elementChangedListenerMasks, 0,
					this.elementChangedListenerMasks = new int[length * 2], 0,
					length);
		}
		this.elementChangedListeners[this.elementChangedListenerCount] = listener;
		this.elementChangedListenerMasks[this.elementChangedListenerCount] = eventMask;
		this.elementChangedListenerCount++;
	}

	public void addPreResourceChangedListener(IResourceChangeListener listener) {
		for (int i = 0; i < this.preResourceChangeListenerCount; i++) {
			if (this.preResourceChangeListeners[i].equals(listener)) {
				return;
			}
		}
		// may need to grow, no need to clone, since iterators will have cached
		// original arrays and max boundary and we only add to the end.
		int length;
		if ((length = this.preResourceChangeListeners.length) == this.preResourceChangeListenerCount) {
			System
					.arraycopy(
							this.preResourceChangeListeners,
							0,
							this.preResourceChangeListeners = new IResourceChangeListener[length * 2],
							0, length);
		}
		this.preResourceChangeListeners[this.preResourceChangeListenerCount] = listener;
		this.preResourceChangeListenerCount++;
	}

	public DeltaProcessor getDeltaProcessor() {
		DeltaProcessor deltaProcessor = (DeltaProcessor) this.deltaProcessors
				.get();
		if (deltaProcessor != null)
			return deltaProcessor;
		deltaProcessor = new DeltaProcessor(this, JavaModelManager
				.getJavaModelManager());
		this.deltaProcessors.set(deltaProcessor);
		return deltaProcessor;
	}

	public void performClasspathResourceChange(JavaProject project,
			IClasspathEntry[] oldResolvedPath,
			IClasspathEntry[] newResolvedPath, IClasspathEntry[] newRawPath,
			boolean canChangeResources) throws JavaModelException {
		ProjectUpdateInfo info = new ProjectUpdateInfo();
		info.project = project;
		info.oldResolvedPath = oldResolvedPath;
		info.newResolvedPath = newResolvedPath;
		info.newRawPath = newRawPath;
		if (canChangeResources) {
			this.projectUpdates.remove(project); // remove possibly awaiting
													// one
			info.updateProjectReferencesIfNecessary();
			return;
		}
		this.recordProjectUpdate(info);
	}

	public void initializeRoots() {

		// recompute root infos only if necessary
		HashMap newRoots = null;
		HashMap newOtherRoots = null;
		HashMap newSourceAttachments = null;
		if (this.rootsAreStale) {
			Thread currentThread = Thread.currentThread();
			boolean addedCurrentThread = false;
			try {
				// if reentering initialization (through a container initializer
				// for example) no need to compute roots again
				// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=47213
				if (!this.initializingThreads.add(currentThread))
					return;
				addedCurrentThread = true;

				newRoots = new HashMap();
				newOtherRoots = new HashMap();
				newSourceAttachments = new HashMap();

				IJavaModel model = JavaModelManager.getJavaModelManager()
						.getJavaModel();
				IJavaProject[] projects;
				try {
					projects = model.getJavaProjects();
				} catch (JavaModelException e) {
					// nothing can be done
					return;
				}
				for (int i = 0, length = projects.length; i < length; i++) {
					JavaProject project = (JavaProject) projects[i];
					// IClasspathEntry[] classpath;
					// try {
					// classpath =
					// project.getResolvedClasspath(true/*ignoreUnresolvedEntry*/,
					// false/*don't generateMarkerOnError*/, false/*don't
					// returnResolutionInProgress*/);
					// } catch (JavaModelException e) {
					// // continue with next project
					// continue;
					// }
					// for (int j= 0, classpathLength = classpath.length; j <
					// classpathLength; j++) {
					// IClasspathEntry entry = classpath[j];
					// if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT)
					// continue;
					//						
					// // root path
					// IPath path = entry.getPath();
					// if (newRoots.get(path) == null) {
					// newRoots.put(path, new DeltaProcessor.RootInfo(project,
					// path,
					// ((ClasspathEntry)entry).fullInclusionPatternChars(),
					// ((ClasspathEntry)entry).fullExclusionPatternChars(),
					// entry.getEntryKind()));
					// } else {
					// ArrayList rootList = (ArrayList)newOtherRoots.get(path);
					// if (rootList == null) {
					// rootList = new ArrayList();
					// newOtherRoots.put(path, rootList);
					// }
					// rootList.add(new DeltaProcessor.RootInfo(project, path,
					// ((ClasspathEntry)entry).fullInclusionPatternChars(),
					// ((ClasspathEntry)entry).fullExclusionPatternChars(),
					// entry.getEntryKind()));
					// }
					//						
					// // source attachment path
					// if (entry.getEntryKind() != IClasspathEntry.CPE_LIBRARY)
					// continue;
					// QualifiedName qName = new
					// QualifiedName(JavaCore.PLUGIN_ID, "sourceattachment: " +
					// path.toOSString()); //$NON-NLS-1$;
					// String propertyString = null;
					// try {
					// propertyString =
					// ResourcesPlugin.getWorkspace().getRoot().getPersistentProperty(qName);
					// } catch (CoreException e) {
					// continue;
					// }
					// IPath sourceAttachmentPath;
					// if (propertyString != null) {
					// int index=
					// propertyString.lastIndexOf(PackageFragmentRoot.ATTACHMENT_PROPERTY_DELIMITER);
					// sourceAttachmentPath = (index < 0) ? new
					// Path(propertyString) : new
					// Path(propertyString.substring(0, index));
					// } else {
					// sourceAttachmentPath = entry.getSourceAttachmentPath();
					// }
					// if (sourceAttachmentPath != null) {
					// newSourceAttachments.put(sourceAttachmentPath, path);
					// }
					// }
				}
			} finally {
				if (addedCurrentThread) {
					this.initializingThreads.remove(currentThread);
				}
			}
		}
		synchronized (this) {
			this.oldRoots = this.roots;
			this.oldOtherRoots = this.otherRoots;
			if (this.rootsAreStale && newRoots != null) { // double check
															// again
				this.roots = newRoots;
				this.otherRoots = newOtherRoots;
				this.sourceAttachments = newSourceAttachments;
				this.rootsAreStale = false;
			}
		}
	}

	public synchronized void recordProjectUpdate(ProjectUpdateInfo newInfo) {

		JavaProject project = newInfo.project;
		ProjectUpdateInfo oldInfo = (ProjectUpdateInfo) this.projectUpdates
				.get(project);
		if (oldInfo != null) { // refresh new classpath information
			oldInfo.newRawPath = newInfo.newRawPath;
			oldInfo.newResolvedPath = newInfo.newResolvedPath;
		} else {
			this.projectUpdates.put(project, newInfo);
		}
	}

	public synchronized ProjectUpdateInfo[] removeAllProjectUpdates() {
		int length = this.projectUpdates.size();
		if (length == 0)
			return null;
		ProjectUpdateInfo[] updates = new ProjectUpdateInfo[length];
		this.projectUpdates.values().toArray(updates);
		this.projectUpdates.clear();
		return updates;
	}

	public void removeElementChangedListener(IElementChangedListener listener) {

		for (int i = 0; i < this.elementChangedListenerCount; i++) {

			if (this.elementChangedListeners[i].equals(listener)) {

				// need to clone defensively since we might be in the middle of
				// listener notifications (#fire)
				int length = this.elementChangedListeners.length;
				IElementChangedListener[] newListeners = new IElementChangedListener[length];
				System.arraycopy(this.elementChangedListeners, 0, newListeners,
						0, i);
				int[] newMasks = new int[length];
				System.arraycopy(this.elementChangedListenerMasks, 0, newMasks,
						0, i);

				// copy trailing listeners
				int trailingLength = this.elementChangedListenerCount - i - 1;
				if (trailingLength > 0) {
					System.arraycopy(this.elementChangedListeners, i + 1,
							newListeners, i, trailingLength);
					System.arraycopy(this.elementChangedListenerMasks, i + 1,
							newMasks, i, trailingLength);
				}

				// update manager listener state (#fire need to iterate over
				// original listeners through a local variable to hold onto
				// the original ones)
				this.elementChangedListeners = newListeners;
				this.elementChangedListenerMasks = newMasks;
				this.elementChangedListenerCount--;
				return;
			}
		}
	}

	public void removePreResourceChangedListener(
			IResourceChangeListener listener) {

		for (int i = 0; i < this.preResourceChangeListenerCount; i++) {

			if (this.preResourceChangeListeners[i].equals(listener)) {

				// need to clone defensively since we might be in the middle of
				// listener notifications (#fire)
				int length = this.preResourceChangeListeners.length;
				IResourceChangeListener[] newListeners = new IResourceChangeListener[length];
				System.arraycopy(this.preResourceChangeListeners, 0,
						newListeners, 0, i);

				// copy trailing listeners
				int trailingLength = this.preResourceChangeListenerCount - i
						- 1;
				if (trailingLength > 0) {
					System.arraycopy(this.preResourceChangeListeners, i + 1,
							newListeners, i, trailingLength);
				}

				// update manager listener state (#fire need to iterate over
				// original listeners through a local variable to hold onto
				// the original ones)
				this.preResourceChangeListeners = newListeners;
				this.preResourceChangeListenerCount--;
				return;
			}
		}
	}

	public void resourceChanged(final IResourceChangeEvent event) {
		boolean isPostChange = event.getType() == IResourceChangeEvent.POST_CHANGE;
		if (isPostChange) {
			for (int i = 0; i < this.preResourceChangeListenerCount; i++) {
				// wrap callbacks with Safe runnable for subsequent listeners to
				// be called when some are causing grief
				final IResourceChangeListener listener = this.preResourceChangeListeners[i];
				Platform.run(new ISafeRunnable() {
					public void handleException(Throwable exception) {
						Util
								.log(exception,
										"Exception occurred in listener of pre Java resource change notification"); //$NON-NLS-1$
					}

					public void run() throws Exception {
						listener.resourceChanged(event);
					}
				});
			}
		}
		try {
			getDeltaProcessor().resourceChanged(event);
		} finally {
			// TODO (jerome) see 47631, may want to get rid of following so as
			// to reuse delta processor ?
			if (isPostChange) {
				this.deltaProcessors.set(null);
			}
		}

	}

	/*
	 * Update the roots that are affected by the addition or the removal of the
	 * given container resource.
	 */
	// public synchronized void updateRoots(IPath containerPath, IResourceDelta
	// containerDelta, DeltaProcessor deltaProcessor) {
	// Map updatedRoots;
	// Map otherUpdatedRoots;
	// if (containerDelta.getKind() == IResourceDelta.REMOVED) {
	// updatedRoots = this.oldRoots;
	// otherUpdatedRoots = this.oldOtherRoots;
	// } else {
	// updatedRoots = this.roots;
	// otherUpdatedRoots = this.otherRoots;
	// }
	// Iterator iterator = updatedRoots.keySet().iterator();
	// while (iterator.hasNext()) {
	// IPath path = (IPath)iterator.next();
	// if (containerPath.isPrefixOf(path) && !containerPath.equals(path)) {
	// IResourceDelta rootDelta =
	// containerDelta.findMember(path.removeFirstSegments(1));
	// if (rootDelta == null) continue;
	// DeltaProcessor.RootInfo rootInfo =
	// (DeltaProcessor.RootInfo)updatedRoots.get(path);
	//	
	// if (!rootInfo.project.getPath().isPrefixOf(path)) { // only consider
	// roots that are not included in the container
	// deltaProcessor.updateCurrentDeltaAndIndex(rootDelta,
	// IJavaElement.PACKAGE_FRAGMENT_ROOT, rootInfo);
	// }
	//				
	// ArrayList rootList = (ArrayList)otherUpdatedRoots.get(path);
	// if (rootList != null) {
	// Iterator otherProjects = rootList.iterator();
	// while (otherProjects.hasNext()) {
	// rootInfo = (DeltaProcessor.RootInfo)otherProjects.next();
	// if (!rootInfo.project.getPath().isPrefixOf(path)) { // only consider
	// roots that are not included in the container
	// deltaProcessor.updateCurrentDeltaAndIndex(rootDelta,
	// IJavaElement.PACKAGE_FRAGMENT_ROOT, rootInfo);
	// }
	// }
	// }
	// }
	// }
	// }
}
