/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here. Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include "purpleaccount.h"
#include "purpleaccountprivate.h"

#include "accounts.h"
#include "core.h"
#include "debug.h"
#include "network.h"
#include "prefs.h"
#include "purpleaddcontactrequest.h"
#include "purpleconversationmanager.h"
#include "purplecredentialmanager.h"
#include "purpleenums.h"
#include "purplenotification.h"
#include "purplenotificationconnectionerror.h"
#include "purplenotificationmanager.h"
#include "purpleprotocolmanager.h"
#include "request.h"
#include "request/purplerequestfieldbool.h"
#include "request/purplerequestfieldstring.h"
#include "util.h"

typedef struct {
	GSList *names;
	guint ref_count;
} PurpleAccountSettingFreezeQueue;

G_LOCK_DEFINE_STATIC(setting_notify_lock);

struct _PurpleAccount {
	GObject parent;

	char *id;
	char *name;
	char *username;
	PurpleContactInfo *contact_info;

	gboolean require_password;
	char *user_info;

	char *buddy_icon_path;

	gboolean enabled;
	gboolean remember_pass;

	char *protocol_id;
	PurpleProtocol *protocol;

	PurpleConnectionState connection_state;
	PurpleConnection *gc;
	gboolean disconnecting;

	GHashTable *settings;
	PurpleAccountSettingFreezeQueue *freeze_queue;

	PurpleProxyInfo *proxy_info;

	PurplePresence *presence;

	GError *error;
	PurpleNotification *error_notification;
} PurpleAccountPrivate;

typedef struct {
	char *ui;
	GValue value;
} PurpleAccountSetting;

enum {
	PROP_0,
	PROP_ID,
	PROP_NAME,
	PROP_USERNAME,
	PROP_CONTACT_INFO,
	PROP_REQUIRE_PASSWORD,
	PROP_ENABLED,
	PROP_CONNECTION_STATE,
	PROP_CONNECTION,
	PROP_PROTOCOL,
	PROP_PROTOCOL_ID,
	PROP_USER_INFO,
	PROP_BUDDY_ICON_PATH,
	PROP_REMEMBER_PASSWORD,
	PROP_PROXY_INFO,
	PROP_ERROR,
	PROP_CONNECTED,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

enum {
	SIG_SETTING_CHANGED,
	SIG_CONNECTION_STATE_CHANGED,
	SIG_CONNECTED,
	SIG_DISCONNECTED,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
purple_account_set_connection_state(PurpleAccount *account,
                                    PurpleConnectionState connection_state)
{

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(account->connection_state != connection_state) {
		PurpleConnectionState old = account->connection_state;

		/* We set the property and notify right away so that the signal
		 * handlers below will see the correct state.
		 */
		account->connection_state = connection_state;
		g_object_notify_by_pspec(G_OBJECT(account),
		                         properties[PROP_CONNECTION_STATE]);

		if(connection_state == PURPLE_CONNECTION_STATE_CONNECTED) {
			GDateTime *timestamp = NULL;

			/* Set the time the account came online */
			timestamp = g_date_time_new_now_utc();
			purple_presence_set_login_time(account->presence, timestamp);
			g_date_time_unref(timestamp);

			g_signal_emit(account, signals[SIG_CONNECTED], 0);
		} else if(connection_state == PURPLE_CONNECTION_STATE_DISCONNECTED) {
			/* The user is no longer online, so clear their login time. */
			purple_presence_set_login_time(account->presence, NULL);

			g_signal_emit(account, signals[SIG_DISCONNECTED], 0);
		}

		g_signal_emit(account, signals[SIG_CONNECTION_STATE_CHANGED], 0,
		              account->connection_state, old);
	}
}

static void
purple_account_set_id(PurpleAccount *account, const char *id) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(g_set_str(&account->id, id)) {
		g_object_notify_by_pspec(G_OBJECT(account), properties[PROP_ID]);
	}
}

static void
purple_account_set_protocol(PurpleAccount *account, PurpleProtocol *protocol) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(g_set_object(&account->protocol, protocol)) {
		GObject *obj = G_OBJECT(account);
		const char *protocol_id = NULL;

		if(PURPLE_IS_PROTOCOL(protocol)) {
			protocol_id = purple_protocol_get_id(protocol);
		}

		g_set_str(&account->protocol_id, protocol_id);

		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_PROTOCOL]);
		g_object_notify_by_pspec(obj, properties[PROP_PROTOCOL_ID]);
		g_object_thaw_notify(obj);
	}
}

static void
purple_account_free_notify_settings(PurpleAccount *account) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(account->freeze_queue != NULL) {
		g_slist_free_full(account->freeze_queue->names, g_free);

		g_slice_free(PurpleAccountSettingFreezeQueue, account->freeze_queue);
		account->freeze_queue = NULL;
	}
}

static void
purple_account_setting_changed_emit(PurpleAccount *account, const char *name) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(name != NULL);

	G_LOCK(setting_notify_lock);

	if(account->freeze_queue != NULL) {
		GSList *names = account->freeze_queue->names;

		if(g_slist_find_custom(names, name, (GCompareFunc)g_strcmp0) == NULL) {
			names = g_slist_prepend(names, g_strdup(name));
			account->freeze_queue->names = names;
		}
	} else {
		g_signal_emit(account, signals[SIG_SETTING_CHANGED],
		              g_quark_from_string(name), name);
	}

	G_UNLOCK(setting_notify_lock);
}

