/*******************************************************************************
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is 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:
 *     Red Hat, Inc. - initial API and implementation
 ******************************************************************************/

package org.jboss.tools.seam.ui.search;

import java.util.ArrayList;
import java.util.regex.Pattern;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.ui.search.ISearchRequestor;
import org.eclipse.search.core.text.TextSearchMatchAccess;
import org.eclipse.search.internal.core.text.PatternConstructor;
import org.eclipse.search.internal.ui.Messages;
import org.eclipse.search.internal.ui.text.FileMatch;
import org.eclipse.search.internal.ui.text.LineElement;
import org.eclipse.search.internal.ui.text.SearchResultUpdater;
import org.eclipse.search.ui.ISearchQuery;
import org.eclipse.search.ui.ISearchResult;
import org.eclipse.search.ui.text.AbstractTextSearchResult;
import org.eclipse.search.ui.text.Match;
import org.jboss.tools.common.el.core.model.ELInvocationExpression;
import org.jboss.tools.common.java.IJavaSourceReference;
import org.jboss.tools.seam.core.ISeamDeclaration;
import org.jboss.tools.seam.core.ISeamProject;
import org.jboss.tools.seam.core.SeamCorePlugin;
import org.jboss.tools.seam.core.SeamCoreMessages;
import org.jboss.tools.seam.internal.core.el.SeamELCompletionEngine;

/**
 * Seam search query implementation
 * 
 * @author Jeremy
 */
public class SeamSearchQuery implements ISearchQuery {

	/**
	 * Result collector is used be a holder for search results
	 *  
	 * @author Jeremy
	 */
	public final static class SeamSearchResultCollector extends SeamSearchRequestor {
		private final AbstractTextSearchResult fResult;
		private ArrayList<Match> fCachedMatches;
		private final ISearchRequestor fParentRequestor; 
		
		public SeamSearchResultCollector(AbstractTextSearchResult result, ISearchRequestor parentRequestor) {
			fResult= result;
			fParentRequestor = parentRequestor;
		}
		
		public boolean acceptFile(IFile file) throws CoreException {
			flushMatches();
			return true;
		}
		
		/* (non-Javadoc)
		 * @see org.eclipse.search.core.text.TextSearchRequestor#reportBinaryFile(org.eclipse.core.resources.IFile)
		 */
		public boolean reportBinaryFile(IFile file) {
			return false;
		}

		public boolean acceptPatternMatch(TextSearchMatchAccess matchRequestor) throws CoreException {
			int matchOffset= matchRequestor.getMatchOffset();
			
			LineElement lineElement= getLineElement(matchOffset, matchRequestor);
			if (lineElement != null)
				fCachedMatches.add(new FileMatch(matchRequestor.getFile(), matchRequestor.getMatchOffset(), matchRequestor.getMatchLength(),lineElement));
			return true;
		}

		private LineElement getLineElement(int offset, TextSearchMatchAccess matchRequestor) {
			int lineNumber= 1;
			int lineStart= 0;
			if (!fCachedMatches.isEmpty()) {
				// match on same line as last?
				FileMatch last= (FileMatch) fCachedMatches.get(fCachedMatches.size() - 1);
				LineElement lineElement= last.getLineElement();
				if (lineElement.contains(offset)) {
					return lineElement;
				}
				// start with the offset and line information from the last match
				lineStart= lineElement.getOffset() + lineElement.getLength();
				lineNumber= lineElement.getLine() + 1;
			}
			if (offset < lineStart) {
				return null; // offset before the last line
			}
			
			int i= lineStart;
			int contentLength= matchRequestor.getFileContentLength();
			while (i < contentLength) {
				char ch= matchRequestor.getFileContentChar(i++);
				if (ch == '\n' || ch == '\r') {
					if (ch == '\r' && i < contentLength && matchRequestor.getFileContentChar(i) == '\n') {
						i++;
					}
					if (offset < i) {
						String lineContent= getContents(matchRequestor, lineStart, i); // include line delimiter
						return new LineElement(matchRequestor.getFile(), lineNumber, lineStart, lineContent);
					}
					lineNumber++;
					lineStart= i;
				}
			}
			if (offset < i) {
				String lineContent= getContents(matchRequestor, lineStart, i); // until end of file
				return new LineElement(matchRequestor.getFile(), lineNumber, lineStart, lineContent);
			}
			return null; // offset outside of range
		}
		
