/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package com.excelsior.xds.ui.editor.modula.spellcheck.internal;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
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.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.core.variables.IStringVariableManager;
import org.eclipse.core.variables.VariablesPlugin;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.texteditor.spelling.SpellingService;

import com.excelsior.xds.core.log.LogHelper;
import com.excelsior.xds.core.preferences.PreferenceConstants;
import com.excelsior.xds.core.preferences.PreferenceKey;
import com.excelsior.xds.core.preferences.PreferenceKeys;
import com.excelsior.xds.ui.editor.modula.spellcheck.internal.engine.DefaultSpellChecker;
import com.excelsior.xds.ui.editor.modula.spellcheck.internal.engine.ISpellCheckEngine;
import com.excelsior.xds.ui.editor.modula.spellcheck.internal.engine.ISpellChecker;
import com.excelsior.xds.ui.editor.modula.spellcheck.internal.engine.ISpellDictionary;
import com.excelsior.xds.ui.editor.modula.spellcheck.internal.engine.LocaleSensitiveSpellDictionary;
import com.excelsior.xds.ui.editor.modula.spellcheck.internal.engine.PersistentSpellDictionary;
import com.excelsior.xds.ui.editor.spellcheck.internal.plugin.SpellcheckPlugin;


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

	/** 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) {
			LogHelper.logError(ex);
			return Collections.emptySet();
		}

		Set<Locale> localesWithInstalledDictionaries= new HashSet<Locale>();
		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) {
			LogHelper.logError(ex);
			return fgLocalesWithInstalledDictionaries= Collections.emptySet();
		}

		fgLocalesWithInstalledDictionaries= new HashSet<Locale>();

		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 SpellcheckPlugin plugin= SpellcheckPlugin.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<ISpellDictionary>();

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

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

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

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

		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
		}
		
		PreferenceKeys.PKEY_SPELLING_LOCALE.addChangeListener(this);
		EditorsUI.getPreferenceStore().addPropertyChangeListener(this);
	}

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

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

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

		resetSpellChecker();

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

		for (Iterator<ISpellDictionary> iterator= fGlobalDictionaries.iterator(); iterator.hasNext();) {
			ISpellDictionary dictionary= iterator.next();
			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() {
	    String locale = PreferenceKeys.PKEY_SPELLING_LOCALE.getStoredValue();
	    if (PreferenceConstants.PREF_VALUE_NO_LOCALE.equals(locale)) {
	        locale = Platform.getNL();
	    }
		return convertToLocale(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()
	 */
	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)
	 */
	public final void propertyChange(final PropertyChangeEvent event) {
	    if (event.getProperty().equals(SpellingService.PREFERENCE_SPELLING_ENABLED) && !EditorsUI.getPreferenceStore().getBoolean(SpellingService.PREFERENCE_SPELLING_ENABLED)) {
	        if (this == fgEngine)
	            SpellCheckEngine.shutdownInstance();
	        else
	            shutdown();
	    }
	}
	
    @Override
    public void preferenceChange(PreferenceChangeEvent event) {
        String key = event.getKey();
		if (key.equals(PreferenceKeys.PKEY_SPELLING_LOCALE.getKey())) {
            resetSpellChecker();
            return;
        }

		if (key.equals(
				PreferenceKeys.PKEY_SPELLING_USER_DICTIONARY.getKey())
				|| key.equals(PreferenceKeys.PKEY_SPELLING_USER_DICTIONARY_ENCODING.getKey())) {
			resetUserDictionary();
			return;
		}
    }


	/**
	 * 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;
		}

		String filePath= PreferenceKeys.PKEY_SPELLING_USER_DICTIONARY.getStoredValue();

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

		IStringVariableManager variableManager= variablesPlugin.getStringVariableManager();
		try {
			filePath= variableManager.performStringSubstitution(filePath);
		} catch (CoreException e) {
            LogHelper.logError(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$
				InputStream stream= url.openStream();
				if (stream != null) {
					try {
						fUserDictionary= new PersistentSpellDictionary(url);
						fChecker.addDictionary(fUserDictionary);
					} finally {
						stream.close();
					}
				}
			} catch (MalformedURLException exception) {
				// Do nothing
			} 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)
	 */
	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)
	 */
	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()
	 */
	public synchronized final void shutdown() {

	    PreferenceKey.removeChangeListener(PreferenceKeys.PKEY_SPELLING_LOCALE.getQualifier(), 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)
	 */
	public synchronized final void unregisterDictionary(final ISpellDictionary dictionary) {
		fGlobalDictionaries.remove(dictionary);
		fLocaleDictionaries.values().remove(dictionary);
		dictionary.unload();
		resetSpellChecker();
	}

}
