/*
 * Purple Zulip - Zulip Protocol Plugin
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple Zulip 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 plugin 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 plugin 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 plugin; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include "purplezulipconnection.h"

#include "purplezulipconstants.h"
#include "purplezulipeventqueue.h"

struct _PurpleZulipConnection {
	PurpleConnection parent;

	PurpleZulipClient *client;
	PurpleZulipEventQueue *event_queue;
};

#define PURPLE_ZULIP_CONNECTION_ERROR (g_quark_from_static_string("purple-zulip-connection"))

/******************************************************************************
 * Helpers
 *****************************************************************************/
static int
purple_zulip_connection_sort_user_ids(gconstpointer a, gconstpointer b) {
	gint64 *id_a = (gpointer)a;
	gint64 *id_b = (gpointer)b;

	return (*id_a) - (*id_b);
}

/******************************************************************************
 * PurpleConnection Implementation
 *****************************************************************************/
static void
purple_zulip_connection_login_cb(GObject *source,
                                 GAsyncResult *result,
                                 gpointer data)
{
	PurpleZulipConnection *connection = NULL;
	PurpleAccount *account = NULL;
	GError *error = NULL;
	GTask *task = data;
	gboolean success = FALSE;

	connection = g_task_get_task_data(task);
	account = purple_connection_get_account(PURPLE_CONNECTION(connection));

	success = purple_zulip_client_login_finish(PURPLE_ZULIP_CLIENT(source),
	                                           result, &error);
	if(error != NULL) {
		g_task_return_error(task, error);
	} else if(!success) {
		g_task_return_new_error_literal(task, PURPLE_ZULIP_CONNECTION_ERROR, 0,
		                                _("unknown error"));
	} else {
		purple_zulip_event_queue_connect(connection->event_queue);

		purple_account_ready(account);

		g_task_return_boolean(task, TRUE);
	}

	g_clear_object(&task);
}

static void
purple_zulip_connection_connect_async(PurpleConnection *purple_connection,
                                      GCancellable *cancellable,
                                      GAsyncReadyCallback callback,
                                      gpointer data)
{
	PurpleZulipConnection *connection = NULL;
	PurpleAccount *account = NULL;
	PurpleAccountSettings *settings = NULL;
	GCancellable *connection_cancellable = NULL;
	GTask *task = NULL;
	const char *server_uri = NULL;

	task = g_task_new(purple_connection, cancellable, callback, data);
	g_task_set_task_data(task, purple_connection, NULL);

	connection = PURPLE_ZULIP_CONNECTION(purple_connection);

	account = purple_connection_get_account(purple_connection);
	connection_cancellable = purple_connection_get_cancellable(purple_connection);
	settings = purple_account_get_settings(account);

	/* The settings should already have been validated by
	 * purple_account_connect.
	 */
	server_uri = purple_account_settings_get_string(settings,
	                                                PURPLE_ZULIP_SETTING_SERVER_URI,
	                                                NULL);

	connection->client = purple_zulip_client_new(server_uri, cancellable);
	connection->event_queue = purple_zulip_event_queue_new(connection,
	                                                       connection->client,
	                                                       connection_cancellable);

	purple_zulip_client_login_async(connection->client, account,
	                                purple_zulip_connection_login_cb,
	                                task);
}

static gboolean
purple_zulip_connection_connect_finish(PurpleConnection *connection,
                                       GAsyncResult *result,
                                       GError **error)
{
	g_return_val_if_fail(PURPLE_ZULIP_IS_CONNECTION(connection), FALSE);
	g_return_val_if_fail(g_task_is_valid(result, connection), FALSE);

	return g_task_propagate_boolean(G_TASK(result), error);
}

