/*******************************************************************************
 * Copyright (c) 2000, 2008 IBM Corporation, Nokia 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package com.nokia.carbide.search.system.ui.text;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.content.IContentType;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.ui.IWorkingSet;

import com.nokia.carbide.search.system.core.text.TextSearchScope;
import com.nokia.carbide.search.system.internal.core.text.PatternConstructor;
import com.nokia.carbide.search.system.internal.ui.Messages;
import com.nokia.carbide.search.system.internal.ui.SearchMessages;
import com.nokia.carbide.search.system.internal.ui.WorkingSetComparator;
import com.nokia.carbide.search.system.internal.ui.text.BasicElementLabels;
import com.nokia.carbide.search.system.internal.ui.util.FileTypeEditor;


/**
 * A text search scope used by the file search dialog. Additionally to roots it allows to define file name
 * patterns and exclude all derived resources.
 *
 * <p>
 * Clients should not instantiate or subclass this class.
 * </p>
 * @since 3.2
 * 
 * @noextend This class is not intended to be subclassed by clients.
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
public final class FileTextSearchScope extends TextSearchScope {
	
	private static final boolean IS_CASE_SENSITIVE_FILESYSTEM = !new File("Temp").equals(new File("temp")); //$NON-NLS-1$ //$NON-NLS-2$

	private String fRootFolder;
	private boolean fIncludeSubfolders;
	private boolean fIncludeHidden;
	private boolean fIncludeArchives;
	
	/**
	 * Returns a scope for the workspace. The created scope contains all resources in the workspace
	 * that match the given file name patterns. Depending on <code>includeDerived</code>, derived resources or
	 * resources inside a derived container are part of the scope or not.
	 * 
	 * @param fileNamePatterns file name pattern that all files have to match <code>null</code> to include all file names.	 
	 * @param includeDerived defines if derived files and files inside derived containers are included in the scope.
	 * @return a scope containing all files in the workspace that match the given file name patterns.
	 */
	public static FileTextSearchScope newWorkspaceScope(String[] fileNamePatterns, boolean includeDerived) {
		return new FileTextSearchScope(SearchMessages.WorkspaceScope, new IResource[] { ResourcesPlugin.getWorkspace().getRoot() }, null, fileNamePatterns, includeDerived,
								null, false, false, false); 
	}

	/**
	 * Returns a scope for the root folder, without regard to the workspace. The created scope contains all files
	 * in or beneath the root folder that match the given file name patterns. Depending on <code>includeSubfolders</code>,
	 * files in subfolders are searched. Depending on <code>includeHidden</code>, hidden files are searched.
	 * TODO: Depending on <code>includeArchives</code>, files within archive files are searched.
	 * 
	 * @param fileNamePatterns file name pattern that all files have to match <code>null</code> to include all file names.	 
	 * @param root folder defining the scope. 
	 * @param includeSubfolders defines whether subfolders are searched.
	 * @param includeHidden defines hidden files are searched.
	 * @param includeArchives TODO: defines whether files within archive files are searched.
	 * @return a scope based on the root folder.
	 */
	public static FileTextSearchScope newSystemScope(String folderName, String[] fileNamePatterns, String rootFolder,
			 				boolean includeSubfolders, boolean includeHidden, boolean includeArchives) {
		return new FileTextSearchScope(folderName, null, null, fileNamePatterns, false,
								rootFolder, includeSubfolders, includeHidden, includeArchives); 
	}
	
	/**
	 * Returns a scope for the given root resources. The created scope contains all root resources and their
	 * children that match the given file name patterns. Depending on <code>includeDerived</code>, derived resources or
	 * resources inside a derived container are part of the scope or not.
	 *
	 * @param roots the roots resources defining the scope.
	 * @param fileNamePatterns file name pattern that all files have to match <code>null</code> to include all file names.
	 * @param includeDerived defines if derived files and files inside derived containers are included in the scope.
	 * @return a scope containing the resources and its children if they match the given file name patterns.
	 */
	public static FileTextSearchScope newSearchScope(IResource[] roots, String[] fileNamePatterns, boolean includeDerived) {
		roots= removeRedundantEntries(roots, includeDerived);
		
		String description;
		if (roots.length == 0) {
			description= SearchMessages.FileTextSearchScope_scope_empty;
		} else if (roots.length == 1) {
			String label= SearchMessages.FileTextSearchScope_scope_single;
			description= Messages.format(label, roots[0].getName());
		} else if (roots.length == 2) {
			String label= SearchMessages.FileTextSearchScope_scope_double;
			description= Messages.format(label, new String[] { roots[0].getName(), roots[1].getName()});
		} else {
			String label= SearchMessages.FileTextSearchScope_scope_multiple;
			description= Messages.format(label, new String[] { roots[0].getName(), roots[1].getName()});
		}
		return new FileTextSearchScope(description, roots, null, fileNamePatterns, includeDerived, null, false, false, false);
	}	

	/**
	 * Returns a scope for the given working sets. The created scope contains all resources in the
	 * working sets that match the given file name patterns. Depending on <code>includeDerived</code>, derived resources or
	 * resources inside a derived container are part of the scope or not.
	 * 
	 * @param workingSets the working sets defining the scope. 
	 * @param fileNamePatterns file name pattern that all files have to match <code>null</code> to include all file names.
	 * @param includeDerived defines if derived files and files inside derived containers are included in the scope.
	 * @return a scope containing the resources in the working set if they match the given file name patterns.
	 */
	public static FileTextSearchScope newSearchScope(IWorkingSet[] workingSets, String[] fileNamePatterns, boolean includeDerived) {
		String description;
		Arrays.sort(workingSets, new WorkingSetComparator());
		if (workingSets.length == 0) {
			description= SearchMessages.FileTextSearchScope_ws_scope_empty;
		} else if (workingSets.length == 1) {
			String label= SearchMessages.FileTextSearchScope_ws_scope_single;
			description= Messages.format(label, workingSets[0].getLabel());
		} else if (workingSets.length == 2) {
			String label= SearchMessages.FileTextSearchScope_ws_scope_double;
			description= Messages.format(label, new String[] { workingSets[0].getLabel(), workingSets[1].getLabel()});
		} else {
			String label= SearchMessages.FileTextSearchScope_ws_scope_multiple;
			description= Messages.format(label, new String[] { workingSets[0].getLabel(), workingSets[1].getLabel()});
		}
		FileTextSearchScope scope= new FileTextSearchScope(description, convertToResources(workingSets, includeDerived), workingSets, fileNamePatterns, includeDerived,
												null, false, false, false);
		return scope;
	}

	private final String fDescription;
	private final IResource[] fRootElements;
	private final String[] fFileNamePatterns;
	private final Matcher fPositiveFileNameMatcher;
	private final Matcher fNegativeFileNameMatcher;
	
	private boolean fVisitDerived;
	private IWorkingSet[] fWorkingSets;

	private FileTextSearchScope(String description, IResource[] resources, IWorkingSet[] workingSets, String[] fileNamePatterns, boolean visitDerived,
								String rootFolder, boolean includeSubfolders, boolean includeHidden, boolean includeArchives) {
		fDescription= description;
		fRootElements= resources;
		fFileNamePatterns= fileNamePatterns;
		fVisitDerived= visitDerived;
		fWorkingSets= workingSets;
		fRootFolder= rootFolder;
		fIncludeSubfolders= includeSubfolders;
		fIncludeHidden= includeHidden;
		fIncludeArchives= includeArchives;
		
		fPositiveFileNameMatcher= createMatcher(fileNamePatterns, false);
		fNegativeFileNameMatcher= createMatcher(fileNamePatterns, true);
	}
	
	/**
	 * Returns the description of the scope
	 * 
	 * @return the description of the scope
	 */
	public String getDescription() {
		return fDescription;
	}

	/**
	 * Returns the file name pattern configured for this scope or <code>null</code> to match
	 * all file names.
	 * 
	 * @return the file name pattern strings
	 */
	public String[] getFileNamePatterns() {
		return fFileNamePatterns;
	}
	
	/**
	 * Returns the working-sets that were used to  configure this scope or <code>null</code> 
	 * if the scope was not created off working sets.
	 * 
	 * @return the working-sets the scope is based on.
	 */
	public IWorkingSet[] getWorkingSets() {
		return fWorkingSets;
	}

	/**
	 * Returns the content types configured for this scope or <code>null</code> to match
	 * all content types.
	 * 
	 * @return the file name pattern starings
	 */
	public IContentType[] getContentTypes() {
		return null;  // to be implemented in the future
	}
	
	/**
	 * Returns a description describing the file name patterns and content types.
	 * 
	 * @return the description of the scope
	 */
	public String getFilterDescription() {
		String[] ext= fFileNamePatterns;
		if (ext == null) {
			return BasicElementLabels.getFilePattern("*"); //$NON-NLS-1$
		}
		Arrays.sort(ext);
		StringBuffer buf= new StringBuffer();
		for (int i= 0; i < ext.length; i++) {
			if (i > 0) {
				buf.append(", "); //$NON-NLS-1$
			}
			buf.append(ext[i]);
		}
		return BasicElementLabels.getFilePattern(buf.toString());
	}
	
	/**
	 * Returns whether derived resources are included in this search scope.
	 * 
	 * @return whether derived resources are included in this search scope.
	 */
	public boolean includeDerived() {
		return fVisitDerived;
	}
	
	/**
	 * Returns the root folder of the search
	 * 
	 * @return root folder of the search
	 */
	public String getRootFolder() {
		return fRootFolder;
	}
	
	/**
	 * Returns whether subfolders of root folder are included in this search scope.
	 * 
	 * @return whether subfolders of root folder are included in this search scope.
	 */
	public boolean includeSubfolders() {
		return fIncludeSubfolders;
	}
	
	/**
	 * Returns whether hidden files are included in this search scope.
	 * 
	 * @return whether hidden files are included in this search scope.
	 */
	public boolean includeHidden() {
		return fIncludeHidden;
	}
	
	/**
	 * Returns whether archive entries are included in this search scope.
	 * 
	 * @return whether archive entries are included in this search scope.
	 */
	public boolean includeArchives() {
		return fIncludeArchives;
	}
	
	/* (non-Javadoc)
	 * @see com.nokia.carbide.search.system.core.text.FileSearchScope#getRoots()
	 */
	public IResource[] getRoots() {
		return fRootElements;
	}

	/* (non-Javadoc)
	 * @see com.nokia.carbide.search.system.core.text.FileSearchScope#contains(org.eclipse.core.resources.IResourceProxy)
	 */
	public boolean contains(IResourceProxy proxy) {
		if (!fVisitDerived && proxy.isDerived()) {
			return false; // all resources in a derived folder are considered to be derived, see bug 103576
		}
		
		if (proxy.getType() == IResource.FILE) {
			return matchesFileName(proxy.getName());
		}
		return true;
	}
	
	public boolean matchesFileName(String fileName) {
		if (fPositiveFileNameMatcher != null && !fPositiveFileNameMatcher.reset(fileName).matches()) {
			return false;
		}
		if (fNegativeFileNameMatcher != null && fNegativeFileNameMatcher.reset(fileName).matches()) {
			return false;
		}
		return true;
	}
	
	private Matcher createMatcher(String[] fileNamePatterns, boolean negativeMatcher) {
		if (fileNamePatterns == null || fileNamePatterns.length == 0) {
			return null;
		}
		ArrayList patterns= new ArrayList();
		for (int i= 0; i < fileNamePatterns.length; i++) {
			String pattern= fFileNamePatterns[i];
			if (negativeMatcher == pattern.startsWith(FileTypeEditor.FILE_PATTERN_NEGATOR)) {
				if (negativeMatcher) {
					pattern= pattern.substring(FileTypeEditor.FILE_PATTERN_NEGATOR.length()).trim();
				}
				if (pattern.length() > 0) {
					patterns.add(pattern);
				}
			}
		}
		if (!patterns.isEmpty()) {
			String[] patternArray= (String[]) patterns.toArray(new String[patterns.size()]);
			Pattern pattern= PatternConstructor.createPattern(patternArray, IS_CASE_SENSITIVE_FILESYSTEM);
			return pattern.matcher(""); //$NON-NLS-1$
		}
		return null;
	}
	
	private static IResource[] removeRedundantEntries(IResource[] elements, boolean includeDerived) {
		ArrayList res= new ArrayList();
		for (int i= 0; i < elements.length; i++) {
			IResource curr= elements[i];
			addToList(res, curr, includeDerived);
		}
		return (IResource[])res.toArray(new IResource[res.size()]);
	}

	private static IResource[] convertToResources(IWorkingSet[] workingSets, boolean includeDerived) {
		ArrayList res= new ArrayList();
		for (int i= 0; i < workingSets.length; i++) {
			IWorkingSet workingSet= workingSets[i];
			if (workingSet.isAggregateWorkingSet() && workingSet.isEmpty()) {
				return new IResource[] { ResourcesPlugin.getWorkspace().getRoot() };
			}
			IAdaptable[] elements= workingSet.getElements();
			for (int k= 0; k < elements.length; k++) {
				IResource curr= (IResource) elements[k].getAdapter(IResource.class);
				if (curr != null) {
					addToList(res, curr, includeDerived);
				}
			}
		}
		return (IResource[]) res.toArray(new IResource[res.size()]);
	}
	
	private static void addToList(ArrayList res, IResource curr, boolean includeDerived) {
		if (!includeDerived && isDerived(curr)) {
			return;
		}
		IPath currPath= curr.getFullPath();
		for (int k= res.size() - 1; k >= 0 ; k--) {
			IResource other= (IResource) res.get(k);
			IPath otherPath= other.getFullPath();
			if (otherPath.isPrefixOf(currPath)) {
				return;
			}
			if (currPath.isPrefixOf(otherPath)) {
				res.remove(k);
			}
		}
		res.add(curr);
	}

	private static boolean isDerived(IResource curr) {
		do {
			if (curr.isDerived()) {
				return true;
			}
			curr= curr.getParent();
		} while (curr != null);
		return false;
	}

	@Override
	public boolean isIncludeHidden() {
		return fIncludeHidden;
	}

	@Override
	public boolean isIncludeSubfolders() {
		return fIncludeSubfolders;
	}
}