static void
purple_account_real_connect(PurpleAccount *account, const char *password) {
	PurpleConnection *connection = NULL;
	PurpleProtocol *protocol = NULL;
	GError *error = NULL;

	protocol = purple_account_get_protocol(account);

	connection = purple_protocol_create_connection(protocol, account, password,
	                                               &error);
	if(error != NULL) {
		purple_account_set_error(account, error);

		return;
	}

	g_return_if_fail(PURPLE_IS_CONNECTION(connection));

	purple_account_set_connection_state(account,
	                                    PURPLE_CONNECTION_STATE_CONNECTING);

	purple_account_set_connection(account, connection);
	if(!purple_connection_connect(connection, &error)) {
		purple_account_set_error(account, error);
		purple_account_set_connection_state(account,
		                                    PURPLE_CONNECTION_STATE_DISCONNECTED);
	}

	/* Finally remove our reference to the connection. */
	g_object_unref(connection);
}

static void
request_password_write_cb(GObject *obj, GAsyncResult *res, gpointer data) {
	PurpleCredentialManager *manager = PURPLE_CREDENTIAL_MANAGER(obj);
	PurpleAccount *account = PURPLE_ACCOUNT(data);
	GError *error = NULL;
	char *password = NULL;

	/* We stash the password on the account to get it to this call back... It's
	 * kind of gross but shouldn't be a big deal because any plugin has access
	 * to the credential store, so it's not really a security leak.
	 */
	password = (char *)g_object_get_data(G_OBJECT(account), "_tmp_password");
	g_object_set_data(G_OBJECT(account), "_tmp_password", NULL);

	if(!purple_credential_manager_write_password_finish(manager, res, &error))
	{
		/* we can't error an account without a connection, so we just drop a
		 * debug message for now and continue to connect the account.
		 */
		purple_debug_info("account",
		                  "failed to save password for account \"%s\": %s",
		                  purple_account_get_username(account),
		                  error != NULL ? error->message : "unknown error");
		g_clear_error(&error);
	}

	purple_account_real_connect(account, password);

	g_free(password);
}

static void
request_password_ok_cb(PurpleAccount *account, PurpleRequestPage *page) {
	const char *entry;
	gboolean remember;

	entry = purple_request_page_get_string(page, "password");
	remember = purple_request_page_get_bool(page, "remember");

	if(purple_strempty(entry)) {
		g_warning(_("Password is required to sign on for '%s'."),
		          account->username);
		return;
	}

	purple_account_set_remember_password(account, remember);

	if(remember) {
		PurpleCredentialManager *manager = NULL;

		manager = purple_credential_manager_get_default();

		/* The requests field can be invalidated by the time we write the
		 * password and we want to use it in the write callback, so we need to
		 * duplicate it for that callback.
		 */
		g_object_set_data(G_OBJECT(account), "_tmp_password", g_strdup(entry));
		purple_credential_manager_write_password_async(manager, account, entry,
		                                               NULL,
		                                               request_password_write_cb,
		                                               account);
	} else {
		purple_account_real_connect(account, entry);
	}
}

static void
request_password_cancel_cb(PurpleAccount *account,
                           G_GNUC_UNUSED PurpleRequestPage *page)
{
	/* Disable the account as the user has cancelled connecting */
	purple_account_set_enabled(account, FALSE);
}


static void
purple_account_connect_got_password_cb(GObject *obj, GAsyncResult *res,
                                       gpointer data)
{
	PurpleCredentialManager *manager = PURPLE_CREDENTIAL_MANAGER(obj);
	PurpleAccount *account = PURPLE_ACCOUNT(data);
	PurpleProtocol *protocol = NULL;
	PurpleProtocolOptions options;
	GError *error = NULL;
	char *password = NULL;
	gboolean require_password = TRUE;

	password = purple_credential_manager_read_password_finish(manager, res,
	                                                          &error);

	if(error != NULL) {
		purple_debug_warning("account", "failed to read password %s",
		                     error->message);

		g_error_free(error);
	}

	protocol = purple_account_get_protocol(account);
	options = purple_protocol_get_options(protocol);
	if(options & OPT_PROTO_PASSWORD_OPTIONAL) {
		require_password = purple_account_get_require_password(account);
	} else if(options & OPT_PROTO_NO_PASSWORD) {
		require_password = FALSE;
	}

	if((password == NULL || *password == '\0') && require_password) {
		purple_account_request_password(account,
		                                G_CALLBACK(request_password_ok_cb),
		                                G_CALLBACK(request_password_cancel_cb),
		                                account);
	} else {
		purple_account_real_connect(account, password);
	}

	g_free(password);
}

static void
no_password_cb(gpointer data) {
	PurpleAccount *account = data;

	purple_account_real_connect(account, NULL);
}

static void
delete_setting(void *data) {
	PurpleAccountSetting *setting = (PurpleAccountSetting *)data;

	g_free(setting->ui);
	g_value_unset(&setting->value);

	g_free(setting);
}

static void
purple_account_can_connect_cb(GObject *source, GAsyncResult *result,
                              gpointer data)
{
	PurpleAccount *account = data;
	PurpleProtocol *protocol = PURPLE_PROTOCOL(source);
	PurpleProtocolOptions options;
	GError *error = NULL;
	gboolean can_connect = FALSE;
	gboolean require_password = TRUE;

	can_connect = purple_protocol_can_connect_finish(protocol, result, &error);
	if(error != NULL) {
		purple_account_set_error(account, error);

		return;
	}

	if(!can_connect) {
		error = g_error_new_literal(PURPLE_CONNECTION_ERROR, 0,
		                            _("Unable to connect for unknown reasons"));

		purple_account_set_error(account, error);

		return;
	}

	options = purple_protocol_get_options(protocol);
	if(options & OPT_PROTO_PASSWORD_OPTIONAL) {
		require_password = purple_account_get_require_password(account);
	} else if(options & OPT_PROTO_NO_PASSWORD) {
		require_password = FALSE;
	}

	if(require_password) {
		PurpleCredentialManager *manager = NULL;

		manager = purple_credential_manager_get_default();
		purple_credential_manager_read_password_async(manager, account, NULL,
		                                              purple_account_connect_got_password_cb,
		                                              account);
	} else {
		g_timeout_add_once(0, no_password_cb, account);
	}
}

