/*******************************************************************************
 * Copyright (c) 2000, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *

 *******************************************************************************/
package org.eclipse.dltk.core.search.indexing;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.CRC32;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.dltk.compiler.CharOperation;
import org.eclipse.dltk.compiler.ISourceElementRequestor;
import org.eclipse.dltk.compiler.util.SimpleLookupTable;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.DLTKLanguageManager;
import org.eclipse.dltk.core.IBuildpathEntry;
import org.eclipse.dltk.core.IDLTKLanguageToolkit;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.ISourceElementParser;
import org.eclipse.dltk.core.ModelException;
import org.eclipse.dltk.core.environment.IFileHandle;
import org.eclipse.dltk.core.search.BasicSearchEngine;
import org.eclipse.dltk.core.search.IDLTKSearchScope;
import org.eclipse.dltk.core.search.SearchDocument;
import org.eclipse.dltk.core.search.SearchEngine;
import org.eclipse.dltk.core.search.SearchParticipant;
import org.eclipse.dltk.core.search.index.Index;
import org.eclipse.dltk.core.search.index.MixinIndex;
import org.eclipse.dltk.internal.core.BuildpathEntry;
import org.eclipse.dltk.internal.core.Model;
import org.eclipse.dltk.internal.core.ModelManager;
import org.eclipse.dltk.internal.core.ScriptProject;
import org.eclipse.dltk.internal.core.search.PatternSearchJob;
import org.eclipse.dltk.internal.core.search.processing.IJob;
import org.eclipse.dltk.internal.core.search.processing.JobManager;
import org.eclipse.dltk.internal.core.util.Messages;
import org.eclipse.dltk.internal.core.util.Util;

public class IndexManager extends JobManager implements IIndexConstants {

	public SimpleLookupTable indexLocations = new SimpleLookupTable();
	/*
	 * key = an IPath, value = an Index
	 */
	private Map indexes = new HashMap(5);
	/* need to save ? */
	private boolean needToSave = false;
	private static final CRC32 checksumCalculator = new CRC32();
	private IPath javaPluginLocation = null;
	/* can only replace a current state if its less than the new one */
	private SimpleLookupTable indexStates = null;
	private File savedIndexNamesFile = new File(
			this.getScriptPluginWorkingLocation()
					.append("savedIndexNames.txt").toOSString()); //$NON-NLS-1$
	public static Integer SAVED_STATE = new Integer(0);
	public static Integer UPDATING_STATE = new Integer(1);
	public static Integer UNKNOWN_STATE = new Integer(2);
	public static Integer REBUILDING_STATE = new Integer(3);

	public static List sourcesToMixin = new ArrayList();

	public synchronized void aboutToUpdateIndex(IPath containerPath,
			Integer newIndexState) {
		// newIndexState is either UPDATING_STATE or REBUILDING_STATE
		// must tag the index as inconsistent, in case we exit before the update
		// job is started
		String indexLocation = this.computeIndexLocation(containerPath);
		Object state = this.getIndexStates().get(indexLocation);
		Integer currentIndexState = state == null ? UNKNOWN_STATE
				: (Integer) state;
		if (currentIndexState.equals(REBUILDING_STATE)) {
			return; // already rebuilding the index
		}
		int compare = newIndexState.compareTo(currentIndexState);
		if (compare > 0) {
			// so UPDATING_STATE replaces SAVED_STATE and REBUILDING_STATE
			// replaces everything
			this.updateIndexState(indexLocation, newIndexState);
		} else if (compare < 0 && this.indexes.get(indexLocation) == null) {
			// if already cached index then there is nothing more to do
			this.rebuildIndex(indexLocation, containerPath);
		}
	}

	// /**
	// * Trigger addition of a resource to an index Note: the actual operation
	// is
	// * performed in background
	// */
	// public void addExternal(IFile resource, IPath containerPath) {
	// if (DLTKCore.getPlugin() == null)
	// return;
	// SearchParticipant participant =
	// SearchEngine.getDefaultSearchParticipant();
	// SearchDocument document =
	// participant.getDocument(resource.getFullPath().toString());
	// String indexLocation = computeIndexLocation(containerPath);
	// scheduleDocumentIndexing(document, containerPath, indexLocation,
	// participant);
	// }

	/**
	 * Trigger addition of a resource to an index Note: the actual operation is
	 * performed in background
	 */
	public void addSource(IFile resource, IPath containerPath,
			ISourceElementParser parser, SourceIndexerRequestor requestor,
			IDLTKLanguageToolkit toolkit) {
		if (DLTKCore.getPlugin() == null) {
			return;
		}
		SearchParticipant participant = SearchEngine
				.getDefaultSearchParticipant();
		SearchDocument document = participant.getDocument(resource
				.getFullPath().toString());
		((InternalSearchDocument) document).parser = parser;
		((InternalSearchDocument) document).requestor = requestor;
		((InternalSearchDocument) document).toolkit = toolkit;
		String indexLocation = this.computeIndexLocation(containerPath);
		this.scheduleDocumentIndexing(document, containerPath, indexLocation,
				participant);
	}

