/*******************************************************************************
 * Copyright (c) 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:
 *     Anton Leherbauer (Wind River Systems) - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.text.contentassist;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.resources.IResourceProxyVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.swt.graphics.Image;

import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.IInclude;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.parser.IExtendedScannerInfo;
import org.eclipse.cdt.core.parser.IScannerInfo;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.text.ICPartitions;
import org.eclipse.cdt.ui.text.contentassist.ContentAssistInvocationContext;
import org.eclipse.cdt.ui.text.contentassist.ICompletionProposalComputer;

import org.eclipse.cdt.internal.ui.viewsupport.CElementImageProvider;

/**
 * A proposal computer for include directives.
 *
 * @since 5.0
 */
public class InclusionProposalComputer implements ICompletionProposalComputer {

	private String fErrorMessage;

	public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) {
		List<ICompletionProposal> proposals= Collections.emptyList();
		fErrorMessage= null;
		
		if (context instanceof CContentAssistInvocationContext) {
			CContentAssistInvocationContext cContext= (CContentAssistInvocationContext) context;
			if (inIncludeDirective(cContext)) {
				// add include file proposals
				proposals= new ArrayList<ICompletionProposal>();
				try {
					addInclusionProposals(cContext, proposals);
				} catch (Exception exc) {
					fErrorMessage= exc.getMessage();
					CUIPlugin.log(exc);
				}
			}
		}
		return proposals;
	}

	public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context, IProgressMonitor monitor) {
		return null;
	}

	public String getErrorMessage() {
		return fErrorMessage;
	}

	public void sessionEnded() {
	}

	public void sessionStarted() {
	}

	/**
	 * Test whether the invocation offset is inside the file name part if an include directive.
	 * 
	 * @param context  the invocation context
	 * @return <code>true</code> if the invocation offset is inside or before the directive keyword
	 */
	private boolean inIncludeDirective(CContentAssistInvocationContext context) {
		IDocument doc = context.getDocument();
		int offset = context.getInvocationOffset();
		
		try {
			final ITypedRegion partition= TextUtilities.getPartition(doc, ICPartitions.C_PARTITIONING, offset, true);
			if (ICPartitions.C_PREPROCESSOR.equals(partition.getType())) {
				String ppPrefix= doc.get(partition.getOffset(), offset - partition.getOffset());
				if (ppPrefix.matches("\\s*#\\s*include\\s*[\"<][^\">]*")) { //$NON-NLS-1$
					// we are inside the file name part of the include directive
					return true;
				}
			}
			
		} catch (BadLocationException exc) {
		}
		return false;
	}

	private void addInclusionProposals(CContentAssistInvocationContext context, List<ICompletionProposal> proposals) throws Exception {
		if (context.isContextInformationStyle()) {
			return;
		}
		String prefix;
		boolean angleBrackets= false;
		prefix = computeIncludePrefix(context);
		if (prefix.length() > 0) {
			angleBrackets= prefix.charAt(0) == '<';
			prefix= prefix.substring(1);
		}
		IPath prefixPath= new Path(prefix);
		final ITranslationUnit tu= context.getTranslationUnit();
		String[] potentialIncludes= collectIncludeFiles(tu, prefixPath, angleBrackets);
		if (potentialIncludes.length > 0) {
			IInclude[] includes= context.getTranslationUnit().getIncludes();
			Set<String> alreadyIncluded= new HashSet<String>();
			for (IInclude includeDirective : includes) {
				alreadyIncluded.add(includeDirective.getElementName());
			}
			Image image = getImage(CElementImageProvider.getIncludeImageDescriptor());
			for (String include : potentialIncludes) {
				if (alreadyIncluded.add(include)) {
					final char openingBracket= angleBrackets ? '<' : '"';
					final char closingBracket= angleBrackets ? '>' : '"';
					String repString= openingBracket + include;
					final String dispString= repString + closingBracket;
					int repLength = prefix.length() + 1;
					int repOffset= context.getInvocationOffset() - repLength;
					final boolean needClosingBracket= context.getDocument().getChar(repOffset + repLength) != closingBracket;
					if (needClosingBracket) {
						repString += closingBracket;
					}
					final boolean isDir= include.endsWith("/"); //$NON-NLS-1$
					final int relevance= computeRelevance(prefix, include) + (isDir ? 0 : 1);
					final CCompletionProposal proposal= createProposal(repOffset, repLength, repString, dispString, image, relevance, context);
					if (!isDir && !needClosingBracket) {
						// put cursor behind closing bracket
						proposal.setCursorPosition(repString.length() + 1);
					}
					proposals.add(proposal);
				}
			}
		}
	}
	
	/**
	 * Collect potential include files for the given translation unit.
	 * 
	 * @param tu  the translation unit to include the file
	 * @param prefixPath  the path part to match the sub-directory and file name
	 * @param angleBrackets  whether angle brackets enclose the include file name
	 * @return an array of incude file names
	 * @throws CoreException
	 */
	private String[] collectIncludeFiles(final ITranslationUnit tu, IPath prefixPath, boolean angleBrackets) throws CoreException {
		final List<String> includeFiles= new ArrayList<String>();
		if (!angleBrackets) {
			// search in current directory
			IResource resource= tu.getResource();
			if (resource != null) {
				IContainer parent= resource.getParent();
				collectIncludeFilesFromContainer(tu, parent, prefixPath, includeFiles);
			} else {
				IPath location= tu.getLocation();
				if (location != null) {
					collectIncludeFilesFromDirectory(tu, location.removeLastSegments(1), prefixPath, includeFiles);
				}
			}
		}
		IScannerInfo info= tu.getScannerInfo(true);
		if (info != null) {
			collectIncludeFilesFromScannerInfo(tu, info, prefixPath, angleBrackets, includeFiles);
		}
		return includeFiles.toArray(new String[includeFiles.size()]);
	}

	/**
	 * @param tu  the translation unit to include the file
	 * @param info  the scanner info for this translation unit
	 * @param prefixPath  the path part to match the sub-directory and file name
	 * @param angleBrackets  whether angle brackets enclose the include file name
	 * @param includeFiles  the result list
	 */
	private void collectIncludeFilesFromScannerInfo(ITranslationUnit tu, IScannerInfo info, IPath prefixPath, boolean angleBrackets, List<String> includeFiles) {
		if (!angleBrackets && info instanceof IExtendedScannerInfo) {
			IExtendedScannerInfo extendedInfo= (IExtendedScannerInfo) info;
			String[] quoteIncludes= extendedInfo.getLocalIncludePath();
			
			if (quoteIncludes != null) {
				for (String quoteInclude : quoteIncludes) {
					IPath includeDir= new Path(quoteInclude);
					collectIncludeFilesFromDirectory(tu, includeDir, prefixPath, includeFiles);
				}
			}
		}
		
		String[] allIncludes= info.getIncludePaths();
		for (String allInclude : allIncludes) {
			IPath includeDir= new Path(allInclude);
			collectIncludeFilesFromDirectory(tu, includeDir, prefixPath, includeFiles);
		}
	}

	/**
	 * Collect include files from the given file system directory.
	 * 
	 * @param tu  the translation unit to include the file
	 * @param directory  the file system path of the directory
	 * @param prefixPath  the path part to match the sub-directory and file name
	 * @param includeFiles  the result list
	 */
	private void collectIncludeFilesFromDirectory(ITranslationUnit tu, IPath directory, IPath prefixPath, List<String> includeFiles) {
		final String namePrefix;
		if (prefixPath.segmentCount() == 0) {
			namePrefix= ""; //$NON-NLS-1$
		} else if (prefixPath.hasTrailingSeparator()) {
			namePrefix= ""; //$NON-NLS-1$
			prefixPath= prefixPath.removeTrailingSeparator();
			directory= directory.append(prefixPath);
		} else {
			namePrefix= prefixPath.lastSegment();
			prefixPath= prefixPath.removeLastSegments(1);
			if (prefixPath.segmentCount() > 0) {
				directory= directory.append(prefixPath);
			}
		}
		final File fileDir = directory.toFile();
		if (!fileDir.exists()) {
			return;
		}
		final int prefixLength = namePrefix.length();
		final IProject project= tu.getCProject().getProject();
		File[] files= fileDir.listFiles();
		for (File file : files) {
			final String name= file.getName();
			if (name.length() >= prefixLength && namePrefix.equalsIgnoreCase(name.substring(0, prefixLength))) {
				if (file.isFile()) {
					if (CoreModel.isValidCXXHeaderUnitName(project, name) || CoreModel.isValidCHeaderUnitName(project, name)) {
						includeFiles.add(prefixPath.append(name).toString());
					}
				} else if (file.isDirectory()) {
					includeFiles.add(prefixPath.append(name).addTrailingSeparator().toString());
				}
			}
		}
	}

	/**
	 * Collect include files from the given resource container.
	 * 
	 * @param tu  the translation unit to include the file
	 * @param parent  the resource container
	 * @param prefixPath  the path part to match the sub-directory and file name
	 * @param includeFiles  the result list
	 * @throws CoreException
	 */
	private void collectIncludeFilesFromContainer(final ITranslationUnit tu, IContainer parent, IPath prefixPath, final List<String> includeFiles) throws CoreException {
		final String namePrefix;
		if (prefixPath.segmentCount() == 0) {
			namePrefix= ""; //$NON-NLS-1$
		} else if (prefixPath.hasTrailingSeparator()) {
			namePrefix= ""; //$NON-NLS-1$
			prefixPath= prefixPath.removeTrailingSeparator();
			parent= parent.getFolder(prefixPath);
		} else {
			namePrefix= prefixPath.lastSegment();
			prefixPath= prefixPath.removeLastSegments(1);
			if (prefixPath.segmentCount() > 0) {
				parent= parent.getFolder(prefixPath);
			}
		}
		if (!parent.exists()) {
			return;
		}
		final IPath cPrefixPath= prefixPath;
		final int prefixLength = namePrefix.length();
		final IProject project= tu.getCProject().getProject();
		parent.accept(new IResourceProxyVisitor() {
			boolean fFirstVisit= true;
			public boolean visit(IResourceProxy proxy) throws CoreException {
				final int type= proxy.getType();
				final String name= proxy.getName();
				if (fFirstVisit) {
					fFirstVisit= false;
					return true;
				}
				if (name.length() >= prefixLength && namePrefix.equalsIgnoreCase(name.substring(0, prefixLength))) {
					if (type == IResource.FILE) {
						if (CoreModel.isValidCXXHeaderUnitName(project, name) || CoreModel.isValidCHeaderUnitName(project, name)) {
							includeFiles.add(cPrefixPath.append(name).toString());
						}
					} else if (type == IResource.FOLDER) {
						includeFiles.add(cPrefixPath.append(name).addTrailingSeparator().toString());
					}
				}
				return false;
			}}, IResource.DEPTH_ONE);
	}

	/**
	 * Compute the file name portion in an incomplete include directive.
	 * 
	 * @param context
	 * @return the file name portion including the opening bracket or quote
	 * @throws BadLocationException
	 */
	private String computeIncludePrefix(CContentAssistInvocationContext context) throws BadLocationException {
		IDocument document= context.getDocument();
		if (document == null)
			return null;
		int end= context.getInvocationOffset();
		int start= end;
		while (--start >= 0) {
			final char ch= document.getChar(start);
			if (ch == '"' || ch == '<')
				break;
		}
		return document.get(start, end - start);
	}


	/**
	 * Compute base relevance depending on quality of name / prefix match.
	 * 
	 * @param prefix  the completion pefix
	 * @param match  the matching identifier
	 * @return a relevance value inidicating the quality of the name match
	 */
	protected int computeRelevance(String prefix, String match) {
		int baseRelevance= 0;
		boolean caseMatch= prefix.length() > 0 && match.startsWith(prefix);
		if (caseMatch) {
			baseRelevance += RelevanceConstants.CASE_MATCH_RELEVANCE;
		}
		return baseRelevance;
	}

	private CCompletionProposal createProposal(int repOffset, int repLength, String repString, String dispString, Image image, int relevance, CContentAssistInvocationContext context) {
		return new CCompletionProposal(repString, repOffset, repLength, image, dispString, dispString, relevance, context.getViewer());
	}

	private Image getImage(ImageDescriptor desc) {
		return desc != null ? CUIPlugin.getImageDescriptorRegistry().get(desc) : null;
	}
	
}
