/* vim: set sw=8: -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* enchant
 * Copyright (C) 2003 Dom Lachowicz
 *
 * 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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, 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 <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(HAVE_FLOCK) || defined(HAVE_LOCKF)
#include <sys/file.h>
#endif /* HAVE_FLOCK || HAVE_LOCKF */

#include <glib.h>
#include <gmodule.h>

#include "enchant.h"
#include "enchant-provider.h"

ENCHANT_PLUGIN_DECLARE("Enchant")

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

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;
	GHashTable *personal;

	char * personal_filename;
	char * language_tag;

	char * error;

	EnchantProvider * provider;
} EnchantSession;

typedef EnchantProvider *(*EnchantProviderInitFunc) (void);

#ifndef BUFSIZ
#define BUFSIZ 1024
#endif

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

static void
enchant_lock_file (FILE * f)
{
#if defined(HAVE_FLOCK)
	flock (fileno (f), LOCK_EX);
#elif defined(HAVE_LOCKF)
	lockf (fileno (f), F_LOCK, 0);
#else
	/* TODO: win32, UNIX fcntl. This race condition probably isn't too bad. */
#endif /* HAVE_FLOCK */
}

static void
enchant_unlock_file (FILE * f)
{
#if defined(HAVE_FLOCK)
	flock (fileno (f), LOCK_UN);
#elif defined(HAVE_LOCKF)
	lockf (fileno (f), F_ULOCK, 0);
#else
	/* TODO: win32, UNIX fcntl. This race condition probably isn't too bad. */
#endif /* HAVE_FLOCK */
}

static char *
enchant_get_module_dir (void)
{
	char * module_dir = NULL;

	module_dir = enchant_get_registry_value ("Config", "Module_Dir");
	if (module_dir)
		return module_dir;

#ifdef ENCHANT_GLOBAL_MODULE_DIR
	return g_strdup (ENCHANT_GLOBAL_MODULE_DIR);
#else
	return NULL;
#endif
}

static char *
enchant_get_conf_dir (void)
{
	char * ordering_dir = NULL;

	ordering_dir = enchant_get_registry_value ("Config", "Data_Dir");
	if (ordering_dir)
		return ordering_dir;

#ifdef ENCHANT_GLOBAL_ORDERING
	return g_strdup (ENCHANT_GLOBAL_ORDERING);
#else
	return NULL;
#endif
}

static char *
enchant_get_registry_value_ex (int current_user, const char * const prefix, const char * const key)
{
#ifndef _WIN32
	/* TODO: GConf? KConfig? */
	return NULL;
#else
	HKEY hKey;
	HKEY baseKey;
	unsigned long lType;	
	DWORD dwSize;
	BYTE* szValue = NULL;

	if (current_user)
		baseKey = HKEY_CURRENT_USER;
	else
		baseKey = HKEY_LOCAL_MACHINE;

	if(RegOpenKeyEx(baseKey, "Software\\Enchant", 0, KEY_READ, &hKey) == ERROR_SUCCESS)
		{
			// Determine size of string
			if(RegQueryValueEx( hKey, key, NULL, &lType, NULL, &dwSize) == ERROR_SUCCESS)
				{
					szValue = g_new0(BYTE, dwSize + 1);
					RegQueryValueEx(hKey, key, NULL, &lType, szValue, &dwSize);
				}
		}
	
	return (char *)szValue;
#endif
}

/**
 * enchant_get_registry_value
 * @prefix: Your category, such as "Ispell" or "Myspell"
 * @key: The tag within your category that you're interested in
 *
 * Returns: the value if it exists, or %null otherwise. Must be free'd.
 *
 * This API is private to the providers.
 */
ENCHANT_MODULE_EXPORT (char *)
enchant_get_registry_value (const char * const prefix, const char * const key)
{
	return enchant_get_registry_value_ex (0, prefix, key);
}

/**
 * enchant_get_user_home_dir
 *
 * Returns: the user's home directory, or %null. Returned value
 * must be free'd.
 *
 * This API is private to the providers.
 */
