/*
 * 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/>.
 */

#ifdef G_LOG_DOMAIN
# undef G_LOG_DOMAIN
#endif /* G_LOG_DOMAIN */
#define G_LOG_DOMAIN "PurpleAccountSettings"

#include <gio/gio.h>

#include <birb.h>

#include "purpleaccountsettings.h"

#include "purpleaccountsettingboolean.h"
#include "purpleaccountsettingint.h"
#include "purpleaccountsettingstring.h"
#include "purpleaccountsettingstringlist.h"

struct _PurpleAccountSettings {
	GObject parent;

	GPtrArray *settings;
};

enum {
	PROP_0,
	PROP_ITEM_TYPE,
	PROP_N_ITEMS,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

enum {
	SIG_SETTING_CHANGED,
	SIG_SETTINGS_UPDATED,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

#define PURPLE_ACCOUNT_SETTINGS_ACCOUNT_SETTING_DATA "_purple_account_settings"

/******************************************************************************
 * Helpers
 *****************************************************************************/
static PurpleAccountSetting *
purple_account_settings_find_setting(PurpleAccountSettings *settings,
                                     const char *id, guint *position)
{
	for(guint i = 0; i < settings->settings->len; i++) {
		PurpleAccountSetting *setting = NULL;

		setting = g_ptr_array_index(settings->settings, i);
		if(birb_str_equal(purple_account_setting_get_id(setting), id)) {
			if(position != NULL) {
				*position = i;
			}

			return setting;
		}
	}

	return NULL;
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
purple_accounts_settings_setting_changed_cb(PurpleAccountSetting *setting,
                                            gpointer data)
{
	g_signal_emit(data,  signals[SIG_SETTING_CHANGED], 0, setting);
}

/******************************************************************************
 * GListModel Implementation
 *****************************************************************************/
static GType
purple_account_settings_get_item_type(G_GNUC_UNUSED GListModel *model) {
	return PURPLE_TYPE_ACCOUNT_SETTING;
}

static guint
purple_account_settings_get_n_items(GListModel *model) {
	PurpleAccountSettings *settings = PURPLE_ACCOUNT_SETTINGS(model);

	return settings->settings->len;
}

static gpointer
purple_account_settings_get_item(GListModel *model, guint position) {
	PurpleAccountSettings *settings = PURPLE_ACCOUNT_SETTINGS(model);
	PurpleAccountSetting *setting = NULL;

	if(position < settings->settings->len) {
		setting = g_ptr_array_index(settings->settings, position);
		g_object_ref(setting);
	}

	return setting;
}

static void
purple_account_settings_list_model_iface_init(GListModelInterface *iface) {
	iface->get_item_type = purple_account_settings_get_item_type;
	iface->get_n_items = purple_account_settings_get_n_items;
	iface->get_item = purple_account_settings_get_item;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleAccountSettings, purple_account_settings,
                              G_TYPE_OBJECT,
                              G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
                                                    purple_account_settings_list_model_iface_init))

static void
purple_account_settings_finalize(GObject *obj) {
	PurpleAccountSettings *settings = PURPLE_ACCOUNT_SETTINGS(obj);

	g_clear_pointer(&settings->settings, g_ptr_array_unref);

	G_OBJECT_CLASS(purple_account_settings_parent_class)->finalize(obj);
}

static void
purple_account_settings_get_property(GObject *obj, guint param_id,
                                     GValue *value, GParamSpec *pspec)
{
	GListModel *model = G_LIST_MODEL(obj);

	switch(param_id) {
	case PROP_ITEM_TYPE:
		g_value_set_gtype(value, g_list_model_get_item_type(model));
		break;
	case PROP_N_ITEMS:
		g_value_set_uint(value, g_list_model_get_n_items(model));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_account_settings_init(PurpleAccountSettings *settings) {
	settings->settings = g_ptr_array_new_full(0, g_object_unref);
}

static void
purple_account_settings_class_init(PurpleAccountSettingsClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = purple_account_settings_finalize;
	obj_class->get_property = purple_account_settings_get_property;

	/* Ensure all of the setting types are registered. Backends may choose to
	 * store the settings using their type names, which won't be available if
	 * the types aren't registered.
	 */
	g_type_ensure(PURPLE_TYPE_ACCOUNT_SETTING_BOOLEAN);
	g_type_ensure(PURPLE_TYPE_ACCOUNT_SETTING_INT);
	g_type_ensure(PURPLE_TYPE_ACCOUNT_SETTING_STRING);
	g_type_ensure(PURPLE_TYPE_ACCOUNT_SETTING_STRING_LIST);

	/**
	 * PurpleAccountSettings:item-type:
	 *
	 * The type of items. See [vfunc@Gio.ListModel.get_item_type].
	 *
	 * Since: 3.0
	 */
	properties[PROP_ITEM_TYPE] = g_param_spec_gtype(
		"item-type", NULL, NULL,
		G_TYPE_OBJECT,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleAccountSettings:n-items:
	 *
	 * The number of items. See [vfunc@Gio.ListModel.get_n_items].
	 *
	 * Since: 3.0
	 */
	properties[PROP_N_ITEMS] = g_param_spec_uint(
		"n-items", NULL, NULL,
		0, G_MAXUINT, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);

	/**
	 * PurpleAccountSettings::setting-changed:
	 * @settings: the instance
	 * @setting: the setting changed
	 * @data: the user data
	 *
	 * This signal propagates the [signal@AccountSetting::changed] signal for
	 * any [class@AccountSetting] that the object knows about.
	 *
	 * Since: 3.0
	 */
	signals[SIG_SETTING_CHANGED] = g_signal_new_class_handler(
		"setting-changed",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		1,
		PURPLE_TYPE_ACCOUNT_SETTING);

	/**
	 * PurpleAccountSettings::settings-updated:
	 *
	 * This is emitted when [method@AccountSettings.update_settings] has
	 * completed.
	 *
	 * Since: 3.0
	 */
	signals[SIG_SETTINGS_UPDATED] = g_signal_new_class_handler(
		"settings-updated",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		0);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
gboolean
purple_account_settings_add_setting(PurpleAccountSettings *settings,
                                    PurpleAccountSetting *setting)
{
	PurpleAccountSetting *existing = NULL;
	GSignalGroup *signal_group = NULL;
	const char *id = NULL;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings), FALSE);
	g_return_val_if_fail(PURPLE_IS_ACCOUNT_SETTING(setting), FALSE);

	id = purple_account_setting_get_id(setting);
	existing = purple_account_settings_find_setting(settings, id, NULL);

	/* Settings may only be added once. So adding a second setting with the
	 * same id is a programming error which we assert on. We also need to
	 * sink the reference to the duplicate setting as we take ownership of it,
	 * which in this case means just throwing it away.
	 */
	if(existing != NULL) {
		g_object_unref(setting);

		g_return_val_if_fail(existing == NULL, FALSE);
	}

	g_ptr_array_add(settings->settings, setting);

	/* Create our signal group and add all the signals. */
	signal_group = g_signal_group_new(PURPLE_TYPE_ACCOUNT_SETTING);
	g_signal_group_connect_object(signal_group, "changed",
	                              G_CALLBACK(purple_accounts_settings_setting_changed_cb),
	                              settings, G_CONNECT_DEFAULT);

	/* Set the target of the signal group and store it on the setting. */
	g_signal_group_set_target(signal_group, setting);
	g_object_set_data_full(G_OBJECT(setting),
	                       PURPLE_ACCOUNT_SETTINGS_ACCOUNT_SETTING_DATA,
	                       signal_group, g_object_unref);

	/* Emit the items changed signal. */
	g_list_model_items_changed(G_LIST_MODEL(settings),
	                           settings->settings->len - 1, 0, 1);

	return TRUE;
}

gboolean
purple_account_settings_get_boolean(PurpleAccountSettings *settings,
                                    const char *id, gboolean fallback)
{
	PurpleAccountSetting *setting = NULL;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings), fallback);
	g_return_val_if_fail(!birb_str_is_empty(id), fallback);

	setting = purple_account_settings_find_setting(settings, id, NULL);
	if(!PURPLE_IS_ACCOUNT_SETTING_BOOLEAN(setting)) {
		return fallback;
	}

	return purple_account_setting_boolean_get_value(PURPLE_ACCOUNT_SETTING_BOOLEAN(setting));
}

int
purple_account_settings_get_int(PurpleAccountSettings *settings,
                                const char *id, int fallback)
{
	PurpleAccountSetting *setting = NULL;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings), fallback);
	g_return_val_if_fail(!birb_str_is_empty(id), fallback);

