/*******************************************************************************
 * Copyright (c) 2000, 2019 IBM Corporation and others.
 *
 * 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:
 *     IBM Corporation - initial API and implementation
 *     Microsoft Corporation - copied to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;

import org.eclipse.core.resources.IResource;

import org.eclipse.ltk.core.refactoring.IRefactoringStatusEntryComparator;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;

import org.eclipse.jdt.internal.corext.util.SearchUtils;

/**
 * Convenience wrapper for {@link SearchEngine} - performs searching and sorts the results by {@link IResource}.
 * TODO: throw CoreExceptions from search(..) methods instead of wrapped JavaModelExceptions.
 */
public class RefactoringSearchEngine {

	private RefactoringSearchEngine(){
		//no instances
	}

	//TODO: throw CoreException
	public static ICompilationUnit[] findAffectedCompilationUnits(SearchPattern pattern,
			IJavaSearchScope scope, final IProgressMonitor pm, RefactoringStatus status, final boolean tolerateInAccurateMatches) throws JavaModelException {

		boolean hasNonCuMatches= false;

		class ResourceSearchRequestor extends SearchRequestor{
			boolean hasPotentialMatches= false ;
			Set<IResource> resources= new HashSet<>(5);
			private IResource fLastResource;

			@Override
			public void acceptSearchMatch(SearchMatch match) {
				if (!tolerateInAccurateMatches && match.getAccuracy() == SearchMatch.A_INACCURATE) {
					hasPotentialMatches= true;
				}
				if (fLastResource != match.getResource()) {
					fLastResource= match.getResource();
					resources.add(fLastResource);
				}
			}
		}
		ResourceSearchRequestor requestor = new ResourceSearchRequestor();
		try {
			new SearchEngine().search(pattern, SearchUtils.getDefaultSearchParticipants(), scope, requestor, pm);
		} catch (CoreException e) {
			throw new JavaModelException(e);
		}

		List<IJavaElement> result= new ArrayList<>(requestor.resources.size());
		for (Iterator<IResource> iter= requestor.resources.iterator(); iter.hasNext(); ) {
			IResource resource= iter.next();
			IJavaElement element= JavaCore.create(resource);
			if (element instanceof ICompilationUnit) {
				result.add(element);
			} else {
				hasNonCuMatches= true;
			}
		}
		addStatusErrors(status, requestor.hasPotentialMatches, hasNonCuMatches);
		return result.toArray(new ICompilationUnit[result.size()]);
	}

	//TODO: throw CoreException
	public static ICompilationUnit[] findAffectedCompilationUnits(SearchPattern pattern,
			IJavaSearchScope scope, final IProgressMonitor pm, RefactoringStatus status) throws JavaModelException {
		return findAffectedCompilationUnits(pattern, scope, pm, status, false);
	}

	/**
	 * Performs a search and groups the resulting {@link SearchMatch}es by
	 * {@link SearchResultGroup#getCompilationUnit()}.
	 * @param pattern the search pattern
	 * @param scope the search scope
	 * @param monitor the progress monitor
	 * @param status an error is added here if inaccurate or non-cu matches have been found
	 * @return a {@link SearchResultGroup}[], where each {@link SearchResultGroup}
	 * 		has a different {@link SearchMatch#getResource() getResource()}s.
	 * @see SearchMatch
	 * @throws JavaModelException when the search failed
	 */
	//TODO: throw CoreException
	public static SearchResultGroup[] search(SearchPattern pattern, IJavaSearchScope scope, IProgressMonitor monitor, RefactoringStatus status)
			throws JavaModelException {
		return internalSearch(new SearchEngine(), pattern, scope, new CollectingSearchRequestor(), monitor, status);
	}

	//TODO: throw CoreException
	public static SearchResultGroup[] search(SearchPattern pattern, WorkingCopyOwner owner, IJavaSearchScope scope, IProgressMonitor monitor, RefactoringStatus status)
			throws JavaModelException {
		return internalSearch(owner != null ? new SearchEngine(owner) : new SearchEngine(), pattern, scope, new CollectingSearchRequestor(), monitor, status);
	}

	//TODO: throw CoreException
	public static SearchResultGroup[] search(SearchPattern pattern, IJavaSearchScope scope, CollectingSearchRequestor requestor,
			IProgressMonitor monitor, RefactoringStatus status) throws JavaModelException {
		return internalSearch(new SearchEngine(), pattern, scope, requestor, monitor, status);
	}

	//TODO: throw CoreException
	public static SearchResultGroup[] search(SearchPattern pattern, WorkingCopyOwner owner, IJavaSearchScope scope,
			CollectingSearchRequestor requestor, IProgressMonitor monitor, RefactoringStatus status) throws JavaModelException {
		return internalSearch(owner != null ? new SearchEngine(owner) : new SearchEngine(), pattern, scope, requestor, monitor, status);
	}