/******************************************************************************
 * XmlNode Helpers
 *****************************************************************************/
static PurpleXmlNode *
proxy_settings_to_xmlnode(PurpleProxyInfo *proxy_info) {
	PurpleXmlNode *node, *child;
	PurpleProxyType proxy_type;
	const char *value;
	int int_value;
	char buf[21];

	proxy_type = purple_proxy_info_get_proxy_type(proxy_info);

	node = purple_xmlnode_new("proxy");

	child = purple_xmlnode_new_child(node, "type");
	purple_xmlnode_insert_data(child,
			(proxy_type == PURPLE_PROXY_TYPE_USE_GLOBAL ? "global" :
			 proxy_type == PURPLE_PROXY_TYPE_NONE       ? "none"   :
			 proxy_type == PURPLE_PROXY_TYPE_HTTP       ? "http"   :
			 proxy_type == PURPLE_PROXY_TYPE_SOCKS4     ? "socks4" :
			 proxy_type == PURPLE_PROXY_TYPE_SOCKS5     ? "socks5" :
			 proxy_type == PURPLE_PROXY_TYPE_TOR        ? "tor" :
			 proxy_type == PURPLE_PROXY_TYPE_USE_ENVVAR ? "envvar" : "unknown"), -1);

	if((value = purple_proxy_info_get_hostname(proxy_info)) != NULL) {
		child = purple_xmlnode_new_child(node, "host");
		purple_xmlnode_insert_data(child, value, -1);
	}

	if((int_value = purple_proxy_info_get_port(proxy_info)) != 0) {
		g_snprintf(buf, sizeof(buf), "%d", int_value);
		child = purple_xmlnode_new_child(node, "port");
		purple_xmlnode_insert_data(child, buf, -1);
	}

	if((value = purple_proxy_info_get_username(proxy_info)) != NULL) {
		child = purple_xmlnode_new_child(node, "username");
		purple_xmlnode_insert_data(child, value, -1);
	}

	if((value = purple_proxy_info_get_password(proxy_info)) != NULL) {
		child = purple_xmlnode_new_child(node, "password");
		purple_xmlnode_insert_data(child, value, -1);
	}

	return node;
}

static PurpleXmlNode *
current_error_to_xmlnode(GError *error) {
	PurpleXmlNode *node = NULL;
	PurpleXmlNode *child = NULL;
	char *value = NULL;

	node = purple_xmlnode_new("current_error");

	if(error == NULL) {
		return node;
	}

	child = purple_xmlnode_new_child(node, "domain");
	purple_xmlnode_insert_data(child, g_quark_to_string(error->domain), -1);

	child = purple_xmlnode_new_child(node, "code");
	value = g_strdup_printf("%d", error->code);
	purple_xmlnode_insert_data(child, value, -1);
	g_free(value);

	child = purple_xmlnode_new_child(node, "message");
	purple_xmlnode_insert_data(child, error->message, -1);

	return node;
}

static void
setting_to_xmlnode(gpointer key, gpointer value, gpointer user_data)
{
	const char *name;
	PurpleAccountSetting *setting;
	PurpleXmlNode *node, *child;
	char buf[21];

	name = (const char *)key;
	setting = (PurpleAccountSetting *)value;
	node = (PurpleXmlNode *)user_data;

	child = purple_xmlnode_new_child(node, "setting");
	purple_xmlnode_set_attrib(child, "name", name);

	if(G_VALUE_HOLDS_INT(&setting->value)) {
		purple_xmlnode_set_attrib(child, "type", "int");
		g_snprintf(buf, sizeof(buf), "%d", g_value_get_int(&setting->value));
		purple_xmlnode_insert_data(child, buf, -1);
	} else if(G_VALUE_HOLDS_STRING(&setting->value) &&
	          g_value_get_string(&setting->value) != NULL)
	{
		purple_xmlnode_set_attrib(child, "type", "string");
		purple_xmlnode_insert_data(child, g_value_get_string(&setting->value),
		                           -1);
	} else if (G_VALUE_HOLDS_BOOLEAN(&setting->value)) {
		purple_xmlnode_set_attrib(child, "type", "bool");
		g_snprintf(buf, sizeof(buf), "%d",
		           g_value_get_boolean(&setting->value));
		purple_xmlnode_insert_data(child, buf, -1);
	}
}