	setting = purple_account_settings_find_setting(settings, id, NULL);
	if(!PURPLE_IS_ACCOUNT_SETTING_INT(setting)) {
		return fallback;
	}

	return purple_account_setting_int_get_value(PURPLE_ACCOUNT_SETTING_INT(setting));
}

const char *
purple_account_settings_get_string(PurpleAccountSettings *settings,
                                   const char *id, const char *fallback)
{
	PurpleAccountSetting *setting = NULL;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings), fallback);
	g_return_val_if_fail(!birb_str_is_empty(id), fallback);

	setting = purple_account_settings_find_setting(settings, id, NULL);
	if(!PURPLE_IS_ACCOUNT_SETTING_STRING(setting)) {
		return fallback;
	}

	return purple_account_setting_string_get_value(PURPLE_ACCOUNT_SETTING_STRING(setting));
}

const char *
purple_account_settings_get_string_list(PurpleAccountSettings *settings,
                                        const char *id, const char *fallback)
{
	PurpleAccountSetting *setting = NULL;
	PurpleAccountSettingStringList *list = NULL;
	const char *ret = NULL;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings), fallback);
	g_return_val_if_fail(!birb_str_is_empty(id), fallback);

	setting = purple_account_settings_find_setting(settings, id, NULL);
	if(!PURPLE_IS_ACCOUNT_SETTING_STRING_LIST(setting)) {
		return fallback;
	}

	list = PURPLE_ACCOUNT_SETTING_STRING_LIST(setting);
	ret = purple_account_setting_string_list_get_active_item(list);
	if(ret != NULL) {
		return ret;
	}

	return fallback;
}

