/*
 * Purple - XMPP Service Disco Browser
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Pidgin 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 program 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
 */

/* TODO list (a little bit of a brain dump):
	* Support more actions than "register" and "add" based on context.
		- Subscribe to pubsub nodes (just...because?)
		- Execute ad-hoc commands
		- Change 'Register' to 'Unregister' if we're registered?
		- Administer MUCs
	* Enumerate pubsub node contents.
		- PEP too? (useful development tool at times)
	* See if we can better handle the ad-hoc commands that ejabberd returns
	  when disco'ing a server as an administrator:
from disco#items:
	<item jid='darkrain42.org' node='announce' name='Announcements'/>
disco#info:
	<iq from='darkrain42.org' type='result'>
		<query xmlns='http://jabber.org/protocol/disco#info' node='announce'/>
	</iq>
	* For services that are a JID w/o a node, handle fetching ad-hoc commands?
*/

#include <glib/gi18n-lib.h>

#include <purple.h>

#include <pidgin.h>

#include "xmppdisco.h"
#include "gtkdisco.h"

/* Variables */
PurplePlugin *my_plugin = NULL;
static GHashTable *iq_callbacks = NULL;
static gboolean iq_listening = FALSE;

typedef void (*XmppIqCallback)(PurpleConnection *pc, const char *type,
                               const char *id, const char *from, PurpleXmlNode *iq,
                               gpointer data);

struct item_data {
	PidginDiscoList *list;
	XmppDiscoService *parent;
	char *name;
	char *node; /* disco#info replies don't always include the node */
};

struct xmpp_iq_cb_data
{
	/*
	 * Every IQ callback in this plugin uses the same structure for the
	 * callback data. It's a hack (it wouldn't scale), but it's used so that
	 * it's easy to clean up all the callbacks when the account disconnects
	 * (see remove_iq_callbacks_by_pc below).
	 */
	struct item_data *context;
	PurpleConnection *pc;
	XmppIqCallback cb;
};


static char *
generate_next_id(void)
{
	static guint32 index = 0;

	if (index == 0) {
		do {
			index = g_random_int();
		} while (index == 0);
	}

	return g_strdup_printf("purpledisco%x", index++);
}

static gboolean
remove_iq_callbacks_by_pc(G_GNUC_UNUSED gpointer key, gpointer value,
                          gpointer user_data)
{
	struct xmpp_iq_cb_data *cb_data = value;

	if (cb_data && cb_data->pc == user_data) {
		struct item_data *item_data = cb_data->context;

		if (item_data) {
			pidgin_disco_list_unref(item_data->list);
			g_free(item_data->name);
			g_free(item_data->node);
			g_free(item_data);
		}

		return TRUE;
	} else
		return FALSE;
}

static gboolean
xmpp_iq_received(PurpleConnection *pc, const char *type, const char *id,
                 const char *from, PurpleXmlNode *iq)
{
	struct xmpp_iq_cb_data *cb_data;

	cb_data = g_hash_table_lookup(iq_callbacks, id);
	if (!cb_data) {
		return FALSE;
	}

	cb_data->cb(cb_data->pc, type, id, from, iq, cb_data->context);

	g_hash_table_remove(iq_callbacks, id);
	if (g_hash_table_size(iq_callbacks) == 0) {
		PurpleProtocol *protocol = purple_connection_get_protocol(pc);
		iq_listening = FALSE;
		purple_signal_disconnect(protocol, "jabber-receiving-iq", my_plugin,
		                         G_CALLBACK(xmpp_iq_received));
	}

	/* Om nom nom nom */
	return TRUE;
}