PurpleXmlNode *
_purple_account_to_xmlnode(PurpleAccount *account) {
	PurpleXmlNode *node, *child;
	char *data = NULL;
	const char *tmp;
	PurpleProxyInfo *proxy_info;

	node = purple_xmlnode_new("account");

	if(account->id != NULL) {
		child = purple_xmlnode_new_child(node, "id");
		purple_xmlnode_insert_data(child, account->id, -1);
	}

	child = purple_xmlnode_new_child(node, "protocol");
	purple_xmlnode_insert_data(child, purple_account_get_protocol_id(account),
	                           -1);

	child = purple_xmlnode_new_child(node, "name");
	purple_xmlnode_insert_data(child, account->username, -1);

	child = purple_xmlnode_new_child(node, "require_password");
	data = g_strdup_printf("%d", account->require_password);
	purple_xmlnode_insert_data(child, data, -1);
	g_clear_pointer(&data, g_free);

	child = purple_xmlnode_new_child(node, "enabled");
	data = g_strdup_printf("%d", account->enabled);
	purple_xmlnode_insert_data(child, data, -1);
	g_clear_pointer(&data, g_free);

	tmp = purple_contact_info_get_alias(account->contact_info);
	if(tmp != NULL) {
		child = purple_xmlnode_new_child(node, "alias");
		purple_xmlnode_insert_data(child, tmp, -1);
	}

	if((tmp = purple_account_get_user_info(account)) != NULL) {
		/* TODO: Do we need to call purple_str_strip_char(tmp, '\r') here? */
		child = purple_xmlnode_new_child(node, "user-info");
		purple_xmlnode_insert_data(child, tmp, -1);
	}

	if(g_hash_table_size(account->settings) > 0) {
		child = purple_xmlnode_new_child(node, "settings");
		g_hash_table_foreach(account->settings, setting_to_xmlnode, child);
	}

	if((proxy_info = purple_account_get_proxy_info(account)) != NULL) {
		child = proxy_settings_to_xmlnode(proxy_info);
		purple_xmlnode_insert_child(node, child);
	}

	child = current_error_to_xmlnode(account->error);
	purple_xmlnode_insert_child(node, child);

	return node;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(PurpleAccount, purple_account, G_TYPE_OBJECT);

static void
purple_account_set_property(GObject *obj, guint param_id, const GValue *value,
                            GParamSpec *pspec)
{
	PurpleAccount *account = PURPLE_ACCOUNT(obj);

	switch(param_id) {
	case PROP_ID:
		purple_account_set_id(account, g_value_get_string(value));
		break;
	case PROP_NAME:
		purple_account_set_name(account, g_value_get_string(value));
		break;
	case PROP_USERNAME:
		purple_account_set_username(account, g_value_get_string(value));
		break;
	case PROP_REQUIRE_PASSWORD:
		purple_account_set_require_password(account,
		                                    g_value_get_boolean(value));
		break;
	case PROP_ENABLED:
		purple_account_set_enabled(account, g_value_get_boolean(value));
		break;
	case PROP_CONNECTION:
		purple_account_set_connection(account, g_value_get_object(value));
		break;
	case PROP_PROTOCOL:
		purple_account_set_protocol(account, g_value_get_object(value));
		break;
	case PROP_PROTOCOL_ID:
		purple_account_set_protocol_id(account, g_value_get_string(value));
		break;
	case PROP_USER_INFO:
		purple_account_set_user_info(account, g_value_get_string(value));
		break;
	case PROP_BUDDY_ICON_PATH:
		purple_account_set_buddy_icon_path(account, g_value_get_string(value));
		break;
	case PROP_REMEMBER_PASSWORD:
		purple_account_set_remember_password(account,
		                                     g_value_get_boolean(value));
		break;
	case PROP_PROXY_INFO:
		purple_account_set_proxy_info(account, g_value_get_object(value));
		break;
	case PROP_ERROR:
		purple_account_set_error(account, g_value_get_boxed(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_account_get_property(GObject *obj, guint param_id, GValue *value,
                            GParamSpec *pspec)
{
	PurpleAccount *account = PURPLE_ACCOUNT(obj);

	switch(param_id) {
	case PROP_ID:
		g_value_set_string(value, purple_account_get_id(account));
		break;
	case PROP_NAME:
		g_value_set_string(value, purple_account_get_name(account));
		break;
	case PROP_USERNAME:
		g_value_set_string(value, purple_account_get_username(account));
		break;
	case PROP_CONTACT_INFO:
		g_value_set_object(value, purple_account_get_contact_info(account));
		break;
	case PROP_REQUIRE_PASSWORD:
		g_value_set_boolean(value,
		                    purple_account_get_require_password(account));
		break;
	case PROP_ENABLED:
		g_value_set_boolean(value, purple_account_get_enabled(account));
		break;
	case PROP_CONNECTION_STATE:
		g_value_set_enum(value, purple_account_get_connection_state(account));
		break;
	case PROP_CONNECTION:
		g_value_set_object(value, purple_account_get_connection(account));
		break;
	case PROP_PROTOCOL:
		g_value_set_object(value, purple_account_get_protocol(account));
		break;
	case PROP_PROTOCOL_ID:
		g_value_set_string(value, purple_account_get_protocol_id(account));
		break;
	case PROP_USER_INFO:
		g_value_set_string(value, purple_account_get_user_info(account));
		break;
	case PROP_BUDDY_ICON_PATH:
		g_value_set_string(value, purple_account_get_buddy_icon_path(account));
		break;
	case PROP_REMEMBER_PASSWORD:
		g_value_set_boolean(value,
		                    purple_account_get_remember_password(account));
		break;
	case PROP_PROXY_INFO:
		g_value_set_object(value, purple_account_get_proxy_info(account));
		break;
	case PROP_ERROR:
		g_value_set_boxed(value, purple_account_get_error(account));
		break;
	case PROP_CONNECTED:
		g_value_set_boolean(value, purple_account_is_connected(account));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_account_init(PurpleAccount *account) {
	account->settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
	                                          delete_setting);
	account->contact_info = purple_contact_info_new(NULL);
	account->presence = purple_presence_new();
}

static void
purple_account_dispose(GObject *object) {
	PurpleAccount *account = PURPLE_ACCOUNT(object);

	if(!purple_account_is_disconnected(account)) {
		purple_account_disconnect(account);
	}

	g_clear_object(&account->error_notification);
	g_clear_object(&account->gc);
	g_clear_object(&account->presence);

	G_OBJECT_CLASS(purple_account_parent_class)->dispose(object);
}

static void
purple_account_finalize(GObject *object) {
	PurpleAccount *account = PURPLE_ACCOUNT(object);

	purple_account_free_notify_settings(account);

	g_clear_pointer(&account->id, g_free);
	g_clear_pointer(&account->name, g_free);
	g_clear_pointer(&account->username, g_free);
	g_clear_object(&account->contact_info);

	g_clear_object(&account->proxy_info);

	g_clear_error(&account->error);

	g_free(account->user_info);
	g_free(account->buddy_icon_path);

	g_free(account->protocol_id);
	g_clear_object(&account->protocol);

	g_hash_table_destroy(account->settings);

	G_OBJECT_CLASS(purple_account_parent_class)->finalize(object);
}

static void
purple_account_class_init(PurpleAccountClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->dispose = purple_account_dispose;
	obj_class->finalize = purple_account_finalize;
	obj_class->get_property = purple_account_get_property;
	obj_class->set_property = purple_account_set_property;

	/**
	 * PurpleAccount:id:
	 *
	 * The unique identifier for the account.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ID] = g_param_spec_string(
		"id", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:name:
	 *
	 * The user supplied name of the account.
	 *
	 * This is used by user interfaces to help the user determine which account
	 * is which.
	 *
	 * Since: 3.0
	 */
	properties[PROP_NAME] = g_param_spec_string(
		"name", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:username:
	 *
	 * The username for the account.
	 *
	 * Since: 3.0
	 */
	properties[PROP_USERNAME] = g_param_spec_string(
		"username", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:contact-info:
	 *
	 * The [class@ContactInfo] for the account.
	 *
	 * This should be completely managed by the protocol that this account was
	 * created for. Writing any properties to this from anything but the
	 * protocol will lead to de-synchronization.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CONTACT_INFO] = g_param_spec_object(
		"contact-info", NULL, NULL,
		PURPLE_TYPE_CONTACT_INFO,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:require-password:
	 *
	 * Whether or not this account should require a password. This is only used
	 * if the [class@Purple.Protocol] that this account is for allows optional
	 * passwords.
	 *
	 * Since: 3.0
	 */
	properties[PROP_REQUIRE_PASSWORD] = g_param_spec_boolean(
		"require-password", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:user-info:
	 *
	 * The user information or profile for the account.
	 *
	 * Since: 3.0
	 */
	properties[PROP_USER_INFO] = g_param_spec_string(
		"user-info", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:buddy-icon-path:
	 *
	 * The path to the file to use as the avatar for this account.
	 *
	 * Since: 3.0
	 */
	properties[PROP_BUDDY_ICON_PATH] = g_param_spec_string(
		"buddy-icon-path", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:enabled:
	 *
	 * Whether or not this account should track the user's global status.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ENABLED] = g_param_spec_boolean(
		"enabled", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:remember-password:
	 *
	 * Whether or not the password for this account should be stored in the
	 * configured [class@CredentialProvider].
	 *
	 * Since: 3.0
	 */
	properties[PROP_REMEMBER_PASSWORD] = g_param_spec_boolean(
		"remember-password", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:connection-state:
	 *
	 * The connection state of the account.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CONNECTION_STATE] = g_param_spec_enum(
		"connection-state", NULL, NULL,
		PURPLE_TYPE_CONNECTION_STATE,
		PURPLE_CONNECTION_STATE_DISCONNECTED,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:connection:
	 *
	 * The [class@Connection] object for this account. This will be %NULL when
	 * the account is offline.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CONNECTION] = g_param_spec_object(
		"connection", NULL, NULL,
		PURPLE_TYPE_CONNECTION,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:protocol:
	 *
	 * The protocol that this account is using.
	 *
	 * This will set [property@Account:protocol-id] to the
	 * [property@Protocol:id] of the new protocol.
	 *
	 * If [property@Account:protocol-id] is set first, this will be lazy
	 * initialized by the first call to [method@Account.get_protocol].
	 *
	 * Since: 3.0
	 */
	properties[PROP_PROTOCOL] = g_param_spec_object(
		"protocol", NULL, NULL,
		PURPLE_TYPE_PROTOCOL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:protocol-id:
	 *
	 * The identifier of the protocol that this account is using.
	 *
	 * If [property@Account:protocol] was set before this changes, it will be
	 * cleared.
	 *
	 * Since: 3.0
	 */
	properties[PROP_PROTOCOL_ID] = g_param_spec_string(
		"protocol-id", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:proxy-info:
	 *
	 * The [class@ProxyInfo] for this account.
	 *
	 * Since: 3.0
	 */
	properties[PROP_PROXY_INFO] = g_param_spec_object(
		"proxy-info", NULL, NULL,
		PURPLE_TYPE_PROXY_INFO,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:error:
	 *
	 * The [type@GLib.Error] of the account. This is set when an account enters
	 * an error state and is automatically cleared when a connection attempt is
	 * made.
	 *
	 * Setting this will not disconnect an account, but this will be set when
	 * there is a connection failure.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ERROR] = g_param_spec_boxed(
		"error", NULL, NULL,
		G_TYPE_ERROR,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccount:connected:
	 *
	 * Whether or not the account is connected.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CONNECTED] = g_param_spec_boolean(
		"connected", NULL, NULL,
		FALSE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);

	/**
	 * PurpleAccount::setting-changed:
	 * @account: The account whose setting changed.
	 * @name: The name of the setting that changed.
	 *
	 * This signal is emitted whenever an account setting is changed.
	 *
	 * This signal supports details, so you can be notified when a single
	 * setting changes. For example, say there's a setting named `foo`,
	 * connecting to `setting-changed::foo` will only be called when the `foo`
	 * setting is changed.
	 *
	 * Since: 3.0
	 */
	signals[SIG_SETTING_CHANGED] = g_signal_new_class_handler(
		"setting-changed",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		1,
		G_TYPE_STRING);

	/**
	 * PurpleAccount::connection-state-changed:
	 * @account: the instance
	 * @new_state: the new connection state
	 * @old_state: the old connection state
	 *
	 * This is emitted when the [property@Account:connection-state] has
	 * changed.
	 *
	 * Since: 3.0
	 */
	signals[SIG_CONNECTION_STATE_CHANGED] = g_signal_new_class_handler(
		"connection-state-changed",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		2,
		PURPLE_TYPE_CONNECTION_STATE,
		PURPLE_TYPE_CONNECTION_STATE);

	/**
	 * PurpleAccount::connected:
	 * @account: The account instance.
	 *
	 * This signal is emitted when the [property@Account:connection-state] has
	 * changed to %PURPLE_CONNECTION_STATE_CONNECTED.
	 *
	 * Since: 3.0
	 */
	signals[SIG_CONNECTED] = g_signal_new_class_handler(
		"connected",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		0);

	/**
	 * PurpleAccount::disconnected:
	 * @account: The account instance.
	 *
	 * This signal is emitted when the [property@Account:connection-state] has
	 * changed to %PURPLE_CONNECTION_STATE_DISCONNECTED.
	 *
	 * Since: 3.0
	 */
	signals[SIG_DISCONNECTED] = g_signal_new_class_handler(
		"disconnected",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		0);
}

/******************************************************************************
 * Private API
 *****************************************************************************/

/* This is a temporary method that the deserializer can call to set the
 * enabled property without bringing the account online.
 */
void
purple_account_set_enabled_plain(PurpleAccount *account, gboolean enabled) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	account->enabled = enabled;
}

/******************************************************************************
 * Public API
 *****************************************************************************/
void
purple_account_connected(PurpleAccount *account) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	purple_account_set_connection_state(account,
	                                    PURPLE_CONNECTION_STATE_CONNECTED);
}

PurpleAccount *
purple_account_new(const char *username, const char *protocol_id) {
	g_return_val_if_fail(username != NULL, NULL);
	g_return_val_if_fail(protocol_id != NULL, NULL);

	return g_object_new(
		PURPLE_TYPE_ACCOUNT,
		"username", username,
		"protocol-id", protocol_id,
		"enabled", FALSE,
		NULL);
}

const char *
purple_account_get_id(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	/* If we weren't given an ID during construction, generate one on the fly.
	 */
	if(purple_strempty(account->id)) {
		GChecksum *checksum = NULL;

		checksum = g_checksum_new(G_CHECKSUM_SHA256);

		g_checksum_update(checksum, (const guchar *)account->protocol_id, -1);
		g_checksum_update(checksum, (const guchar *)account->username, -1);

		purple_account_set_id(account, g_checksum_get_string(checksum));

		g_checksum_free(checksum);
	}

	return account->id;
}

const char *
purple_account_get_username(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->username;
}

void
purple_account_set_username(PurpleAccount *account, const char *username) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(g_set_str(&account->username, username)) {
		g_object_notify_by_pspec(G_OBJECT(account), properties[PROP_USERNAME]);
	}
}

PurpleContactInfo *
purple_account_get_contact_info(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->contact_info;
}

void
purple_account_connect(PurpleAccount *account)
{
	PurpleProtocol *protocol = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	purple_account_set_error(account, NULL);

	if(!purple_account_get_enabled(account)) {
		purple_debug_info("account",
		                  "Account %s not enabled, not connecting.\n",
		                  account->username);
		return;
	}

	protocol = purple_account_get_protocol(account);
	if(protocol == NULL) {
		PurpleNotification *notification = NULL;
		PurpleNotificationManager *manager = NULL;
		char *value = NULL;

		value = g_strdup_printf(_("Failed to load account '%s'"),
		                        account->username);
		notification = purple_notification_new(NULL, value);
		g_free(value);

		purple_notification_set_account(notification, account);

		value = g_strdup_printf(_("Failed to find a protocol with id '%s'"),
		                        account->protocol_id);
		purple_notification_set_subtitle(notification, value);
		g_free(value);

		manager = purple_notification_manager_get_default();
		purple_notification_manager_add(manager, notification);
		g_clear_object(&notification);

		return;
	}

	purple_protocol_can_connect_async(protocol, account, NULL,
	                                  purple_account_can_connect_cb, account);
}

void
purple_account_disconnect(PurpleAccount *account) {
	GError *error = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(!purple_account_is_disconnecting(account));
	g_return_if_fail(!purple_account_is_disconnected(account));

	purple_debug_info("account", "Disconnecting account %s (%p)\n",
	                  account->username, account);

	purple_account_set_connection_state(account,
	                                    PURPLE_CONNECTION_STATE_DISCONNECTING);

	if(!purple_connection_disconnect(account->gc, &error)) {
		g_warning("error while disconnecting account %s (%s): %s",
		          account->username,
		          purple_account_get_protocol_id(account),
		          (error != NULL) ? error->message : "unknown error");
		g_clear_error(&error);
	}

	purple_account_set_connection(account, NULL);
	purple_account_set_connection_state(account,
	                                    PURPLE_CONNECTION_STATE_DISCONNECTED);
}

void
purple_account_disconnect_with_error(PurpleAccount *account, GError *error) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(error != NULL);

	purple_account_disconnect(account);
	purple_account_set_error(account, error);
}

gboolean
purple_account_is_disconnecting(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), TRUE);

	return (account->connection_state == PURPLE_CONNECTION_STATE_DISCONNECTING);
}

void
purple_account_request_close_with_account(PurpleAccount *account) {
	PurpleNotificationManager *manager = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	manager = purple_notification_manager_get_default();
	purple_notification_manager_remove_with_account(manager, account, FALSE);
}

void
purple_account_request_password(PurpleAccount *account, GCallback ok_cb,
                                GCallback cancel_cb, void *user_data)
{
	char *primary;
	const char *username;
	PurpleRequestGroup *group;
	PurpleRequestField *field;
	PurpleRequestPage *page;

	/* Close any previous password request windows */
	purple_request_close_with_handle(account);

	username = purple_account_get_username(account);
	primary = g_strdup_printf(_("Enter password for %s (%s)"), username,
	                          purple_protocol_get_name(account->protocol));

	page = purple_request_page_new();
	group = purple_request_group_new(NULL);
	purple_request_page_add_group(page, group);

	field = purple_request_field_string_new("password", _("Enter Password"),
	                                        NULL, FALSE);
	purple_request_field_string_set_masked(PURPLE_REQUEST_FIELD_STRING(field),
	                                       TRUE);
	purple_request_field_set_required(field, TRUE);
	purple_request_group_add_field(group, field);

	field = purple_request_field_bool_new("remember", _("Save password"),
	                                      FALSE);
	purple_request_group_add_field(group, field);

	purple_request_fields(account, NULL, primary, NULL, page, _("OK"), ok_cb,
	                      _("Cancel"), cancel_cb,
	                      purple_request_cpar_from_account(account),
	                      user_data);
	g_free(primary);
}

void
purple_account_set_user_info(PurpleAccount *account, const char *user_info) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(g_set_str(&account->user_info, user_info)) {
		g_object_notify_by_pspec(G_OBJECT(account),
		                         properties[PROP_USER_INFO]);

		purple_accounts_schedule_save();
	}
}

void
purple_account_set_buddy_icon_path(PurpleAccount *account, const char *path) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(g_set_str(&account->buddy_icon_path, path)) {
		g_object_notify_by_pspec(G_OBJECT(account),
		                         properties[PROP_BUDDY_ICON_PATH]);

		purple_accounts_schedule_save();
	}
}

void
purple_account_set_protocol_id(PurpleAccount *account, const char *protocol_id)
{
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(protocol_id != NULL);
	g_return_if_fail(purple_account_is_disconnected(account));

	if(g_set_str(&account->protocol_id, protocol_id)) {
		GObject *obj = G_OBJECT(account);

		/* Setting this directly clears any cached protocol. */
		g_clear_object(&account->protocol);

		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_PROTOCOL_ID]);
		g_object_notify_by_pspec(obj, properties[PROP_PROTOCOL]);
		g_object_thaw_notify(obj);

		purple_accounts_schedule_save();
	}
}

void
purple_account_set_connection(PurpleAccount *account, PurpleConnection *gc) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(g_set_object(&account->gc, gc)) {
		g_object_notify_by_pspec(G_OBJECT(account),
		                         properties[PROP_CONNECTION]);
	}
}

void
purple_account_set_remember_password(PurpleAccount *account, gboolean value) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	account->remember_pass = value;

	g_object_notify_by_pspec(G_OBJECT(account),
	                         properties[PROP_REMEMBER_PASSWORD]);

	purple_accounts_schedule_save();
}

void
purple_account_set_enabled(PurpleAccount *account, gboolean value) {
	gboolean was_enabled = FALSE;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	was_enabled = account->enabled;
	account->enabled = value;
	if(was_enabled != value) {
		g_object_notify_by_pspec(G_OBJECT(account), properties[PROP_ENABLED]);
	}

	if(value && purple_presence_is_online(account->presence)) {
		purple_account_connect(account);
	} else if (!value && !purple_account_is_disconnected(account)) {
		purple_account_disconnect(account);
	}

	purple_accounts_schedule_save();
}

void
purple_account_set_proxy_info(PurpleAccount *account, PurpleProxyInfo *info) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(g_set_object(&account->proxy_info, info)) {
		g_object_notify_by_pspec(G_OBJECT(account),
		                         properties[PROP_PROXY_INFO]);

		purple_accounts_schedule_save();
	}
}

void
purple_account_set_int(PurpleAccount *account, const char *name, int value) {
	PurpleAccountSetting *setting;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(name != NULL);

	setting = g_new0(PurpleAccountSetting, 1);

	g_value_init(&setting->value, G_TYPE_INT);
	g_value_set_int(&setting->value, value);

	g_hash_table_insert(account->settings, g_strdup(name), setting);

	purple_account_setting_changed_emit(account, name);

	purple_accounts_schedule_save();
}

void
purple_account_set_string(PurpleAccount *account, const char *name,
                          const char *value)
{
	PurpleAccountSetting *setting;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(name != NULL);

	setting = g_new0(PurpleAccountSetting, 1);

	g_value_init(&setting->value, G_TYPE_STRING);
	g_value_set_string(&setting->value, value);

	g_hash_table_insert(account->settings, g_strdup(name), setting);

	purple_account_setting_changed_emit(account, name);

	purple_accounts_schedule_save();
}

void
purple_account_set_bool(PurpleAccount *account, const char *name,
                        gboolean value)
{
	PurpleAccountSetting *setting;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(name != NULL);

	setting = g_new0(PurpleAccountSetting, 1);

	g_value_init(&setting->value, G_TYPE_BOOLEAN);
	g_value_set_boolean(&setting->value, value);

	g_hash_table_insert(account->settings, g_strdup(name), setting);

	purple_account_setting_changed_emit(account, name);

	purple_accounts_schedule_save();
}

gboolean
purple_account_is_connected(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);

	return (account->connection_state == PURPLE_CONNECTION_STATE_CONNECTED);
}