static gboolean
purple_zulip_connection_disconnect(PurpleConnection *purple_connection,
                                   G_GNUC_UNUSED const char *message,
                                   G_GNUC_UNUSED GError **error)
{
	PurpleZulipConnection *connection = NULL;

	connection = PURPLE_ZULIP_CONNECTION(purple_connection);

	purple_zulip_event_queue_disconnect(connection->event_queue);

	g_clear_object(&connection->client);
	g_clear_object(&connection->event_queue);

	return TRUE;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_DYNAMIC_TYPE_EXTENDED(
	PurpleZulipConnection,
	purple_zulip_connection,
	PURPLE_TYPE_CONNECTION,
	G_TYPE_FLAG_FINAL,
	{})

static void
purple_zulip_connection_finalize(GObject *obj) {
	PurpleZulipConnection *connection = PURPLE_ZULIP_CONNECTION(obj);

	g_clear_object(&connection->client);
	g_clear_object(&connection->event_queue);

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

static void
purple_zulip_connection_init(G_GNUC_UNUSED PurpleZulipConnection *connection) {
}

static void
purple_zulip_connection_class_finalize(G_GNUC_UNUSED PurpleZulipConnectionClass *klass)
{
}

static void
purple_zulip_connection_class_init(PurpleZulipConnectionClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
	PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);

	obj_class->finalize = purple_zulip_connection_finalize;

	connection_class->connect_async = purple_zulip_connection_connect_async;
	connection_class->connect_finish = purple_zulip_connection_connect_finish;
	connection_class->disconnect = purple_zulip_connection_disconnect;
}

/******************************************************************************
 * Local Exports
 *****************************************************************************/
PurpleConversation *
purple_zulip_connection_find_or_create_dm(PurpleZulipConnection *connection,
                                          GArray *user_ids)
{
	PurpleAccount *account = NULL;
	PurpleContactInfo *account_info = NULL;
	PurpleCore *core = NULL;
	PurpleContactManager *contact_manager = NULL;
	PurpleConversation *conversation = NULL;
	PurpleConversationManager *conversation_manager = NULL;
	PurpleConversationMembers *members = NULL;
	PurpleConversationType conversation_type = PURPLE_CONVERSATION_TYPE_DM;
	GChecksum *checksum = NULL;
	const char *conversation_id = NULL;
	gint64 account_id = G_GINT64_CONSTANT(0);

	g_return_val_if_fail(PURPLE_ZULIP_IS_CONNECTION(connection), NULL);

	core = purple_core_get_default();
	conversation_manager = purple_core_get_conversation_manager(core);
	contact_manager = purple_core_get_contact_manager(core);

	/* Store the account id so we can ignore it when generating the
	 * conversation id.
	 */
	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	account_info = purple_account_get_contact_info(account);
	account_id = g_ascii_strtoll(purple_contact_info_get_id(account_info),
	                             NULL, 10);

	/* Sort the user list lowest to highest to get a consistent order. */
	g_array_sort(user_ids, purple_zulip_connection_sort_user_ids);

	/* Create a checksum to hash the ordered list. */
	checksum = g_checksum_new(G_CHECKSUM_SHA256);

	for(guint i = 0; i < user_ids->len; i++) {
		gint64 user_id = g_array_index(user_ids, gint64, i);

		/* We don't use the account id as part of the checksum. */
		if(user_id == account_id) {
			continue;
		}

		/* Make sure the user_id is always in little endian to make the
		 * checksum match across endianness.
		 */
		user_id = GUINT64_TO_LE(user_id);

		g_checksum_update(checksum, (gpointer)&user_id, sizeof(user_id));

		/* If we've seen more than one user that is not the account, then we're
		 * looking for a group dm instead of a dm.
		 */
		if(i > 1) {
			conversation_type = PURPLE_CONVERSATION_TYPE_GROUP_DM;
		}
	}

	conversation_id = g_checksum_get_string(checksum);

	/* Now lookup the conversation. */
	conversation = purple_conversation_manager_find(conversation_manager,
	                                                account,
	                                                conversation_type,
	                                                conversation_id);
	if(!PURPLE_IS_CONVERSATION(conversation)) {
		/* Create the conversation. */
		conversation = purple_conversation_new(account, conversation_type,
		                                       conversation_id);

		/* Add the conversation to the conversation manager and remove our
		 * reference as this is transfer none and the manager holds a
		 * reference.
		 */
		purple_conversation_manager_add(conversation_manager, conversation);
		g_object_unref(conversation);
	}

	/* We don't need the checksum anymore. */
	g_clear_pointer(&checksum, g_checksum_free);

	/* Synchronize the contacts. We do this in the event that a contact was
	 * lost from the back end.
	 */
	members = purple_conversation_get_members(conversation);
	for(guint i = 0; i < user_ids->len; i++) {
		PurpleContact *contact = NULL;
		char *contact_id = NULL;
		gint64 user_id = G_GINT64_CONSTANT(0);

		/* The account gets automatically added, so we can skip it here
		 * quickly.
		 */
		user_id = g_array_index(user_ids, gint64, i);
		if(user_id == account_id) {
			continue;
		}

		contact_id = g_strdup_printf("%" G_GINT64_FORMAT, user_id);

		contact = purple_contact_manager_find_or_create(contact_manager,
		                                                account,
		                                                contact_id,
		                                                NULL);
		purple_conversation_members_add_member(members,
		                                       PURPLE_CONTACT_INFO(contact),
		                                       FALSE,
		                                       NULL);
	}

	return conversation;
}

PurpleZulipClient *
purple_zulip_connection_get_client(PurpleZulipConnection *connection) {
	g_return_val_if_fail(PURPLE_ZULIP_IS_CONNECTION(connection), NULL);

	return connection->client;
}

void
purple_zulip_connection_register(GPluginNativePlugin *plugin) {
	purple_zulip_connection_register_type(G_TYPE_MODULE(plugin));
}
