/*******************************************************************************
 * Copyright (c) 2000, 2011 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.jdt.internal.ui.text.spelling;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.variables.IStringVariableManager;
import org.eclipse.core.variables.VariablesPlugin;

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

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

import org.eclipse.ui.texteditor.spelling.SpellingService;

import org.eclipse.ui.editors.text.EditorsUI;

import org.eclipse.jdt.ui.PreferenceConstants;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.text.spelling.engine.DefaultSpellChecker;
import org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellCheckEngine;
import org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellChecker;
import org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellDictionary;
import org.eclipse.jdt.internal.ui.text.spelling.engine.LocaleSensitiveSpellDictionary;
import org.eclipse.jdt.internal.ui.text.spelling.engine.PersistentSpellDictionary;


/**
 * Spell check engine for Java source spell checking.
 *
 * @since 3.0
 */
public class SpellCheckEngine implements ISpellCheckEngine, IPropertyChangeListener {

	/** The dictionary location */
	public static final String DICTIONARY_LOCATION= "dictionaries/"; //$NON-NLS-1$

	/** The singleton engine instance */
	private static ISpellCheckEngine fgEngine= null;

	/**
	 * Caches the locales of installed dictionaries.
	 *
	 * @since 3.3
	 */
	private static Set<Locale> fgLocalesWithInstalledDictionaries;

	/**
	 * Returns the locales for which this
	 * spell check engine has dictionaries in certain location.
	 *
	 * @param location dictionaries location
	 * @return The available locales for this engine
	 */
	private static Set<Locale> getLocalesWithInstalledDictionaries(URL location) {
		String[] fileNames;
		try {
			URL url= FileLocator.toFileURL(location);
			File file= new File(url.getFile());
			if (!file.isDirectory())
				return Collections.emptySet();
			fileNames= file.list();
			if (fileNames == null)
				return Collections.emptySet();
		} catch (IOException ex) {
			JavaPlugin.log(ex);
			return Collections.emptySet();
		}

		Set<Locale> localesWithInstalledDictionaries= new HashSet<>();
		int fileNameCount= fileNames.length;
		for (int i= 0; i < fileNameCount; i++) {
			String fileName= fileNames[i];
			int localeEnd= fileName.indexOf(".dictionary"); //$NON-NLS-1$
			if (localeEnd > 1) {
				String localeName= fileName.substring(0, localeEnd);
				int languageEnd=localeName.indexOf('_');
				if (languageEnd == -1)
					localesWithInstalledDictionaries.add(new Locale(localeName));
				else if (languageEnd == 2 && localeName.length() == 5)
					localesWithInstalledDictionaries.add(new Locale(localeName.substring(0, 2), localeName.substring(3)));
				else if (localeName.length() > 6 && localeName.charAt(5) == '_')
					localesWithInstalledDictionaries.add(new Locale(localeName.substring(0, 2), localeName.substring(3, 5), localeName.substring(6)));
			}
		}

		return localesWithInstalledDictionaries;
	}


	/**
	 * Returns the locales for which this
	 * spell check engine has dictionaries.
	 *
	 * @return The available locales for this engine
	 */
	public static Set<Locale> getLocalesWithInstalledDictionaries() {
		if (fgLocalesWithInstalledDictionaries != null)
			return fgLocalesWithInstalledDictionaries;

		Enumeration<URL> locations;
		try {
			locations= getDictionaryLocations();
			if (locations == null)
				return fgLocalesWithInstalledDictionaries= Collections.emptySet();
		} catch (IOException ex) {
			JavaPlugin.log(ex);
			return fgLocalesWithInstalledDictionaries= Collections.emptySet();
		}

		fgLocalesWithInstalledDictionaries= new HashSet<>();

		while (locations.hasMoreElements()) {
			URL location= locations.nextElement();
			Set<Locale> locales= getLocalesWithInstalledDictionaries(location);
			fgLocalesWithInstalledDictionaries.addAll(locales);
		}

		return fgLocalesWithInstalledDictionaries;
	}

	/**
	 * Returns the default locale for this engine.
	 *
	 * @return The default locale
	 */
	public static Locale getDefaultLocale() {
		return Locale.getDefault();
	}