	/**
	 * Trigger addition of a resource to an index Note: the actual operation is
	 * performed in background
	 */
	public void addBinary(IFile resource, IPath containerPath) {
		if (DLTKCore.getPlugin() == null) {
			return;
		}
		SearchParticipant participant = SearchEngine
				.getDefaultSearchParticipant();
		SearchDocument document = participant.getDocument(resource
				.getFullPath().toString());
		String indexLocation = this.computeIndexLocation(containerPath);
		this.scheduleDocumentIndexing(document, containerPath, indexLocation,
				participant);
	}

	/*
	 * Removes unused indexes from disk.
	 */
	public void cleanUpIndexes() {
		SimpleLookupTable knownPaths = new SimpleLookupTable();
		IDLTKSearchScope scope = BasicSearchEngine.createWorkspaceScope(null);
		PatternSearchJob job = new PatternSearchJob(null, SearchEngine
				.getDefaultSearchParticipant(), scope, null);
		Index[] selectedIndexes = job.getIndexes(null);
		for (int j = 0, max = selectedIndexes.length; j < max; j++) {
			// TODO should use getJavaPluginWorkingLocation()+index simple name
			// to avoid bugs such as
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=62267
			String path = selectedIndexes[j].getIndexFile().getAbsolutePath();
			knownPaths.put(path, path);
		}
		if (this.indexStates != null) {
			Object[] keys = this.indexStates.keyTable;
			int keysLength = keys.length;
			int updates = 0;
			String locations[] = new String[keysLength];
			for (int i = 0, l = keys.length; i < l; i++) {
				String key = (String) keys[i];
				if (key != null && !knownPaths.containsKey(key)) {
					locations[updates++] = key;
				}
			}
			if (updates > 0) {
				this.removeIndexesState(locations);
			}
		}
		File indexesDirectory = new File(this.getScriptPluginWorkingLocation()
				.toOSString());
		if (indexesDirectory.isDirectory()) {
			File[] indexesFiles = indexesDirectory.listFiles();
			if (indexesFiles != null) {
				for (int i = 0, indexesFilesLength = indexesFiles.length; i < indexesFilesLength; i++) {
					String fileName = indexesFiles[i].getAbsolutePath();
					if (!knownPaths.containsKey(fileName)
							&& fileName.toLowerCase().endsWith(".index")) { //$NON-NLS-1$
						if (VERBOSE) {
							Util
									.verbose("Deleting index file " + indexesFiles[i]); //$NON-NLS-1$
						}
						indexesFiles[i].delete();
					}
				}
			}
		}
	}

	public String computeIndexLocation(IPath containerPath) {
		String indexLocation = (String) this.indexLocations.get(containerPath);
		if (indexLocation == null) {
			String pathString = containerPath.toString();
			checksumCalculator.reset();
			checksumCalculator.update(pathString.getBytes());
			String fileName = Long.toString(checksumCalculator.getValue())
					+ ".index"; //$NON-NLS-1$
			if (VERBOSE) {
				Util
						.verbose("-> index name for " + pathString + " is " + fileName); //$NON-NLS-1$ //$NON-NLS-2$
			}
			indexLocation = this.getScriptPluginWorkingLocation().append(fileName)
					.toOSString();
			this.indexLocations.put(containerPath, indexLocation);
		}
		return indexLocation;
	}

	/*
	 * Creates an empty index at the given location, for the given container
	 * path, if none exist.
	 */
	public void ensureIndexExists(String indexLocation, IPath containerPath) {
		SimpleLookupTable states = this.getIndexStates();
		Object state = states.get(indexLocation);
		if (state == null) {
			this.updateIndexState(indexLocation, REBUILDING_STATE);
			this.getIndex(containerPath, indexLocation, true, true);
		}
	}

	public SourceIndexerRequestor getSourceRequestor(
			IScriptProject scriptProject) {
		IDLTKLanguageToolkit toolkit = null;
		toolkit = DLTKLanguageManager.getLanguageToolkit(scriptProject);
		if (toolkit != null) {
			return DLTKLanguageManager.createSourceRequestor(toolkit
					.getNatureId());
		}
		return null;
	}

	/**
	 * @deprecated
	 */
	public ISourceElementParser getSourceElementParser(IScriptProject project,
			ISourceElementRequestor requestor) {
		final ISourceElementParser parser = getSourceElementParser(project);
		if (parser != null) {
			parser.setRequestor(requestor);
		}
		return parser;
	}
	
	/**
	 * Method to be used when <i>requestor will be set by indexer</i>
	 */
	public ISourceElementParser getSourceElementParser(IScriptProject project) {
		// disable task tags to speed up parsing
		// Map options = project.getOptions(true);
		// options.put(DLTKCore.COMPILER_TASK_TAGS, ""); //$NON-NLS-1$
		IDLTKLanguageToolkit toolkit = DLTKLanguageManager
				.getLanguageToolkit(project);
		if (toolkit != null) {
			return DLTKLanguageManager.getSourceElementParser(toolkit
					.getNatureId());
		}
		return null;
	}

