/*******************************************************************************
 * Copyright (c) 2004, 2015 Wind River Systems, Inc.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Markus Schorn - initial API and implementation
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.refactoring.rename;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.formatter.scanner.SimpleScanner;
import org.eclipse.cdt.internal.formatter.scanner.Token;
import org.eclipse.cdt.utils.PathUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.search.core.text.TextSearchEngine;
import org.eclipse.search.core.text.TextSearchMatchAccess;
import org.eclipse.search.core.text.TextSearchRequestor;
import org.eclipse.search.core.text.TextSearchScope;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.PlatformUI;

/**
 * Wraps the platform text search and uses a scanner to categorize the text-matches
 * by location (comments, string-literals, etc.).
 */
public class TextSearchWrapper {
	public final static int SCOPE_FILE = 1;
	public final static int SCOPE_WORKSPACE = 2;
	public final static int SCOPE_RELATED_PROJECTS = 3;
	public final static int SCOPE_SINGLE_PROJECT = 4;
	public final static int SCOPE_WORKING_SET = 5;

	private static class SearchScope extends TextSearchScope {
		public static SearchScope newSearchScope(IFile[] files, IWorkingSet ws) {
			IAdaptable[] adaptables = ws.getElements();
			ArrayList<IResource> resources = new ArrayList<>();
			for (int i = 0; i < adaptables.length; i++) {
				IAdaptable adaptable = adaptables[i];
				IResource resource = adaptable.getAdapter(IResource.class);
				if (resource != null) {
					resources.add(resource);
				}
			}
			return newSearchScope(files, resources.toArray(new IResource[resources.size()]));
		}

		public static SearchScope newSearchScope(IFile[] files, IResource[] roots) {
			if (files != null) {
				ArrayList<IResource> resources = new ArrayList<>(files.length + roots.length);
				for (IFile file : files) {
					if (!isInForest(file, roots)) {
						resources.add(file);
					}
				}
				Collections.addAll(resources, roots);
				roots = resources.toArray(new IResource[resources.size()]);
			}
			return new SearchScope(roots);
		}

		/**
		 * Checks is a file belongs to one of the given containers.
		 */
		private static boolean isInForest(IResource file, IResource[] roots) {
			IPath filePath = file.getFullPath();
			for (IResource root : roots) {
				if (PathUtil.isPrefix(root.getFullPath(), filePath)) {
					return true;
				}
			}
			return false;
		}

		private IResource[] fRootResources;
		private ArrayList<Matcher> fFileMatcher = new ArrayList<>();

		private SearchScope(IResource[] roots) {
			fRootResources = roots;
		}

		@Override
		public IResource[] getRoots() {
			return fRootResources;
		}

		@Override
		public boolean contains(IResourceProxy proxy) {
			if (proxy.isDerived()) {
				return false;
			}
			if (proxy.getType() == IResource.FILE) {
				return containsFile(proxy.getName());
			}
			return true;
		}

		private boolean containsFile(String name) {
			for (Iterator<Matcher> iter = fFileMatcher.iterator(); iter.hasNext();) {
				Matcher matcher = iter.next();
				matcher.reset(name);
				if (matcher.matches()) {
					return true;
				}
			}
			return false;
		}

		public void addFileNamePattern(String filePattern) {
			Pattern p = Pattern.compile(filePatternToRegex(filePattern));
			fFileMatcher.add(p.matcher("")); //$NON-NLS-1$
		}

		private String filePatternToRegex(String filePattern) {
			StringBuilder result = new StringBuilder();
			for (int i = 0; i < filePattern.length(); i++) {
				char c = filePattern.charAt(i);
				switch (c) {
				case '\\':
				case '(':
				case ')':
				case '{':
				case '}':
				case '.':
				case '[':
				case ']':
				case '$':
				case '^':
				case '+':
				case '|':
					result.append('\\');
					result.append(c);
					break;
				case '?':
					result.append('.');
					break;
				case '*':
					result.append(".*"); //$NON-NLS-1$
					break;
				default:
					result.append(c);
					break;
				}
			}
			return result.toString();
		}
	}