static void
xmpp_iq_register_callback(PurpleConnection *pc, gchar *id, gpointer data,
                          XmppIqCallback cb)
{
	struct xmpp_iq_cb_data *cbdata = g_new0(struct xmpp_iq_cb_data, 1);

	cbdata->context = data;
	cbdata->cb = cb;
	cbdata->pc = pc;

	g_hash_table_insert(iq_callbacks, id, cbdata);

	if (!iq_listening) {
		PurpleProtocol *protocol = NULL;
		PurpleProtocolManager *manager = purple_protocol_manager_get_default();
		protocol = purple_protocol_manager_find(manager, XMPP_PROTOCOL_ID);

		iq_listening = TRUE;
		purple_signal_connect(protocol, "jabber-receiving-iq", my_plugin,
		                      G_CALLBACK(xmpp_iq_received), NULL);
	}
}

static void
xmpp_disco_info_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
                   const char *node, XmppIqCallback cb)
{
	PurpleXmlNode *iq, *query;
	char *id = generate_next_id();

	iq = purple_xmlnode_new("iq");
	purple_xmlnode_set_attrib(iq, "type", "get");
	purple_xmlnode_set_attrib(iq, "to", jid);
	purple_xmlnode_set_attrib(iq, "id", id);

	query = purple_xmlnode_new_child(iq, "query");
	purple_xmlnode_set_namespace(query, NS_DISCO_INFO);
	if (node) {
		purple_xmlnode_set_attrib(query, "node", node);
	}

	/* Steals id */
	xmpp_iq_register_callback(pc, id, cbdata, cb);

	purple_signal_emit(purple_connection_get_protocol(pc), "jabber-sending-xmlnode",
	                   pc, &iq);
	if (iq != NULL) {
		purple_xmlnode_free(iq);
	}
}

static void
xmpp_disco_items_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
                    const char *node, XmppIqCallback cb)
{
	PurpleXmlNode *iq, *query;
	char *id = generate_next_id();

	iq = purple_xmlnode_new("iq");
	purple_xmlnode_set_attrib(iq, "type", "get");
	purple_xmlnode_set_attrib(iq, "to", jid);
	purple_xmlnode_set_attrib(iq, "id", id);

	query = purple_xmlnode_new_child(iq, "query");
	purple_xmlnode_set_namespace(query, NS_DISCO_ITEMS);
	if (node) {
		purple_xmlnode_set_attrib(query, "node", node);
	}

	/* Steals id */
	xmpp_iq_register_callback(pc, id, cbdata, cb);

	purple_signal_emit(purple_connection_get_protocol(pc), "jabber-sending-xmlnode",
	                   pc, &iq);
	if (iq != NULL) {
		purple_xmlnode_free(iq);
	}
}

static XmppDiscoServiceType
disco_service_type_from_identity(PurpleXmlNode *identity)
{
	const char *category, *type;

	if (!identity) {
		return XMPP_DISCO_SERVICE_TYPE_OTHER;
	}

	category = purple_xmlnode_get_attrib(identity, "category");
	type = purple_xmlnode_get_attrib(identity, "type");

	if (!category) {
		return XMPP_DISCO_SERVICE_TYPE_OTHER;
	}

	if (purple_strequal(category, "conference")) {
		return XMPP_DISCO_SERVICE_TYPE_CHAT;
	} else if (purple_strequal(category, "directory")) {
		return XMPP_DISCO_SERVICE_TYPE_DIRECTORY;
	} else if (purple_strequal(category, "gateway")) {
		return XMPP_DISCO_SERVICE_TYPE_GATEWAY;
	} else if (purple_strequal(category, "pubsub")) {
		if (!type || purple_strequal(type, "collection")) {
			return XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION;
		} else if (purple_strequal(type, "leaf")) {
			return XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF;
		} else if (purple_strequal(type, "service")) {
			return XMPP_DISCO_SERVICE_TYPE_OTHER;
		} else {
			purple_debug_warning("xmppdisco", "Unknown pubsub type '%s'\n", type);
			return XMPP_DISCO_SERVICE_TYPE_OTHER;
		}
	}

	return XMPP_DISCO_SERVICE_TYPE_OTHER;
}