	/**
	 * Returns the index for a given project, according to the following
	 * algorithm: - if index is already in memory: answers this one back - if
	 * (reuseExistingFile) then read it and return this index and record it in
	 * memory - if (createIfMissing) then create a new empty index and record it
	 * in memory
	 *
	 * Warning: Does not check whether index is consistent (not being used)
	 */
	public synchronized Index getIndex(IPath containerPath,
			boolean reuseExistingFile, boolean createIfMissing) {
		String indexLocation = this.computeIndexLocation(containerPath);
		return this.getIndex(containerPath, indexLocation, reuseExistingFile,
				createIfMissing);
	}

	/**
	 * This indexes aren't required to be rebuilded.
	 *
	 * @param id
	 * @return
	 */
	public synchronized Index getSpecialIndex(String id, String path,
			String containerPath/* , IProject project */) {
		// String containerPath = project.getFullPath().toOSString();
		// String path = project.getFullPath();

		boolean mixin = id.equals("mixin"); //$NON-NLS-1$

		String indexLocation = this.computeIndexLocation(new Path("#special#" + id //$NON-NLS-1$
				+ "#" + path)); //$NON-NLS-1$

		Index index = (Index) this.indexes.get(indexLocation);

		if (index == null) {
			Object state = this.getIndexStates().get(indexLocation);
			Integer currentIndexState = state == null ? UNKNOWN_STATE
					: (Integer) state;
			// index isn't cached, consider reusing an existing index file

			File indexFile = new File(indexLocation);
			if (indexFile.exists()) { // check before creating index so as
				// to avoid creating a new empty
				// index if file is missing
				try {
					if (mixin) {
						index = new MixinIndex(indexLocation, containerPath,
								true
						/* reuse index file */
						);
					} else {
						index = new Index(indexLocation, containerPath, true /*
																				 * reuse
																				 * index
																				 * file
																				 */);
					}
					this.indexes.put(indexLocation, index);
					return index;
				} catch (IOException e) {
					// failed to read the existing file or its no longer
					// compatible
					if (currentIndexState != REBUILDING_STATE) { // rebuild
						/*
						 * index if existing file is corrupt, unless the index
						 * is already being rebuilt
						 */
						if (VERBOSE) {
							Util
									.verbose("-> cannot reuse existing index: " + indexLocation + " path: " + id); //$NON-NLS-1$ //$NON-NLS-2$
						}
					}
					/* index = null; */// will fall thru to createIfMissing
					// & create a empty index for the
					// rebuild all job to populate
				}
			}

			// index wasn't found on disk, consider creating an empty new one

			try {
				if (VERBOSE) {
					Util
							.verbose("-> create empty index: " + indexLocation + " path: " + id); //$NON-NLS-1$ //$NON-NLS-2$
				}

				if (mixin) {
					index = new MixinIndex(indexLocation, containerPath, false
					/*
					 * do not reuse index file
					 */
					);
				} else {
					index = new Index(indexLocation, containerPath, false /*
																			 * do
																			 * not
																			 * reuse
																			 * index
																			 * file
																			 */);
				}
				this.indexes.put(indexLocation, index);
				return index;
			} catch (IOException e) {
				if (VERBOSE) {
					Util
							.verbose("-> unable to create empty index: " + indexLocation + " path: " + containerPath); //$NON-NLS-1$ //$NON-NLS-2$
				}
				// The file could not be created. Possible reason: the
				// project has been deleted.
				return null;
			}
		}
		// System.out.println(" index name: " + path.toOSString() + " <----> " +
		// index.getIndexFile().getName());
		this.updateIndexState(indexLocation, REBUILDING_STATE);
		return index;
	}

