/*******************************************************************************
 * Copyright (c) 2007, 2008 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:
 *    Markus Schorn - initial API and implementation
 *******************************************************************************/ 
package org.eclipse.cdt.internal.core.pdom;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.IASTDeclSpecifier;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIncludeStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorMacroDefinition;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorUndefStatement;
import org.eclipse.cdt.core.dom.ast.IASTProblem;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.ICompositeType;
import org.eclipse.cdt.core.dom.ast.IEnumeration;
import org.eclipse.cdt.core.dom.ast.IProblemBinding;
import org.eclipse.cdt.core.dom.ast.ITypedef;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTUsingDirective;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassTemplate;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPNamespace;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPNamespaceAlias;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTCompositeTypeSpecifier.ICPPASTBaseSpecifier;
import org.eclipse.cdt.core.index.IIndexFileLocation;
import org.eclipse.cdt.core.parser.IProblem;
import org.eclipse.cdt.internal.core.dom.parser.ASTInternal;
import org.eclipse.cdt.internal.core.index.IIndexFragmentFile;
import org.eclipse.cdt.internal.core.index.IWritableIndex;
import org.eclipse.cdt.internal.core.index.IWritableIndex.IncludeInformation;
import org.eclipse.cdt.internal.core.parser.scanner.LocationMap;
import org.eclipse.cdt.internal.core.pdom.dom.PDOMASTAdapter;
import org.eclipse.cdt.internal.core.pdom.dom.PDOMNotImplementedError;
import org.eclipse.cdt.internal.core.pdom.indexer.IndexerASTVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;

/**
 * Abstract class to write information from AST 
 * @since 4.0
 */
abstract public class PDOMWriter {
	public static int SKIP_ALL_REFERENCES= -1;
	public static int SKIP_TYPE_REFERENCES=  1;
	public static int SKIP_MACRO_REFERENCES= 2;
	public static int SKIP_NO_REFERENCES= 0;
	
	private static class Symbols {
		ArrayList<IASTName[]> fNames= new ArrayList<IASTName[]>();
		ArrayList<IASTPreprocessorStatement> fMacros= new ArrayList<IASTPreprocessorStatement>();
		ArrayList<IASTPreprocessorIncludeStatement> fIncludes= new ArrayList<IASTPreprocessorIncludeStatement>();
	}
	private boolean fShowProblems;
	private boolean fShowInclusionProblems;
	private boolean fShowScannerProblems;
	private boolean fShowSyntaxProblems;
	protected boolean fShowActivity;
	protected final IndexerStatistics fStatistics;
	protected final IndexerInputAdapter fResolver;
	
	private IndexerProgress fInfo= new IndexerProgress();
	private int fSkipReferences= SKIP_NO_REFERENCES;
	
	public PDOMWriter(IndexerInputAdapter resolver) {
		fStatistics= new IndexerStatistics();
		fResolver= resolver;
	}
	
	public void setShowActivity(boolean val) {
		fShowActivity= val;
	}
	
	public void setShowInclusionProblems(boolean val) {
		fShowInclusionProblems= val;
	}

	public void setShowScannerProblems(boolean val) {
		fShowScannerProblems= val;
	}

	public void setShowSyntaxProblems(boolean val) {
		fShowSyntaxProblems= val;
	}

	public void setShowProblems(boolean val) {
		fShowProblems= val;
	}
	
	/**
	 * Determines whether references are skipped or not. Provide one of 
	 * {@link #SKIP_ALL_REFERENCES}, {@link #SKIP_NO_REFERENCES} or a combination of
	 * {@link #SKIP_TYPE_REFERENCES} or {@link #SKIP_MACRO_REFERENCES}.
	 */
	public void setSkipReferences(int options) {
		fSkipReferences= options;
	}
	
	public int getSkipReferences() {
		return fSkipReferences;
	}
			