		private static String getContents(TextSearchMatchAccess matchRequestor, int start, int end) {
			StringBuffer buf= new StringBuffer();
			for (int i= start; i < end; i++) {
				char ch= matchRequestor.getFileContentChar(i);
				if (Character.isWhitespace(ch) || Character.isISOControl(ch)) {
					buf.append(' ');
				} else {
					buf.append(ch);
				}
			}
			return buf.toString();
		}
		public boolean acceptSeamDeclarationSourceReferenceMatch(IJavaSourceReference element) throws CoreException {
			fCachedMatches.add(new SeamElementMatch(element));
			return true;
		}
		
		public boolean acceptSeamDeclarationMatch(ISeamDeclaration element) throws CoreException {
			fCachedMatches.add(new SeamElementMatch(element));
			return true;
		}

		public void beginReporting() {
			fCachedMatches= new ArrayList();
		}
		
		public void endReporting() {
			flushMatches();
			fCachedMatches= null;
		}

		private void flushMatches() {
			if (!fCachedMatches.isEmpty()) {
				if (fResult != null) fResult.addMatches((Match[]) fCachedMatches.toArray(new Match[fCachedMatches.size()]));
				if (fParentRequestor != null) {
					for (Match match : fCachedMatches) {
						fParentRequestor.reportMatch(match);
					}
				}
				fCachedMatches.clear();
			}
		}
		
		public void reportMatch(Match match) {
			fCachedMatches.add(match);
		}

	}
	
	private ELInvocationExpression fTokens;
	private IJavaElement[] fJavaElements;
	private final SeamSearchScope fScope;
	private SeamSearchResult fResult;
	private IFile fSourceFile;
	private ISearchRequestor fParentRequestor; 
	
	/**
	 * Constructs Seam search query for a given {@link ELInvocationExpression} objects list
	 *  
	 * @param tokens
	 * @param sourceFile
	 * @param scope
	 */
	public SeamSearchQuery(ELInvocationExpression tokens, IFile sourceFile, SeamSearchScope scope) {
		fTokens = tokens;
		fJavaElements = null;
		fSourceFile = sourceFile;
		fScope= scope;
	}
	
	/**
	 * Constructs Seam search query for a given {@link IJavaElement} objects array
	 * 
	 * @param javaElements
	 * @param sourceFile
	 * @param scope
	 */
	public SeamSearchQuery(IJavaElement[] javaElements, IFile sourceFile, SeamSearchScope scope) {
		fTokens = null;
		fJavaElements = javaElements;
		fSourceFile = sourceFile;
		fScope= scope;
	}


	/**
	 * Sets up a parent ISearchRequestor
	 * 
	 * @param requestor
	 */
	public void setParentRequestor(ISearchRequestor requestor) {
		this.fParentRequestor = requestor;
	}

	/**
	 * Returns parent requestor
	 * 
	 * @return
	 */
	public ISearchRequestor getParentRequestor() {
		return this.fParentRequestor;
	}
	
	/**
	 * Returns Seam Search Scope
	 * 
	 * @return
	 */
	public SeamSearchScope getSearchScope() {
		return fScope;
	}
	
	/**
	 * @Override
	 */
	public boolean canRunInBackground() {
		return false;
	}

	
	/**
	 * @Override
	 */
	public IStatus run(final IProgressMonitor monitor) {
		AbstractTextSearchResult textResult= (AbstractTextSearchResult) getSearchResult();
		textResult.removeAll();
		
		if (fJavaElements != null) {
			return queryByJavaElements(textResult, monitor);
		}

		if (fTokens != null) {
			return queryByTokens(textResult, monitor);
		}
		return Status.OK_STATUS;
	}
	
	private IStatus queryByTokens(AbstractTextSearchResult textResult, 
			final IProgressMonitor monitor) {
		IProject project = (fSourceFile == null ? null : fSourceFile.getProject());

		ISeamProject seamProject = SeamCorePlugin.getSeamProject(project, true);
		if (seamProject == null)
			return Status.OK_STATUS;

		SeamELCompletionEngine engine = new SeamELCompletionEngine();
		
//		List<IJavaElement> elements = engine.getJavaElementsForELOperandTokens(seamProject, fSourceFile, fTokens)
		SeamSearchResultCollector collector= new SeamSearchResultCollector(textResult, getParentRequestor());
		return SeamSearchEngine.getInstance().search(fScope, collector, fSourceFile, fTokens, monitor);
	}

