/*
 * 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 <libsoup/soup.h>

#include <birb.h>

#include "purplezulipeventqueue.h"

#include "purplezulipconstants.h"
#include "purplezulipevents.h"
#include "purplezuliptypes.h"

struct _PurpleZulipEventQueue {
	GObject parent;

	GCancellable *cancellable;

	PurpleZulipConnection *connection;
	PurpleZulipClient *client;

	gboolean reconnect;

	char *queue_id;
	int last_event_id;
};

enum {
	PROP_0,
	PROP_CANCELLABLE,
	PROP_CLIENT,
	PROP_CONNECTION,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

#define PURPLE_ZULIP_EVENT_QUEUE_ERROR (g_quark_from_static_string("purple-zulip-event-queue"))

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
purple_zulip_event_queue_set_cancellable(PurpleZulipEventQueue *event_queue,
                                         GCancellable *cancellable)
{
	g_return_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue));

	if(g_set_object(&event_queue->cancellable, cancellable)) {
		g_object_notify_by_pspec(G_OBJECT(event_queue),
		                         properties[PROP_CANCELLABLE]);
	}
}

static void
purple_zulip_event_queue_set_client(PurpleZulipEventQueue *event_queue,
                                    PurpleZulipClient *client)
{
	g_return_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue));

	if(g_set_object(&event_queue->client, client)) {
		g_object_notify_by_pspec(G_OBJECT(event_queue),
		                         properties[PROP_CLIENT]);
	}
}

static void
purple_zulip_event_queue_set_connection(PurpleZulipEventQueue *event_queue,
                                        PurpleZulipConnection *connection)
{
	g_return_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue));

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

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
purple_zulip_event_queue_get_events_cb(GObject *source, GAsyncResult *result,
                                       gpointer data)
{
	PurpleZulipClient *client = PURPLE_ZULIP_CLIENT(source);
	PurpleZulipGetEventsResponse *response = NULL;
	PurpleZulipEventQueue *event_queue = data;
	GError *error = NULL;

	response = purple_zulip_client_get_events_finish(client, result, &error);
	if(error != NULL) {
		g_warning("failed to get events: %s", error->message);

		g_clear_error(&error);
	} else {
		while(response->events != NULL) {
			PurpleZulipEvent *event = response->events->data;

			/* This is sorted to optimize away from unnecessary string
			 * comparisons.
			 */
			if(birb_str_equal(event->type, PURPLE_ZULIP_EVENT_MESSAGE)) {
				PurpleZulipMessage *message = event->message;

				g_message("message: %p", message);
				g_message("from %d to %d: %s", message->sender_id,
				          message->recipient_id, message->content);
			} else if(birb_str_equal(event->type, PURPLE_ZULIP_EVENT_HEARTBEAT)) {
				g_message("received heartbeat message");
			} else {
				g_message("unhandled '%s' event with id %d", event->type, event->id);
			}

			event_queue->last_event_id = event->id;

			/* Free the event and remove it from the start of the list. */
			purple_zulip_event_free(event);
			response->events = g_list_delete_link(response->events,
			                                      response->events);
		}

		/* Delete the rest of the response. */
		purple_zulip_get_events_response_free(response);
	}

	if(event_queue->reconnect) {
		purple_zulip_client_get_events_async(client, event_queue->queue_id,
		                                     event_queue->last_event_id,
		                                     purple_zulip_event_queue_get_events_cb,
		                                     g_object_ref(event_queue));
	}

	/* We pass the callback, this function, a reference to make sure that the
	 * event queue persists between calls. This unref is purposely after the
	 * async call to make sure the pointer is still valid when adding the ref.
	 */
	g_clear_object(&event_queue);
}