gboolean
purple_account_is_connecting(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);

	return (account->connection_state == PURPLE_CONNECTION_STATE_CONNECTING);
}

gboolean
purple_account_is_disconnected(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);

	return (account->connection_state == PURPLE_CONNECTION_STATE_DISCONNECTED);
}

const char *
purple_account_get_user_info(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->user_info;
}

const char *
purple_account_get_buddy_icon_path(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->buddy_icon_path;
}

const char *
purple_account_get_protocol_id(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->protocol_id;
}

PurpleProtocol *
purple_account_get_protocol(PurpleAccount *account) {
	PurpleProtocolManager *manager = NULL;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	if(!PURPLE_IS_PROTOCOL(account->protocol)) {
		manager = purple_protocol_manager_get_default();
		if(manager != NULL) {
			PurpleProtocol *protocol = NULL;

			protocol = purple_protocol_manager_find(manager,
			                                        account->protocol_id);

			purple_account_set_protocol(account, protocol);
		}
	}

	return account->protocol;
}

PurpleConnectionState
purple_account_get_connection_state(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account),
	                     PURPLE_CONNECTION_STATE_DISCONNECTED);

	return account->connection_state;
}

PurpleConnection *
purple_account_get_connection(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->gc;
}

gboolean
purple_account_get_remember_password(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);

	return account->remember_pass;
}