	/**
	 * Extracts symbols from the given ast and adds them to the index. 
	 * 
	 * When flushIndex is set to <code>false</code>, you must make sure to flush the 
	 * index after your last write operation.
	 * @since 4.0
	 */
	public void addSymbols(IASTTranslationUnit ast, IIndexFileLocation[] ifls, IWritableIndex index, 
			int readlockCount, boolean flushIndex, int configHash, ITodoTaskUpdater taskUpdater, IProgressMonitor pm) 
			throws InterruptedException, CoreException {
		if (fShowProblems) {
			fShowInclusionProblems= true;
			fShowScannerProblems= true;
			fShowSyntaxProblems= true;
		}
		final Map<IIndexFileLocation, Symbols> symbolMap= new HashMap<IIndexFileLocation, Symbols>();
		for (IIndexFileLocation ifl : ifls) {
			prepareInMap(symbolMap, ifl);
		}
		ArrayList<IStatus> stati= new ArrayList<IStatus>();

		HashSet<IASTPreprocessorIncludeStatement> contextIncludes= new HashSet<IASTPreprocessorIncludeStatement>();
		extractSymbols(ast, symbolMap, contextIncludes);
			
		// name resolution
		resolveNames(symbolMap, ifls, stati, pm);

		// index update
		storeSymbolsInIndex(symbolMap, ifls, ast.getLinkage().getLinkageID(), configHash, contextIncludes, index, readlockCount, flushIndex,	stati, pm);

		if (taskUpdater != null) {
			taskUpdater.updateTasks(ast.getComments(), ifls);
		}
		if (!stati.isEmpty()) {
			String path= null;
			if (ifls.length > 0) {
				path= ifls[ifls.length-1].getURI().getPath();
			}
			else {
				path= ast.getFilePath().toString();
			}
			String msg= NLS.bind(Messages.PDOMWriter_errorWhileParsing, path);
			if (stati.size() == 1) {
				IStatus status= stati.get(0);
				if (msg.equals(status.getMessage())) {
					throw new CoreException(status);
				}
				throw new CoreException(new Status(status.getSeverity(), status.getPlugin(), status.getCode(), 
						msg + ':' + status.getMessage(), status.getException()));
			}
			throw new CoreException(new MultiStatus(CCorePlugin.PLUGIN_ID, 0, 
					stati.toArray(new IStatus[stati.size()]), msg, null));
		}
	}

	private void storeSymbolsInIndex(final Map<IIndexFileLocation, Symbols> symbolMap, IIndexFileLocation[] ifls, int linkageID, int configHash,
			HashSet<IASTPreprocessorIncludeStatement> contextIncludes, IWritableIndex index, int readlockCount, boolean flushIndex,
			ArrayList<IStatus> stati, IProgressMonitor pm) throws InterruptedException, CoreException {
		index.acquireWriteLock(readlockCount);
		long start= System.currentTimeMillis();
		try {
			for (int i=0; i<ifls.length; i++) {
				if (pm.isCanceled()) 
					return;

				final IIndexFileLocation ifl= ifls[i];
				if (ifl != null) {
					if (fShowActivity) {
						System.out.println("Indexer: adding " + ifl.getURI());  //$NON-NLS-1$
					}
					Throwable th= null;
					try {
						storeFileInIndex(index, ifl, symbolMap, linkageID, configHash, contextIncludes);
					} catch (RuntimeException e) {
						th= e; 
					} catch (PDOMNotImplementedError e) {
						th= e; 
					} catch (StackOverflowError e) {
						th= e;
					}
					if (th != null) {
						stati.add(CCorePlugin.createStatus(
								NLS.bind(Messages.PDOMWriter_errorWhileParsing, ifl.getURI().getPath()), th));
					}
					if (i<ifls.length-1) {
						updateFileCount(0, 0, 1); // update header count
					}
				}
			}
		} finally {
			index.releaseWriteLock(readlockCount, flushIndex);
		}
		fStatistics.fAddToIndexTime+= System.currentTimeMillis()-start;
	}