	//TODO: throw CoreException
	private static SearchResultGroup[] internalSearch(SearchEngine searchEngine, SearchPattern pattern, IJavaSearchScope scope,
			CollectingSearchRequestor requestor, IProgressMonitor monitor, RefactoringStatus status) throws JavaModelException {
		try {
			searchEngine.search(pattern, SearchUtils.getDefaultSearchParticipants(), scope, requestor, monitor);
		} catch (CoreException e) {
			throw new JavaModelException(e);
		}
		return groupByCu(requestor.getResults(), status);
	}

	public static SearchResultGroup[] groupByCu(SearchMatch[] matches, RefactoringStatus status) {
		return groupByCu(Arrays.asList(matches), status);
	}

	/**
	 * @param matchList a List of SearchMatch
	 * @param status the status to report errors.
	 * @return a SearchResultGroup[], grouped by SearchMatch#getResource()
	 */
	public static SearchResultGroup[] groupByCu(List<SearchMatch> matchList, RefactoringStatus status) {
		Map<IResource, List<SearchMatch>> grouped= new HashMap<>();
		boolean hasPotentialMatches= false;
		boolean hasNonCuMatches= false;

		for (Iterator<SearchMatch> iter= matchList.iterator(); iter.hasNext();) {
			SearchMatch searchMatch= iter.next();
			if (searchMatch.getAccuracy() == SearchMatch.A_INACCURATE)
				hasPotentialMatches= true;
			if (! grouped.containsKey(searchMatch.getResource()))
				grouped.put(searchMatch.getResource(), new ArrayList<SearchMatch>(1));
			grouped.get(searchMatch.getResource()).add(searchMatch);
		}

		for (Iterator<IResource> iter= grouped.keySet().iterator(); iter.hasNext();) {
			IResource resource= iter.next();
			IJavaElement element= JavaCore.create(resource);
			if (! (element instanceof ICompilationUnit)) {
				iter.remove();
				hasNonCuMatches= true;
			}
		}

		SearchResultGroup[] result= new SearchResultGroup[grouped.keySet().size()];
		int i= 0;
		for (Iterator<IResource> iter= grouped.keySet().iterator(); iter.hasNext();) {
			IResource resource= iter.next();
			List<SearchMatch> searchMatches= grouped.get(resource);
			SearchMatch[] matchArray= searchMatches.toArray(new SearchMatch[searchMatches.size()]);
			result[i]= new SearchResultGroup(resource, matchArray);
			i++;
		}
		addStatusErrors(status, hasPotentialMatches, hasNonCuMatches);
		return result;
	}

	public static SearchPattern createOrPattern(IJavaElement[] elements, int limitTo) {
		if (elements == null || elements.length == 0)
			return null;
		Set<IJavaElement> set= new HashSet<>(Arrays.asList(elements));
		Iterator<IJavaElement> iter= set.iterator();
		IJavaElement first= iter.next();
		SearchPattern pattern= SearchPattern.createPattern(first, limitTo, SearchUtils.GENERICS_AGNOSTIC_MATCH_RULE);
		if (pattern == null) // check for bug 90138
			throw new IllegalArgumentException("Invalid java element: " + first.getHandleIdentifier() + "\n" + first.toString()); //$NON-NLS-1$ //$NON-NLS-2$
		while(iter.hasNext()){
			IJavaElement each= iter.next();
			SearchPattern nextPattern= SearchPattern.createPattern(each, limitTo, SearchUtils.GENERICS_AGNOSTIC_MATCH_RULE);
			if (nextPattern == null) // check for bug 90138
				throw new IllegalArgumentException("Invalid java element: " + each.getHandleIdentifier() + "\n" + each.toString()); //$NON-NLS-1$ //$NON-NLS-2$
			pattern= SearchPattern.createOrPattern(pattern, nextPattern);
		}
		return pattern;
	}

	private static boolean containsStatusEntry(final RefactoringStatus status, final RefactoringStatusEntry other) {
		return status.getEntries(new IRefactoringStatusEntryComparator() {
			@Override
			public final int compare(final RefactoringStatusEntry entry1, final RefactoringStatusEntry entry2) {
				return entry1.getMessage().compareTo(entry2.getMessage());
			}
		}, other).length > 0;
	}

	private static void addStatusErrors(RefactoringStatus status, boolean hasPotentialMatches, boolean hasNonCuMatches) {
		if (hasPotentialMatches) {
			final RefactoringStatusEntry entry= new RefactoringStatusEntry(RefactoringStatus.ERROR, RefactoringCoreMessages.RefactoringSearchEngine_potential_matches);
			if (!containsStatusEntry(status, entry))
				status.addEntry(entry);
		}
		if (hasNonCuMatches) {
			final RefactoringStatusEntry entry= new RefactoringStatusEntry(RefactoringStatus.ERROR, RefactoringCoreMessages.RefactoringSearchEngine_non_cu_matches);
			if (!containsStatusEntry(status, entry))
				status.addEntry(entry);
		}
	}
}