static void
purple_zulip_event_queue_register_cb(GObject *source, GAsyncResult *result,
                                     gpointer data)
{
	PurpleZulipClient *client = PURPLE_ZULIP_CLIENT(source);
	PurpleZulipEventQueue *event_queue = data;
	PurpleZulipRegisterEventQueueResponse *response = NULL;
	PurpleAccount *account = NULL;
	PurpleCore *core = NULL;
	PurpleContactInfo *info = NULL;
	PurpleContactManager *contact_manager = NULL;
	GError *error = NULL;
	char *id = NULL;

	response = purple_zulip_client_register_event_queue_finish(client, result,
	                                                           &error);
	if(error != NULL) {
		g_warning("failed to register event queue: %s", error->message);

		g_clear_error(&error);

		g_clear_object(&event_queue);

		return;
	}

	core = purple_core_get_default();
	account = purple_zulip_client_get_account(client);

	g_set_str(&event_queue->queue_id, response->queue_id);
	event_queue->last_event_id = response->last_event_id;

	/* Update the contact info for the account. */
	info = purple_account_get_contact_info(account);
	purple_contact_info_set_display_name(info, response->full_name);
	purple_contact_info_set_email(info, response->email);

	id = g_strdup_printf("%" G_GINT64_FORMAT, response->user_id);
	purple_contact_info_set_id(info, id);
	g_clear_pointer(&id, g_free);

	/* Add the users. */
	contact_manager = purple_core_get_contact_manager(core);
	for(guint i = 0; i < response->realm_users->len; i++) {
		PurpleZulipUser *user = NULL;
		PurpleContact *contact = NULL;

		user = g_ptr_array_index(response->realm_users, i);
		contact = purple_zulip_user_to_contact(user, account);

		purple_contact_manager_add(contact_manager, contact);
		g_clear_object(&contact);
	}

	/* Cleanup. */
	purple_zulip_register_event_queue_response_free(response);

	g_message("registered event queue: %s", event_queue->queue_id);

	event_queue->reconnect = TRUE;

	purple_zulip_client_get_events_async(client, event_queue->queue_id,
	                                     event_queue->last_event_id,
	                                     purple_zulip_event_queue_get_events_cb,
	                                     g_object_ref(event_queue));

	/* We don't pass our reference to the get_events_cb above to keep all of
	 * the async method calls consistent.
	 */
	g_clear_object(&event_queue);
}

static void
purple_zulip_event_queue_disconnect_cb(GObject *source, GAsyncResult *result,
                                       gpointer data)
{
	PurpleZulipClient *client = PURPLE_ZULIP_CLIENT(source);
	PurpleZulipEventQueue *event_queue = data;
	GError *error = NULL;
	gboolean success = FALSE;

	success = purple_zulip_client_delete_event_queue_finish(client, result,
	                                                        &error);
	if(error != NULL) {
		g_warning("failed to delete event queue %s: %s", event_queue->queue_id,
		          error->message);

		g_clear_error(&error);
	} else if(!success) {
		g_warning("failed to delete event queue %s: unknown error",
		          event_queue->queue_id);
	} else {
		g_message("deleted event queue %s", event_queue->queue_id);
	}

	event_queue->reconnect = FALSE;

	g_clear_pointer(&event_queue->queue_id, g_free);
	g_object_unref(event_queue);
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleZulipEventQueue, purple_zulip_event_queue,
                               G_TYPE_OBJECT, G_TYPE_FLAG_FINAL, {})