	private void resolveNames(final Map<IIndexFileLocation, Symbols> symbolMap, IIndexFileLocation[] ifls, ArrayList<IStatus> stati, IProgressMonitor pm) {
		long start= System.currentTimeMillis();
		for (IIndexFileLocation path : ifls) {
			if (pm.isCanceled()) {
				return;
			}
			Symbols symbols= symbolMap.get(path);

			final ArrayList<IASTName[]> names= symbols.fNames;
			boolean reported= false;
			for (Iterator<IASTName[]> j = names.iterator(); j.hasNext();) {
				final IASTName[] na= j.next();
				final IASTName name = na[0];
				if (name != null) { // should not be null, just be defensive.
					Throwable th= null;
					try {
						final IBinding binding = name.resolveBinding();
						if (binding instanceof IProblemBinding) {
							fStatistics.fProblemBindingCount++;
							if (fShowProblems) {
								reportProblem((IProblemBinding) binding);
							}
						}
						else if (name.isReference()) {
							if (fSkipReferences == SKIP_TYPE_REFERENCES) {
								if (isTypeReferenceBinding(binding) && !isRequiredReference(name)) {
									na[0]= null;
									fStatistics.fReferenceCount--;
								}
							}
							fStatistics.fReferenceCount++;
						}
						else {
							fStatistics.fDeclarationCount++;
						}
					} catch (RuntimeException e) {
						th= e;
					} catch (PDOMNotImplementedError e) {
						th= e;
					} catch (StackOverflowError e) {
						th= e;
					}
					if (th != null) {
						if (!reported) {
							stati.add(CCorePlugin.createStatus(
									NLS.bind(Messages.PDOMWriter_errorResolvingName, name.toString(), path.getURI().getPath()), th));
						}
						reported= true;
						j.remove();
					} 
				}
			}
		}
		fStatistics.fResolutionTime += System.currentTimeMillis()-start;
	}

