/*******************************************************************************
 * Copyright (c) 2007 Red Hat, Inc.
 * 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.io.File;
import java.util.List;
import java.util.Set;
import java.util.zip.CRC32;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.search.core.text.TextSearchEngine;
import org.jboss.tools.common.el.core.model.ELInvocationExpression;
import org.jboss.tools.common.el.core.resolver.ElVarSearcher;
import org.jboss.tools.common.el.core.resolver.Var;
import org.jboss.tools.seam.core.ISeamContextVariable;
import org.jboss.tools.seam.core.ISeamProject;
import org.jboss.tools.seam.core.SeamCoreMessages;
import org.jboss.tools.seam.core.SeamCorePlugin;
import org.jboss.tools.seam.internal.core.el.SeamELCompletionEngine;
import org.jboss.tools.seam.ui.SeamGuiPlugin;

/**
 * A helper class used for search operations
 * 
 * @author Jeremy
 */
public abstract class SeamSearchEngine {
	private static SeamSearchEngine fInstance = null;
    private final IProgressMonitor fMonitor = new NullProgressMonitor();
    private SearchParticipant fParticipant = null;
    private IPath fSeamUIPluginLocation = null;
    private final CRC32 fChecksumCalculator = new CRC32();

	/**
	 * Returns an instance. If the instance isn't initialized creates an instance 
	 * of the search engine. 
	 * @return the created {@link SeamSearchEngine}.
	 */
	public static SeamSearchEngine getInstance() {
		if (fInstance == null) {
			fInstance = createDefault();
		}
		return fInstance;
	}

	/**
	 * Creates the default, built-in, text search engine that implements a brute-force search, not using
	 * any search index.
	 * Note that clients should always use the search engine provided by {@link #create()}.
	 * @return an instance of the default text search engine {@link TextSearchEngine}.
	 */
	private static SeamSearchEngine createDefault() {
		return new SeamSearchEngine() {

			@Override
			public IStatus search(SeamSearchScope javaScope,
					SeamSearchRequestor requestor,
					IFile sourceFile,
					ELInvocationExpression tokens,
					IProgressMonitor monitor) {

				if (tokens == null /*|| tokens.size() == 0*/) {
					return Status.OK_STATUS;
				}

				IProject project = (sourceFile == null ? null : sourceFile.getProject());

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

				SeamELCompletionEngine engine = new SeamELCompletionEngine();
				
				
				//Find Seam variable names
				// - if the tokens are the variable name only - search for variable declaration in Seam project
				String variableName = tokens.getText();   //SeamSearchVisitor.tokensToString(tokens);
				
				Set<ISeamContextVariable> variables = seamProject.getVariablesByName(variableName);
				if (variables != null && !variables.isEmpty()) {
					return search(javaScope, requestor, sourceFile, variables.toArray(new ISeamContextVariable[0]), monitor);
				}

				// - else try to find Java Elements and declarations/references for them
				List<IJavaElement> elements = null;
				try {
					elements = engine.getJavaElementsForELOperandTokens(seamProject, sourceFile, tokens);
				} catch (StringIndexOutOfBoundsException e) {
					SeamGuiPlugin.getPluginLog().logError(e);
					return Status.OK_STATUS;
				} catch (BadLocationException e) {
					SeamGuiPlugin.getPluginLog().logError(e);
					return Status.OK_STATUS;
				}

				if (elements != null && !elements.isEmpty()) {
					return search(javaScope, requestor, sourceFile, elements.toArray(new IJavaElement[0]), monitor);
				}
				
				// Try to find a local Var (a pair of variable-value attributes)
				ElVarSearcher varSearcher = new ElVarSearcher(sourceFile, engine);
				// Find a Var in the EL 
				int start = tokens.getStartPosition();
				int end = tokens.getEndPosition();
				
				StringBuffer elText = new StringBuffer();
				elText.append(tokens.toString());

				if (elText == null || elText.length() == 0)
					return Status.OK_STATUS;
				
				List<Var> allVars= varSearcher.findAllVars(sourceFile, tokens.getStartPosition());
				Var var = varSearcher.findVarForEl(elText.toString(), null, allVars, true);
				if (var == null) {
					// Find a Var in the current offset assuming that it's a node with var/value attribute pair
					var = varSearcher.findVar(sourceFile, tokens.getStartPosition());
				}
				if (var == null)
					return Status.OK_STATUS;

				if (tokens.getLeft() == null) {
					// The only Var is selected to search for
					if (isSearchForDeclarations(javaScope.getLimitTo())) {
						
						boolean res= SeamSearchVisitor.acceptPaternMatch(
								requestor, sourceFile, var.getDeclarationOffset(), var.getDeclarationLength());
						if (!res) {
							return Status.OK_STATUS; // no further reporting requested
						}
					} else {
						return search(javaScope, requestor, sourceFile, new Var[] {var}, monitor);
					}

					
					return Status.OK_STATUS;
				}

				// Need to extract the var value and search for the real elements
				
				return Status.OK_STATUS;
			}
				
			
			@Override
			public IStatus search(SeamSearchScope scope, 
					SeamSearchRequestor requestor, 
					IFile sourceFile,
					IJavaElement[] elements, 
					IProgressMonitor monitor) {
				IProject project = (sourceFile == null ? null : sourceFile.getProject());
				return new SeamSearchVisitor(requestor, elements, project).search(scope, monitor);
			}

			@Override
			public IStatus search(SeamSearchScope scope, 
					SeamSearchRequestor requestor, 
					IFile sourceFile,
					Var[] vars, 
					IProgressMonitor monitor) {
				IProject project = (sourceFile == null ? null : sourceFile.getProject());
				return new SeamSearchVisitor(requestor, vars, sourceFile).search(scope, monitor);
			}

			@Override
		    public IStatus search(String searchText, SeamSearchScope scope, 
		    		int searchFor, int limitTo, int matchMode, boolean isCaseSensitive, SearchRequestor requestor) {

		        SeamJavaSearchJob job = new SeamJavaSearchJob(searchText, scope, searchFor, limitTo, matchMode, isCaseSensitive, requestor);
		        setCanceled(false);
		        job.setUser(true);
		        job.schedule();
		        return Status.OK_STATUS;
		    }


			@Override
			public IStatus search(SeamSearchScope scope,
					SeamSearchRequestor requestor, IFile sourceFile,
					ISeamContextVariable[] variables, IProgressMonitor monitor) {
				IProject project = (sourceFile == null ? null : sourceFile.getProject());
				return new SeamSearchVisitor(requestor, variables, project).search(scope, monitor);
			}
		};
	}