static void
purple_zulip_event_queue_finalize(GObject *obj) {
	PurpleZulipEventQueue *event_queue = PURPLE_ZULIP_EVENT_QUEUE(obj);

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

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

static void
purple_zulip_event_queue_get_property(GObject *obj, guint param_id,
                                      GValue *value, GParamSpec *pspec)
{
	PurpleZulipEventQueue *event_queue = PURPLE_ZULIP_EVENT_QUEUE(obj);

	switch(param_id) {
	case PROP_CANCELLABLE:
		g_value_set_object(value,
		                   purple_zulip_event_queue_get_cancellable(event_queue));
		break;
	case PROP_CLIENT:
		g_value_set_object(value,
		                   purple_zulip_event_queue_get_client(event_queue));
		break;
	case PROP_CONNECTION:
		g_value_set_object(value,
		                   purple_zulip_event_queue_get_connection(event_queue));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_zulip_event_queue_set_property(GObject *obj, guint param_id,
                                      const GValue *value, GParamSpec *pspec)
{
	PurpleZulipEventQueue *event_queue = PURPLE_ZULIP_EVENT_QUEUE(obj);

	switch(param_id) {
	case PROP_CANCELLABLE:
		purple_zulip_event_queue_set_cancellable(event_queue,
		                                         g_value_get_object(value));
		break;
	case PROP_CLIENT:
		purple_zulip_event_queue_set_client(event_queue,
		                                    g_value_get_object(value));
		break;
	case PROP_CONNECTION:
		purple_zulip_event_queue_set_connection(event_queue,
		                                        g_value_get_object(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_zulip_event_queue_init(G_GNUC_UNUSED PurpleZulipEventQueue *event_queue)
{
}

static void
purple_zulip_event_queue_class_finalize(G_GNUC_UNUSED PurpleZulipEventQueueClass *klass)
{
}

static void
purple_zulip_event_queue_class_init(PurpleZulipEventQueueClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = purple_zulip_event_queue_finalize;
	obj_class->get_property = purple_zulip_event_queue_get_property;
	obj_class->set_property = purple_zulip_event_queue_set_property;

	properties[PROP_CANCELLABLE] = g_param_spec_object(
		"cancellable", NULL, NULL,
		G_TYPE_CANCELLABLE,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
		G_PARAM_EXPLICIT_NOTIFY);

	properties[PROP_CLIENT] = g_param_spec_object(
		"client", NULL, NULL,
		PURPLE_ZULIP_TYPE_CLIENT,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
		G_PARAM_EXPLICIT_NOTIFY);

	properties[PROP_CONNECTION] = g_param_spec_object(
		"connection", NULL, NULL,
		PURPLE_ZULIP_TYPE_CONNECTION,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
		G_PARAM_EXPLICIT_NOTIFY);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * API
 *****************************************************************************/
void
purple_zulip_event_queue_connect(PurpleZulipEventQueue *event_queue) {
	g_return_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue));

	purple_zulip_client_register_event_queue_async(event_queue->client,
	                                               purple_zulip_event_queue_register_cb,
	                                               g_object_ref(event_queue));
}

void
purple_zulip_event_queue_disconnect(PurpleZulipEventQueue *event_queue) {
	g_return_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue));

	if(birb_str_is_empty(event_queue->queue_id)) {
		return;
	}

	purple_zulip_client_delete_event_queue_async(event_queue->client,
	                                             event_queue->queue_id,
	                                             purple_zulip_event_queue_disconnect_cb,
	                                             g_object_ref(event_queue));
}

GCancellable *
purple_zulip_event_queue_get_cancellable(PurpleZulipEventQueue *event_queue) {
	g_return_val_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue), NULL);

	return event_queue->cancellable;
}

PurpleZulipClient *
purple_zulip_event_queue_get_client(PurpleZulipEventQueue *event_queue) {
	g_return_val_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue), NULL);

	return event_queue->client;
}

PurpleZulipConnection *
purple_zulip_event_queue_get_connection(PurpleZulipEventQueue *event_queue) {
	g_return_val_if_fail(PURPLE_ZULIP_IS_EVENT_QUEUE(event_queue), NULL);

	return event_queue->connection;
}

PurpleZulipEventQueue *
purple_zulip_event_queue_new(PurpleZulipConnection *connection,
                             PurpleZulipClient *client,
                             GCancellable *cancellable)
{
	return g_object_new(
		PURPLE_ZULIP_TYPE_EVENT_QUEUE,
		"cancellable", cancellable,
		"client", client,
		"connection", connection,
		NULL);
}

void
purple_zulip_event_queue_register(GPluginNativePlugin *plugin) {
	purple_zulip_event_queue_register_type(G_TYPE_MODULE(plugin));
}