gboolean
purple_account_get_enabled(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);

	return account->enabled;
}

PurpleProxyInfo *
purple_account_get_proxy_info(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->proxy_info;
}

PurplePresence *
purple_account_get_presence(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->presence;
}

int
purple_account_get_int(PurpleAccount *account, const char *name,
                       int default_value)
{
	PurpleAccountSetting *setting;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), default_value);
	g_return_val_if_fail(name    != NULL, default_value);

	setting = g_hash_table_lookup(account->settings, name);

	if(setting == NULL) {
		return default_value;
	}

	g_return_val_if_fail(G_VALUE_HOLDS_INT(&setting->value), default_value);

	return g_value_get_int(&setting->value);
}

const char *
purple_account_get_string(PurpleAccount *account, const char *name,
                          const char *default_value)
{
	PurpleAccountSetting *setting;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), default_value);
	g_return_val_if_fail(name != NULL, default_value);

	setting = g_hash_table_lookup(account->settings, name);

	if(setting == NULL) {
		return default_value;
	}

	g_return_val_if_fail(G_VALUE_HOLDS_STRING(&setting->value), default_value);

	return g_value_get_string(&setting->value);
}

gboolean
purple_account_get_bool(PurpleAccount *account, const char *name,
                        gboolean default_value)
{
	PurpleAccountSetting *setting;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), default_value);
	g_return_val_if_fail(name != NULL, default_value);

	setting = g_hash_table_lookup(account->settings, name);

	if(setting == NULL) {
		return default_value;
	}

	g_return_val_if_fail(G_VALUE_HOLDS_BOOLEAN(&setting->value), default_value);

	return g_value_get_boolean(&setting->value);
}