static const struct {
	const char *from;
	const char *to;
} disco_type_mappings[] = {
	{ "gadu-gadu", "gadu-gadu" }, /* the protocol is gg, but list_icon returns "gadu-gadu" */
	{ "xmpp",      "jabber" }, /* jabber (mentioned in case the protocol is renamed so this line will match) */
	{ NULL,        NULL }
};

static const gchar *
disco_type_from_string(const gchar *str)
{
	int i = 0;

	g_return_val_if_fail(str != NULL, "");

	for ( ; disco_type_mappings[i].from; ++i) {
		if (!g_ascii_strcasecmp(str, disco_type_mappings[i].from)) {
			return disco_type_mappings[i].to;
		}
	}

	/* fallback to the string itself */
	return str;
}

static void
got_info_cb(G_GNUC_UNUSED PurpleConnection *pc, const char *type,
            G_GNUC_UNUSED const char *id, const char *from, PurpleXmlNode *iq,
            gpointer data)
{
	struct item_data *item_data = data;
	PidginDiscoList *list = item_data->list;
	PurpleXmlNode *query;

	--list->fetch_count;

	if (!list->in_progress) {
		goto out;
	}

	if (purple_strequal(type, "result") &&
			(query = purple_xmlnode_get_child(iq, "query"))) {
		PurpleXmlNode *identity = purple_xmlnode_get_child(query, "identity");
		XmppDiscoService *service = NULL;
		PurpleXmlNode *feature;

		service = xmpp_disco_service_new(item_data->list);
		purple_debug_info("xmppdisco", "parent for %s is %p", from, item_data->parent);
		xmpp_disco_service_set_service_type(service,
		                                    disco_service_type_from_identity(identity));

		if (item_data->node) {
			if (item_data->name) {
				xmpp_disco_service_set_name(service, item_data->name);
				g_clear_pointer(&item_data->name, g_free);
			} else {
				xmpp_disco_service_set_name(service, item_data->node);
			}

			xmpp_disco_service_set_node(service, item_data->node);
			g_clear_pointer(&item_data->node, g_free);

			if(xmpp_disco_service_get_service_type(service) == XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION) {
				xmpp_disco_service_add_flags(service, XMPP_DISCO_BROWSE);
			}
		} else {
			xmpp_disco_service_set_name(service, from);
		}

		if(!xmpp_disco_service_get_node(service)) {
			/* Only support adding JIDs, not JID+node combos */
			xmpp_disco_service_add_flags(service, XMPP_DISCO_ADD);
		}

		if (item_data->name) {
			xmpp_disco_service_set_description(service, item_data->name);
			g_clear_pointer(&item_data->name, g_free);
		} else if (identity) {
			xmpp_disco_service_set_description(service,
			                                   purple_xmlnode_get_attrib(identity, "name"));
		}

		/* TODO: Overlap with service->name a bit */
		xmpp_disco_service_set_jid(service, from);

		for (feature = purple_xmlnode_get_child(query, "feature"); feature;
				feature = purple_xmlnode_get_next_twin(feature)) {
			const char *var;
			if (!(var = purple_xmlnode_get_attrib(feature, "var"))) {
				continue;
			}

			if (purple_strequal(var, NS_REGISTER)) {
				xmpp_disco_service_add_flags(service, XMPP_DISCO_REGISTER);
			} else if (purple_strequal(var, NS_DISCO_ITEMS)) {
				xmpp_disco_service_add_flags(service, XMPP_DISCO_BROWSE);
			} else if (purple_strequal(var, NS_MUC)) {
				xmpp_disco_service_add_flags(service, XMPP_DISCO_BROWSE);
				xmpp_disco_service_set_service_type(service,
				                                    XMPP_DISCO_SERVICE_TYPE_CHAT);
			}
		}

		if(xmpp_disco_service_get_service_type(service) == XMPP_DISCO_SERVICE_TYPE_GATEWAY) {
			xmpp_disco_service_set_gateway_type(service,
				disco_type_from_string(
					purple_xmlnode_get_attrib(identity, "type")));
		}

		pidgin_disco_add_service(list, service, item_data->parent);
	}

out:
	if (list->fetch_count == 0) {
		pidgin_disco_list_set_in_progress(list, FALSE);
	}

	g_free(item_data->name);
	g_free(item_data->node);
	g_free(item_data);
	pidgin_disco_list_unref(list);
}