	/**
	 * Uses a given tokens to find matches in the content of 
	 * workspace file resources. If a file is open in an editor, the
	 * editor buffer is searched.
	 * 
	 * @param scope
	 * @param requestor
	 * @param sourceFile
	 * @param tokens
	 * @param monitor
	 * @return the status containing information about problems in resources searched.
	 */
	public abstract IStatus search(SeamSearchScope scope, SeamSearchRequestor requestor, 
			IFile sourceFile, ELInvocationExpression tokens, IProgressMonitor monitor);

	/**
	 * Uses a given IJavaElement-s to find matches in the content of 
	 * workspace file resources. If a file is open in an editor, the
	 * editor buffer is searched.
     * 
	 * @param scope
	 * @param requestor
	 * @param sourceFile
	 * @param elements
	 * @param monitor
	 * @return
	 */
	public abstract IStatus search(SeamSearchScope scope, SeamSearchRequestor requestor, 
			IFile sourceFile, IJavaElement[] elements, IProgressMonitor monitor);

	/**
	 * Uses a given {@link ISeamContextVariable} objects to find matches in the content of 
	 * workspace file resources. If a file is open in an editor, the
	 * editor buffer is searched.
	 * 
	 * @param scope
	 * @param requestor
	 * @param sourceFile
	 * @param variables
	 * @param monitor
	 * @return
	 */
	public abstract IStatus search(SeamSearchScope scope, SeamSearchRequestor requestor, 
			IFile sourceFile, ISeamContextVariable[] variables, IProgressMonitor monitor);

	/**
	 * Uses a given {@link Var} objects to find matches in the content of 
	 * workspace file resources. If a file is open in an editor, the
	 * editor buffer is searched.
	 * 
	 * @param scope
	 * @param requestor
	 * @param sourceFile
	 * @param vars
	 * @param monitor
	 * @return
	 */
	public abstract IStatus search(SeamSearchScope scope, SeamSearchRequestor requestor, 
			IFile sourceFile, Var[] vars, IProgressMonitor monitor);