PurpleAccountSettings *
purple_account_settings_new(void) {
	return g_object_new(PURPLE_TYPE_ACCOUNT_SETTINGS, NULL);
}

gboolean
purple_account_settings_remove_setting(PurpleAccountSettings *settings,
                                       const char *id)
{
	PurpleAccountSetting *existing = NULL;
	guint position = 0;

	g_return_val_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings), FALSE);
	g_return_val_if_fail(!birb_str_is_empty(id), FALSE);

	existing = purple_account_settings_find_setting(settings, id, &position);
	if(!PURPLE_IS_ACCOUNT_SETTING(existing)) {
		return FALSE;
	}

	g_object_set_data(G_OBJECT(existing),
	                  PURPLE_ACCOUNT_SETTINGS_ACCOUNT_SETTING_DATA,
	                  NULL);

	g_ptr_array_remove_index(settings->settings, position);

	g_list_model_items_changed(G_LIST_MODEL(settings), position, 1, 0);

	return TRUE;
}

void
purple_account_settings_remove_all_settings(PurpleAccountSettings *settings) {
	guint n_items = 0;

	g_return_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings));

	n_items = settings->settings->len;
	if(n_items == 0) {
		return;
	}

	g_ptr_array_remove_range(settings->settings, 0, n_items);
	g_list_model_items_changed(G_LIST_MODEL(settings), 0, n_items, 0);
}

void
purple_account_settings_set_boolean(PurpleAccountSettings *settings,
                                    const char *id, gboolean value)
{
	PurpleAccountSetting *setting = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings));
	g_return_if_fail(!birb_str_is_empty(id));

	setting = purple_account_settings_find_setting(settings, id, NULL);

	if(PURPLE_IS_ACCOUNT_SETTING_BOOLEAN(setting)) {
		purple_account_setting_boolean_set_value(PURPLE_ACCOUNT_SETTING_BOOLEAN(setting),
		                                         value);
	}
}

