/* enchant
 * Copyright (C) 2003, 2004 Dom Lachowicz
 * Copyright (C) 2017 Reuben Thomas <rrt@sc3d.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * In addition, as a special exception, Dom Lachowicz
 * gives permission to link the code of this program with
 * non-LGPL Spelling Provider libraries (eg: a MSFT Office
 * spell checker backend) and distribute linked combinations including
 * the two.  You must obey the GNU Lesser General Public License in all
 * respects for all of the code used other than said providers.  If you modify
 * this file, you may extend this exception to your version of the
 * file, but you are not obligated to do so.  If you do not wish to
 * do so, delete this exception statement from your version.
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <glib.h>
#include <gmodule.h>
#include <glib/gstdio.h>
#include <locale.h>

#ifdef _WIN32
#include <windows.h>
#endif

#include "enchant.h"
#include "enchant-provider.h"
#include "pwl.h"
#include "unused-parameter.h"
#include "relocatable.h"
#include "configmake.h"

/********************************************************************************/

struct str_enchant_broker
{
	GSList *provider_list;	/* list of all of the spelling backend providers */
	GHashTable *dict_map;		/* map of language tag -> dictionary */
	GHashTable *provider_ordering; /* map of language tag -> provider order */

	gchar * error;
};

typedef struct str_enchant_session
{
	GHashTable *session_include;
	GHashTable *session_exclude;
	EnchantPWL *personal;
	EnchantPWL *exclude;

	char * personal_filename;
	char * exclude_filename;
	char * language_tag;

	char * error;

	gboolean is_pwl;

	EnchantProvider * provider;
} EnchantSession;

typedef struct str_enchant_dict_private_data
{
	unsigned int reference_count;
	EnchantSession* session;
} EnchantDictPrivateData;

typedef EnchantProvider *(*EnchantProviderInitFunc) (void);
typedef void             (*EnchantPreConfigureFunc) (EnchantProvider * provider, const char * module_dir);

/********************************************************************************/
/********************************************************************************/

/* Relocate a path and ensure the result is allocated on the heap */
char *
enchant_relocate (const char *path)
{
	char *newpath = (char *) relocate (path);
	if (path == newpath)
		newpath = strdup (newpath);
	return newpath;
}

static void
enchant_ensure_dir_exists (const char* dir)
{
	if (dir && !g_file_test (dir, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))
		{
			(void)g_remove (dir);
			g_mkdir_with_parents (dir, 0700);
		}
}

char *
enchant_get_user_config_dir (void)
{
	const gchar * env = g_getenv("ENCHANT_CONFIG_DIR");
	if (env)
		return g_filename_to_utf8(env, -1, NULL, NULL, NULL);
	return g_build_filename (g_get_user_config_dir (), "enchant", NULL);
}

GSList *
enchant_get_conf_dirs (void)
{
	GSList *conf_dirs = NULL;
	char *pkgdatadir = NULL;
	char *sysconfdir = NULL;
	char *pkgconfdir = NULL;
	char *user_config_dir = NULL;

	if ((pkgdatadir = enchant_relocate (PKGDATADIR)) == NULL)
		goto error_exit;
	conf_dirs = g_slist_append (conf_dirs, pkgdatadir);

	if ((sysconfdir = enchant_relocate (SYSCONFDIR)) == NULL)
		goto error_exit;
	if ((pkgconfdir = g_build_filename (sysconfdir, "enchant", NULL)) == NULL)
		goto error_exit;
	conf_dirs = g_slist_append (conf_dirs, pkgconfdir);
	free (sysconfdir);

	if ((user_config_dir = enchant_get_user_config_dir ()) == NULL)
		goto error_exit;
	conf_dirs = g_slist_append (conf_dirs, user_config_dir);

	return conf_dirs;

 error_exit:
	free (pkgdatadir);
	free (sysconfdir);
	free (pkgconfdir);
	free (user_config_dir);
	return NULL;
}

/********************************************************************************/
/********************************************************************************/

/* returns TRUE if tag is valid
 * for requires alphanumeric ASCII or underscore
 */
static _GL_ATTRIBUTE_PURE int
enchant_is_valid_dictionary_tag(const char * const tag)
{
	const char * it;
	for (it = tag; *it; ++it)
		{
			if(!g_ascii_isalnum(*it) && *it != '_')
				return 0;
		}

	return it != tag; /*empty tag invalid*/
}

static char *
enchant_normalize_dictionary_tag (const char * const dict_tag)
{
	char * new_tag = g_strstrip (strdup (dict_tag));

	/* strip off en_GB@euro */
	*strchrnul (new_tag, '@') = '\0';

	/* strip off en_GB.UTF-8 */
	*strchrnul (new_tag, '.') = '\0';

	/* turn en-GB into en_GB */
	char * needle;
	if ((needle = strchr (new_tag, '-')) != NULL)
		*needle = '_';

	/* everything before first '_' is converted to lower case */
	needle = strchrnul (new_tag, '_');
	for (gchar *it = new_tag; it != needle; ++it)
		*it = g_ascii_tolower (*it);
	/* everything after first '_' is converted to upper case */
	for (gchar *it = needle; *it; ++it)
		*it = g_ascii_toupper (*it);

	return new_tag;
}

static char *
enchant_iso_639_from_tag (const char * const dict_tag)
{
	char * new_tag = strdup (dict_tag);
	char * needle = strchr (new_tag, '_');

	if (needle != NULL)
		*needle = '\0';

	return new_tag;
}