static void
got_items_cb(PurpleConnection *pc, const char *type,
             G_GNUC_UNUSED const char *id, G_GNUC_UNUSED const char *from,
             PurpleXmlNode *iq, gpointer data)
{
	struct item_data *item_data = data;
	PidginDiscoList *list = item_data->list;
	PurpleXmlNode *query;
	gboolean has_items = FALSE;

	--list->fetch_count;

	if (!list->in_progress) {
		goto out;
	}

	if (purple_strequal(type, "result") &&
			(query = purple_xmlnode_get_child(iq, "query"))) {
		PurpleXmlNode *item;

		for (item = purple_xmlnode_get_child(query, "item"); item;
				item = purple_xmlnode_get_next_twin(item)) {
			const char *jid = purple_xmlnode_get_attrib(item, "jid");
			const char *name = purple_xmlnode_get_attrib(item, "name");
			const char *node = purple_xmlnode_get_attrib(item, "node");

			has_items = TRUE;

			if(xmpp_disco_service_get_service_type(item_data->parent) == XMPP_DISCO_SERVICE_TYPE_CHAT) {
				/* This is a hacky first-order approximation. Any MUC
				 * component that has a >1 level hierarchy (a Yahoo MUC
				 * transport component probably does) will violate this.
				 *
				 * On the other hand, this is better than querying all the
				 * chats at conference.jabber.org to enumerate them.
				 */
				XmppDiscoService *service = xmpp_disco_service_new(item_data->list);
				xmpp_disco_service_set_flags(service, XMPP_DISCO_ADD);
				xmpp_disco_service_set_service_type(service,
				                                    XMPP_DISCO_SERVICE_TYPE_CHAT);

				xmpp_disco_service_set_name(service, jid);
				xmpp_disco_service_set_description(service, name);
				xmpp_disco_service_set_jid(service, jid);
				xmpp_disco_service_set_node(service, node);
				pidgin_disco_add_service(list, service, item_data->parent);
			} else {
				struct item_data *item_data2 = g_new0(struct item_data, 1);

				item_data2->list = item_data->list;
				item_data2->parent = item_data->parent;
				item_data2->name = g_strdup(name);
				item_data2->node = g_strdup(node);

				++list->fetch_count;
				pidgin_disco_list_ref(list);
				xmpp_disco_info_do(pc, item_data2, jid, node, got_info_cb);
			}
		}
	}

	if (!has_items) {
		pidgin_disco_add_service(list, NULL, item_data->parent);
	}

out:
	if (list->fetch_count == 0) {
		pidgin_disco_list_set_in_progress(list, FALSE);
	}

	g_free(item_data);
	pidgin_disco_list_unref(list);
}

static void
server_items_cb(PurpleConnection *pc, const char *type,
                G_GNUC_UNUSED const char *id,
                G_GNUC_UNUSED const char *from, PurpleXmlNode *iq,
                gpointer data)
{
	struct item_data *cb_data = data;
	PidginDiscoList *list = cb_data->list;
	PurpleXmlNode *query;

	g_free(cb_data);
	--list->fetch_count;

	if (purple_strequal(type, "result") &&
			(query = purple_xmlnode_get_child(iq, "query"))) {
		PurpleXmlNode *item;

		for (item = purple_xmlnode_get_child(query, "item"); item;
				item = purple_xmlnode_get_next_twin(item)) {
			const char *jid = purple_xmlnode_get_attrib(item, "jid");
			const char *name = purple_xmlnode_get_attrib(item, "name");
			const char *node = purple_xmlnode_get_attrib(item, "node");
			struct item_data *item_data;

			if (!jid) {
				continue;
			}

			item_data = g_new0(struct item_data, 1);
			item_data->list = list;
			item_data->name = g_strdup(name);
			item_data->node = g_strdup(node);

			++list->fetch_count;
			pidgin_disco_list_ref(list);
			xmpp_disco_info_do(pc, item_data, jid, node, got_info_cb);
		}
	}

	if (list->fetch_count == 0) {
		pidgin_disco_list_set_in_progress(list, FALSE);
	}

	pidgin_disco_list_unref(list);
}