	public TextSearchWrapper() {
	}

	private TextSearchScope createSearchScope(IFile[] files, int scope, IFile file, String workingSetName,
			String[] patterns) {
		switch (scope) {
		case SCOPE_WORKSPACE:
			return defineSearchScope(files, file.getWorkspace().getRoot(), patterns);
		case SCOPE_SINGLE_PROJECT:
			return defineSearchScope(files, file.getProject(), patterns);
		case SCOPE_FILE:
			return defineSearchScope(files, file, patterns);
		case SCOPE_WORKING_SET: {
			return defineWorkingSetAsSearchScope(files, workingSetName, patterns);
		}
		}
		return defineRelatedProjectsAsSearchScope(files, file.getProject(), patterns);
	}

	private TextSearchScope defineRelatedProjectsAsSearchScope(IFile[] files, IProject project, String[] patterns) {
		HashSet<IProject> projects = new HashSet<>();
		LinkedList<IProject> workThrough = new LinkedList<>();
		workThrough.add(project);
		while (!workThrough.isEmpty()) {
			IProject proj = workThrough.removeLast();
			if (projects.add(proj)) {
				try {
					workThrough.addAll(Arrays.asList(proj.getReferencedProjects()));
					workThrough.addAll(Arrays.asList(proj.getReferencingProjects()));
				} catch (CoreException e) {
					// need to ignore
				}
			}
		}
		IResource[] roots = projects.toArray(new IResource[projects.size()]);
		return defineSearchScope(files, roots, patterns);
	}

	private TextSearchScope defineWorkingSetAsSearchScope(IFile[] files, String workingSetName, String[] patterns) {
		IWorkingSet workingSet = workingSetName != null
				? PlatformUI.getWorkbench().getWorkingSetManager().getWorkingSet(workingSetName)
				: null;
		SearchScope result = workingSet != null ? SearchScope.newSearchScope(files, workingSet)
				: SearchScope.newSearchScope(files, new IResource[0]);
		applyFilePatterns(result, patterns);
		return result;
	}

	private void applyFilePatterns(SearchScope scope, String[] patterns) {
		for (String pattern : patterns) {
			scope.addFileNamePattern(pattern);
		}
	}

	private TextSearchScope defineSearchScope(IFile[] files, IResource root, String[] patterns) {
		SearchScope result = SearchScope.newSearchScope(files, new IResource[] { root });
		applyFilePatterns(result, patterns);
		return result;
	}

	private TextSearchScope defineSearchScope(IFile[] files, IResource[] roots, String[] patterns) {
		SearchScope result = SearchScope.newSearchScope(files, roots);
		applyFilePatterns(result, patterns);
		return result;
	}

	/**
	 * Searches for a given word.
	 *
	 * @param filesToSearch The files to search.
	 * @param scope Together with {@code file} and {@code workingSet} defines set of additional
	 *     file to search. One of SCOPE_FILE, SCOPE_WORKSPACE, SCOPE_RELATED_PROJECTS,
	 *     SCOPE_SINGLE_PROJECT, or SCOPE_WORKING_SET.
	 * @param scopeAnchor The file used as an anchor for the scope.
	 * @param workingSet The name of a working set. Ignored if {@code scope} is not
	 *     SCOPE_WORKING_SET.
	 * @param patterns File name patterns.
	 * @param word The word to search for.
	 * @param monitor A progress monitor.
	 * @param target The list that gets populated with search results.
	 */
	public IStatus searchWord(IFile[] filesToSearch, int scope, IFile scopeAnchor, String workingSet, String[] patterns,
			String word, IProgressMonitor monitor, final List<CRefactoringMatch> target) {
		int startPos = target.size();
		TextSearchEngine engine = TextSearchEngine.create();
		StringBuilder searchPattern = new StringBuilder(word.length() + 8);
		searchPattern.append("\\b"); //$NON-NLS-1$
		searchPattern.append("\\Q"); //$NON-NLS-1$
		searchPattern.append(word);
		searchPattern.append("\\E"); //$NON-NLS-1$
		searchPattern.append("\\b"); //$NON-NLS-1$

		Pattern pattern = Pattern.compile(searchPattern.toString());

		TextSearchScope searchscope = createSearchScope(filesToSearch, scope, scopeAnchor, workingSet, patterns);
		TextSearchRequestor requestor = new TextSearchRequestor() {
			@Override
			public boolean acceptPatternMatch(TextSearchMatchAccess access) {
				IFile file = access.getFile();
				ICElement elem = CoreModel.getDefault().create(file);
				if (elem instanceof ITranslationUnit) {
					target.add(new CRefactoringMatch(file, access.getMatchOffset(), access.getMatchLength(), 0));
				}
				return true;
			}
		};
		IStatus result = engine.search(searchscope, requestor, pattern, new SubProgressMonitor(monitor, 95));
		categorizeMatches(target.subList(startPos, target.size()), new SubProgressMonitor(monitor, 5));

		return result;
	}

