/*******************************************************************************
 * 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
 *******************************************************************************/
package org.eclipse.help.internal.search;

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Hashtable;

import javax.xml.parsers.*;

import org.eclipse.core.runtime.*;
import org.eclipse.help.IHelpResource;
import org.eclipse.help.internal.base.*;
import org.eclipse.help.internal.base.util.ProxyUtil;
import org.eclipse.help.internal.entityresolver.LocalEntityResolver;
import org.eclipse.help.search.ISearchEngine;
import org.eclipse.help.search.ISearchEngineResult;
import org.eclipse.help.search.ISearchEngineResultCollector;
import org.eclipse.help.search.ISearchScope;
import org.w3c.dom.*;
import org.xml.sax.*;

/**
 * This implementation of <code>ISearchEngine</code> interface performs search
 * by running a query on the remote InfoCenter and presenting the results
 * locally. Instances of this engine type are required to supply the URL to the
 * InfoCenter.
 *
 * <p>
 * This class is made public in order to be instantiated and parametrized
 * directly in the extentsions. Clients are required to supply the required URL
 * as a parameter <code>url</code>.
 *
 * <p>
 * This class is not expected to be subclassed or otherwise accessed
 * programmatically.
 *
 * @since 3.1
 */

public final class InfoCenter implements ISearchEngine {
	private Hashtable<String, IHelpResource> tocs;

	public static class Scope implements ISearchScope {
		String url;

		boolean searchSelected;

		String[] tocs;

		public Scope(String url, boolean searchSelected, String[] tocs) {
			this.url = url;
			this.searchSelected = searchSelected;
			this.tocs = tocs;
		}
	}

	private class InfoCenterResult implements ISearchEngineResult {
		private IHelpResource category;

		private Element node;

		private String baseURL;

		public InfoCenterResult(String baseURL, Element node) {
			this.baseURL = baseURL;
			this.node = node;
			createCategory(node);
		}

		private void createCategory(Element node) {
			final String href = node.getAttribute("toc"); //$NON-NLS-1$
			final String label = node.getAttribute("toclabel"); //$NON-NLS-1$
			if (href != null && label != null) {
				category = tocs.get(href);
				if (category == null) {
					category = new IHelpResource() {
						@Override
						public String getLabel() {
							return label;
						}

						@Override
						public String getHref() {
							return href;
						}
					};
					tocs.put(href, category);
				}
			}
		}

		@Override
		public String getLabel() {
			return node.getAttribute("label"); //$NON-NLS-1$
		}

		@Override
		public String getDescription() {
			return null;
		}

		@Override
		public IHelpResource getCategory() {
			return category;
		}

		@Override
		public String getHref() {
			return node.getAttribute("href"); //$NON-NLS-1$
		}

		@Override
		public float getScore() {
			String value = node.getAttribute("score"); //$NON-NLS-1$
			if (value != null)
				return Float.parseFloat(value);
			return (float) 0.0;
		}

		@Override
		public boolean getForceExternalWindow() {
			return false;
		}