static void
enchant_session_destroy (EnchantSession * session)
{
	g_hash_table_destroy (session->session_include);
	g_hash_table_destroy (session->session_exclude);
	enchant_pwl_free (session->personal);
	enchant_pwl_free (session->exclude);
	g_free (session->personal_filename);
	g_free (session->exclude_filename);
	free (session->language_tag);

	if (session->error)
		g_free (session->error);

	g_free (session);
}

static EnchantSession *
enchant_session_new_with_pwl (EnchantProvider * provider,
			      const char * const pwl,
			      const char * const excl,
			      const char * const lang,
			      gboolean fail_if_no_pwl)
{
	EnchantPWL *personal = NULL;
	if (pwl)
		personal = enchant_pwl_init_with_file (pwl);
	if (personal == NULL) {
		if (fail_if_no_pwl)
			return NULL;
		else
			personal = enchant_pwl_init ();
	}

	EnchantPWL *exclude = NULL;
	if (excl)
		exclude = enchant_pwl_init_with_file (excl);
	if (exclude == NULL)
		exclude = enchant_pwl_init ();

	EnchantSession * session = g_new0 (EnchantSession, 1);
	session->session_include = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	session->session_exclude = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	session->personal = personal;
	session->exclude = exclude;
	session->provider = provider;
	session->language_tag = strdup (lang);
	session->personal_filename = g_strdup (pwl); /* Need g_strdup because may be NULL */
	session->exclude_filename = g_strdup (excl); /* Need g_strdup because may be NULL */

	return session;
}

static EnchantSession *
_enchant_session_new (EnchantProvider *provider, const char * const user_config_dir,
		      const char * const lang, gboolean fail_if_no_pwl)
{
	if (!user_config_dir || !lang)
		return NULL;

	char *filename = g_strdup_printf ("%s.dic", lang);
	char *dic = g_build_filename (user_config_dir, filename, NULL);
	g_free (filename);

	filename = g_strdup_printf ("%s.exc", lang);
	char *excl = g_build_filename (user_config_dir, filename, NULL);
	g_free (filename);

	EnchantSession * session = enchant_session_new_with_pwl (provider, dic, excl, lang, fail_if_no_pwl);

	g_free (dic);
	g_free (excl);

	return session;
}

static EnchantSession *
enchant_session_new (EnchantProvider *provider, const char * const lang)
{
	char *user_config_dir = enchant_get_user_config_dir ();

	EnchantSession * session = NULL;
	session = _enchant_session_new (provider, user_config_dir, lang, TRUE);

	if (session == NULL && user_config_dir != NULL)
		{
			enchant_ensure_dir_exists (user_config_dir);
			session = _enchant_session_new (provider, user_config_dir, lang, FALSE);
		}

	g_free (user_config_dir);

	return session;
}

static void
enchant_session_add (EnchantSession * session, const char * const word, size_t len)
{
	char* key = g_strndup (word, len);
	g_hash_table_remove (session->session_exclude, key);
	g_hash_table_insert (session->session_include, key, GINT_TO_POINTER(TRUE));
}

static void
enchant_session_remove (EnchantSession * session, const char * const word, size_t len)
{
	char* key = g_strndup (word, len);
	g_hash_table_remove (session->session_include, key);
	g_hash_table_insert (session->session_exclude, key, GINT_TO_POINTER(TRUE));
}

static void
enchant_session_add_personal (EnchantSession * session, const char * const word, size_t len)
{
	enchant_pwl_add(session->personal, word, len);
}

static void
enchant_session_remove_personal (EnchantSession * session, const char * const word, size_t len)
{
	enchant_pwl_remove(session->personal, word, len);
}

static void
enchant_session_add_exclude (EnchantSession * session, const char * const word, size_t len)
{
	enchant_pwl_add(session->exclude, word, len);
}

static void
enchant_session_remove_exclude (EnchantSession * session, const char * const word, size_t len)
{
	enchant_pwl_remove(session->exclude, word, len);
}

/* a word is excluded if it is in the exclude dictionary or in the session exclude list
 *  AND the word has not been added to the session include list
 */
static gboolean
enchant_session_exclude (EnchantSession * session, const char * const word, size_t len)
{
	char * utf = g_strndup (word, len);
	gboolean result = !g_hash_table_lookup (session->session_include, utf) &&
		(g_hash_table_lookup (session->session_exclude, utf) ||
		 enchant_pwl_check (session->exclude, word, len) == 0);
	g_free (utf);

	return result;
}

static gboolean
enchant_session_contains (EnchantSession * session, const char * const word, size_t len)
{
	char * utf = g_strndup (word, len);
	gboolean result = g_hash_table_lookup (session->session_include, utf) ||
		(enchant_pwl_check (session->personal, word, len) == 0 &&
		 (!enchant_pwl_check (session->exclude, word, len)) == 0);
	g_free (utf);

	return result;
}

static void
enchant_session_clear_error (EnchantSession * session)
{
	if (session->error)
		{
			g_free (session->error);
			session->error = NULL;
		}
}

/********************************************************************************/
/********************************************************************************/

static void
enchant_free_string_list (char ** string_list)
{
	g_strfreev (string_list);
}

void
enchant_dict_set_error (EnchantDict * dict, const char * const err)
{
	g_return_if_fail (dict);
	g_return_if_fail (err);
	g_return_if_fail (g_utf8_validate(err, -1, NULL));

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);
	session->error = strdup (err);
}