	/**
	 * Returns the index for a given project, according to the following
	 * algorithm: - if index is already in memory: answers this one back - if
	 * (reuseExistingFile) then read it and return this index and record it in
	 * memory - if (createIfMissing) then create a new empty index and record it
	 * in memory
	 *
	 * Warning: Does not check whether index is consistent (not being used)
	 */
	public synchronized Index getIndex(IPath containerPath,
			String indexLocation, boolean reuseExistingFile,
			boolean createIfMissing) {
		boolean mixin = containerPath.toString().startsWith("#special#mixin#"); //$NON-NLS-1$
		// Path is already canonical per construction
		Index index = (Index) this.indexes.get(indexLocation);
		if (index == null) {
			Object state = this.getIndexStates().get(indexLocation);
			Integer currentIndexState = state == null ? UNKNOWN_STATE
					: (Integer) state;
			if (currentIndexState.equals(UNKNOWN_STATE)) {
				// should only be reachable for query jobs
				// IF you put an index in the cache, then AddArchiveFileToIndex
				// fails because it thinks there is nothing to do
				this.rebuildIndex(indexLocation, containerPath);
				if( !mixin ) {
					return null;
				}
			}
			// index isn't cached, consider reusing an existing index file
			String containerPathString = containerPath.toString();
			if (reuseExistingFile) {
				File indexFile = new File(indexLocation);
				if (indexFile.exists()) { // check before creating index so as
					// to avoid creating a new empty
					// index if file is missing
					try {
						if (mixin) {
							index = new MixinIndex(indexLocation,
									containerPathString, true /*
																 * reuse index
																 * file
																 */);
						} else {
							index = new Index(indexLocation,
									containerPathString, true /*
																 * reuse index
																 * file
																 */);
						}
						this.indexes.put(indexLocation, index);
						return index;
					} catch (IOException e) {
						// failed to read the existing file or its no longer
						// compatible
						if (currentIndexState != REBUILDING_STATE) { // rebuild
							/*
							 * index if existing file is corrupt, unless the
							 * index is already being rebuilt
							 */
							if (VERBOSE) {
								Util
										.verbose("-> cannot reuse existing index: " + indexLocation + " path: " + containerPathString); //$NON-NLS-1$ //$NON-NLS-2$
							}
							this.rebuildIndex(indexLocation, containerPath);
							return null;
						}
						/* index = null; */// will fall thru to createIfMissing
						// & create a empty index for the
						// rebuild all job to populate
					}
				}
				if (currentIndexState == SAVED_STATE) { // rebuild index if
					// existing file is
					// missing
					this.rebuildIndex(indexLocation, containerPath);
					return null;
				}
			}
			// index wasn't found on disk, consider creating an empty new one
			if (createIfMissing) {
				try {
					if (VERBOSE) {
						Util
								.verbose("-> create empty index: " + indexLocation + " path: " + containerPathString); //$NON-NLS-1$ //$NON-NLS-2$
					}
					if (mixin) {
						index = new MixinIndex(indexLocation,
								containerPathString, false /*
															 * do not reuse
															 * index file
															 */);
					} else {
						index = new Index(indexLocation, containerPathString,
								false /*
										 * do not reuse index file
										 */);
					}
					this.indexes.put(indexLocation, index);
					return index;
				} catch (IOException e) {
					if (VERBOSE) {
						Util
								.verbose("-> unable to create empty index: " + indexLocation + " path: " + containerPathString); //$NON-NLS-1$ //$NON-NLS-2$
					}
					// The file could not be created. Possible reason: the
					// project has been deleted.
					return null;
				}
			}
		}
		// System.out.println(" index name: " + path.toOSString() + " <----> " +
		// index.getIndexFile().getName());
		return index;
	}

	public synchronized Index getIndex(String indexLocation) {
		return (Index) this.indexes.get(indexLocation); // is null if unknown, call
		// if the containerPath must
		// be computed
	}

	public synchronized Index getIndexForUpdate(IPath containerPath,
			boolean reuseExistingFile, boolean createIfMissing) {
		String indexLocation = this.computeIndexLocation(containerPath);
		if (this.getIndexStates().get(indexLocation) == REBUILDING_STATE) {
			return this.getIndex(containerPath, indexLocation, reuseExistingFile,
					createIfMissing);
		}
		return null; // abort the job since the index has been removed from
		// the REBUILDING_STATE
	}

	private SimpleLookupTable getIndexStates() {
		if (this.indexStates != null) {
			return this.indexStates;
		}
		this.indexStates = new SimpleLookupTable();
		char[] savedIndexNames = this.readIndexState();
		if (savedIndexNames.length > 0) {
			char[][] names = CharOperation.splitOn('\n', savedIndexNames);
			if (names.length > 0) {
				// check to see if workspace has moved, if so then do not trust
				// saved indexes
				File indexesDirectory = new File(
						this.getScriptPluginWorkingLocation().toOSString());
				char[] dirName = indexesDirectory.getAbsolutePath()
						.toCharArray();
				int delimiterPos = dirName.length;
				if (CharOperation.match(names[0], 0, delimiterPos, dirName, 0,
						delimiterPos, true)) {
					for (int i = 0, l = names.length; i < l; i++) {
						char[] name = names[i];
						if (name.length > 0) {
							this.indexStates.put(new String(name), SAVED_STATE);
						}
					}
				} else {
					this.savedIndexNamesFile.delete(); // forget saved indexes &
					// delete each index file
					File[] files = indexesDirectory.listFiles();
					if (files != null) {
						for (int i = 0, l = files.length; i < l; i++) {
							String fileName = files[i].getAbsolutePath();
							if (fileName.toLowerCase().endsWith(".index")) { //$NON-NLS-1$
								if (VERBOSE) {
									Util
											.verbose("Deleting index file " + files[i]); //$NON-NLS-1$
								}
								files[i].delete();
							}
						}
					}
				}
			}
		}
		return this.indexStates;
	}

