/*******************************************************************************
 * Copyright (c) 2000, 2016 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
 *******************************************************************************/
package org.eclipse.help.internal.search;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.help.internal.base.HelpBasePlugin;
import org.eclipse.help.internal.base.remote.RemoteHelp;
import org.eclipse.help.internal.base.remote.RemoteSearchManager;
import org.eclipse.help.internal.search.federated.FederatedSearchEntry;
import org.eclipse.help.internal.search.federated.FederatedSearchJob;
import org.eclipse.help.search.AbstractSearchProcessor;
import org.eclipse.help.search.ISearchResult;

/*
 * Manages both local and remote searching, as well as merging of results.
 */
public class SearchManager {

	private LocalSearchManager localManager = new LocalSearchManager();
	private RemoteSearchManager remoteManager = new RemoteSearchManager();

	private class SearchState {

		public IProgressMonitor localMonitor;
		public IProgressMonitor remoteMonitor;

		public ISearchQuery searchQuery;
		public BufferedSearchHitCollector bufferedCollector = new BufferedSearchHitCollector();

		public Job localSearchJob;
		public Job remoteSearchJob;

		public SearchState() {
			/*
			 * We use these jobs to perform the local and remote searches in parallel in the
			 * background.
			 */
			localSearchJob = new Job("localSearchJob") { //$NON-NLS-1$

				@Override
				protected IStatus run(IProgressMonitor monitor) {
					localManager.search(searchQuery, bufferedCollector, localMonitor);
					return Status.OK_STATUS;
				}
			};
			remoteSearchJob = new Job("remoteSearchJob") { //$NON-NLS-1$

				@Override
				protected IStatus run(IProgressMonitor monitor) {
					remoteManager.search(searchQuery, bufferedCollector, remoteMonitor);
					return Status.OK_STATUS;
				}
			};
			localSearchJob.setSystem(true);
			remoteSearchJob.setSystem(true);
		}
	}

	/*
	 * Constructs a new SearchManager.
	 */
	public SearchManager() {

	}

	/*
	 * Perform the given search both locally and remotely if configured.
	 */
	public void search(ISearchQuery searchQuery, ISearchHitCollector collector, IProgressMonitor pm)
			throws QueryTooComplexException {
		if (RemoteHelp.isEnabled()) {
			searchLocalAndRemote(searchQuery, collector, pm);
		}
		else {
			searchLocal(searchQuery, collector, pm);
		}
	}

	/*
	 * Perform the given search locally only.
	 */
	public void searchLocal(ISearchQuery searchQuery, ISearchHitCollector collector, IProgressMonitor pm)
			throws QueryTooComplexException {
		localManager.search(searchQuery, collector, pm);
	}

	/*
	 * Perform the given search both locally and remotely.
	 */
	public void searchLocalAndRemote(ISearchQuery searchQuery, ISearchHitCollector collector, IProgressMonitor pm)
			throws QueryTooComplexException {
		SearchState state = new SearchState();
		state.searchQuery = searchQuery;

		SubMonitor subMonitor = SubMonitor.convert(pm, 100);

		// allocate half of the progress bar for each
		state.localMonitor = subMonitor.split(50, SubMonitor.SUPPRESS_SUBTASK);
		state.remoteMonitor = subMonitor.split(50, SubMonitor.SUPPRESS_SUBTASK);

		// start both searches in parallel
		state.localSearchJob.schedule();
		state.remoteSearchJob.schedule();

		// wait until finished
		try {
			state.localSearchJob.join();
			state.remoteSearchJob.join();
		}
		catch (InterruptedException e) {
			String msg = "Unexpected InterruptedException while waiting for help search jobs to finish"; //$NON-NLS-1$
			HelpBasePlugin.logError(msg, e);
		}

		// results are in; send them off to the collector
		state.bufferedCollector.flush(collector);
		pm.done();
	}

	/**
	 * Performs the federated search.
	 */
	public void search(String expression, FederatedSearchEntry[] entries) {
		for (int i = 0; i < entries.length; i++) {
			FederatedSearchJob job = new FederatedSearchJob(expression, entries[i]);
			job.schedule();
		}
	}