const char *
enchant_dict_get_error (EnchantDict * dict)
{
	g_return_val_if_fail (dict, NULL);

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	return session->error;
}

int
enchant_dict_check (EnchantDict * dict, const char *const word, ssize_t len)
{
	g_return_val_if_fail (dict, -1);
	g_return_val_if_fail (word, -1);

	if (len < 0)
		len = strlen (word);

	g_return_val_if_fail (len, -1);
	g_return_val_if_fail (g_utf8_validate(word, len, NULL),-1);

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	/* first, see if it's to be excluded*/
	if (enchant_session_exclude (session, word, len))
		return 1;

	/* then, see if it's in our pwl or session*/
	if (enchant_session_contains(session, word, len))
		return 0;

	if (dict->check)
		return (*dict->check) (dict, word, len);
	else if (session->is_pwl)
		return 1;

	return -1;
}

/* @suggs must have at least n_suggs + n_new_suggs space allocated
 * @n_suggs is the number if items currently appearing in @suggs
 *
 * returns the number of items in @suggs after merge is complete
 */
static int
enchant_dict_merge_suggestions(char ** suggs, size_t n_suggs, char ** new_suggs, size_t n_new_suggs)
{
	for (size_t i = 0; i < n_new_suggs; i++)
		{
			char * normalized_new_sugg = g_utf8_normalize (new_suggs[i], -1, G_NORMALIZE_NFD);

			int is_duplicate = 0;
			for (size_t j = 0; !is_duplicate && j < n_suggs; j++)
				{
					char* normalized_sugg = g_utf8_normalize (suggs[j], -1, G_NORMALIZE_NFD);
					is_duplicate = strcmp (normalized_sugg, normalized_new_sugg) == 0;
					g_free (normalized_sugg);
				}
			g_free (normalized_new_sugg);

			if (!is_duplicate)
				suggs[n_suggs++] = strdup (new_suggs[i]);
		}

	return n_suggs;
}

static char **
enchant_dict_get_good_suggestions(EnchantDict * dict, char ** suggs, size_t n_suggs, size_t* out_n_filtered_suggs)
{
	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;

	char ** filtered_suggs = g_new0 (char *, n_suggs + 1);
	size_t n_filtered_suggs = 0;
	for (size_t i = 0; i < n_suggs; i++)
		{
			size_t sugg_len = strlen(suggs[i]);

			if (sugg_len == 0)
				continue;

			if (g_utf8_validate(suggs[i], sugg_len, NULL) &&
			    !enchant_session_exclude(session, suggs[i], sugg_len))
				filtered_suggs[n_filtered_suggs++] = strdup (suggs[i]);
		}

	if (out_n_filtered_suggs)
		*out_n_filtered_suggs = n_filtered_suggs;

	return filtered_suggs;
}

char **
enchant_dict_suggest (EnchantDict * dict, const char *const word, ssize_t len, size_t * out_n_suggs)
{
	g_return_val_if_fail (dict, NULL);
	g_return_val_if_fail (word, NULL);

	if (len < 0)
		len = strlen (word);

	g_return_val_if_fail (len, NULL);
	g_return_val_if_fail (g_utf8_validate(word, len, NULL), NULL);

	size_t n_dict_suggs = 0, n_pwl_suggs = 0, n_suggsT = 0;
	char **dict_suggs = NULL, **pwl_suggs = NULL, **suggsT;

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	/* Check for suggestions from provider dictionary */
	if (dict->suggest)
		{
			dict_suggs = (*dict->suggest) (dict, word, len, &n_dict_suggs);
			if (dict_suggs)
				{
					suggsT = enchant_dict_get_good_suggestions(dict, dict_suggs, n_dict_suggs, &n_suggsT);
					enchant_free_string_list (dict_suggs);
					dict_suggs = suggsT;
					n_dict_suggs = n_suggsT;
				}
		}

	/* Check for suggestions from personal dictionary */
	if (session->personal)
		{
			pwl_suggs = enchant_pwl_suggest(session->personal, word, len, dict_suggs, &n_pwl_suggs);
			if (pwl_suggs)
				{
					suggsT = enchant_dict_get_good_suggestions(dict, pwl_suggs, n_pwl_suggs, &n_suggsT);
					enchant_free_string_list (pwl_suggs);
					pwl_suggs = suggsT;
					n_pwl_suggs = n_suggsT;
				}
		}

	/* Clone suggestions, if any */
	char **suggs = NULL;
	size_t n_suggs = n_pwl_suggs + n_dict_suggs;
	if (n_suggs > 0)
		{
			suggs = g_new0 (char *, n_suggs + 1);
			n_suggs = enchant_dict_merge_suggestions(suggs, 0, dict_suggs, n_dict_suggs);
			n_suggs = enchant_dict_merge_suggestions(suggs, n_suggs, pwl_suggs, n_pwl_suggs);
		}

	g_strfreev(dict_suggs);
	g_strfreev(pwl_suggs);

	if (out_n_suggs)
		*out_n_suggs = n_suggs;

	return suggs;
}

void
enchant_dict_add (EnchantDict * dict, const char *const word, ssize_t len)
{
	g_return_if_fail (dict);
	g_return_if_fail (word);

	if (len < 0)
		len = strlen (word);

	g_return_if_fail (len);
	g_return_if_fail (g_utf8_validate(word, len, NULL));

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);
	enchant_session_add_personal (session, word, len);
	enchant_session_remove_exclude (session, word, len);

	if (dict->add_to_personal)
		(*dict->add_to_personal) (dict, word, len);
}