		@Override
		public String toAbsoluteHref(String href, boolean frames) {
			String url = baseURL;
			if (!url.endsWith("/")) //$NON-NLS-1$
				url = url + "/"; //$NON-NLS-1$
			if (frames) {
				return url + "topic" + href; //$NON-NLS-1$
			}
			if (url.indexOf('?')> 0)  {
				return url + "topic" + href + "&noframes=true"; //$NON-NLS-1$ //$NON-NLS-2$
			}
			return url + "topic" + href + "?noframes=true"; //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * The default constructor.
	 */
	public InfoCenter() {
		tocs = new Hashtable<>();
	}

	@Override
	public void run(String query, ISearchScope scope,
			ISearchEngineResultCollector collector, IProgressMonitor monitor)
			throws CoreException {
		URL url = createURL(query, (Scope) scope);
		if (url == null)
			return;
		InputStream is = null;
		tocs.clear();
		try {
			URLConnection connection = ProxyUtil.getConnection(url);
			SubMonitor subMonitor = SubMonitor.convert(monitor, HelpBaseResources.InfoCenter_connecting, 5);
			is = connection.getInputStream();
			try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
				subMonitor.worked(1);
				load(((Scope) scope).url, reader, collector, subMonitor.split(4));
			}
		} catch (FileNotFoundException e) {
			reportError(HelpBaseResources.InfoCenter_fileNotFound, e, collector);
		} catch (IOException e) {
			reportError(HelpBaseResources.InfoCenter_io, e, collector);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
				}
			}
		}
	}

	private void reportError(String message, IOException e,
			ISearchEngineResultCollector collector) {
		Status status = new Status(IStatus.ERROR, HelpBasePlugin.PLUGIN_ID,
				IStatus.OK, message, e);
		collector.error(status);
	}

	private void load(String baseURL, Reader r,
			ISearchEngineResultCollector collector, IProgressMonitor monitor) {
		Document document = null;
		try {
			DocumentBuilder parser = DocumentBuilderFactory.newInstance()
					.newDocumentBuilder();
			parser.setEntityResolver(new LocalEntityResolver());
			if (monitor.isCanceled())
				return;
			SubMonitor subMonitor = SubMonitor.convert(monitor, 5);
			subMonitor.subTask(HelpBaseResources.InfoCenter_searching);
			document = parser.parse(new InputSource(r));
			if (monitor.isCanceled())
				return;

			// Strip out any comments first
			Node root = document.getFirstChild();
			while (root.getNodeType() == Node.COMMENT_NODE) {
				document.removeChild(root);
				root = document.getFirstChild();
				if (monitor.isCanceled())
					return;
			}
			subMonitor.worked(1);
			load(baseURL, document, (Element) root, collector, subMonitor.split(4));
		} catch (ParserConfigurationException e) {
			// ignore
		} catch (IOException e) {
			// ignore
		} catch (SAXException e) {
			// ignore
		}
	}

	private void load(String baseURL, Document doc, Element root,
			ISearchEngineResultCollector collector, IProgressMonitor monitor) {
		NodeList topics = root.getElementsByTagName("hit"); //$NON-NLS-1$
		ISearchEngineResult[] results = new ISearchEngineResult[topics
				.getLength()];
		monitor.subTask(HelpBaseResources.InfoCenter_processing);
		monitor.beginTask("", results.length); //$NON-NLS-1$
		for (int i = 0; i < topics.getLength(); i++) {
			Element el = (Element) topics.item(i);
			if (monitor.isCanceled())
				break;
			results[i] = new InfoCenterResult(baseURL, el);
			monitor.worked(1);
		}
		collector.accept(results);
	}

	private URL createURL(String query, Scope scope) {
		if (scope.url == null) {
			return null;
		}
		StringBuilder buf = new StringBuilder();
		buf.append(scope.url);
		if (!scope.url.endsWith("/")) //$NON-NLS-1$
			buf.append("/search?phrase="); //$NON-NLS-1$
		else
			buf.append("search?phrase="); //$NON-NLS-1$
		try {
			buf.append(URLEncoder.encode(query, "UTF-8")); //$NON-NLS-1$
		} catch (UnsupportedEncodingException e) {
			buf.append(query);
		}
		buf.append("&locale="); //$NON-NLS-1$
		buf.append(Platform.getNL());
		if (scope.searchSelected && scope.tocs != null) {
			buf.append("&scopedSearch=true"); //$NON-NLS-1$
			for (int i = 0; i < scope.tocs.length; i++) {
				String toc;
				try {
					toc = URLEncoder.encode(scope.tocs[i], "UTF-8"); //$NON-NLS-1$
				} catch (UnsupportedEncodingException e) {
					toc = scope.tocs[i];
				}
				buf.append("&scope="); //$NON-NLS-1$
				buf.append(toc);
			}
		}
		try {
			return new URL(buf.toString());
		} catch (MalformedURLException e) {
			return null;
		}
	}
}
