/*******************************************************************************
 * Copyright (c) 2007, 2012 Wind River Systems, Inc. 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
 *
 * Contributors:
 *     Anton Leherbauer (Wind River Systems) - initial API and implementation
 *     Markus Schorn (Wind River Systems)
 *     Sergey Prigogin (Google)
 ******************************************************************************/
package org.eclipse.cdt.internal.core.model;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexManager;
import org.eclipse.cdt.core.model.ILanguage;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.core.dom.parser.ASTTranslationUnit;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;

/**
 * Provides a shared AST of a single translation unit at a time.
 *
 * @since 4.0
 */
public class ASTCache {
	/**
	 * Tells whether this class is in debug mode.
	 */
	private static final boolean DEBUG= "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.cdt.core/debug/ASTCache"));  //$NON-NLS-1$//$NON-NLS-2$
	private static final String DEBUG_PREFIX= "[ASTCache] "; //$NON-NLS-1$

	/** Fast parse mode (use PDOM) */
	public static int PARSE_MODE = ITranslationUnit.AST_SKIP_ALL_HEADERS
			| ITranslationUnit.AST_CONFIGURE_USING_SOURCE_CONTEXT
			| ITranslationUnit.AST_SKIP_TRIVIAL_EXPRESSIONS_IN_AGGREGATE_INITIALIZERS
			| ITranslationUnit.AST_PARSE_INACTIVE_CODE;

	/**
	 * Do something with an AST.
	 *
	 * @see #runOnAST(ILanguage, IASTTranslationUnit)
	 */
	public static interface ASTRunnable {
		/**
		 * Do something with the given AST.
		 *
		 * @param lang the language with which the AST has been created.
		 * @param ast  the translation unit AST, may be <code>null</code>
		 * @return a status object
		 */
		IStatus runOnAST(ILanguage lang, IASTTranslationUnit ast) throws CoreException;
	}

	private final int fParseMode;
	private final Object fCacheMutex= new Object();

	/** The active translation unit for which to cache the AST */
	private ITranslationUnit fActiveTU;
	/** The cached AST if any */
	private IASTTranslationUnit fAST;
	/**
	 * The timestamp of the last index write access at the time
	 * the AST got cached. A cached AST becomes invalid on any index
	 * write access afterwards.
	 */
	private long fLastWriteOnIndex;
	/** Indicates whether the AST is currently being computed */
	private boolean fIsReconciling;

	/**
	 * Create a new AST cache.
	 */
	public ASTCache() {
		fParseMode= PARSE_MODE;
	}

	/**
	 * Returns a shared translation unit AST for the given translation unit.
	 * <p>
	 * Clients are not allowed to modify the AST and must hold an index read
	 * lock prior to calling this method and continue to hold the lock as long
	 * as the AST is being used.
	 * </p>
	 *
	 * @param tUnit				the translation unit
	 * @param index				the index used to create the AST, needs to be read-locked
	 * @param wait				if <code>true</code>, wait for AST to be computed (might compute a new AST)
	 * @param progressMonitor	the progress monitor or <code>null</code>
	 * @return					the AST or <code>null</code> if the AST is not available
	 */
	private IASTTranslationUnit getAST(ITranslationUnit tUnit, IIndex index, boolean wait,
			IProgressMonitor progressMonitor) {
		if (tUnit == null)
			return null;

		while (true) {
			if (progressMonitor != null && progressMonitor.isCanceled())
				return null;

			final boolean isActiveElement;
			synchronized (fCacheMutex) {
				isActiveElement= tUnit.equals(fActiveTU);
				if (isActiveElement) {
					if (fAST != null) {
						// AST is cached
						if (fLastWriteOnIndex < index.getLastWriteAccess()) {
							// AST has been invalidated by index write access
							disposeAST();
						} else {
							// cached AST is valid
							if (DEBUG)
								System.out.println(DEBUG_PREFIX + getThreadName() + "returning cached AST:" + toString(fAST) + " for: " + tUnit.getElementName()); //$NON-NLS-1$ //$NON-NLS-2$
							return fAST;
						}
					}
					// no cached AST
					if (!wait) {
						// no AST, no wait - we are done
						if (DEBUG)
							System.out.println(DEBUG_PREFIX + getThreadName() + "returning null (WAIT_NO) for: " + tUnit.getElementName()); //$NON-NLS-1$
						return null;
					}
				}
				// no cached AST, but wait
				if (isActiveElement && isReconciling(tUnit)) {
					try {
						// Wait for AST
						if (DEBUG)
							System.out.println(DEBUG_PREFIX + getThreadName() + "waiting for AST for: " + tUnit.getElementName()); //$NON-NLS-1$
						fCacheMutex.wait();
						// Check whether active element is still valid
						if (fAST != null) {
							if (DEBUG)
								System.out.println(DEBUG_PREFIX + getThreadName() + "...got AST for: " + tUnit.getElementName()); //$NON-NLS-1$
							return fAST;
						}
						// try again
						continue;
					} catch (InterruptedException e) {
						return null; // thread has been interrupted don't compute AST
					}
				} else if (!wait) {
					return null;
				}
			}

			if (isActiveElement)
				aboutToBeReconciled(tUnit);

			if (DEBUG)
				System.err.println(DEBUG_PREFIX + getThreadName() + "creating AST for " + tUnit.getElementName()); //$NON-NLS-1$

			IASTTranslationUnit ast= null;
			try {
				ast= createAST(tUnit, index, progressMonitor);
				if (progressMonitor != null && progressMonitor.isCanceled())
					ast= null;
				else if (DEBUG && ast != null)
					System.err.println(DEBUG_PREFIX + getThreadName() + "created AST for: " + tUnit.getElementName()); //$NON-NLS-1$
			} finally {
				if (isActiveElement) {
					if (fAST != null) {
						if (DEBUG)
							System.out.println(DEBUG_PREFIX + getThreadName() + "Ignore created AST for " + tUnit.getElementName() + "- AST from reconciler is newer"); //$NON-NLS-1$ //$NON-NLS-2$
						// other reconciler was faster, still need to trigger notify
						reconciled(fAST, tUnit);
					} else
						reconciled(ast, tUnit);
				}
			}
			return ast;
		}
	}

	/**
	 * Executes {@link ASTRunnable#runOnAST(ILanguage, IASTTranslationUnit)} with the AST
	 * provided by this cache for the given translation unit. Handles acquiring
	 * and releasing the index read-lock for the client.
	 *
	 * @param tUnit
	 *            the translation unit
	 * @param wait
	 *            <code>true</code> if the AST should be computed or waited
	 *            upon in case it is not yet available
	 * @param monitor  a progress monitor, may be <code>null</code>
	 * @param astRunnable  the runnable taking the AST
	 * @return the status returned by the ASTRunnable
	 */
	public IStatus runOnAST(ITranslationUnit tUnit, boolean wait, IProgressMonitor monitor,
			ASTRunnable astRunnable) {
		IIndex index;
		try {
			index = CCorePlugin.getIndexManager().getIndex(tUnit.getCProject(),
					IIndexManager.ADD_EXTENSION_FRAGMENTS_EDITOR);
			index.acquireReadLock();
		} catch (CoreException e) {
			return e.getStatus();
		} catch (InterruptedException e) {
			return Status.CANCEL_STATUS;
		}

		try {
			IASTTranslationUnit ast= acquireSharedAST(tUnit, index, wait, monitor);
			ILanguage lang= (tUnit instanceof TranslationUnit) ? ((TranslationUnit) tUnit).getLanguageOfContext() : tUnit.getLanguage();
			if (ast == null) {
				return astRunnable.runOnAST(lang, ast);
			}
			try {
				return astRunnable.runOnAST(lang, ast);
			} finally {
				releaseSharedAST(ast);
			}
		} catch (CoreException e) {
			return e.getStatus();
		} finally {
			index.releaseReadLock();
		}
	}

	/**
	 * Returns a shared AST for the given translation unit and locks it for
	 * exclusive access. An AST obtained from this method has to be released
	 * by calling {@link #releaseSharedAST(IASTTranslationUnit)}.
	 * Subsequent call to this method will block until the AST is released.
	 * <p>
	 * The AST can be released by a thread other than the one that acquired it.
	 * <p>
	 * Clients are not allowed to modify the AST and must hold an index read
	 * lock prior to calling this method and continue to hold the lock as long
	 * as the AST is being used.
	 * </p>
	 *
	 * @param tUnit				the translation unit
	 * @param index				the index used to create the AST, needs to be read-locked
	 * @param wait				if <code>true</code>, wait for AST to be computed
	 *                          (might compute a new AST)
	 * @param progressMonitor	the progress monitor or <code>null</code>
	 * @return					the AST or <code>null</code> if the AST is not available
	 */
	public final IASTTranslationUnit acquireSharedAST(ITranslationUnit tUnit, IIndex index,
			boolean wait, IProgressMonitor progressMonitor) {
		IASTTranslationUnit ast= getAST(tUnit, index, wait, progressMonitor);
		if (ast != null) {
			try {
				((ASTTranslationUnit) ast).beginExclusiveAccess();
			} catch (InterruptedException e) {
				throw new OperationCanceledException();
			}
		}
		return ast;
	}

	/**
	 * Releases a shared AST previously acquired by calling
	 * {@link #acquireSharedAST(ITranslationUnit, IIndex, boolean, IProgressMonitor)}.
	 * <p>
	 * Can be called by a thread other than the one that acquired the AST.
	*
	 * @param ast   the AST to release.
	 */
	public final void releaseSharedAST(IASTTranslationUnit ast) {
		((ASTTranslationUnit) ast).endExclusiveAccess();
	}

	/**
	 * Caches the given AST for the given translation unit.
	 *
	 * @param ast    the AST
	 * @param tUnit  the translation unit
	 */
	private void cache(IASTTranslationUnit ast, ITranslationUnit tUnit) {
		assert Thread.holdsLock(fCacheMutex);
		if (fActiveTU != null && !fActiveTU.equals(tUnit)) {
			if (DEBUG && tUnit != null) // don't report call from disposeAST()
				System.out.println(DEBUG_PREFIX + getThreadName() + "don't cache AST for inactive: " + toString(tUnit)); //$NON-NLS-1$
			return;
		}

		if (DEBUG && (tUnit != null || ast != null)) // don't report call from disposeAST()
			System.out.println(DEBUG_PREFIX + getThreadName() + "caching AST: " + toString(ast) + " for: " + toString(tUnit)); //$NON-NLS-1$ //$NON-NLS-2$

		if (fAST != null)
			disposeAST();

		fAST= ast;
		fLastWriteOnIndex= fAST == null ? 0 : fAST.getIndex().getLastWriteAccess();

		// Signal AST change
		fCacheMutex.notifyAll();
	}

	/**
	 * Disposes the cached AST.
	 */
	public void disposeAST() {
		synchronized (fCacheMutex) {
			if (fAST == null)
				return;

			if (DEBUG)
				System.out.println(DEBUG_PREFIX + getThreadName() + "disposing AST: " + toString(fAST) + " for: " + toString(fActiveTU)); //$NON-NLS-1$ //$NON-NLS-2$

			fAST= null;
			cache(null, null);
		}
	}

	/**
	 * Creates a new translation unit AST.
	 *
	 * @param tUnit  the translation unit for which to create the AST
	 * @param index  the index for AST generation, needs to be read-locked.
	 * @param progressMonitor  a progress monitor, may be <code>null</code>
	 * @return an AST for the translation unit, or <code>null</code> if the operation was cancelled
	 */
	public IASTTranslationUnit createAST(final ITranslationUnit tUnit, final IIndex index, final IProgressMonitor progressMonitor) {
		if (progressMonitor != null && progressMonitor.isCanceled())
			return null;

		final IASTTranslationUnit root[]= new IASTTranslationUnit[1];

		SafeRunner.run(new ISafeRunnable() {
			@Override
			public void run() throws CoreException {
				try {
					if (progressMonitor != null && progressMonitor.isCanceled()) {
						root[0]= null;
					} else {
						root[0]= tUnit.getAST(index, fParseMode);
					}
				} catch (OperationCanceledException ex) {
					root[0]= null;
				}
			}
			@Override
			public void handleException(Throwable ex) {
				IStatus status= new Status(IStatus.ERROR, CCorePlugin.PLUGIN_ID, IStatus.OK, "Error in CDT Core during AST creation", ex);  //$NON-NLS-1$
				CCorePlugin.getDefault().getLog().log(status);
			}
		});

		return root[0];
	}

	/**
	 * Set the given translation unit as active element to cache an AST for.
	 *
	 * @param tUnit  the translation unit
	 */
	public void setActiveElement(ITranslationUnit tUnit) {
		if (tUnit == fActiveTU) {
			return;
		}
		synchronized (fCacheMutex) {
			fIsReconciling= false;
			fActiveTU= tUnit;
			cache(null, tUnit);
		}
		if (DEBUG)
			System.out.println(DEBUG_PREFIX + getThreadName() + "active element is: " + toString(tUnit)); //$NON-NLS-1$
	}

	/**
	 * Check whether the given translation unit is the active element of this cache.
	 *
	 * @param tUnit  the translation unit
	 * @return  <code>true</code>, if this cache manages the given translation unit
	 */
	public boolean isActiveElement(ITranslationUnit tUnit) {
		synchronized (fCacheMutex) {
			return fActiveTU != null && fActiveTU.equals(tUnit);
		}
	}

	/**
	 * Informs that reconciling (computation of the AST) for the given element
	 * is about to be started.
	 *
	 * @param tUnit  the translation unit
	 */
	public void aboutToBeReconciled(ITranslationUnit tUnit) {
		if (tUnit == null)
			return;

		synchronized (fCacheMutex) {
			if (fActiveTU == null || !fActiveTU.equals(tUnit)) {
				return;
			}

			if (DEBUG)
				System.out.println(DEBUG_PREFIX + getThreadName() + "about to reconcile: " + toString(tUnit)); //$NON-NLS-1$

			fIsReconciling= true;
			cache(null, tUnit);
		}
	}

	/**
	 * Informs that reconciling of the AST of the given translation unit has finished.
	 *
	 * @param ast  the translation unit AST
	 * @param tUnit  the translation unit
	 */
	public void reconciled(IASTTranslationUnit ast, ITranslationUnit tUnit) {
		synchronized (fCacheMutex) {
			if (tUnit == null || !tUnit.equals(fActiveTU)) {
				if (DEBUG)
					System.out.println(DEBUG_PREFIX + getThreadName() + "ignoring AST of out-dated element"); //$NON-NLS-1$
				return;
			}
			if (DEBUG)
				System.out.println(DEBUG_PREFIX + getThreadName() + "reconciled: " + toString(tUnit) + ", AST: " + toString(ast)); //$NON-NLS-1$ //$NON-NLS-2$

			fIsReconciling= false;
			cache(ast, tUnit);
		}
	}

	/**
	 * Tells whether the given C element is the one
	 * reported as currently being reconciled.
	 *
	 * @param tUnit  the translation unit
	 * @return <code>true</code> if reported as currently being reconciled
	 */
	public boolean isReconciling(ITranslationUnit tUnit) {
		synchronized (fCacheMutex) {
			if (fActiveTU == null || tUnit == null) {
				return false;
			}
			return fIsReconciling && (fActiveTU.equals(tUnit));
		}
	}

	private static String getThreadName() {
		String name= Thread.currentThread().getName();
		if (name != null)
			return name + ": "; //$NON-NLS-1$
		else
			return Thread.currentThread().toString() + ": "; //$NON-NLS-1$
	}

	/**
	 * Returns a string for the given C element used for debugging.
	 *
	 * @param tUnit  the translation unit
	 * @return a string used for debugging
	 */
	private static String toString(ITranslationUnit tUnit) {
		if (tUnit == null)
			return "null"; //$NON-NLS-1$
		else
			return tUnit.getElementName();
	}

	/**
	 * Returns a string for the given AST used for debugging.
	 *
	 * @param ast  the translation unit AST
	 * @return a string used for debugging
	 */
	private static String toString(IASTTranslationUnit ast) {
		if (ast == null)
			return "null"; //$NON-NLS-1$

		return ast.getFilePath();
	}
}