	/**
	 * Returns the dictionary closest to the given locale.
	 *
	 * @param locale the locale
	 * @return the dictionary or <code>null</code> if none is suitable
	 * @since 3.3
	 */
	public ISpellDictionary findDictionary(Locale locale) {
		ISpellDictionary dictionary= fLocaleDictionaries.get(locale);
		if (dictionary != null)
			return dictionary;

		// Try same language
		String language= locale.getLanguage();
		Iterator<Entry<Locale, ISpellDictionary>> iter= fLocaleDictionaries.entrySet().iterator();
		while (iter.hasNext()) {
			Entry<Locale, ISpellDictionary> entry= iter.next();
			Locale dictLocale= entry.getKey();
			if (dictLocale.getLanguage().equals(language))
				return entry.getValue();
		}

		return null;
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellCheckEngine#findDictionary(java.util.Locale)
	 * @since 3.3
	 */
	public static Locale findClosestLocale(Locale locale) {
		if (locale == null || locale.toString().length() == 0)
			return locale;

		if (getLocalesWithInstalledDictionaries().contains(locale))
			return locale;

		// Try same language
		String language= locale.getLanguage();
		Iterator<Locale> iter= getLocalesWithInstalledDictionaries().iterator();
		while (iter.hasNext()) {
			Locale dictLocale= iter.next();
			if (dictLocale.getLanguage().equals(language))
				return dictLocale;
		}

		// Try whether American English is present
		Locale defaultLocale= Locale.US;
		if (getLocalesWithInstalledDictionaries().contains(defaultLocale))
			return defaultLocale;

		return null;
	}

	/**
	 * Returns the enumeration of URLs for the dictionary locations where
	 * the Platform dictionaries are located.
	 * <p>
	 * This is in <code>org.eclipse.jdt.ui/dictionaries/</code>
	 * which can also be populated via fragments.
	 * </p>
	 *
	 * @throws IOException if there is an I/O error
	 * @return The dictionary locations, or <code>null</code> iff the locations are not known
	 */
	public static Enumeration<URL> getDictionaryLocations() throws IOException {
		final JavaPlugin plugin= JavaPlugin.getDefault();
		if (plugin != null)
			return plugin.getBundle().getResources("/" + DICTIONARY_LOCATION); //$NON-NLS-1$
		return null;
	}

	/**
	 * Returns the singleton instance of the spell check engine.
	 *
	 * @return The singleton instance of the spell check engine
	 */
	public static synchronized final ISpellCheckEngine getInstance() {

		if (fgEngine == null)
			fgEngine= new SpellCheckEngine();

		return fgEngine;
	}

	/**
	 * Shuts down the singleton instance of the spell check engine.
	 */
	public static synchronized final void shutdownInstance() {
		if (fgEngine != null) {
			fgEngine.shutdown();
			fgEngine= null;
		}
	}

	/** The registered locale insensitive dictionaries */
	private Set<ISpellDictionary> fGlobalDictionaries= new HashSet<>();

	/** The spell checker for fLocale */
	private ISpellChecker fChecker= null;

	/** The registered locale sensitive dictionaries */
	private Map<Locale, ISpellDictionary> fLocaleDictionaries= new HashMap<>();

	/** The user dictionary */
	private ISpellDictionary fUserDictionary= null;

	/**
	 * Creates a new spell check manager.
	 */
	private SpellCheckEngine() {

		fGlobalDictionaries.add(new TaskTagDictionary());
		fGlobalDictionaries.add(new HtmlTagDictionary());
		fGlobalDictionaries.add(new JavaDocTagDictionary());

		try {

			Locale locale= null;
			final Enumeration<URL> locations= getDictionaryLocations();

			while (locations != null && locations.hasMoreElements()) {
				URL location= locations.nextElement();

				for (final Iterator<Locale> iterator= getLocalesWithInstalledDictionaries(location).iterator(); iterator.hasNext();) {

					locale= iterator.next();
					fLocaleDictionaries.put(locale, new LocaleSensitiveSpellDictionary(locale, location));
				}
			}

		} catch (IOException exception) {
			// Do nothing
		}

		JavaPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this);
		EditorsUI.getPreferenceStore().addPropertyChangeListener(this);
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellCheckEngine#getSpellChecker()
	 */
	@Override
	public synchronized final ISpellChecker getSpellChecker() throws IllegalStateException {
		if (fGlobalDictionaries == null)
			throw new IllegalStateException("spell checker has been shut down"); //$NON-NLS-1$

		IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore();
		Locale locale= getCurrentLocale(store);
		if (fUserDictionary == null && "".equals(locale.toString())) //$NON-NLS-1$
			return null;

		if (fChecker != null && fChecker.getLocale().equals(locale))
			return fChecker;

		resetSpellChecker();

		fChecker= new DefaultSpellChecker(store, locale);
		resetUserDictionary();

		for (ISpellDictionary dictionary : fGlobalDictionaries) {
			fChecker.addDictionary(dictionary);
		}

		ISpellDictionary dictionary= findDictionary(fChecker.getLocale());
		if (dictionary != null)
			fChecker.addDictionary(dictionary);

		return fChecker;
	}

	/**
	 * Returns the current locale of the spelling preferences.
	 *
	 * @param store the preference store
	 * @return The current locale of the spelling preferences
	 */
	private Locale getCurrentLocale(IPreferenceStore store) {
		return convertToLocale(store.getString(PreferenceConstants.SPELLING_LOCALE));
	}

	public static Locale convertToLocale(String locale) {
		Locale defaultLocale= SpellCheckEngine.getDefaultLocale();
		if (locale.equals(defaultLocale.toString()))
			return defaultLocale;

		int length= locale.length();
		if (length >= 5)
			return new Locale(locale.substring(0, 2), locale.substring(3, 5));

		if (length == 2 && locale.indexOf('_') == -1)
			return new Locale(locale);

		if (length == 3 && locale.charAt(0) == '_')
			return new Locale("", locale.substring(1)); //$NON-NLS-1$

		return new Locale(""); //$NON-NLS-1$
	}

	/*
	 * @see org.eclipse.jdt.ui.text.spelling.engine.ISpellCheckEngine#getLocale()
	 */
	@Override
	public synchronized final Locale getLocale() {
		if (fChecker == null)
			return null;

		return fChecker.getLocale();
	}

	/*
	 * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
	 */
	@Override
	public final void propertyChange(final PropertyChangeEvent event) {
		if (PreferenceConstants.SPELLING_LOCALE.equals(event.getProperty())) {
			resetSpellChecker();
			return;
		}

		if (PreferenceConstants.SPELLING_USER_DICTIONARY.equals(event.getProperty())) {
			resetUserDictionary();
			return;
		}

		if (PreferenceConstants.SPELLING_USER_DICTIONARY_ENCODING.equals(event.getProperty())) {
			resetUserDictionary();
			return;
		}

		if (SpellingService.PREFERENCE_SPELLING_ENABLED.equals(event.getProperty()) && !EditorsUI.getPreferenceStore().getBoolean(SpellingService.PREFERENCE_SPELLING_ENABLED)) {
			if (this == fgEngine)
				SpellCheckEngine.shutdownInstance();
			else
				shutdown();
		}
	}

	/**
	 * Resets the current checker's user dictionary.
	 */
	private synchronized void resetUserDictionary() {
		if (fChecker == null)
			return;

		// Update user dictionary
		if (fUserDictionary != null) {
			fChecker.removeDictionary(fUserDictionary);
			fUserDictionary.unload();
			fUserDictionary= null;
		}

		IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore();
		String filePath= store.getString(PreferenceConstants.SPELLING_USER_DICTIONARY);

		VariablesPlugin variablesPlugin= VariablesPlugin.getDefault();
		if (variablesPlugin == null)
			return;

		IStringVariableManager variableManager= variablesPlugin.getStringVariableManager();
		try {
			filePath= variableManager.performStringSubstitution(filePath);
		} catch (CoreException e) {
			JavaPlugin.log(e);
			return;
		}
		if (filePath.length() > 0) {
			try {
				File file= new File(filePath);
				if (!file.exists() && !file.createNewFile())
					return;

				final URL url= new URL("file", null, filePath); //$NON-NLS-1$
				try (InputStream stream= url.openStream()) {
					if (stream != null) {
						fUserDictionary= new PersistentSpellDictionary(url);
						fChecker.addDictionary(fUserDictionary);
					}
				}
			} catch (IOException exception) {
				// Do nothing
			}
		}
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellCheckEngine#registerDictionary(org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellDictionary)
	 */
	@Override
	public synchronized final void registerGlobalDictionary(final ISpellDictionary dictionary) {
		fGlobalDictionaries.add(dictionary);
		resetSpellChecker();
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellCheckEngine#registerDictionary(java.util.Locale, org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellDictionary)
	 */
	@Override
	public synchronized final void registerDictionary(final Locale locale, final ISpellDictionary dictionary) {
		fLocaleDictionaries.put(locale, dictionary);
		resetSpellChecker();
	}

	/*
	 * @see org.eclipse.jdt.internal.ui.text.spelling.engine.ISpellCheckEngine#unload()
	 */
	@Override
	public synchronized final void shutdown() {

		JavaPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this);
		EditorsUI.getPreferenceStore().removePropertyChangeListener(this);

		ISpellDictionary dictionary= null;
		for (final Iterator<ISpellDictionary> iterator= fGlobalDictionaries.iterator(); iterator.hasNext();) {
			dictionary= iterator.next();
			dictionary.unload();
		}
		fGlobalDictionaries= null;

		for (final Iterator<ISpellDictionary> iterator= fLocaleDictionaries.values().iterator(); iterator.hasNext();) {
			dictionary= iterator.next();
			dictionary.unload();
		}
		fLocaleDictionaries= null;

		fUserDictionary= null;
		fChecker= null;
	}

	private synchronized void resetSpellChecker() {
		if (fChecker != null) {
			ISpellDictionary dictionary= fLocaleDictionaries.get(fChecker.getLocale());
			if (dictionary != null)
				dictionary.unload();
		}
		fChecker= null;
	}

	/*
	 * @see org.eclipse.jdt.ui.text.spelling.engine.ISpellCheckEngine#unregisterDictionary(org.eclipse.jdt.ui.text.spelling.engine.ISpellDictionary)
	 */
	@Override
	public synchronized final void unregisterDictionary(final ISpellDictionary dictionary) {
		fGlobalDictionaries.remove(dictionary);
		fLocaleDictionaries.values().remove(dictionary);
		dictionary.unload();
		resetSpellChecker();
	}
}