static void
server_info_cb(PurpleConnection *pc, const char *type,
               G_GNUC_UNUSED const char *id,
               const char *from, PurpleXmlNode *iq, gpointer data)
{
	struct item_data *cb_data = data;
	PidginDiscoList *list = cb_data->list;
	PurpleXmlNode *query;
	PurpleXmlNode *error;
	gboolean items = FALSE;

	--list->fetch_count;

	if (purple_strequal(type, "result") &&
			(query = purple_xmlnode_get_child(iq, "query"))) {
		PurpleXmlNode *feature;

		for (feature = purple_xmlnode_get_child(query, "feature"); feature;
				feature = purple_xmlnode_get_next_twin(feature)) {
			const char *var = purple_xmlnode_get_attrib(feature, "var");
			if (purple_strequal(var, NS_DISCO_ITEMS)) {
				items = TRUE;
				break;
			}
		}

		if (items) {
			xmpp_disco_items_do(pc, cb_data, from, NULL /* node */, server_items_cb);
			++list->fetch_count;
			pidgin_disco_list_ref(list);
		}
		else {
			pidgin_disco_list_set_in_progress(list, FALSE);
			g_free(cb_data);
		}
	}
	else {
		error = purple_xmlnode_get_child(iq, "error");
		if (purple_xmlnode_get_child(error, "remote-server-not-found")
		 || purple_xmlnode_get_child(error, "jid-malformed")) {
			purple_notify_error(my_plugin, _("Error"),
			                    _("Server does not exist"),
 			                    NULL, NULL);
		} else {
			purple_notify_error(my_plugin, _("Error"),
			                    _("Server does not support service discovery"),
			                    NULL, NULL);
		}
		pidgin_disco_list_set_in_progress(list, FALSE);
		g_free(cb_data);
	}

	pidgin_disco_list_unref(list);
}

void xmpp_disco_start(PidginDiscoList *list)
{
	struct item_data *cb_data;

	g_return_if_fail(list != NULL);

	++list->fetch_count;
	pidgin_disco_list_ref(list);

	cb_data = g_new0(struct item_data, 1);
	cb_data->list = list;

	xmpp_disco_info_do(list->pc, cb_data, list->server, NULL, server_info_cb);
}

void xmpp_disco_service_expand(XmppDiscoService *service)
{
	PidginDiscoList *list = NULL;
	struct item_data *item_data;

	g_return_if_fail(XMPP_DISCO_IS_SERVICE(service));

	if(xmpp_disco_service_get_expanded(service)) {
		return;
	}

	list = xmpp_disco_service_get_list(service);

	item_data = g_new0(struct item_data, 1);
	item_data->list = list;
	item_data->parent = service;

	++list->fetch_count;
	pidgin_disco_list_ref(list);

	pidgin_disco_list_set_in_progress(list, TRUE);

	xmpp_disco_items_do(list->pc, item_data,
	                    xmpp_disco_service_get_jid(service),
	                    xmpp_disco_service_get_node(service),
	                    got_items_cb);
	xmpp_disco_service_set_expanded(service, TRUE);
}