	private IPath getScriptPluginWorkingLocation() {
		if (this.javaPluginLocation != null) {
			return this.javaPluginLocation;
		}
		IPath stateLocation = DLTKCore.getPlugin().getStateLocation();
		return this.javaPluginLocation = stateLocation;
	}

	public void indexDocument(SearchDocument searchDocument,
			SearchParticipant searchParticipant, Index index,
			IPath indexLocation) {
		try {
			((InternalSearchDocument) searchDocument).index = index;
			searchParticipant.indexDocument(searchDocument, indexLocation);
		} finally {
			((InternalSearchDocument) searchDocument).index = null;
		}
	}

	/**
	 * Trigger addition of the entire content of a project Note: the actual
	 * operation is performed in background
	 */
	public void indexAll(IProject project) {
		if (DLTKCore.getPlugin() == null) {
			return;
		}
		// Also request indexing of binaries on the buildpath
		// determine the new children
		try {
			Model model = ModelManager.getModelManager().getModel();
			ScriptProject scriptProject = (ScriptProject) model
					.getScriptProject(project);
			// only consider immediate libraries - each project will do the same
			// NOTE: force to resolve CP variables before calling indexer -
			// 19303, so that initializers
			// will be run in the current thread.
			IBuildpathEntry[] entries = scriptProject.getResolvedBuildpath(
					true/* ignoreUnresolvedEntry */,
					false/* don't generateMarkerOnError */, false/*
																	 * don't
																	 * returnResolutionInProgress
																	 */);
			for (int i = 0; i < entries.length; i++) {
				IBuildpathEntry entry = entries[i];
				if (entry.getEntryKind() == IBuildpathEntry.BPE_LIBRARY) {
					this.indexLibrary(entry.getPath(), project, ((BuildpathEntry)entry).fullInclusionPatternChars(), ((BuildpathEntry)entry).fullExclusionPatternChars());
				}
			}
		} catch (ModelException e) { // cannot retrieve buildpath info
		}
		// check if the same request is not already in the queue
		IndexRequest request = new IndexAllProject(project, this);
		if (!this.isJobWaiting(request)) {
			this.request(request);
		}
	}

	/**
	 * Trigger addition of a library to an index Note: the actual operation is
	 * performed in background
	 *
	 * @param exclusionPatterns
	 * @param inclusionPatterns
	 */
	public void indexLibrary(IPath path, IProject requestingProject, char[][] inclusionPatterns,
			char[][] exclusionPatterns) {
		// requestingProject is no longer used to cancel jobs but leave it here
		// just in case
		if (DLTKCore.getPlugin() == null) {
			return;
		}
		Object target = Model.getTarget(ResourcesPlugin.getWorkspace()
				.getRoot(), path, true);
		IndexRequest request = null;
		if (target instanceof IFile) {
			// request = new AddArchiveFileToIndex((IFile) target, this);
			return;
		} else if (target instanceof IFileHandle) {
			if (((IFileHandle) target).isFile()) {
				// request = new AddArchiveFileToIndex(path, this);
				return;
			} else {
				request = new AddExternalFolderToIndex(path, requestingProject,
						inclusionPatterns, exclusionPatterns, this);
			}
		} else if (target instanceof IContainer) {
			// request = new IndexContainerFolder((IContainer) target, this,
			// requestingProject);
			return;
		} else if (target == null
				&& path.toString().startsWith(
						IBuildpathEntry.BUILTIN_EXTERNAL_ENTRY_STR)) {
			request = new AddBuiltinFolderToIndex(path, requestingProject, this);
		}
		// check if the same request is not already in the queue
		// TODO: Uncheck this. After adding some library indexing.
		if (request != null) {
			if (!this.isJobWaiting(request)) {
				this.request(request);
			}
		}
	}

	/**
	 * Index the content of the given source folder.
	 */
	public void indexSourceFolder(ScriptProject scriptProject,
			IPath sourceFolder, char[][] inclusionPatterns,
			char[][] exclusionPatterns) {
		IProject project = scriptProject.getProject();
		if (this.jobEnd > this.jobStart) {
			// skip it if a job to index the project is already in the queue
			IndexRequest request = new IndexAllProject(project, this);
			if (this.isJobWaiting(request)) {
				return;
			}
		}
		this.request(new AddFolderToIndex(sourceFolder, project,
				inclusionPatterns, exclusionPatterns, this));
	}

	public void jobWasCancelled(IPath containerPath) {
		String indexLocation = this.computeIndexLocation(containerPath);
		Object o = this.indexes.get(indexLocation);
		if (o instanceof Index) {
			((Index) o).monitor = null;
			this.indexes.remove(indexLocation);
		}
		this.updateIndexState(indexLocation, UNKNOWN_STATE);
	}