ENCHANT_MODULE_EXPORT (char *)
enchant_get_user_home_dir (void)
{
	const char * home_dir = NULL;

	home_dir = enchant_get_registry_value_ex (1, "Config", "Home_Dir");
	if (home_dir)
		return (char *)home_dir;

	home_dir = g_get_home_dir ();
	if (home_dir)
		return g_strdup (home_dir);
	return NULL;
}

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

static void
enchant_session_destroy (EnchantSession * session)
{
	g_hash_table_destroy (session->session);
	g_hash_table_destroy (session->personal);
	g_free (session->personal_filename);
	g_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 lang,
			      gboolean fail_if_no_pwl)
{
	EnchantSession * session;
	FILE * f;
	char line[BUFSIZ];

	session = g_new0 (EnchantSession, 1);
	session->session = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	session->personal = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
	session->provider = provider;
	session->language_tag = g_strdup (lang);

	if (pwl) 
		{
			session->personal_filename = g_strdup (pwl);
			
			/* populate personal filename */
			f = fopen (pwl, "r");
			if (f) 
				{
					enchant_lock_file (f);
					
					while (NULL != (fgets (line, sizeof (line), f)))
						g_hash_table_insert (session->personal, g_strdup (line), GINT_TO_POINTER(TRUE));
					
					enchant_unlock_file (f);
					fclose (f);
				} 
			else if (fail_if_no_pwl) 
				{
					enchant_session_destroy (session);
					return NULL;
				}
		} 
	else if (fail_if_no_pwl) 
		{
			enchant_session_destroy (session);
			return NULL;
		}
	
	return session;
}

static EnchantSession *
enchant_session_new (EnchantProvider *provider, const char * const lang)
{
	EnchantSession * session;
	char * home_dir, * dic = NULL, * filename;

	home_dir = enchant_get_user_home_dir ();
	if (home_dir) 
		{
			filename = g_strdup_printf ("%s.dic", lang);
			dic = g_build_filename (home_dir,
						".enchant",
						filename,
						NULL);
			g_free (filename);
			g_free (home_dir);
		}
	
	session = enchant_session_new_with_pwl (provider, dic, lang, FALSE);	
	
	if (dic)
		g_free (dic);
	
	return session;
}

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

static void
enchant_session_add_personal (EnchantSession * session, const char * const word, size_t len)
{
	FILE * f;

	if (session->personal_filename) 
		{
			f = fopen (session->personal_filename, "a");
			
			if (f) 
				{
					enchant_lock_file (f);
					
					fwrite (word, sizeof(char), len, f);
					fwrite ("\n", sizeof(char), 1, f);
					fclose (f);
					
					enchant_unlock_file (f);
				}
		}
}

static gboolean
enchant_session_contains (EnchantSession * session, const char * const word, size_t len)
{
	gboolean result = FALSE;
	
	char * utf = g_strndup (word, len);
	
	if (g_hash_table_lookup (session->session, utf) ||
	    g_hash_table_lookup (session->personal, utf))
		result = TRUE;
	
	g_free (utf);

	return result;
}

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

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

/**
 * enchant_dict_set_error
 * @dict: A non-null dictionary
 * @err: A non-null error message
 *
 * Sets the current runtime error to @err. This API is private to the
 * providers.
 */
ENCHANT_MODULE_EXPORT(void)
enchant_dict_set_error (EnchantDict * dict, const char * const err)
{
	EnchantSession * session;

	g_return_if_fail (dict);
	g_return_if_fail (err);
	
	session = (EnchantSession*)dict->enchant_private_data;

	enchant_session_clear_error (session);
	session->error = g_strdup (err);	
}

/**
 * enchant_dict_get_error
 * @dict: A non-null dictionary
 *
 * Returns a const char string or NULL describing the last exception.
 * WARNING: error is transient. It will likely be cleared as soon as 
 * the next dictionary operation is called
 *
 * Returns: an error message
 */
ENCHANT_MODULE_EXPORT(char *)
enchant_dict_get_error (EnchantDict * dict)
{
	EnchantSession * session;

	g_return_val_if_fail (dict, NULL);
	
	session = (EnchantSession*)dict->enchant_private_data;

	return session->error;
}

