/*******************************************************************************
 * Copyright (c) 2005, 2016 Intel Corporation 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:
 *     Intel Corporation - initial API and implementation
 *     IBM Corporation - 122967 [Help] Remote help system
 *                       163558 Dynamic content support for all UA
 *                       165168 [Help] Better control of how help content is arranged and ordered
 *******************************************************************************/
package org.eclipse.help.internal.index;

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

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import org.eclipse.help.AbstractIndexProvider;
import org.eclipse.help.IIndex;
import org.eclipse.help.IIndexContribution;
import org.eclipse.help.internal.HelpData;
import org.eclipse.help.internal.HelpPlugin;
import org.eclipse.help.internal.UAElementFactory;

public class IndexManager {

	private static final String EXTENSION_POINT_ID_INDEX = HelpPlugin.PLUGIN_ID + ".index"; //$NON-NLS-1$
	private static final String ELEMENT_NAME_INDEX_PROVIDER = "indexProvider"; //$NON-NLS-1$
	private static final String ATTRIBUTE_NAME_CLASS = "class"; //$NON-NLS-1$

	private Map<String, IIndexContribution[]> indexContributionsByLocale = new HashMap<>();
	private Map<String, Index> indexesByLocale = new HashMap<>();
	private AbstractIndexProvider[] indexProviders;

	public synchronized IIndex getIndex(String locale) {
		Index index = indexesByLocale.get(locale);
		if (index == null) {
			HelpPlugin.getTocManager().getTocs(locale);  // Ensure Tocs and index not built simultaneously
			long start = System.currentTimeMillis();
			if (HelpPlugin.DEBUG_INDEX) {
			    System.out.println("Start to update keyword index for locale " + locale); //$NON-NLS-1$
			}
			List<IndexContribution> contributions = new ArrayList<>(Arrays.asList(readIndexContributions(locale)));
			filterIndexContributions(contributions);
			IndexAssembler assembler = new IndexAssembler();
			index = assembler.assemble(contributions, locale);
			indexesByLocale.put(locale, index);
			long stop = System.currentTimeMillis();
			if (HelpPlugin.DEBUG_INDEX) {
			    System.out.println("Milliseconds to update keyword index for locale " + locale +  " = " + (stop - start)); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		return index;
	}

	/*
	 * Returns all index contributions for the given locale, from all
	 * providers.
	 */
	public synchronized IIndexContribution[] getIndexContributions(String locale) {
		IIndexContribution[] contributions = indexContributionsByLocale.get(locale);
		if (contributions == null) {
			contributions = readIndexContributions(locale);
			indexContributionsByLocale.put(locale, contributions);
		}
		return contributions;
	}

	private IndexContribution[] readIndexContributions(String locale) {
		IndexContribution[] cached;
		List<IndexContribution> contributions = new ArrayList<>();
		AbstractIndexProvider[] providers = getIndexProviders();
		for (int i=0;i<providers.length;++i) {
			IIndexContribution[] contrib;
			try {
				contrib = providers[i].getIndexContributions(locale);
				// check for nulls and root element
				for (int j = 0; j < contrib.length; ++j) {
					if (contrib[j] == null) {
						String msg = "Help keyword index provider \"" + providers[i].getClass().getName() + "\" returned a null contribution (skipping)"; //$NON-NLS-1$ //$NON-NLS-2$
						HelpPlugin.logError(msg);
					} else if (contrib[j].getIndex() == null) {
						String msg = "Help keyword index provider \"" + providers[i].getClass().getName() + "\" returned a contribution with a null root element (expected a \"" + Index.NAME + "\" element; skipping)"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						HelpPlugin.logError(msg);
					} else {
						IndexContribution contribution = new IndexContribution();
						contribution.setId(contrib[j].getId());
						contribution.setLocale(contrib[j].getLocale());
						IIndex index = contrib[j].getIndex();
						contribution.setIndex(index instanceof Index ? (Index) index
								: (Index) UAElementFactory.newElement(index));
						contributions.add(contribution);
					}
				}
			} catch (Throwable t) {
				// log, and skip the offending provider
				String msg = "Error getting help keyword index data from provider: " + providers[i].getClass().getName() + " (skipping provider)"; //$NON-NLS-1$ //$NON-NLS-2$
				HelpPlugin.logError(msg, t);
				continue;
			}
		}
		cached = contributions.toArray(new IndexContribution[contributions.size()]);
		return cached;
	}

	/*
	 * Clears all cached contributions, forcing the manager to query the
	 * providers again next time a request is made.
	 */
	public void clearCache() {
		indexContributionsByLocale.clear();
		indexesByLocale.clear();
	}

	/*
	 * Internal hook for unit testing.
	 */
	public AbstractIndexProvider[] getIndexProviders() {
		if (indexProviders == null) {
			List<AbstractIndexProvider> providers = new ArrayList<>();
			IExtensionRegistry registry = Platform.getExtensionRegistry();
			IConfigurationElement[] elements = registry.getConfigurationElementsFor(EXTENSION_POINT_ID_INDEX);
			for (int i=0;i<elements.length;++i) {
				IConfigurationElement elem = elements[i];
				if (elem.getName().equals(ELEMENT_NAME_INDEX_PROVIDER)) {
					try {
						AbstractIndexProvider provider = (AbstractIndexProvider)elem.createExecutableExtension(ATTRIBUTE_NAME_CLASS);
						providers.add(provider);
					}
					catch (CoreException e) {
						// log and skip
						String msg = "Error instantiating help keyword index provider class \"" + elem.getAttribute(ATTRIBUTE_NAME_CLASS) + '"'; //$NON-NLS-1$
						HelpPlugin.logError(msg, e);
					}
				}
			}
			indexProviders = providers.toArray(new AbstractIndexProvider[providers.size()]);
		}
		return indexProviders;
	}

	/*
	 * Returns whether or not the index has been completely loaded for the
	 * given locale yet or not.
	 */
	public boolean isIndexLoaded(String locale) {
		return indexesByLocale.get(locale) != null;
	}

	/*
	 * Internal hook for unit testing.
	 */
	public void setIndexProviders(AbstractIndexProvider[] indexProviders) {
		this.indexProviders = indexProviders;
	}

	/*
	 * Filters the given contributions according to product preferences. If
	 * either the contribution's id or its category's id is listed in the
	 * ignoredIndexes, filter the contribution.
	 */
	private void filterIndexContributions(List<IndexContribution> unfiltered) {
		Set<String> indexesToFilter = getIgnoredIndexContributions();
		ListIterator<IndexContribution> iter = unfiltered.listIterator();
		while (iter.hasNext()) {
			IIndexContribution contribution = iter.next();
			if (indexesToFilter.contains(contribution.getId())) {
				iter.remove();
			}
		}
	}

	private Set<String> getIgnoredIndexContributions() {
		HelpData helpData = HelpData.getProductHelpData();
		if (helpData != null) {
			return helpData.getHiddenIndexes();
		} else {
			HashSet<String> ignored = new HashSet<>();
			String preferredIndexes = Platform.getPreferencesService().getString(HelpPlugin.PLUGIN_ID, HelpPlugin.IGNORED_INDEXES_KEY, "", null); //$NON-NLS-1$
			if (preferredIndexes.length() > 0) {
				StringTokenizer suggestdOrderedInfosets = new StringTokenizer(preferredIndexes, " ;,"); //$NON-NLS-1$
				while (suggestdOrderedInfosets.hasMoreTokens()) {
					ignored.add(suggestdOrderedInfosets.nextToken());
				}
			}
			return ignored;
		}
	}
}