void
enchant_dict_add_to_session (EnchantDict * dict, const char *const word, ssize_t len)
{
	g_return_if_fail (dict);
	g_return_if_fail (word);

	if (len < 0)
		len = strlen (word);

	g_return_if_fail (len);
	g_return_if_fail (g_utf8_validate(word, len, NULL));

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	enchant_session_add (session, word, len);
	if (dict->add_to_session)
		(*dict->add_to_session) (dict, word, len);
}

int
enchant_dict_is_added (EnchantDict * dict, const char *const word, ssize_t len)
{
	g_return_val_if_fail (dict, 0);
	g_return_val_if_fail (word, 0);

	if (len < 0)
		len = strlen (word);

	g_return_val_if_fail (len, 0);
	g_return_val_if_fail (g_utf8_validate(word, len, NULL), 0);

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	return enchant_session_contains (session, word, len);
}

void
enchant_dict_remove (EnchantDict * dict, const char *const word, ssize_t len)
{
	g_return_if_fail (dict);
	g_return_if_fail (word);

	if (len < 0)
		len = strlen (word);

	g_return_if_fail (len);
	g_return_if_fail (g_utf8_validate(word, len, NULL));

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	enchant_session_remove_personal (session, word, len);
	enchant_session_add_exclude(session, word, len);

	if (dict->add_to_exclude)
		(*dict->add_to_exclude) (dict, word, len);
}

void
enchant_dict_remove_from_session (EnchantDict * dict, const char *const word, ssize_t len)
{
	g_return_if_fail (dict);
	g_return_if_fail (word);

	if (len < 0)
		len = strlen (word);

	g_return_if_fail (len);
	g_return_if_fail (g_utf8_validate(word, len, NULL));

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	enchant_session_remove (session, word, len);
}

int
enchant_dict_is_removed (EnchantDict * dict, const char *const word, ssize_t len)
{
	g_return_val_if_fail (dict, 0);
	g_return_val_if_fail (word, 0);

	if (len < 0)
		len = strlen (word);

	g_return_val_if_fail (len, 0);
	g_return_val_if_fail (g_utf8_validate(word, len, NULL), 0);

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	return enchant_session_exclude (session, word, len);
}

void
enchant_dict_store_replacement (EnchantDict * dict,
				const char *const mis, ssize_t mis_len,
				const char *const cor, ssize_t cor_len)
{
	g_return_if_fail (dict);
	g_return_if_fail (mis);
	g_return_if_fail (cor);

	if (mis_len < 0)
		mis_len = strlen (mis);

	if (cor_len < 0)
		cor_len = strlen (cor);

	g_return_if_fail (mis_len);
	g_return_if_fail (cor_len);

	g_return_if_fail (g_utf8_validate(mis, mis_len, NULL));
	g_return_if_fail (g_utf8_validate(cor, cor_len, NULL));

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);

	/* if it's not implemented, it's not worth emulating */
	if (dict->store_replacement)
		(*dict->store_replacement) (dict, mis, mis_len, cor, cor_len);
}

void
enchant_dict_free_string_list (EnchantDict * dict, char **string_list)
{
	g_return_if_fail (dict);

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);
	g_strfreev(string_list);
}

void
enchant_dict_describe (EnchantDict * dict, EnchantDictDescribeFn fn, void * user_data)
{
	g_return_if_fail (dict);
	g_return_if_fail (fn);

	EnchantSession * session = ((EnchantDictPrivateData*)dict->enchant_private_data)->session;
	enchant_session_clear_error (session);
	EnchantProvider * provider = session->provider;

	const char * name, * desc, * file;
	if (provider)
		{
			GModule *module = (GModule *) provider->enchant_private_data;
			file = g_module_name (module);
			name = (*provider->identify) (provider);
			desc = (*provider->describe) (provider);
		}
	else
		{
			file = session->personal_filename;
			name = "Personal Wordlist";
			desc = "Personal Wordlist";
		}

	const char *tag = session->language_tag;
	(*fn) (tag, name, desc, file, user_data);
}

/***********************************************************************************/
/***********************************************************************************/

static void
enchant_broker_clear_error (EnchantBroker * broker)
{
	if (broker->error)
		{
			g_free (broker->error);
			broker->error = NULL;
		}
}

static void
enchant_broker_set_error (EnchantBroker * broker, const char * const err)
{
	enchant_broker_clear_error (broker);
	broker->error = strdup (err);
}

static int
enchant_provider_is_valid(EnchantProvider * provider)
{
	if (provider == NULL)
		g_warning ("EnchantProvider cannot be NULL\n");
	else if (provider->identify == NULL)
		g_warning ("EnchantProvider's identify method cannot be NULL\n");
	else if (!g_utf8_validate((*provider->identify)(provider), -1, NULL))
		g_warning ("EnchantProvider's identify method does not return valid UTF-8.\n");
	else if (provider->describe == NULL)
		g_warning ("EnchantProvider's describe method cannot be NULL\n");
	else if (!g_utf8_validate((*provider->describe)(provider), -1, NULL))
		g_warning ("EnchantProvider's describe method does not return valid UTF-8.\n");
	else if (provider->dispose == NULL)
		g_warning ("EnchantProvider's dispose method cannot be NULL\n");
	else if (provider->dispose_dict == NULL)
		g_warning ("EnchantProvider's dispose_dict method cannot be NULL\n");
	else if (provider->list_dicts == NULL)
		g_warning ("EnchantProvider's list_dicts method cannot be NULL\n");
	else
		return 1;

	return 0;
}