/**
 * enchant_dict_check
 * @dict: A non-null #EnchantDict
 * @word: The non-null word you wish to correct, in UTF-8 encoding
 * @len: The non-zero byte length of @word
 *
 * Will return an "incorrect" value if any of those pre-conditions
 * are not met.
 *
 * Returns: 0 if the word is correctly spelled, positive if not, negative if error
 */
ENCHANT_MODULE_EXPORT (int)
enchant_dict_check (EnchantDict * dict, const char *const word, size_t len)
{
	EnchantSession * session;

	g_return_val_if_fail (dict, -1);
	g_return_val_if_fail (word, -1);
	g_return_val_if_fail (len, -1);
	
	session = (EnchantSession*)dict->enchant_private_data;
	enchant_session_clear_error (session);

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

	if (dict->check)
		return (*dict->check) (dict, word, len);
	
	return -1;
}

/**
 * enchant_dict_suggest
 * @dict: A non-null #EnchantDict
 * @word: The non-null word you wish to find suggestions for, in UTF-8 encoding
 * @len: The non-zero byte length of @word
 * @out_n_suggs: The non-null location to store the # of suggestions returned
 *
 * Will return an %null value if any of those pre-conditions
 * are not met.
 *
 * Returns: A %null terminated list of UTF-8 encoded suggestions, or %null
 */
ENCHANT_MODULE_EXPORT (char **)
enchant_dict_suggest (EnchantDict * dict, const char *const word,
		      size_t len, size_t * out_n_suggs)
{
	g_return_val_if_fail (dict, NULL);
	g_return_val_if_fail (word, NULL);
	g_return_val_if_fail (len, NULL);
	g_return_val_if_fail (out_n_suggs, NULL);
	
	if (dict->suggest)
		return (*dict->suggest) (dict, word, len, out_n_suggs);
	
	*out_n_suggs = 0;
	return NULL;
}

/**
 * enchant_dict_add_to_personal
 * @dict: A non-null #EnchantDict
 * @word: The non-null word you wish to add to your personal dictionary, in UTF-8 encoding
 * @len: The non-zero byte length of @word
 *
 */
ENCHANT_MODULE_EXPORT (void)
enchant_dict_add_to_personal (EnchantDict * dict, const char *const word,
			      size_t len)
{
	EnchantSession * session;

	g_return_if_fail (dict);
	g_return_if_fail (word);
	g_return_if_fail (len);

	/* add to session backend regardless */
	enchant_dict_add_to_session (dict, word, len);

	session = (EnchantSession*)dict->enchant_private_data;
	enchant_session_clear_error (session);
	enchant_session_add_personal (session, word, len);
	
	if (dict->add_to_personal)
		(*dict->add_to_personal) (dict, word, len);
}

/**
 * enchant_dict_add_to_session
 * @dict: A non-null #EnchantDict
 * @word: The non-null word you wish to add to this spell-checking session, in UTF-8 encoding
 * @len: The non-zero byte length of @word
 *
 */
ENCHANT_MODULE_EXPORT (void)
enchant_dict_add_to_session (EnchantDict * dict, const char *const word,
			     size_t len)
{
	EnchantSession * session;

	g_return_if_fail (dict);
	g_return_if_fail (word);
	g_return_if_fail (len);
	
	session = (EnchantSession*)dict->enchant_private_data;
	enchant_session_clear_error (session);

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

ENCHANT_MODULE_EXPORT (int)
enchant_dict_is_in_session (EnchantDict * dict, const char *const word,
			    size_t len)
{
	EnchantSession * session;

	g_return_val_if_fail (dict, 0);
	g_return_val_if_fail (word, 0);
	g_return_val_if_fail (len, 0);
	
	session = (EnchantSession*)dict->enchant_private_data;
	enchant_session_clear_error (session);

	return enchant_session_contains (session, word, len);
}

/**
 * enchant_dict_store_replacement
 * @dict: A non-null #EnchantDict
 * @mis: The non-null word you wish to add a correction for, in UTF-8 encoding
 * @mis_len: The non-zero byte length of @mis
 * @cor: The non-null correction word, in UTF-8 encoding
 * @cor_len: The non-zero byte length of @cor
 *
 * Notes that you replaced @mis with @cor, so it's possibly more likely
 * that future occurrences of @mis will be replaced with @cor. So it might
 * bump @cor up in the suggestion list.
 */
ENCHANT_MODULE_EXPORT (void)
enchant_dict_store_replacement (EnchantDict * dict,
				const char *const mis, size_t mis_len,
				const char *const cor, size_t cor_len)
{
	EnchantSession * session;

	g_return_if_fail (dict);
	g_return_if_fail (mis);
	g_return_if_fail (mis_len);
	g_return_if_fail (cor);
	g_return_if_fail (cor_len);

	session = (EnchantSession*)dict->enchant_private_data;
	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);
}