void
purple_account_change_password(PurpleAccount *account,
                               G_GNUC_UNUSED const char *orig_pw,
                               const char *new_pw)
{
	PurpleCredentialManager *manager = NULL;

	/* just going to fire and forget this for now as not many protocols even
	 * implement the change password stuff.
	 */
	manager = purple_credential_manager_get_default();
	purple_credential_manager_write_password_async(manager, account, new_pw,
	                                               NULL, NULL, NULL);
}

GError *
purple_account_get_error(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->error;
}

void
purple_account_set_error(PurpleAccount *account, GError *error) {
	PurpleNotificationManager *manager = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	if(account->error == error) {
		return;
	}

	/* Set the new error. */
	g_clear_error(&account->error);
	account->error = error;

	manager = purple_notification_manager_get_default();

	/* Clear the old account connection error notification if we have one. */
	if(PURPLE_IS_NOTIFICATION(account->error_notification)) {
		if(PURPLE_IS_NOTIFICATION_MANAGER(manager)) {
			purple_notification_manager_remove(manager,
			                                   account->error_notification);
		}

		g_clear_object(&account->error_notification);
	}

	/* If we have a new error, create a new error notification. */
	if(error != NULL) {
		PurpleNotification *notification = NULL;

		notification = purple_notification_connection_error_new(NULL, account);

		if(PURPLE_IS_NOTIFICATION_MANAGER(manager)) {
			purple_notification_manager_add(manager, notification);
		}

		account->error_notification = notification;
	}

	g_object_notify_by_pspec(G_OBJECT(account), properties[PROP_ERROR]);

	purple_accounts_schedule_save();
}