void
purple_account_settings_set_int(PurpleAccountSettings *settings,
                                const char *id, int value)
{
	PurpleAccountSetting *setting = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings));
	g_return_if_fail(!birb_str_is_empty(id));

	setting = purple_account_settings_find_setting(settings, id, NULL);

	if(PURPLE_IS_ACCOUNT_SETTING_INT(setting)) {
		purple_account_setting_int_set_value(PURPLE_ACCOUNT_SETTING_INT(setting),
		                                     value);
	}
}

void
purple_account_settings_set_string(PurpleAccountSettings *settings,
                                   const char *id, const char *value)
{
	PurpleAccountSetting *setting = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings));
	g_return_if_fail(!birb_str_is_empty(id));

	setting = purple_account_settings_find_setting(settings, id, NULL);

	if(PURPLE_IS_ACCOUNT_SETTING_STRING(setting)) {
		purple_account_setting_string_set_value(PURPLE_ACCOUNT_SETTING_STRING(setting),
		                                        value);
	}
}

void
purple_account_settings_set_string_list(PurpleAccountSettings *settings,
                                        const char *id, const char *item_id)
{
	PurpleAccountSetting *setting = NULL;

	g_return_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings));
	g_return_if_fail(!birb_str_is_empty(id));

	setting = purple_account_settings_find_setting(settings, id, NULL);

	if(PURPLE_IS_ACCOUNT_SETTING_STRING_LIST(setting)) {
		purple_account_setting_string_list_set_active_item(PURPLE_ACCOUNT_SETTING_STRING_LIST(setting),
		                                                   item_id);
	}
}

void
purple_account_settings_update_settings(PurpleAccountSettings *settings,
                                        PurpleAccountSettings *updates)
{
	g_return_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(settings));
	g_return_if_fail(PURPLE_IS_ACCOUNT_SETTINGS(updates));

	for(guint i = 0; i < updates->settings->len; i++) {
		PurpleAccountSetting *update = NULL;
		PurpleAccountSetting *found = NULL;
		const char *id = NULL;

		update = g_ptr_array_index(updates->settings, i);
		id = purple_account_setting_get_id(update);

		found = purple_account_settings_find_setting(settings, id, NULL);
		if(!PURPLE_IS_ACCOUNT_SETTING(found)) {
			/* add_setting takes ownership, so we have to give it a fresh
			 * reference.
			 */
			purple_account_settings_add_setting(settings,
			                                    g_object_ref(update));

			continue;
		}

		if(G_OBJECT_TYPE(found) != G_OBJECT_TYPE(update)) {
			continue;
		}

		if(PURPLE_IS_ACCOUNT_SETTING_BOOLEAN(update)) {
			gboolean value = FALSE;

			value = purple_account_setting_boolean_get_value(PURPLE_ACCOUNT_SETTING_BOOLEAN(update));
			purple_account_setting_boolean_set_value(PURPLE_ACCOUNT_SETTING_BOOLEAN(found),
			                                         value);
		} else if(PURPLE_IS_ACCOUNT_SETTING_INT(update)) {
			int value = 0;

			value = purple_account_setting_int_get_value(PURPLE_ACCOUNT_SETTING_INT(update));
			purple_account_setting_int_set_value(PURPLE_ACCOUNT_SETTING_INT(found),
			                                     value);
		} else if(PURPLE_IS_ACCOUNT_SETTING_STRING(update)) {
			const char *value = NULL;

			value = purple_account_setting_string_get_value(PURPLE_ACCOUNT_SETTING_STRING(update));
			purple_account_setting_string_set_value(PURPLE_ACCOUNT_SETTING_STRING(found),
			                                        value);
		} else if(PURPLE_IS_ACCOUNT_SETTING_STRING_LIST(update)) {
			const char *active = NULL;

			active = purple_account_setting_string_list_get_active_item(PURPLE_ACCOUNT_SETTING_STRING_LIST(update));
			purple_account_setting_string_list_set_active_item(PURPLE_ACCOUNT_SETTING_STRING_LIST(found),
			                                                   active);
		} else {
			g_warning("unsupported type '%s'", G_OBJECT_TYPE_NAME(update));
		}
	}

	g_signal_emit(settings, signals[SIG_SETTINGS_UPDATED], 0);
}