static void
enchant_load_providers_in_dir (EnchantBroker * broker, const char *dir_name)
{
	GDir *dir = g_dir_open (dir_name, 0, NULL);
	if (!dir)
		return;

	size_t g_module_suffix_len = strlen (G_MODULE_SUFFIX);
	const char *dir_entry;
	while ((dir_entry = g_dir_read_name (dir)) != NULL)
		{
			GModule *module = NULL;
			EnchantProvider *provider = NULL;

			size_t entry_len = strlen (dir_entry);
			if ((entry_len > g_module_suffix_len) &&
				!strcmp(dir_entry+(entry_len-g_module_suffix_len), G_MODULE_SUFFIX))
				{
#ifdef _WIN32
					/* Suppress error popups for failing to load plugins */
					UINT old_error_mode = SetErrorMode(SEM_FAILCRITICALERRORS);
#endif
					char * filename = g_build_filename (dir_name, dir_entry, NULL);
					module = g_module_open (filename, (GModuleFlags) 0);
					if (module)
						{
							EnchantProviderInitFunc init_func;
							if (g_module_symbol (module, "init_enchant_provider", (gpointer *) (&init_func))
							    && init_func)
								{
									provider = init_func ();
									if (!enchant_provider_is_valid(provider))
										{
											g_warning ("Error loading plugin: %s's init_enchant_provider returned invalid provider.\n", dir_entry);
											if(provider)
												{
													provider->dispose(provider);
													provider = NULL;
												}
											g_module_close (module);
										}
								}
							else
								{
									g_module_close (module);
								}
						}
					else
						{
							g_warning ("Error loading plugin: %s\n", g_module_error());
						}

					g_free (filename);
#ifdef _WIN32
					/* Restore the original error mode */
					SetErrorMode(old_error_mode);
#endif
				}
			if (provider)
				{
					/* optional entry point to allow modules to look for associated files */
					EnchantPreConfigureFunc conf_func;
					if (g_module_symbol (module, "configure_enchant_provider", (gpointer *) (&conf_func))
					    && conf_func)
						{
							conf_func (provider, dir_name);
							if (!enchant_provider_is_valid(provider))
								{
									g_warning ("Error loading plugin: %s's configure_enchant_provider modified provider and it is now invalid.\n", dir_entry);
									provider->dispose(provider);
									provider = NULL;
									g_module_close (module);
								}
						}
				}
			if (provider)
				{
					provider->enchant_private_data = (void *) module;
					provider->owner = broker;
					broker->provider_list = g_slist_append (broker->provider_list, (gpointer)provider);
				}
		}

	g_dir_close (dir);
}

static void
enchant_load_providers (EnchantBroker * broker)
{
	char *module_dir = enchant_relocate (PKGLIBDIR "-" ENCHANT_MAJOR_VERSION);
	if (module_dir)
		enchant_load_providers_in_dir (broker, module_dir);
	free (module_dir);
}

static void
enchant_load_ordering_from_file (EnchantBroker * broker, const char * file)
{
	GIOChannel * ch = g_io_channel_new_file (file, "r", NULL);
	if (!ch)
		return;

	gchar *line;
	gsize terminator;
	while (G_IO_STATUS_NORMAL == g_io_channel_read_line (ch, &line, NULL, &terminator, NULL)) {
		char *colon = strchr (line, ':');
		if (colon != NULL)
			{
				char * tag = g_strndup (line, colon - line);
				char * ordering = g_strndup (colon + 1, terminator - 1);
				enchant_broker_set_ordering (broker, tag, ordering);

				g_free (tag);
				g_free (ordering);
			}
		g_free (line);
	}

	g_io_channel_unref (ch);
}

static void
enchant_load_provider_ordering (EnchantBroker * broker)
{
	broker->provider_ordering = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

	GSList *conf_dirs = enchant_get_conf_dirs ();
	for (GSList *iter = conf_dirs; iter; iter = iter->next)
		{
			char *ordering_file = g_build_filename (iter->data, "enchant.ordering", NULL);
			enchant_load_ordering_from_file (broker, ordering_file);
			g_free (ordering_file);
		}

	g_slist_free_full (conf_dirs, g_free);
}

static GSList *
enchant_get_ordered_providers (EnchantBroker * broker, const char * const tag)
{
	char * ordering = (char *)g_hash_table_lookup (broker->provider_ordering, (gpointer)tag);
	if (!ordering)
		ordering = (char *)g_hash_table_lookup (broker->provider_ordering, (gpointer)"*");

	GSList * list = NULL;

	if (ordering)
		{
			char **tokens = g_strsplit (ordering, ",", 0);
			if (tokens)
				{
					for (size_t i = 0; tokens[i]; i++)
						{
							char *token = g_strstrip(tokens[i]);

							for (GSList * iter = broker->provider_list; iter != NULL; iter = g_slist_next (iter))
								{
									EnchantProvider *provider = (EnchantProvider*)iter->data;
									if (provider && !strcmp (token, (*provider->identify)(provider)))
										list = g_slist_append (list, (gpointer)provider);
								}
						}
					g_strfreev (tokens);
				}
		}

	/* append providers not in the list, or from an unordered list */
	for (GSList * iter = broker->provider_list; iter != NULL; iter = g_slist_next (iter))
		{
			if (!g_slist_find (list, iter->data))
				list = g_slist_append (list, iter->data);
		}

	return list;
}