	private void extractSymbols(IASTTranslationUnit ast, final Map<IIndexFileLocation, Symbols> symbolMap, Collection<IASTPreprocessorIncludeStatement> contextIncludes) throws CoreException {
		
		final HashSet<IIndexFileLocation> contextIFLs= new HashSet<IIndexFileLocation>();
		final IIndexFileLocation astIFL = fResolver.resolveASTPath(ast.getFilePath());
		
		int unresolvedIncludes= 0;
		IASTPreprocessorStatement[] stmts = ast.getAllPreprocessorStatements();
		for (final IASTPreprocessorStatement stmt : stmts) {
			// includes
			if (stmt instanceof IASTPreprocessorIncludeStatement) {
				IASTPreprocessorIncludeStatement include= (IASTPreprocessorIncludeStatement) stmt;

				final IASTFileLocation astLoc= include.getFileLocation();
				final IIndexFileLocation sourceIFL= astLoc != null ? fResolver.resolveASTPath(astLoc.getFileName()) : astIFL; // command-line includes
				final boolean updateSource= symbolMap.containsKey(sourceIFL);
				if (updateSource) {
					addToMap(symbolMap, sourceIFL, include);
				}
				if (include.isActive()) {
					if (!include.isResolved()) {
						unresolvedIncludes++;
					}
					else if (updateSource) {
						// the include was parsed, check if we want to update the included file in the index
						final IIndexFileLocation targetIFL= fResolver.resolveASTPath(include.getPath());
						if (symbolMap.containsKey(targetIFL) && contextIFLs.add(targetIFL)) {
							contextIncludes.add(include);
						}
					}
				}
			}
			// macros
			else if (stmt instanceof IASTPreprocessorMacroDefinition || stmt instanceof IASTPreprocessorUndefStatement) {
				IASTFileLocation sourceLoc = stmt.getFileLocation();
				if (sourceLoc != null) { // skip built-ins and command line macros
					IIndexFileLocation path2 = fResolver.resolveASTPath(sourceLoc.getFileName());
					addToMap(symbolMap, path2, stmt);
				}
			}
		}
		
		// names
		final IndexerASTVisitor visitor = new IndexerASTVisitor() {
			@Override
			public void visit(IASTName name, IASTName caller) {
				if (fSkipReferences == SKIP_ALL_REFERENCES) {
					if (name.isReference()) {
						if (!isRequiredReference(name)) {
							return;
						}
					}
				}
					
				// assign a location to anonymous types.
				name= PDOMASTAdapter.getAdapterIfAnonymous(name);
				if (name != null) {
					IASTFileLocation nameLoc = name.getFileLocation();
					if (nameLoc != null) {
						IIndexFileLocation location = fResolver.resolveASTPath(nameLoc.getFileName());
						addToMap(symbolMap, location, new IASTName[]{name, caller});
					}
				}
			}
		};
		ast.accept(visitor);
		
		if ((fSkipReferences & SKIP_MACRO_REFERENCES) == 0) {
			LocationMap lm= (LocationMap) ast.getAdapter(LocationMap.class);
			if (lm != null) {
				IASTName[] refs= lm.getMacroReferences();
				for (IASTName name : refs) {
					IASTFileLocation nameLoc = name.getFileLocation();
					if (nameLoc != null) {
						IIndexFileLocation location = fResolver.resolveASTPath(nameLoc.getFileName());
						addToMap(symbolMap, location, new IASTName[]{name, null});
					}
				}
			}
		}
		
		fStatistics.fUnresolvedIncludesCount += unresolvedIncludes;
		fStatistics.fPreprocessorProblemCount+= ast.getPreprocessorProblemsCount() - unresolvedIncludes;
		if (fShowScannerProblems || fShowInclusionProblems) {
			final boolean reportAll= fShowScannerProblems && fShowInclusionProblems;
			IASTProblem[] scannerProblems= ast.getPreprocessorProblems();
			for (IASTProblem problem : scannerProblems) {
				if (reportAll || (problem.getID() == IProblem.PREPROCESSOR_INCLUSION_NOT_FOUND) == fShowInclusionProblems) {
					reportProblem(problem);
				}
			}
		}

		final List<IASTProblem> problems= visitor.getProblems();
		fStatistics.fSyntaxProblemsCount+= problems.size();
		if (fShowSyntaxProblems) {
			for (IASTProblem problem : problems) {
				reportProblem(problem); 
			}
		}
	}
	
	protected final boolean isRequiredReference(IASTName name) {
		IASTNode parentNode= name.getParent();
		if (parentNode instanceof ICPPASTBaseSpecifier) {
			return true;
		}
		else if (parentNode instanceof IASTDeclSpecifier) {
			IASTDeclSpecifier ds= (IASTDeclSpecifier) parentNode;
			return ds.getStorageClass() == IASTDeclSpecifier.sc_typedef;
		}
		else if (parentNode instanceof ICPPASTUsingDirective) {
			return true;
		}
		return false;
	}

	private boolean isTypeReferenceBinding(IBinding binding) {
		if (binding instanceof ICompositeType ||
				binding instanceof IEnumeration ||
				binding instanceof ITypedef ||
				binding instanceof ICPPNamespace ||
				binding instanceof ICPPNamespaceAlias ||
				binding instanceof ICPPClassTemplate) {
			return true;
		}
		return false;
	}

	private void addToMap(Map<IIndexFileLocation, Symbols> map, IIndexFileLocation location, IASTName[] thing) {
		Symbols lists= map.get(location);
		if (lists != null) 
			lists.fNames.add(thing);
	}		

	private void addToMap(Map<IIndexFileLocation, Symbols> map, IIndexFileLocation location, IASTPreprocessorIncludeStatement thing) {
		Symbols lists= map.get(location);
		if (lists != null) 
			lists.fIncludes.add(thing);
	}		