/**
 * enchant_dict_free_suggestions
 * @dict: A non-null #EnchantDict
 * @suggestions: The non-null suggestion list returned by
 *               'enchant_dict_suggest'
 *
 * Releases the suggestions
 */
ENCHANT_MODULE_EXPORT (void)
enchant_dict_free_suggestions (EnchantDict * dict, char **suggestions)
{
	EnchantSession * session;

	g_return_if_fail (dict);
	g_return_if_fail (suggestions);

	session = (EnchantSession*)dict->enchant_private_data;
	enchant_session_clear_error (session);
	
	if (dict->free_suggestions)
		(*dict->free_suggestions) (dict, suggestions);
}

/**
 * enchant_dict_describe
 * @broker: A non-null #EnchantDict
 * @dict: A non-null #EnchantDictDescribeFn
 * @user_data: Optional user-data
 *
 * Describes an individual dictionary
 */
ENCHANT_MODULE_EXPORT (void)
enchant_dict_describe (EnchantDict * dict,
		       EnchantDictDescribeFn fn,
		       void * user_data)
{
	EnchantSession * session;
	EnchantProvider * provider;
	GModule *module;

	const char * tag, * name, * desc, * file;

	g_return_if_fail (dict);
	g_return_if_fail (fn);

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

	if (provider) 
		{
			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";
		}
	
	tag = session->language_tag;
	(*fn) (tag, name, desc, file, user_data);
	
	return;
}

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

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

static void
enchant_load_providers_in_dir (EnchantBroker * broker, const char *dir_name)
{
	GModule *module;
	GDir *dir;
	G_CONST_RETURN char *dir_entry;
	size_t entry_len, g_module_suffix_len;
	
	char * filename;
	
	EnchantProvider *provider;
	EnchantProviderInitFunc init_func;
	
	dir = g_dir_open (dir_name, 0, NULL);
	if (!dir) 
		return;
	
	g_module_suffix_len = strlen (G_MODULE_SUFFIX);

	while ((dir_entry = g_dir_read_name (dir)) != NULL)
		{
			entry_len = strlen (dir_entry);
			if ((entry_len > g_module_suffix_len) && 
			    !strcmp(dir_entry+(entry_len-g_module_suffix_len), G_MODULE_SUFFIX))
				{
					filename = g_build_filename (dir_name, dir_entry, NULL);
					
					module = g_module_open (filename, (GModuleFlags) 0);
					if (module) 
						{
							if (g_module_symbol
							    (module, "init_enchant_provider", (gpointer *) (&init_func))
							    && init_func)
								{
									provider = init_func ();
									if (provider)
										{
											provider->enchant_private_data = (void *) module;
											provider->owner = broker;
											broker->provider_list = g_slist_append (broker->provider_list, (gpointer)provider);
										}
								}
							else
								{
									g_module_close (module);
								}
						} 
					else 
						{
							g_warning ("Module error: %s\n", g_module_error());
						}
					
					g_free (filename);
				}
		}
	
	g_dir_close (dir);
}