    /**
     * Perform a java search w/ the given parameters. Runs in a background Job
     * (results may still come in after this method call)
     * 
     * @param searchText
     *            the string of text to search on
     * @param searchFor
     *            IJavaSearchConstants.TYPE, METHOD, FIELD, PACKAGE, etc...
     * @param limitTo
     *            IJavaSearchConstants.DECLARATIONS,
     *            IJavaSearchConstants.REFERENCES,
     *            IJavaSearchConstants.IMPLEMENTORS, or
     *            IJavaSearchConstants.ALL_OCCURRENCES
     * @param matchMode
     *            allow * wildcards or not
     * @param isCaseSensitive
     * @param requestor
     *            passed in to accept search matches (and do "something" with
     *            them)
     */
    public abstract IStatus search(String searchText, SeamSearchScope scope, int searchFor, int limitTo, int matchMode, boolean isCaseSensitive, SearchRequestor requestor);

    /**
     * Seam Indexing and Search jobs check this
     * 
     * @return
     */
    public synchronized final void setCanceled(boolean cancel) {
        fMonitor.setCanceled(cancel);
    }

    /**
     * Seam Indexing and Search jobs check this
     * 
     * @return
     */
    public synchronized final boolean isCanceled() {
        return fMonitor.isCanceled();
    }

    // This is called from SeamPathIndexer
    public final IPath computeIndexLocation(IPath containerPath) {

        IPath indexLocation = null;
        String pathString = containerPath.toOSString();
        this.fChecksumCalculator.reset();
        this.fChecksumCalculator.update(pathString.getBytes());
        String fileName = Long.toString(this.fChecksumCalculator.getValue()) + ".index"; //$NON-NLS-1$
        indexLocation = getSeamUIPluginWorkingLocation().append(fileName);
        JavaModelManager.getJavaModelManager().getIndexManager().indexLocations.put(containerPath, indexLocation);
        return indexLocation;
    }

    // copied from JDT IndexManager
    public IPath getSeamUIPluginWorkingLocation() {

        if (this.fSeamUIPluginLocation != null)
            return this.fSeamUIPluginLocation;

        // Append the folder name "seamsearch" to keep the state location area cleaner
        IPath stateLocation = SeamGuiPlugin.getDefault().getStateLocation().append("seamsearch");

        String device = stateLocation.getDevice();
        if (device != null && device.charAt(0) == '/')
            stateLocation = stateLocation.setDevice(device.substring(1));

        // ensure that it exists on disk
        File folder = new File(stateLocation.toOSString());
		if (!folder.isDirectory()) {
			try {
				folder.mkdir();
			}
			catch (SecurityException e) {
			}
		}

        return this.fSeamUIPluginLocation = stateLocation;
    }

    /**
     * This operation ensures that the live resource's search markers show up in
     * the open editor. It also allows the ability to pass in a ProgressMonitor
     */
    private class SeamJavaSearchJob extends Job implements IJavaSearchConstants {

        String fSearchText = ""; //$NON-NLS-1$

        IJavaSearchScope fScope = null;

        int fSearchFor = FIELD;

        int fLimitTo = ALL_OCCURRENCES;

        int fMatchMode = SearchPattern.R_PATTERN_MATCH;

        boolean fIsCaseSensitive = false;

        SearchRequestor fRequestor = null;

        IJavaElement fElement = null;

        // constructor w/ java element
        public SeamJavaSearchJob(IJavaElement element, IJavaSearchScope scope, SearchRequestor requestor) {
            super(SeamCoreMessages.SeamSearch + element.getElementName());
            this.fElement = element;
            this.fScope = scope;
            this.fRequestor = requestor;
        }

        // constructor w/ search text
        public SeamJavaSearchJob(String searchText, IJavaSearchScope scope, int searchFor, int limitTo, int matchMode, boolean isCaseSensitive, SearchRequestor requestor) {
            super(SeamCoreMessages.SeamSearch + searchText);
            this.fSearchText = searchText;
            this.fScope = scope;
            this.fSearchFor = searchFor;
            this.fLimitTo = limitTo;
            this.fMatchMode = matchMode;
            this.fIsCaseSensitive = isCaseSensitive;
            this.fRequestor = requestor;
        }