	private IStatus queryByJavaElements(
			AbstractTextSearchResult textResult, 
			final IProgressMonitor monitor) {
		IProject project = (fSourceFile == null ? null : fSourceFile.getProject());

		ISeamProject seamProject = SeamCorePlugin.getSeamProject(project, true);
		if (seamProject == null)
			return Status.OK_STATUS;

		SeamELCompletionEngine engine = new SeamELCompletionEngine();
		
		SeamSearchResultCollector collector= new SeamSearchResultCollector(textResult, getParentRequestor());
		return SeamSearchEngine.getInstance().search(fScope, collector, fSourceFile, fJavaElements, monitor);
	}

	private boolean isScopeAllFileTypes() {
		String[] fileNamePatterns= fScope.getFileNamePatterns();
		if (fileNamePatterns == null)
			return true;
		for (int i= 0; i < fileNamePatterns.length; i++) {
			if ("*".equals(fileNamePatterns[i])) { //$NON-NLS-1$
				return true;
			}
		}
		return false;
	}
	

	/**
	 * @see org.eclipse.search.ui.ISearchQuery#getLabel()
	 */
	public String getLabel() {
		Object[] args= { fScope.getLimitToDescription() };
		return Messages.format(SeamCoreMessages.SeamSearchQuery_label, args);
	}

	/**
	 * Returns Search String
	 * @return
	 */
	public String getSearchString() {
		String searchString = "";
		if (fJavaElements != null) {
			StringBuffer buf= new StringBuffer();
			for (int i= 0; i < fJavaElements.length; i++) {
				if (i > 0) {
					buf.append(", "); //$NON-NLS-1$
				}
				buf.append(fJavaElements[i]);
			}
			searchString = buf.toString();
		} else if (fTokens != null) {
			searchString = fTokens.getText();
		}
		return searchString;
	}

	/**
	 * Returns Search Result Label
	 * 
	 * @param nMatches
	 * @return
	 */
	public String getResultLabel(int nMatches) {
		String searchString= getSearchString();
		if (searchString.length() > 0) {
			if (SeamSearchEngine.isSearchForDeclarations(fScope.getLimitTo())) {
				// search is limited to declarations only
				if (nMatches == 1) {
					Object[] args= { searchString, fScope.getDescription(), fScope.getLimitToDescription() };
					return Messages.format(SeamCoreMessages.SeamSearchQuery_singularPatternWithLimitTo, args);
				}
				Object[] args= { searchString, new Integer(nMatches), fScope.getDescription(), fScope.getLimitToDescription() };
				return Messages.format(SeamCoreMessages.SeamSearchQuery_pluralPatternWithLimitTo, args);
			}
			if (SeamSearchEngine.isSearchForReferences(fScope.getLimitTo())) {
				// text search
				if (isScopeAllFileTypes()) {
					// search all file extensions
					if (nMatches == 1) {
						Object[] args= { searchString, fScope.getDescription(), fScope.getLimitToDescription() };
						return Messages.format(SeamCoreMessages.SeamSearchQuery_singularLabel, args);
					}
					Object[] args= { searchString, new Integer(nMatches), fScope.getDescription(), fScope.getLimitToDescription() };
					return Messages.format(SeamCoreMessages.SeamSearchQuery_pluralPattern, args); 
				}
			}
		}

		return "";
	}

	/*
	 * returns a search pattern for a given name
	 */
	public static Pattern getSearchPattern(String variableName) {
		return PatternConstructor.createPattern(variableName, true, false);
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.search.ui.ISearchQuery#canRerun()
	 */
	public boolean canRerun() {
		return true;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.search.ui.ISearchQuery#getSearchResult()
	 */
	public ISearchResult getSearchResult() {
		if (fResult == null) {
			fResult= new SeamSearchResult(this);
			new SearchResultUpdater(fResult);
		}
		return fResult;
	}
}