	private void addToMap(Map<IIndexFileLocation, Symbols> map, IIndexFileLocation location, IASTPreprocessorStatement thing) {
		Symbols lists= map.get(location);
		if (lists != null) 
			lists.fMacros.add(thing);
	}		
	
	private boolean prepareInMap(Map<IIndexFileLocation, Symbols> map, IIndexFileLocation location) {
		if (map.get(location) == null) {
			map.put(location, new Symbols());
		}
		return false;
	}

	private IIndexFragmentFile storeFileInIndex(IWritableIndex index, IIndexFileLocation location, Map<IIndexFileLocation, Symbols> symbolMap, 
			int linkageID, int configHash, Set<IASTPreprocessorIncludeStatement> contextIncludes) throws CoreException {
		Set<IIndexFileLocation> clearedContexts= Collections.emptySet();
		IIndexFragmentFile file= index.getWritableFile(linkageID, location);
		if (file != null) {
			clearedContexts= new HashSet<IIndexFileLocation>();
			index.clearFile(file, clearedContexts);
		} else {
			file= index.addFile(linkageID, location);
		}
		file.setTimestamp(fResolver.getLastModified(location));
		file.setScannerConfigurationHashcode(configHash);
		Symbols lists= symbolMap.get(location);
		if (lists != null) {
			IASTPreprocessorStatement[] macros= lists.fMacros.toArray(new IASTPreprocessorStatement[lists.fMacros.size()]);
			IASTName[][] names= lists.fNames.toArray(new IASTName[lists.fNames.size()][]);
			for (IASTName[] name2 : names) {
				final IASTName name= name2[0];
				if (name != null) {
					ASTInternal.setFullyResolved(name.getBinding(), true);
				}
			}

			IncludeInformation[] includeInfos= new IncludeInformation[lists.fIncludes.size()];
			for (int i=0; i<lists.fIncludes.size(); i++) {
				final IASTPreprocessorIncludeStatement include = lists.fIncludes.get(i);
				final IncludeInformation info= includeInfos[i]= new IncludeInformation();
				info.fStatement= include;
				if (include.isResolved()) {
					info.fLocation= fResolver.resolveASTPath(include.getPath());
					info.fIsContext= include.isActive() && 
						(contextIncludes.contains(include) || clearedContexts.contains(info.fLocation));
				}
			}
			index.setFileContent(file, linkageID, includeInfos, macros, names, fResolver);
		}
		return file;
	}
	
	/**
	 * Makes a copy of the current progress information and returns it.
	 * @since 4.0
	 */
	public IndexerProgress getProgressInformation() {
		synchronized (fInfo) {
			return new IndexerProgress(fInfo);
		}
	}

	/**
	 * Updates current progress information with the provided delta.
	 */
	protected final void updateFileCount(int sources, int primaryHeader, int header) {
		synchronized(fInfo) {
			fInfo.fCompletedSources+= sources;
			fInfo.fPrimaryHeaderCount+= primaryHeader;
			fInfo.fCompletedHeaders+= header;
		}
	}
	
	/**
	 * Updates current progress information with the provided delta.
	 */
	protected final void updateRequestedFiles(int delta) {
		synchronized(fInfo) {
			fInfo.fRequestedFilesCount+= delta;
		}
	}
		
	private String getLocationInfo(String filename, int lineNumber) {
		return " at " + filename + "(" + lineNumber + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}

	private void reportProblem(IProblemBinding problem) {
		String msg= "Indexer: unresolved name" + getLocationInfo(problem.getFileName(), problem.getLineNumber()); //$NON-NLS-1$
		String pmsg= problem.getMessage();
		if (pmsg != null && pmsg.length() > 0) 
			msg+= "; " + problem.getMessage(); //$NON-NLS-1$
		System.out.println(msg);
	}
	
	private void reportProblem(IASTProblem problem) {
		String msg= "Indexer: " + problem.getMessageWithLocation(); //$NON-NLS-1$
		System.out.println(msg);
	}

}