	public void categorizeMatches(List<CRefactoringMatch> matches, IProgressMonitor monitor) {
		monitor.beginTask(RenameMessages.TextSearch_monitor_categorizeMatches, matches.size());
		IFile file = null;
		ArrayList<int[]> locations = null;
		for (Iterator<CRefactoringMatch> iter = matches.iterator(); iter.hasNext();) {
			CRefactoringMatch match = iter.next();
			IFile tfile = match.getFile();
			if (file == null || !file.equals(tfile)) {
				file = tfile;
				locations = new ArrayList<>();
				computeLocations(file, locations);
			}
			match.setLocation(findLocation(match, locations));
			monitor.worked(1);
		}
	}

	final static Comparator<int[]> COMPARE_FIRST_INTEGER = new Comparator<int[]>() {
		@Override
		public int compare(int[] o1, int[] o2) {
			return (o1)[0] - (o2)[0];
		}
	};

	private int findLocation(CRefactoringMatch match, ArrayList<int[]> states) {
		int pos = Collections.binarySearch(states, new int[] { match.getOffset() }, COMPARE_FIRST_INTEGER);
		if (pos < 0) {
			pos = -pos - 2;
			if (pos < 0) {
				pos = 0;
			}
		}
		int endOffset = match.getOffset() + match.getLength();
		int location = 0;
		while (pos < states.size()) {
			int[] info = states.get(pos);
			if (info[0] >= endOffset) {
				break;
			}
			location |= info[1];
			pos++;
		}
		return location;
	}

	private void computeLocations(IFile file, ArrayList<int[]> locations) {
		Reader reader;
		SimpleScanner scanner = new SimpleScanner();
		try {
			reader = new BufferedReader(new InputStreamReader(file.getContents(), file.getCharset()));
		} catch (CoreException e) {
			return;
		} catch (UnsupportedEncodingException e) {
			return;
		}
		try {
			scanner.initialize(reader, null);
			scanner.setReuseToken(true);
			Token token;
			int lastState = 0;
			while ((token = scanner.nextToken()) != null) {
				int state = CRefactory.OPTION_IN_CODE_REFERENCES;
				switch (token.getType()) {
				case Token.tLINECOMMENT:
				case Token.tBLOCKCOMMENT:
					state = CRefactory.OPTION_IN_COMMENT;
					break;
				case Token.tSTRING:
				case Token.tLSTRING:
				case Token.tCHAR:
					state = CRefactory.OPTION_IN_STRING_LITERAL;
					break;
				case Token.tPREPROCESSOR:
					state = CRefactory.OPTION_IN_PREPROCESSOR_DIRECTIVE;
					break;
				case Token.tPREPROCESSOR_DEFINE:
					state = CRefactory.OPTION_IN_MACRO_DEFINITION;
					break;
				case Token.tPREPROCESSOR_INCLUDE:
					state = CRefactory.OPTION_IN_INCLUDE_DIRECTIVE;
					break;
				}
				if (state != lastState) {
					locations.add(new int[] { token.getOffset(), state });
					lastState = state;
				}
			}
		} finally {
			try {
				reader.close();
			} catch (IOException e) {
			}
		}
	}
}