	/**
	 * Advance to the next available job, once the current one has been
	 * completed. Note: clients awaiting until the job count is zero are still
	 * waiting at this point.
	 */
	protected synchronized void moveToNextJob() {
		// remember that one job was executed, and we will need to save indexes
		// at some point
		this.needToSave = true;
		super.moveToNextJob();
	}

	/**
	 * No more job awaiting.
	 */
	protected void notifyIdle(long idlingTime) {
		if (idlingTime > 1000 && this.needToSave) {
			this.saveIndexes();
		}
	}

	/**
	 * Name of the background process
	 */
	public String processName() {
		return Messages.process_name;
	}

	private void rebuildIndex(String indexLocation, IPath containerPath) {
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		if (workspace == null) {
			return;
		}

		Object target = Model.getTarget(workspace.getRoot(), containerPath,
				true);
		if (target == null) {
			return;
		}
		if (VERBOSE) {
			Util
					.verbose("-> request to rebuild index: " + indexLocation + " path: " + containerPath.toString()); //$NON-NLS-1$ //$NON-NLS-2$
		}
		
		this.updateIndexState(indexLocation, REBUILDING_STATE);
		IndexRequest request = null;
		if (target instanceof IProject) {
			IProject p = (IProject) target;
			if (ScriptProject.hasScriptNature(p)) {
				request = new IndexAllProject(p, this);
			}
		} else if (target instanceof IFolder) {
			// request = new IndexBinaryFolder((IFolder) target, this);
			return;
		} else if (target instanceof IFile) {
			// request = new AddArchiveFileToIndex((IFile) target, this);
			return;
		} else if (target instanceof IFileHandle) {
			// request = new AddArchiveFileToIndex(containerPath, this);
			return;
		}
		if (request != null) {
			this.request(request);
		}
	}

	/**
	 * Recreates the index for a given path, keeping the same read-write
	 * monitor. Returns the new empty index or null if it didn't exist before.
	 * Warning: Does not check whether index is consistent (not being used)
	 */
	public synchronized Index recreateIndex(IPath containerPath) {
		boolean mixin = containerPath.toString().startsWith("#special#mixin#"); //$NON-NLS-1$
		// only called to over write an existing cached index...
		String containerPathString = containerPath.toString();
		try {
			// Path is already canonical
			String indexLocation = this.computeIndexLocation(containerPath);
			Index index = (Index) this.indexes.get(indexLocation);
			ReadWriteMonitor monitor = index == null ? null : index.monitor;
			if (VERBOSE) {
				Util
						.verbose("-> recreating index: " + indexLocation + " for path: " + containerPathString); //$NON-NLS-1$ //$NON-NLS-2$
			}
			if (mixin) {
				index = new MixinIndex(indexLocation, containerPathString,
						false /*
								 * reuse index file
								 */);
			} else {
				index = new Index(indexLocation, containerPathString, false /*
																			 * do
																			 * not
																			 * reuse
																			 * index
																			 * file
																			 */);
			}
			this.indexes.put(indexLocation, index);
			index.monitor = monitor;
			return index;
		} catch (IOException e) {
			// The file could not be created. Possible reason: the project has
			// been deleted.
			if (VERBOSE) {
				Util
						.verbose("-> failed to recreate index for path: " + containerPathString); //$NON-NLS-1$
				e.printStackTrace();
			}
			return null;
		}
	}

	/**
	 * Trigger removal of a resource to an index Note: the actual operation is
	 * performed in background
	 */
	public void remove(String containerRelativePath, IPath indexedContainer) {
		this.request(new RemoveFromIndex(containerRelativePath, indexedContainer,
				this));
	}

	/**
	 * Removes the index for a given path. This is a no-op if the index did not
	 * exist.
	 */
	public synchronized void removeIndex(IPath containerPath) {
		if (VERBOSE) {
			Util.verbose("removing index " + containerPath); //$NON-NLS-1$
		}
		String indexLocation = this.computeIndexLocation(containerPath);
		File indexFile = new File(indexLocation);
		if (indexFile.exists()) {
			indexFile.delete();
		}
		Object o = this.indexes.get(indexLocation);
		if (o instanceof Index) {
			((Index) o).monitor = null;
		}
		this.indexes.remove(indexLocation);
		this.updateIndexState(indexLocation, null);
	}

	/**
	 * Removes all indexes whose paths start with (or are equal to) the given
	 * path.
	 */
	public synchronized void removeIndexPath(IPath path) {
		Set keySet = this.indexes.keySet();
		Iterator keys = keySet.iterator();
		String[] locations = null;
		int max = keySet.size();
		int ptr = 0;
		while (keys.hasNext()) {
			String indexLocation = (String) keys.next();
			IPath indexPath = new Path(indexLocation);
			if (path.isPrefixOf(indexPath)) {
				Index index = (Index) this.indexes.get(indexLocation);
				if (index != null) {
					index.monitor = null;
				}
				if (locations == null) {
					locations = new String[max];
				}
				locations[ptr++] = indexLocation;
				File indexFile = new File(indexLocation);
				if (indexFile.exists()) {
					indexFile.delete();
				}
			} else if (locations == null) {
				max--;
			}
		}
		if (locations != null) {
			for (int i = 0; i < ptr; i++) {
				this.indexes.remove(locations[i]);
			}
			this.removeIndexesState(locations);
		}
	}