        public IStatus run(IProgressMonitor jobMonitor) {

            if (jobMonitor != null && jobMonitor.isCanceled())
                return Status.CANCEL_STATUS;
            if (SeamSearchEngine.getInstance().isCanceled())
                return Status.CANCEL_STATUS;

            SearchPattern javaSearchPattern = null;
            // if an element is available, use that to create search pattern
            // (eg. LocalVariable)
            // otherwise use the text and other paramters
            boolean searchForDeclarations = isSearchForDeclarations(this.fLimitTo);
            boolean searchForReferences = isSearchForDeclarations(this.fLimitTo);

            if (this.fElement != null) {
                javaSearchPattern = SearchPattern.createPattern(this.fElement, this.fLimitTo);
            } else {
                javaSearchPattern = SearchPattern.createPattern(this.fSearchText, this.fSearchFor, this.fLimitTo, this.fMatchMode);
            }

            // if is searching for the declarations:
            // if (fElement != null): 
            // - No components can be found for the element (the declaration for the element itself will be found by java search)
            // - No properties/methods can be found for the Variable/Method (the declaration for the element itself will be found by java search)
            // if (fSearchText != null):
            // try to find the Seam Variable or the Seam variable's Method/Property and search for its declaration.
            
            if (isSearchForDeclarations(this.fLimitTo)) {
                if (this.fElement != null) {
                    javaSearchPattern = SearchPattern.createPattern(this.fElement, this.fLimitTo);
                } else {
                    javaSearchPattern = SearchPattern.createPattern(this.fSearchText, this.fSearchFor, this.fLimitTo, this.fMatchMode);
                }
            }
            // if is searching for the references:
            // if (fElement != null): 
            // ? Search for references to the components which are declared by this element's class
            // - Search for properties/methods can be found for the Variable/Method
            // if (fSearchText != null):
            // try to find the Seam Variable or the Seam variable's Method/Property and search for its declaration.
            if (isSearchForReferences(this.fLimitTo)) {
	            if (this.fElement != null) {
	                javaSearchPattern = SearchPattern.createPattern(this.fElement, this.fLimitTo);
	                
	            } else {
	                javaSearchPattern = SearchPattern.createPattern(this.fSearchText, this.fSearchFor, this.fLimitTo, this.fMatchMode);
	            }
            }
            
            if (javaSearchPattern != null) {
            	
            }
            return Status.OK_STATUS;
        }
        
    }

    /**
     * Checks if the given limitTo flag is limited to declarations
     * 
     * @param limitTo
     * @return
     */
    public static boolean isSearchForDeclarations(int limitTo) {
    	int maskedLimitTo = limitTo & ~(IJavaSearchConstants.IGNORE_DECLARING_TYPE+IJavaSearchConstants.IGNORE_RETURN_TYPE);
    	if (maskedLimitTo == IJavaSearchConstants.DECLARATIONS || maskedLimitTo == IJavaSearchConstants.ALL_OCCURRENCES) {
    		return true;
    	}
    	
    	return false;
    }
    
    /**
     * Checks if the given limitTo flag is limited to references
     * 
     * @param limitTo
     * @return
     */
    public static boolean isSearchForReferences(int limitTo) {
    	int maskedLimitTo = limitTo & ~(IJavaSearchConstants.IGNORE_DECLARING_TYPE+IJavaSearchConstants.IGNORE_RETURN_TYPE);
    	if (maskedLimitTo == IJavaSearchConstants.REFERENCES || maskedLimitTo == IJavaSearchConstants.ALL_OCCURRENCES) {
    		return true;
    	}
    
    	return false;
    }
    
    /**
     * Checks if the given element is IField
     * 
     * @param element
     * @return
     */
    public static boolean isField(IJavaElement element) {
    	if (element == null)
    		return false;
    	
    	return (element.getElementType() == IJavaElement.FIELD);
    }

    /**
     * Checks if the given element is IMethod
     * 
     * @param element
     * @return
     */
    public static boolean isMethod(IJavaElement element) {
    	if (element == null)
    		return false;
    	
    	return (element.getElementType() == IJavaElement.METHOD);
    }

    /**
     * Checks if the given element is IType
     * 
     * @param element
     * @return
     */
    public static boolean isType(IJavaElement element) {
    	if (element == null)
    		return false;
 
    	return (element.getElementType() == IJavaElement.TYPE);
    } 
}