void
xmpp_disco_register_service(XmppDiscoService *service)
{
	PidginDiscoList *list = NULL;
	PurpleXmlNode *iq, *query;
	char *id = generate_next_id();

	iq = purple_xmlnode_new("iq");
	purple_xmlnode_set_attrib(iq, "type", "get");
	purple_xmlnode_set_attrib(iq, "to", xmpp_disco_service_get_jid(service));
	purple_xmlnode_set_attrib(iq, "id", id);

	query = purple_xmlnode_new_child(iq, "query");
	purple_xmlnode_set_namespace(query, NS_REGISTER);

	list = xmpp_disco_service_get_list(service);
	purple_signal_emit(purple_connection_get_protocol(list->pc),
	                   "jabber-sending-xmlnode", list->pc, &iq);
	if (iq != NULL) {
		purple_xmlnode_free(iq);
	}
	g_free(id);
}

static void
create_dialog(G_GNUC_UNUSED GSimpleAction *action,
              G_GNUC_UNUSED GVariant *parameter,
              G_GNUC_UNUSED gpointer data)
{
	pidgin_disco_dialog_new();
}

static void
signed_off_cb(PurpleConnection *pc, G_GNUC_UNUSED gpointer data)
{
	/* Deal with any dialogs */
	pidgin_disco_signed_off_cb(pc);

	/* Remove all the IQ callbacks for this connection */
	g_hash_table_foreach_remove(iq_callbacks, remove_iq_callbacks_by_pc, pc);
}

static GPluginPluginInfo *
xmpp_disco_query(G_GNUC_UNUSED GError **error)
{
	GActionEntry entries[] = {
		{
			.name = "dialog",
			.activate = create_dialog,
		}
	};
	GMenu *menu = NULL;
	GSimpleActionGroup *group = NULL;
	const gchar * const authors[] = {
		"Paul Aurich <paul@darkrain42.org>",
		NULL
	};

	group = g_simple_action_group_new();
	g_action_map_add_action_entries(G_ACTION_MAP(group), entries,
	                                G_N_ELEMENTS(entries), NULL);

	menu = g_menu_new();
	g_menu_append(menu, _("XMPP Service Discovery"), "dialog");

	return purple_plugin_info_new(
		"id",           PLUGIN_ID,
		"name",         N_("XMPP Service Discovery"),
		"version",      DISPLAY_VERSION,
		"category",     N_("Protocol utility"),
		"summary",      N_("Allows browsing and registering services."),
		"description",  N_("This plugin is useful for registering with legacy "
		                   "transports or other XMPP services."),
		"authors",      authors,
		"website",      PURPLE_WEBSITE,
		"abi-version",  PURPLE_ABI_VERSION,
		"action-group", group,
		"action-menu",  menu,
		NULL
	);
}

static gboolean
xmpp_disco_load(GPluginPlugin *plugin, GError **error)
{
	PurpleProtocol *xmpp_protocol;
	PurpleProtocolManager *manager;

	my_plugin = plugin;

	manager = purple_protocol_manager_get_default();
	xmpp_protocol = purple_protocol_manager_find(manager, XMPP_PROTOCOL_ID);
	if (NULL == xmpp_protocol) {
		g_set_error_literal(error, PLUGIN_DOMAIN, 0,
		                    _("XMPP protocol is not loaded."));
		return FALSE;
	}

	xmpp_disco_service_register(plugin);
	pidgin_disco_dialog_register(plugin);

	purple_signal_connect(purple_connections_get_handle(), "signing-off",
	                      plugin, G_CALLBACK(signed_off_cb), NULL);

	iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);

	return TRUE;
}

static gboolean
xmpp_disco_unload(GPluginPlugin *plugin, G_GNUC_UNUSED gboolean unload,
                  G_GNUC_UNUSED GError **error)
{
	g_clear_pointer(&iq_callbacks, g_hash_table_destroy);

	purple_signals_disconnect_by_handle(plugin);
	pidgin_disco_dialogs_destroy_all();

	return TRUE;
}

GPLUGIN_NATIVE_PLUGIN_DECLARE(xmpp_disco)