	/**
	 * Removes all indexes whose paths start with (or are equal to) the given
	 * path.
	 */
	public synchronized void removeIndexFamily(IPath path) {
		// only finds cached index files... shutdown removes all non-cached
		// index files
		ArrayList toRemove = null;
		Object[] containerPaths = this.indexLocations.keyTable;
		for (int i = 0, length = containerPaths.length; i < length; i++) {
			IPath containerPath = (IPath) containerPaths[i];
			if (containerPath == null) {
				continue;
			}
			if (path.isPrefixOf(containerPath)) {
				if (toRemove == null) {
					toRemove = new ArrayList();
				}
				toRemove.add(containerPath);
			}
		}
		if (toRemove != null) {
			for (int i = 0, length = toRemove.size(); i < length; i++) {
				this.removeIndex((IPath) toRemove.get(i));
			}
		}
	}

	/**
	 * Remove the content of the given source folder from the index.
	 */
	public void removeSourceFolderFromIndex(ScriptProject scriptProject,
			IPath sourceFolder, char[][] inclusionPatterns,
			char[][] exclusionPatterns) {
		IProject project = scriptProject.getProject();
		if (this.jobEnd > this.jobStart) {
			// skip it if a job to index the project is already in the queue
			IndexRequest request = new IndexAllProject(project, this);
			if (this.isJobWaiting(request)) {
				return;
			}
		}
		this.request(new RemoveFolderFromIndex(sourceFolder, inclusionPatterns,
				exclusionPatterns, project, this));
	}

	/**
	 * Flush current state
	 */
	public synchronized void reset() {
		super.reset();
		if (this.indexes != null) {
			this.indexes = new HashMap(5);
			this.indexStates = null;
		}
		this.indexLocations = new SimpleLookupTable();
		this.javaPluginLocation = null;
	}

	public synchronized void saveIndex(Index index) throws IOException {
		// must have permission to write from the write monitor
		if (index.hasChanged()) {
			if (VERBOSE) {
				Util.verbose("-> saving index " + index.getIndexFile()); //$NON-NLS-1$
			}
			index.save();
		}
		// TODO should use getJavaPluginWorkingLocation()+index simple name to
		// avoid bugs such as
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=62267
		String indexLocation = index.getIndexFile().getPath();
		if (this.jobEnd > this.jobStart) {
			Object containerPath = this.indexLocations
					.keyForValue(indexLocation);
			if (containerPath != null) {
				synchronized (this) {
					for (int i = this.jobEnd; i > this.jobStart; i--) { // skip
						// the
						// current
						// job
						IJob job = this.awaitingJobs[i];
						if (job instanceof IndexRequest) {
							if (((IndexRequest) job).containerPath
									.equals(containerPath)) {
								return;
							}
						}
					}
				}
			}
		}
		this.updateIndexState(indexLocation, SAVED_STATE);
	}

	/**
	 * Commit all index memory changes to disk
	 */
	public void saveIndexes() {
		// only save cached indexes... the rest were not modified
		ArrayList toSave = new ArrayList();
		synchronized (this) {
			for (Iterator iter = this.indexes.values().iterator(); iter
					.hasNext();) {
				Object o = iter.next();
				if (o instanceof Index) {
					toSave.add(o);
				}
			}
		}
		boolean allSaved = true;
		for (int i = 0, length = toSave.size(); i < length; i++) {
			Index index = (Index) toSave.get(i);
			ReadWriteMonitor monitor = index.monitor;
			if (monitor == null) {
				continue; // index got deleted since acquired
			}
			try {
				// take read lock before checking if index has changed
				// don't take write lock yet since it can cause a deadlock (see
				// https://bugs.eclipse.org/bugs/show_bug.cgi?id=50571)
				monitor.enterRead();
				if (index.hasChanged()) {
					if (monitor.exitReadEnterWrite()) {
						try {
							this.saveIndex(index);
						} catch (IOException e) {
							if (VERBOSE) {
								Util
										.verbose(
												"-> got the following exception while saving:", System.err); //$NON-NLS-1$
								e.printStackTrace();
							}
							allSaved = false;
						} finally {
							monitor.exitWriteEnterRead();
						}
					} else {
						allSaved = false;
					}
				}
			} finally {
				monitor.exitRead();
			}
		}
		this.needToSave = !allSaved;
	}