static void
enchant_load_providers (EnchantBroker * broker)
{
	gchar *user_dir, *home_dir;       
	
	/* load USER providers first. since the GSList is ordered,
	   this intentionally gives preference to USER providers */

	home_dir = enchant_get_user_home_dir ();

	if (home_dir) 
		{
			user_dir = g_build_filename (home_dir, ".enchant", NULL);
			enchant_load_providers_in_dir (broker, user_dir);
			g_free (user_dir);
			g_free (home_dir);
		}
	
	enchant_load_providers_in_dir (broker, ENCHANT_GLOBAL_MODULE_DIR);
}

static void
enchant_load_ordering_from_file (EnchantBroker * broker, const char * file)
{
	char line [1024];
	char * tag, * ordering;

	size_t i, len;

	FILE * f;

	f = fopen (file, "r");
	if (!f)
		return;

	while (NULL != fgets (line, sizeof(line), f)) {
		for (i = 0, len = strlen(line); i < len && line[i] != ':'; i++) 
			;

		if (i < len) 
			{
				tag = g_strndup (line, i);
				ordering = g_strndup (line+(i+1), len - i);			
				
				enchant_broker_set_ordering (broker, tag, ordering);
				
				g_free (tag);
				g_free (ordering);
			}
	}
	
	fclose (f);
}

static void
enchant_load_provider_ordering (EnchantBroker * broker)
{
	char * ordering_file, * home_dir, * global_ordering;

	broker->provider_ordering = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

	global_ordering = enchant_get_conf_dir ();
	if (global_ordering) 
		{
			ordering_file = g_build_filename (global_ordering, "enchant.ordering", NULL);
			enchant_load_ordering_from_file (broker, ordering_file);
			g_free (ordering_file);
			g_free (global_ordering);
		}
	
	home_dir = enchant_get_user_home_dir ();
	
	if (home_dir) 
		{
			ordering_file = g_build_filename (home_dir, ".enchant", "enchant.ordering", NULL);
			enchant_load_ordering_from_file (broker, ordering_file);
			g_free (ordering_file);
			g_free (home_dir);
		}
}