void
purple_account_set_require_password(PurpleAccount *account,
                                    gboolean require_password)
{
	gboolean old = FALSE;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	old = account->require_password;
	account->require_password = require_password;

	if(old != require_password) {
		g_object_notify_by_pspec(G_OBJECT(account),
		                         properties[PROP_REQUIRE_PASSWORD]);
	}
}

gboolean
purple_account_get_require_password(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);

	return account->require_password;
}

void
purple_account_freeze_notify_settings(PurpleAccount *account) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	G_LOCK(setting_notify_lock);
	if(account->freeze_queue == NULL) {
		account->freeze_queue = g_slice_new0(PurpleAccountSettingFreezeQueue);
	}

	account->freeze_queue->ref_count++;

	G_UNLOCK(setting_notify_lock);
}

void
purple_account_thaw_notify_settings(PurpleAccount *account) {
	GSList *names = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	G_LOCK(setting_notify_lock);
	if(G_UNLIKELY(account->freeze_queue->ref_count == 0)) {
		G_UNLOCK(setting_notify_lock);

		g_critical("purple_account_settings_thaw_notify called for account %s "
		           "(%s) when not frozen",
		           purple_account_get_username(account),
		           purple_account_get_protocol_id(account));

		return;
	}

	account->freeze_queue->ref_count--;
	if(account->freeze_queue->ref_count > 0) {
		G_UNLOCK(setting_notify_lock);

		return;
	}

	/* This was the last ref, so fire off the signals. */
	names = account->freeze_queue->names;
	while(names != NULL) {
		char *name = names->data;

		g_signal_emit(account, signals[SIG_SETTING_CHANGED],
		              g_quark_from_string(name), name);

		names = g_slist_delete_link(names, names);
		g_free(name);
	}
	account->freeze_queue->names = names;

	purple_account_free_notify_settings(account);

	G_UNLOCK(setting_notify_lock);
}

int
purple_account_compare(PurpleAccount *a, PurpleAccount *b) {
	if (a != NULL && b == NULL) {
		return -1;
	}
	if (a == NULL && b != NULL) {
		return 1;
	}
	if (a == NULL && b == NULL) {
		return 0;
	}

	return g_strcmp0(a->id, b->id);
}

gboolean
purple_account_equal(PurpleAccount *a, PurpleAccount *b) {
	return purple_account_compare(a, b) == 0;
}

const char *
purple_account_get_name(PurpleAccount *account) {
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);

	return account->name;
}

void
purple_account_set_name(PurpleAccount *account, const char *name) {
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
	g_return_if_fail(!purple_strempty(name));

	if(g_set_str(&account->name, name)) {
		g_object_notify_by_pspec(G_OBJECT(account), properties[PROP_NAME]);
	}
}