	public void scheduleDocumentIndexing(final SearchDocument searchDocument,
			IPath container, final String indexLocation,
			final SearchParticipant searchParticipant) {
		this.request(new IndexRequest(container, this) {
			public boolean execute(IProgressMonitor progressMonitor) {
				if (this.isCancelled || progressMonitor != null
						&& progressMonitor.isCanceled()) {
					return true;
				}
				/* ensure no concurrent write access to index */
				Index index = IndexManager.this.getIndex(this.containerPath, indexLocation, true, /*
																				 * reuse
																				 * index
																				 * file
																				 */true /*
						 * create if none
						 */);
				if (index == null) {
					return true;
				}
				ReadWriteMonitor monitor = index.monitor;
				if (monitor == null) {
					return true; // index got deleted since acquired
				}
				try {
					monitor.enterWrite(); // ask permission to write
					IndexManager.this.indexDocument(searchDocument, searchParticipant, index,
							new Path(indexLocation));
				} finally {
					monitor.exitWrite(); // free write lock
				}
				return true;
			}

			public String toString() {
				return "indexing " + searchDocument.getPath(); //$NON-NLS-1$
			}
		});
	}

	public String toString() {
		StringBuffer buffer = new StringBuffer(10);
		buffer.append(super.toString());
		buffer.append("In-memory indexes:\n"); //$NON-NLS-1$
		int count = 0;
		for (Iterator iter = this.indexes.values().iterator(); iter.hasNext();) {
			buffer.append(++count)
					.append(" - ").append(iter.next().toString()).append('\n'); //$NON-NLS-1$
		}
		return buffer.toString();
	}

	private char[] readIndexState() {
		try {
			return org.eclipse.dltk.compiler.util.Util.getFileCharContent(
					this.savedIndexNamesFile, null);
		} catch (IOException ignored) {
			if (VERBOSE) {
				Util.verbose("Failed to read saved index file names"); //$NON-NLS-1$
			}
			return new char[0];
		}
	}

	private synchronized void removeIndexesState(String[] locations) {
		this.getIndexStates(); // ensure the states are initialized
		int length = locations.length;
		boolean changed = false;
		for (int i = 0; i < length; i++) {
			if (locations[i] == null) {
				continue;
			}
			if ((this.indexStates.removeKey(locations[i]) != null)) {
				changed = true;
				if (VERBOSE) {
					Util
							.verbose("-> index state updated to: ? for: " + locations[i]); //$NON-NLS-1$
				}
			}
		}
		if (!changed) {
			return;
		}
		this.writeSavedIndexNamesFile();
	}

	private synchronized void updateIndexState(String indexLocation,
			Integer indexState) {
		this.getIndexStates(); // ensure the states are initialized
		if (indexState != null) {
			if (indexState.equals(this.indexStates.get(indexLocation))) {
				return; // not changed
			}
			this.indexStates.put(indexLocation, indexState);
		} else {
			if (!this.indexStates.containsKey(indexLocation)) {
				return; // did not exist anyway
			}
			this.indexStates.removeKey(indexLocation);
		}
		this.writeSavedIndexNamesFile();
		if (VERBOSE) {
			String state = "?"; //$NON-NLS-1$
			if (indexState == SAVED_STATE) {
				state = "SAVED"; //$NON-NLS-1$
			} else if (indexState == UPDATING_STATE) {
				state = "UPDATING"; //$NON-NLS-1$
			} else if (indexState == UNKNOWN_STATE) {
				state = "UNKNOWN"; //$NON-NLS-1$
			} else if (indexState == REBUILDING_STATE) {
				state = "REBUILDING"; //$NON-NLS-1$
			}
			Util
					.verbose("-> index state updated to: " + state + " for: " + indexLocation); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private void writeSavedIndexNamesFile() {
		BufferedWriter writer = null;
		try {
			writer = new BufferedWriter(new FileWriter(this.savedIndexNamesFile));
			Object[] keys = this.indexStates.keyTable;
			Object[] states = this.indexStates.valueTable;
			for (int i = 0, l = states.length; i < l; i++) {
				if (states[i] == SAVED_STATE) {
					writer.write((String) keys[i]);
					writer.write('\n');
				}
			}
		} catch (IOException ignored) {
			if (VERBOSE) {
				Util.verbose(
						"Failed to write saved index file names", System.err); //$NON-NLS-1$
			}
		} finally {
			if (writer != null) {
				try {
					writer.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
	}

	public synchronized void rebuild() {
		this.disable();
		File indexesDirectory = new File(this.getScriptPluginWorkingLocation()
				.toOSString());
		// this.
		if (indexesDirectory.isDirectory()) {
			File[] indexesFiles = indexesDirectory.listFiles();
			if (indexesFiles != null) {
				for (int i = 0, indexesFilesLength = indexesFiles.length; i < indexesFilesLength; i++) {
					indexesFiles[i].delete();
				}
			}
		}
		this.reset();
		// this.indexAll(project)

		this.enable();
		IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
		IProject[] projects = root.getProjects();
		for (int i = 0; i < projects.length; i++) {
			if( DLTKLanguageManager.hasScriptNature(projects[i]) ) {
				this.indexAll(projects[i]);
			}
		}
	}

}