static void
enchant_dict_destroyed (gpointer data)
{
	g_return_if_fail (data);

	EnchantDict *dict = (EnchantDict *) data;
	EnchantDictPrivateData *enchant_dict_private_data = (EnchantDictPrivateData*)dict->enchant_private_data;
	EnchantSession *session = enchant_dict_private_data->session;
	EnchantProvider *owner = session->provider;

	if (owner)
		(*owner->dispose_dict) (owner, dict);
	else if(session->is_pwl)
		g_free (dict);

	g_free(enchant_dict_private_data);

	enchant_session_destroy (session);
}

static void
enchant_provider_free (gpointer data)
{
	g_return_if_fail (data);

	EnchantProvider *provider = (EnchantProvider *) data;
	GModule *module = (GModule *) provider->enchant_private_data;
	(*provider->dispose) (provider);

	/* close module only after invoking dispose */
	g_module_close (module);
}

EnchantBroker *
enchant_broker_init (void)
{
	g_return_val_if_fail (g_module_supported (), NULL);

	EnchantBroker *broker = g_new0 (EnchantBroker, 1);
	broker->dict_map = g_hash_table_new_full (g_str_hash, g_str_equal,
						  g_free, enchant_dict_destroyed);
	enchant_load_providers (broker);
	enchant_load_provider_ordering (broker);

	return broker;
}

void
enchant_broker_free (EnchantBroker * broker)
{
	g_return_if_fail (broker);

	guint n_remaining = g_hash_table_size (broker->dict_map);
	if (n_remaining)
		g_warning ("%u dictionaries weren't free'd.\n", n_remaining);

	/* will destroy any remaining dictionaries for us */
	g_hash_table_destroy (broker->dict_map);
	g_hash_table_destroy (broker->provider_ordering);

	g_slist_free_full (broker->provider_list, enchant_provider_free);
	enchant_broker_clear_error (broker);
	g_free (broker);
}

EnchantDict *
enchant_broker_request_pwl_dict (EnchantBroker * broker, const char *const pwl)
{
	g_return_val_if_fail (broker, NULL);
	g_return_val_if_fail (pwl && strlen(pwl), NULL);

	enchant_broker_clear_error (broker);

	EnchantDict *dict = (EnchantDict*)g_hash_table_lookup (broker->dict_map, (gpointer) pwl);
	if (dict) {
		((EnchantDictPrivateData*)dict->enchant_private_data)->reference_count++;
		return dict;
	}

	/* since the broker pwl file is a read/write file (there is no readonly dictionary associated)
	 * there is no need for complementary exclude file to add a word to. The word just needs to be
	 * removed from the broker pwl file
	 */
	EnchantSession *session = enchant_session_new_with_pwl (NULL, pwl, NULL, "Personal Wordlist", TRUE);
	if (!session)
		{
			broker->error = g_strdup_printf ("Couldn't open personal wordlist '%s'", pwl);
			return NULL;
		}

	session->is_pwl = 1;

	dict = g_new0 (EnchantDict, 1);
	EnchantDictPrivateData *enchant_dict_private_data = g_new0 (EnchantDictPrivateData, 1);
	enchant_dict_private_data->reference_count = 1;
	enchant_dict_private_data->session = session;
	dict->enchant_private_data = (void *)enchant_dict_private_data;

	g_hash_table_insert (broker->dict_map, (gpointer)strdup (pwl), dict);

	return dict;
}

static EnchantDict *
_enchant_broker_request_dict (EnchantBroker * broker, const char *const tag)
{
	EnchantDict *dict = (EnchantDict*)g_hash_table_lookup (broker->dict_map, (gpointer) tag);
	if (dict) {
		((EnchantDictPrivateData*)dict->enchant_private_data)->reference_count++;
		return dict;
	}

	GSList * list = enchant_get_ordered_providers (broker, tag);
	for (GSList *listIter = list; listIter != NULL; listIter = g_slist_next (listIter))
		{
			EnchantProvider * provider;

			provider = (EnchantProvider *) listIter->data;

			if (provider->request_dict)
				{
					dict = (*provider->request_dict) (provider, tag);

					if (dict)
						{

							EnchantSession *session = enchant_session_new (provider, tag);
							EnchantDictPrivateData *enchant_dict_private_data = g_new0 (EnchantDictPrivateData, 1);
							enchant_dict_private_data->reference_count = 1;
							enchant_dict_private_data->session = session;
							dict->enchant_private_data = (void *)enchant_dict_private_data;
							g_hash_table_insert (broker->dict_map, (gpointer)strdup (tag), dict);
							break;
						}
				}
		}
	g_slist_free (list);

	return dict;
}

EnchantDict *
enchant_broker_request_dict (EnchantBroker * broker, const char *const tag)
{
	EnchantDict *dict = NULL;

	g_return_val_if_fail (broker, NULL);
	g_return_val_if_fail (tag && strlen(tag), NULL);

	enchant_broker_clear_error (broker);

	char * normalized_tag = enchant_normalize_dictionary_tag (tag);
	if(!enchant_is_valid_dictionary_tag(normalized_tag))
		{
			enchant_broker_set_error (broker, "invalid tag character found");
		}
	else if ((dict = _enchant_broker_request_dict (broker, normalized_tag)) == NULL)
		{
			char * iso_639_only_tag = enchant_iso_639_from_tag (normalized_tag);
			dict = _enchant_broker_request_dict (broker, iso_639_only_tag);
			free (iso_639_only_tag);
		}
	free (normalized_tag);

	return dict;
}