	/*
	 * Returns the manager responsible for handling local searching.
	 */
	public LocalSearchManager getLocalSearchManager() {
		return localManager;
	}

	/*
	 * Returns the manager responsible for handling remote searching.
	 */
	public RemoteSearchManager getRemoteSearchManager() {
		return remoteManager;
	}

	/*
	 * Performs any necessary cleanup (workbench is shutting down).
	 */
	public void close() {
		localManager.close();
	}

	/*
	 * Gets the list of registered search processors
	 */
	public static AbstractSearchProcessor[] getSearchProcessors()
	{
		IConfigurationElement[] configs =
			Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.help.base.searchProcessor"); //$NON-NLS-1$

		ArrayList<Object> processors = new ArrayList<>();

		for (int c=0;c<configs.length;c++)
		{
			try {
				processors.add(
						configs[c].createExecutableExtension("class"));//$NON-NLS-1$
			} catch (CoreException e) {}
		}

		return processors.toArray(new AbstractSearchProcessor[processors.size()]);
	}

	/*
	 * Convert Lucene SearchHits to ISearchResults
	 */
	public static ISearchResult[] convertHitsToResults(SearchHit hits[]) {

		ISearchResult results[] = new ISearchResult[hits.length];
		for (int r=0;r<results.length;r++)
		{
			SearchResult result = new SearchResult();
			if (hits[r].getHref()!=null)
				result.setHref(hits[r].getHref());
			if (hits[r].getId()!=null)
				result.setId(hits[r].getId());
			if (hits[r].getParticipantId()!=null)
				result.setParticipantId(hits[r].getParticipantId());
			if (hits[r].getDescription()!=null)
				result.setDescription(hits[r].getDescription());
			if (hits[r].getLabel()!=null)
				result.setLabel(hits[r].getLabel());
			if (hits[r].getSummary()!=null)
				result.setSummary(hits[r].getSummary());
			if (hits[r].getToc()!=null)
				result.setToc(hits[r].getToc());
			if (hits[r].getIconURL()!=null)
				result.setIcon(hits[r].getIconURL());
			result.setScore(hits[r].getScore());
			result.setPotentialHit(hits[r].isPotentialHit());
			results[r] = result;
		}
		return results;
	}

	/*
	 * Convert ISearchResults to SearchHits
	 */
	public static SearchHit[] convertResultsToHits(ISearchResult[] results) {

		SearchHit hits[] = new SearchHit[results.length];
		for (int r=0;r<results.length;r++)
		{
			hits[r] = new SearchHit(
					results[r].getHref(),
					results[r].getLabel(),
					results[r].getSummary(),
					results[r].getScore(),
					results[r].getToc(),
					results[r].getId(),
					results[r].getParticipantId(),
					results[r].isPotentialHit());
		}
		return hits;
	}


	/*
	 * Buffers hits, and only sends them off to the wrapped collector
	 * when flush() is called.
	 */
	private class BufferedSearchHitCollector implements ISearchHitCollector {
		private Set<SearchHit> allHits = new HashSet<>();
		private String wordsSearched = null;

		/* (non-Javadoc)
		 * @see org.eclipse.help.internal.search.ISearchHitCollector#addHits(java.util.List, java.lang.String)
		 */
		@Override
		public void addHits(List<SearchHit> hits, String wordsSearched) {
			if (wordsSearched != null) {
				this.wordsSearched = wordsSearched;
			}
			allHits.addAll(hits);
		}

		/*
		 * Send all the buffered hits to the underlying collector,
		 * and reset the buffers.
		 */
		public void flush(ISearchHitCollector collector) {
			// sort by score
			List<SearchHit> hitsList = new ArrayList<>(allHits);
			Collections.sort(hitsList);
			collector.addHits(hitsList, wordsSearched);
			allHits.clear();
			wordsSearched = null;
		}

		@Override
		public void addQTCException(QueryTooComplexException exception) throws QueryTooComplexException {
			throw exception;
		}
	}
}