static GSList *
enchant_get_ordered_providers (EnchantBroker * broker,
			       const char * const tag)
{
	EnchantProvider *provider;
	GSList * list = NULL, * iter = NULL;

	char * ordering = NULL, ** tokens, *token;
	size_t i;

	ordering = (char *)g_hash_table_lookup (broker->provider_ordering, (gpointer)tag);
	if (!ordering)
		ordering = (char *)g_hash_table_lookup (broker->provider_ordering, (gpointer)"*");

	if (!ordering) 
		{
			/* return an unordered copy of the list */
			for (iter = broker->provider_list; iter != NULL; iter = g_slist_next (iter))
					list = g_slist_append (list, iter->data);
			return list;
		}
	
	tokens = g_strsplit (ordering, ",", 0);
	if (tokens) 
		{
			for (i = 0; tokens[i]; i++) 
				{
					token = tokens[i];
					
					for (iter = broker->provider_list; iter != NULL; iter = g_slist_next (iter)) 
						{
							provider = (EnchantProvider*)iter->data;
							
							if (provider && !strcmp (token, (*provider->identify)(provider)))
								list = g_slist_append (list, (gpointer)provider);
						}
				}
			
			g_strfreev (tokens);
		}
	
	/* providers not in the list need to be appended at the end */
	for (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)
{
	EnchantDict *dict;
	EnchantProvider *owner;
	EnchantSession *session;
	
	g_return_if_fail (data);
	
	dict = (EnchantDict *) data;
	session = (EnchantSession*)dict->enchant_private_data;
	owner = session->provider;
	
	if (owner && owner->dispose_dict) 
		(*owner->dispose_dict) (owner, dict);

	enchant_session_destroy (session);
}

static void
enchant_provider_free (gpointer data, gpointer user_data)
{
	EnchantProvider *provider;
	GModule *module;
	
	g_return_if_fail (data);
	
	provider = (EnchantProvider *) data;	
	module = (GModule *) provider->enchant_private_data;
	
	if (provider->dispose) 
		(*provider->dispose) (provider);
	
	/* close module only after invoking dispose */
	g_module_close (module);
}

/**
 * enchant_broker_init
 *
 * Returns: A new broker object capable of requesting
 * dictionaries from
 */
ENCHANT_MODULE_EXPORT (EnchantBroker *) 
enchant_broker_init (void)
{
	EnchantBroker *broker = NULL;
	
	g_return_val_if_fail (g_module_supported (), NULL);
	
	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;
}

/**
 * enchant_broker_free
 * @broker: A non-null #EnchantBroker
 *
 * Destroys the broker object
 */
ENCHANT_MODULE_EXPORT (void) 
enchant_broker_free (EnchantBroker * broker)
{
	guint n_remaining;

	g_return_if_fail (broker);       

	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_foreach (broker->provider_list, enchant_provider_free, NULL);
	g_slist_free (broker->provider_list);

	enchant_broker_clear_error (broker);

	g_free (broker);
}

/**
 * enchant_broker_request_pwl_dict
 *
 * PWL is a personal wordlist file, 1 entry per line
 *
 * Returns: 
 */
ENCHANT_MODULE_EXPORT (EnchantDict *)
enchant_broker_request_pwl_dict (EnchantBroker * broker, const char *const pwl)
{
	EnchantSession *session;
	EnchantDict *dict = NULL;
	
	g_return_val_if_fail (broker, NULL);
	g_return_val_if_fail (pwl && strlen(pwl), NULL);

	enchant_broker_clear_error (broker);

	dict = (EnchantDict*)g_hash_table_lookup (broker->dict_map, (gpointer) pwl);
	if (dict)
		return dict;

	session = enchant_session_new_with_pwl (NULL, pwl, "Personal WordList", TRUE);
	if (!session) 
		{
			broker->error = g_strdup_printf ("Couldn't open personal wordlist '%s'", pwl);
			return NULL;
		}

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

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

	return dict;
}

/**
 * enchant_broker_request_dict
 * @broker: A non-null #EnchantBroker
 * @tag: The non-null language tag you wish to request a dictionary for ("en_US", "de_DE", ...)
 *
 * Returns: An #EnchantDict, or %null if no suitable dictionary could be found.
 */
ENCHANT_MODULE_EXPORT (EnchantDict *)
enchant_broker_request_dict (EnchantBroker * broker, const char *const tag)
{
	EnchantSession *session;
	EnchantProvider *provider;
	EnchantDict *dict = NULL;
	GSList *list = NULL;
	
	g_return_val_if_fail (broker, NULL);
	g_return_val_if_fail (tag && strlen(tag), NULL);

	enchant_broker_clear_error (broker);
	
	dict = (EnchantDict*)g_hash_table_lookup (broker->dict_map, (gpointer) tag);
	if (dict)
		return dict;
	
	for (list = enchant_get_ordered_providers (broker, tag); list != NULL; list = g_slist_next (list))
		{
			provider = (EnchantProvider *) list->data;
			
			if (provider->request_dict)
				{
					dict = (*provider->request_dict) (provider, tag);
					
					if (dict)
						{
							session = enchant_session_new (provider, tag);
							dict->enchant_private_data = (void*)session;
							g_hash_table_insert (broker->dict_map, (gpointer)g_strdup (tag), dict);
							break;
						}
				}
		}

	g_slist_free (list);
	
	/* Nothing found */
	return dict;
}

/**
 * enchant_broker_describe
 * @broker: A non-null #EnchantBroker
 * @dict: A non-null #EnchantBrokerDescribeFn
 * @user_data: Optional user-data
 *
 * Enumerates the Enchant providers and tells
 * you some rudimentary information about them.
 */
ENCHANT_MODULE_EXPORT (void)
enchant_broker_describe (EnchantBroker * broker,
			 EnchantBrokerDescribeFn fn,
			 void * user_data)
{
	GSList *list;
	EnchantProvider *provider;
	GModule *module;

	const char * name, * desc, * file;

	g_return_if_fail (broker);
	g_return_if_fail (fn);

	enchant_broker_clear_error (broker);

	for (list = broker->provider_list; list != NULL; list = g_slist_next (list))
		{
			provider = (EnchantProvider *) list->data;
			module = (GModule *) provider->enchant_private_data;
			
			name = (*provider->identify) (provider);
			desc = (*provider->describe) (provider);
			file = g_module_name (module);
			
			(*fn) (name, desc, file, user_data);
		}
}

/**
 * enchant_broker_free_dict
 * @broker: A non-null #EnchantBroker
 * @dict: A non-null #EnchantDict
 *
 * Releases the dictionary when you are done using it
 */
ENCHANT_MODULE_EXPORT (void)
enchant_broker_free_dict (EnchantBroker * broker, EnchantDict * dict)
{
	EnchantSession * session;

	g_return_if_fail (broker);
	g_return_if_fail (dict);

	enchant_broker_clear_error (broker);

	session = (EnchantSession*)dict->enchant_private_data;
	
	if (session->provider)
		g_hash_table_remove (broker->dict_map, session->language_tag);
	else
		g_hash_table_remove (broker->dict_map, session->personal_filename);
}

/**
 * enchant_broker_dict_exists
 * @broker: A non-null #EnchantBroker
 * @tag: The non-null language tag you wish to request a dictionary for ("en_US", "de_DE", ...)
 *
 * Return existance of the requested dictionary (1 == true, 0 == false)
 */
ENCHANT_MODULE_EXPORT (int)
enchant_broker_dict_exists (EnchantBroker * broker,
			    const char * const tag)
{
	EnchantProvider *provider;
	GSList *list;

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

	enchant_broker_clear_error (broker);

	/* 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 (list = broker->provider_list; list != NULL; list = g_slist_next (list))
		{
			provider = (EnchantProvider *) list->data;

			if (provider->dictionary_exists)
				{
					if ((*provider->dictionary_exists) (provider, tag))
						return 1;
				}
		}

	return 0;
}

/**
 * enchant_broker_set_ordering
 * @broker: A non-null #EnchantBroker
 * @tag: A non-null language tag (en_US)
 * @ordering: A non-null ordering (aspell,myspell,ispell,uspell,hspell)
 *
 * Declares a preference of dictionaries to use for the language
 * described/referred to by @tag. The ordering is a comma delimited
 * list of provider names. As a special exception, the "*" tag can
 * be used as a language tag to declare a default ordering for any
 * language that does not explictly declare an ordering.
 */
ENCHANT_MODULE_EXPORT (void)
enchant_broker_set_ordering (EnchantBroker * broker,
			     const char * const tag,
			     const char * const ordering)
{
	char * tag_dupl;
	char * ordering_dupl;

	g_return_if_fail (broker);
	g_return_if_fail (tag && strlen(tag));
	g_return_if_fail (ordering && strlen(ordering));

	enchant_broker_clear_error (broker);

	tag_dupl = g_strdup (tag);
	ordering_dupl = g_strdup (ordering);

	tag_dupl = g_strstrip (tag_dupl);
	ordering_dupl = g_strstrip (ordering_dupl);

	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);
		}
}

/**
 * enchant_provider_set_error
 * @provider: A non-null provider
 * @err: A non-null error message
 * 
 * Sets the current runtime error to @err. This API is private to
 * the providers.
 */
ENCHANT_MODULE_EXPORT(void)
enchant_provider_set_error (EnchantProvider * provider, const char * const err)
{
	EnchantBroker * broker;

	g_return_if_fail (provider);
	g_return_if_fail (err);

	broker = provider->owner;
	g_return_if_fail (broker);
	
	enchant_broker_clear_error (broker);
	broker->error = g_strdup (err);	
}

/**
 * enchant_broker_get_error
 * @broker: A non-null broker
 *
 * Returns a const char string or NULL describing the last exception.
 * WARNING: error is transient and is likely cleared as soon as the 
 * next broker operation happens
 */
ENCHANT_MODULE_EXPORT(char *)
enchant_broker_get_error (EnchantBroker * broker)
{
	g_return_val_if_fail (broker, NULL);
	
	return broker->error;
}