void
enchant_broker_describe (EnchantBroker * broker, EnchantBrokerDescribeFn fn, void * user_data)
{
	g_return_if_fail (broker);
	g_return_if_fail (fn);

	enchant_broker_clear_error (broker);

	for (GSList *list = broker->provider_list; list != NULL; list = g_slist_next (list))
		{
			EnchantProvider *provider = (EnchantProvider *) list->data;
			GModule *module = (GModule *) provider->enchant_private_data;

			const char *name = (*provider->identify) (provider);
			const char *desc = (*provider->describe) (provider);
			const char *file = g_module_name (module);

			(*fn) (name, desc, file, user_data);
		}
}

void
enchant_broker_list_dicts (EnchantBroker * broker, EnchantDictDescribeFn fn, void * user_data)
{
	g_return_if_fail (broker);
	g_return_if_fail (fn);

	GHashTable *tags = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

	enchant_broker_clear_error (broker);

	for (GSList *list = broker->provider_list; list != NULL; list = g_slist_next (list))
		{
			EnchantProvider *provider = (EnchantProvider *) list->data;

			if (provider->list_dicts)
				{
					size_t n_dicts;
					char ** dicts = (*provider->list_dicts) (provider, &n_dicts);

					for (size_t i = 0; i < n_dicts; i++)
						{
							const char * tag;

							tag = dicts[i];
							if (enchant_is_valid_dictionary_tag (tag)) {
								gpointer ptr;
								GSList *providers;
								gint this_priority;

								providers = enchant_get_ordered_providers (broker, tag);
								this_priority = g_slist_index (providers, provider);
								if (this_priority != -1) {
									gint min_priority;

									min_priority = this_priority + 1;
									ptr = g_hash_table_lookup (tags, tag);
									if (ptr != NULL)
										min_priority = g_slist_index (providers, ptr);
									if (this_priority < min_priority)
										g_hash_table_insert (tags, strdup (tag), provider);
								}
								g_slist_free (providers);
							}
						}

					enchant_free_string_list (dicts);
				}
		}

	GHashTableIter iter;
	gpointer key, value;
	g_hash_table_iter_init (&iter, tags);
	while (g_hash_table_iter_next (&iter, &key, &value))
		{
			const char * tag, * name, * desc, * file;
			EnchantProvider * provider;
			GModule *module;

			tag = (const char *) key;
			provider = (EnchantProvider *) value;
			module = (GModule *) provider->enchant_private_data;
			name = (*provider->identify) (provider);
			desc = (*provider->describe) (provider);
			file = g_module_name (module);
			(*fn) (tag, name, desc, file, user_data);
		}

	g_hash_table_destroy (tags);
}

void
enchant_broker_free_dict (EnchantBroker * broker, EnchantDict * dict)
{
	g_return_if_fail (broker);
	g_return_if_fail (dict);

	enchant_broker_clear_error (broker);

	EnchantDictPrivateData * dict_private_data = (EnchantDictPrivateData*)dict->enchant_private_data;
	dict_private_data->reference_count--;
	if(dict_private_data->reference_count == 0)
		{
			EnchantSession * session = dict_private_data->session;

			if (session->provider)
				g_hash_table_remove (broker->dict_map, session->language_tag);
			else
				g_hash_table_remove (broker->dict_map, session->personal_filename);
		}
}

static int
enchant_provider_dictionary_exists (EnchantProvider * provider, const char * const tag)
{
	int exists = 0;

	if (provider->dictionary_exists)
		{
			exists = (*provider->dictionary_exists) (provider, tag);
		}
	else if (provider->list_dicts)
		{
			size_t n_dicts;
			char ** dicts = (*provider->list_dicts) (provider, &n_dicts);

			for (size_t i = 0; i < n_dicts; i++)
				{
					if (!strcmp(dicts[i], tag)) {
						exists = 1;
						break;
					}
				}

			enchant_free_string_list (dicts);
		}

	return exists;
}

static int
_enchant_broker_dict_exists (EnchantBroker * broker, const char * const tag)
{
	/* don't query the providers if it is an empty string */
	if (tag == NULL || *tag == '\0')
		return 0;

	/* don't query the providers if we can just do a quick map lookup */
	if (g_hash_table_lookup (broker->dict_map, (gpointer) tag) != NULL)
		return 1;

	for (GSList *list = broker->provider_list; list != NULL; list = g_slist_next (list)) {
		if (enchant_provider_dictionary_exists ((EnchantProvider *) list->data, tag))
			return 1;
	}

	return 0;
}

int
enchant_broker_dict_exists (EnchantBroker * broker, const char * const tag)
{
	g_return_val_if_fail (broker, 0);
	g_return_val_if_fail (tag && strlen(tag), 0);

	enchant_broker_clear_error (broker);

	char * normalized_tag = enchant_normalize_dictionary_tag (tag);
	int exists = 0;

	if(!enchant_is_valid_dictionary_tag(normalized_tag))
		{
			enchant_broker_set_error (broker, "invalid tag character found");
		}
	else if ((exists = _enchant_broker_dict_exists (broker, normalized_tag)) == 0)
		{
			char * iso_639_only_tag;

			iso_639_only_tag = enchant_iso_639_from_tag (normalized_tag);

			if (strcmp (normalized_tag, iso_639_only_tag) != 0)
				{
					exists = _enchant_broker_dict_exists (broker, iso_639_only_tag);
				}

			free (iso_639_only_tag);
		}

	free (normalized_tag);
	return exists;
}

_GL_ATTRIBUTE_PURE const char *
enchant_dict_get_extra_word_characters (EnchantDict *dict)
{
	g_return_val_if_fail (dict, NULL);

	return dict->get_extra_word_characters ? (*dict->get_extra_word_characters) (dict) : "";
}

_GL_ATTRIBUTE_PURE int
enchant_dict_is_word_character (EnchantDict * dict, uint32_t uc_in, size_t n)
{
	g_return_val_if_fail (n <= 2, 0);

	if (dict && dict->is_word_character)
		return (*dict->is_word_character) (dict, uc_in, n);

	gunichar uc = (gunichar)uc_in;

	/* Accept quote marks anywhere except at the end of a word */
	if (uc == g_utf8_get_char("'") || uc == g_utf8_get_char("’")) {
		return n < 2;
	}

	GUnicodeType type = g_unichar_type(uc);

	switch (type) {
	case G_UNICODE_MODIFIER_LETTER:
	case G_UNICODE_LOWERCASE_LETTER:
	case G_UNICODE_TITLECASE_LETTER:
	case G_UNICODE_UPPERCASE_LETTER:
	case G_UNICODE_OTHER_LETTER:
	case G_UNICODE_COMBINING_MARK: /* Older name for G_UNICODE_SPACING_MARK; deprecated since glib 2.30 */
	case G_UNICODE_ENCLOSING_MARK:
	case G_UNICODE_NON_SPACING_MARK:
	case G_UNICODE_DECIMAL_NUMBER:
	case G_UNICODE_LETTER_NUMBER:
	case G_UNICODE_OTHER_NUMBER:
	case G_UNICODE_CONNECT_PUNCTUATION:
		return 1;     /* Enchant 1.3.0 defines word chars like this. */

	case G_UNICODE_DASH_PUNCTUATION:
		if ((n == 1) && (type == G_UNICODE_DASH_PUNCTUATION)) {
			return 1; /* hyphens only accepted within a word. */
		}
		/* Fallthrough */

	case G_UNICODE_CONTROL:
	case G_UNICODE_FORMAT:
	case G_UNICODE_UNASSIGNED:
	case G_UNICODE_PRIVATE_USE:
	case G_UNICODE_SURROGATE:
	case G_UNICODE_CLOSE_PUNCTUATION:
	case G_UNICODE_FINAL_PUNCTUATION:
	case G_UNICODE_INITIAL_PUNCTUATION:
	case G_UNICODE_OTHER_PUNCTUATION:
	case G_UNICODE_OPEN_PUNCTUATION:
	case G_UNICODE_CURRENCY_SYMBOL:
	case G_UNICODE_MODIFIER_SYMBOL:
	case G_UNICODE_MATH_SYMBOL:
	case G_UNICODE_OTHER_SYMBOL:
	case G_UNICODE_LINE_SEPARATOR:
	case G_UNICODE_PARAGRAPH_SEPARATOR:
	case G_UNICODE_SPACE_SEPARATOR:
	default:
		return 0;
	}
}

void
enchant_broker_set_ordering (EnchantBroker * broker, const char * const tag, const char * const ordering)
{
	g_return_if_fail (broker);
	g_return_if_fail (tag && strlen(tag));
	g_return_if_fail (ordering && strlen(ordering));

	enchant_broker_clear_error (broker);

	char *tag_dupl = enchant_normalize_dictionary_tag (tag);
	char *ordering_dupl = g_strstrip (g_strdup (ordering));

	if (tag_dupl && strlen(tag_dupl) &&
		ordering_dupl && strlen(ordering_dupl))
		{
			/* we will free ordering_dupl && tag_dupl when the hash is destroyed */
			g_hash_table_insert (broker->provider_ordering, (gpointer)tag_dupl,
					     (gpointer)(ordering_dupl));
		}
	else
		{
			g_free (tag_dupl);
			g_free (ordering_dupl);
		}
}

void
enchant_provider_set_error (EnchantProvider * provider, const char * const err)
{
	g_return_if_fail (provider);
	g_return_if_fail (err);
	g_return_if_fail (g_utf8_validate(err, -1, NULL));

	EnchantBroker * broker = provider->owner;
	g_return_if_fail (broker);

	enchant_broker_set_error (broker, err);
}

const char *
enchant_broker_get_error (EnchantBroker * broker)
{
	g_return_val_if_fail (broker, NULL);

	return broker->error;
}

char *
enchant_get_user_language(void)
{
#if defined(G_OS_WIN32)
	return g_win32_getlocale ();
#else

	const char * locale = g_getenv ("LANG");

#if defined(HAVE_LC_MESSAGES)
	if(!locale)
		locale = setlocale (LC_MESSAGES, NULL);
#endif

	if(!locale)
		locale = setlocale (LC_ALL, NULL);

	if(!locale || strcmp(locale, "C") == 0)
		locale = "en";

	return strdup (locale);
#endif /* !G_OS_WIN32 */
}


char *
enchant_get_prefix_dir(void)
{
	return enchant_relocate (INSTALLPREFIX);
}

void
enchant_set_prefix_dir(const char *new_prefix)
{
#ifdef ENABLE_RELOCATABLE
	set_relocation_prefix (INSTALLPREFIX, new_prefix);
#else
	(void)new_prefix;
#endif
}

const char * _GL_ATTRIBUTE_CONST
enchant_get_version (void) {
	return ENCHANT_VERSION_STRING;
}
