/*
 * Ibis - IRCv3 Library
 * Copyright (C) 2022-2024 Ibis Developers
 *
 * Ibis 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 <hasl.h>

#include "ibiscapabilities.h"

#include "ibisconstants.h"
#include "ibiscore.h"
#include "ibissasl.h"
#include "ibisstring.h"

/* Windows is does something weird with signal handling that includes defining
 * SIG_ACK. We don't care about that here, so we undef it if we find it.
 * See https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-action-constants?view=msvc-170
 */
#ifdef SIG_ACK
# undef SIG_ACK
#endif /* SIG_ACK */

enum {
	SIG_READY,
	SIG_ACK,
	SIG_NAK,
	SIG_DONE,
	SIG_NEW,
	SIG_DEL,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

struct _IbisCapabilities {
	GObject parent;

	IbisClient *client;

	GHashTable *caps;
	GPtrArray *requests;

	gboolean negotiating;
	gatomicrefcount wait_counters;
};

static gboolean
ibis_capabilities_message_cb(IbisClient *client, const char *command,
                             IbisMessage *message, gpointer data);

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
ibis_capabilities_finish(IbisCapabilities *capabilities) {
	IbisMessage *message = NULL;

	message = ibis_message_new(IBIS_MSG_CAP);
	ibis_message_set_params(message, "END", NULL);
	ibis_client_write(capabilities->client, message);

	/* Set negotiating to false before emitting the signal in case anyone
	 * tries to add a wait in the signal handler.
	 */
	capabilities->negotiating = FALSE;

	g_signal_emit(capabilities, signals[SIG_DONE], 0);
}

static void
ibis_capabilities_add(IbisCapabilities *capabilities, const char *capability) {
	char *equals = g_strstr_len(capability, -1, "=");

	if(equals != NULL) {
		char *key = g_strndup(capability, equals - capability);
		char *value = g_strdup(equals + 1);

		g_hash_table_insert(capabilities->caps, key, value);
	} else {
		g_hash_table_insert(capabilities->caps, g_strdup(capability), NULL);
	}
}

/******************************************************************************
 * Command handlers
 *****************************************************************************/
static void
ibis_capabilities_handle_list(IbisCapabilities *capabilities, guint n_params,
                              GStrv params)
{
	gboolean done = TRUE;
	gchar **parts = NULL;

	/* Check if we have more messages coming. */
	if(n_params > 1 && ibis_str_equal(params[0], "*")) {
		parts = g_strsplit(params[1], " ", -1);
		done = FALSE;
	} else {
		parts = g_strsplit(params[0], " ", -1);
	}

	/* Add each capability to our hash table, splitting the keys and values. */
	for(int i = 0; parts[i] != NULL; i++) {
		ibis_capabilities_add(capabilities, parts[i]);
	}

	g_strfreev(parts);

	if(done) {
		g_signal_emit(capabilities, signals[SIG_READY], 0, signals[SIG_READY]);

		/* If no capabilities were requested after we emitted the ready signal
		 * we're done with capability negotiation.
		 */
		if(capabilities->requests->len == 0) {
			ibis_capabilities_remove_wait(capabilities);
		}
	}
}

static void
ibis_capabilities_handle_ack_nak(IbisCapabilities *capabilities, GStrv params,
                                 guint sig, const char *method, GError **error)
{
	char *caps = g_strjoinv(" ", params);
	guint index = 0;
	gboolean found = FALSE;

	g_signal_emit(capabilities, sig, g_quark_from_string(caps),
	              capabilities->client, caps);

	found = g_ptr_array_find_with_equal_func(capabilities->requests, caps,
	                                         g_str_equal, &index);
	if(found) {
		g_ptr_array_remove_index(capabilities->requests, index);
	} else {
		g_set_error(error, IBIS_DOMAIN, 0,
		            "received CAP %s for unknown capability %s", method, caps);
	}
	g_free(caps);

	if(capabilities->requests->len == 0) {
		ibis_capabilities_remove_wait(capabilities);
	}
}

static void
ibis_capabilities_handle_new(IbisCapabilities *capabilities, guint n_params,
                             GStrv params)
{
	for(guint i = 0; i < n_params; i++) {
		ibis_capabilities_add(capabilities, params[i]);
	}

	g_signal_emit(capabilities, signals[SIG_NEW], 0, capabilities->client,
	              params);
}

static void
ibis_capabilities_handle_del(IbisCapabilities *capabilities, guint n_params,
                             GStrv params)
{
	for(guint i = 0; i < n_params; i++) {
		g_hash_table_remove(capabilities->caps, params[i]);
	}

	g_signal_emit(capabilities, signals[SIG_DEL], 0, capabilities->client,
	              params);
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
ibis_capabilities_message_tags_ack_cb(IbisCapabilities *capabilities,
                                      G_GNUC_UNUSED IbisClient *client,
                                      G_GNUC_UNUSED const char *capability,
                                      G_GNUC_UNUSED gpointer data)
{
	/* The server-time capability just tells the server to send a tag on
	 * messages, so we just need to request it.
	 */
	ibis_capabilities_lookup_and_request(capabilities,
	                                     IBIS_CAPABILITY_SERVER_TIME);
}

static gboolean
ibis_capabilities_message_cb(IbisClient *client,
                             G_GNUC_UNUSED const char *command,
                             IbisMessage *message, gpointer data)
{
	IbisCapabilities *capabilities = data;
	GError *error = NULL;
	GStrv params = NULL;
	GStrv subparams = NULL;
	char *subcommand = NULL;
	guint n_params = 0;
	guint n_subparams = 0;

	params = ibis_message_get_params(message);
	if(params != NULL) {
		n_params = g_strv_length(params);
	}

	if(n_params < 2) {
		return FALSE;
	}

	capabilities = ibis_client_get_capabilities(client);

	/* Initialize some variables to make it easier to call our sub command
	 * handlers.
	 *
	 * params[0] is the nick or * if it hasn't been negotiated yet, we don't
	 * have a need for this, so we ignore it.
	 *
	 * params[1] is the CAP subcommand sent from the server. We use it here
	 * purely for dispatching to our subcommand handlers.
	 *
	 * params[2] and higher are the parameters to the subcommand. To make the
	 * code a bit easier all around, we subtract 2 from n_params to remove
	 * references to the nick and subcommand name. Like wise, we add 2 to the
	 * params GStrv which will now point to the second item in the array again
	 * ignoring the nick and subcommand.
	 */
	subcommand = g_utf8_strup(params[1], -1);
	n_subparams = n_params - 2;
	subparams = params + 2;

	/* Dispatch the subcommand. */
	if(ibis_str_equal(subcommand, "LS") || ibis_str_equal(subcommand, "LIST"))
	{
		ibis_capabilities_handle_list(capabilities, n_subparams, subparams);
	} else if(ibis_str_equal(subcommand, "ACK")) {
		ibis_capabilities_handle_ack_nak(capabilities, subparams,
		                                 signals[SIG_ACK], "ACK", &error);
	} else if(ibis_str_equal(subcommand, "NAK")) {
		ibis_capabilities_handle_ack_nak(capabilities, subparams,
		                                 signals[SIG_NAK], "NAK", &error);
	} else if(ibis_str_equal(subcommand, "NEW")) {
		ibis_capabilities_handle_new(capabilities, n_subparams, subparams);
	} else if(ibis_str_equal(subcommand, "DEL")) {
		ibis_capabilities_handle_del(capabilities, n_subparams, subparams);
	} else {
		error = g_error_new(IBIS_DOMAIN, 0,
		                    "No handler for CAP subcommand '%s'", subcommand);
	}

	if(error != NULL) {
		ibis_message_take_error(message, error);
	}

	g_clear_pointer(&subcommand, g_free);

	return TRUE;
}

/******************************************************************************
 * IbisCapabilities Implementation
 *****************************************************************************/
static void
ibis_capabilities_default_ready_cb(IbisCapabilities *capabilities) {
	HaslContext *hasl_context = NULL;

	/* If we have a hasl context, we need to request SASL. */
	hasl_context = ibis_client_get_hasl_context(capabilities->client);
	if(HASL_IS_CONTEXT(hasl_context)) {
		ibis_sasl_request(capabilities);
	}

	/* cap-notify is implied when we use CAP LS 302, so this is really just to
	 * make sure it's requested.
	 */
	ibis_capabilities_lookup_and_request(capabilities,
	                                     IBIS_CAPABILITY_CAP_NOTIFY);

	/* message-tags is used for a lot of stuff so we need to tell everyone we
	 * do in fact support it.
	 */
	if(ibis_capabilities_lookup_and_request(capabilities,
	                                        IBIS_CAPABILITY_MESSAGE_TAGS))
	{
		g_signal_connect(capabilities, "ack::" IBIS_CAPABILITY_MESSAGE_TAGS,
		                 G_CALLBACK(ibis_capabilities_message_tags_ack_cb),
		                 NULL);
	}
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(IbisCapabilities, ibis_capabilities, G_TYPE_OBJECT)

static void
ibis_capabilities_finalize(GObject *obj) {
	IbisCapabilities *capabilities = IBIS_CAPABILITIES(obj);

	g_clear_object(&capabilities->client);

	g_hash_table_destroy(capabilities->caps);
	g_ptr_array_free(capabilities->requests, TRUE);

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

static void
ibis_capabilities_init(IbisCapabilities *capabilities) {
	capabilities->caps = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
	                                           g_free);
	capabilities->requests = g_ptr_array_new_full(0, g_free);

	capabilities->negotiating = FALSE;
	g_atomic_ref_count_init(&capabilities->wait_counters);
}

static void
ibis_capabilities_class_init(IbisCapabilitiesClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = ibis_capabilities_finalize;

	/**
	 * IbisCapabilities::ready:
	 *
	 * Emitted when @self has finished receiving the list of
	 * capabilities from the server at startup.
	 *
	 * For dynamically added capabilities see the [signal@Capabilities::new]
	 * and [signal@Capabilities::del] signals.
	 *
	 * Since: 0.1
	 */
	signals[SIG_READY] = g_signal_new_class_handler(
		"ready",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		G_CALLBACK(ibis_capabilities_default_ready_cb),
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		0);

	/**
	 * IbisCapabilities::ack:
	 * @capabilities: the instance
	 * @client: the client instance
	 * @capability: the capability string
	 *
	 * Emitted when the server has acknowledged a `CAP REQ` call from
	 * [method@Capabilities.request].
	 *
	 * The value of @capability will be the same as the one that was requested.
	 *
	 * Since: 0.1
	 */
	signals[SIG_ACK] = g_signal_new_class_handler(
		"ack",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		2,
		IBIS_TYPE_CLIENT,
		G_TYPE_STRING);

	/**
	 * IbisCapabilities::nak:
	 * @capabilities: the instance
	 * @client: the client instance
	 * @capability: the capability string
	 *
	 * Emitted when the server has nacked a `CAP REQ` call from
	 * [method@Capabilities.request].
	 *
	 * The value of @capability will be the same as the one that was requested.
	 *
	 * Since: 0.1
	 */
	signals[SIG_NAK] = g_signal_new_class_handler(
		"nak",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		2,
		IBIS_TYPE_CLIENT,
		G_TYPE_STRING);

	/**
	 * IbisCapabilities::done:
	 * @capabilities: the instance
	 *
	 * Emitted when all of the requested capabilities have been either ack'd or
	 * nak'd by the server.
	 *
	 * This will only be emitted for the initial negotiation during
	 * registration and will not be emitted as a result of CAP NEW or CAP DEL
	 * messages.
	 *
	 * Since: 0.1
	 */
	signals[SIG_DONE] = g_signal_new_class_handler(
		"done",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		0);

	/**
	 * IbisCapabilities::new:
	 * @capabilities: the instance
	 * @client: the client instance
	 * @added: the newly added capabilities
	 *
	 * Emitted when the server sends the `CAP NEW` command. @added is a
	 * [type@GLib.Strv] of the new capabilities the server added.
	 *
	 * There are two approaches to how you can use this signal. You can check
	 * each item in @added for the values you need and parsing their values, or
	 * you can call [method@Capabilities.lookup] to see if the capabilities
	 * you're interested in have been added.
	 *
	 * Since: 0.1
	 */
	signals[SIG_NEW] = g_signal_new_class_handler(
		"new",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		2,
		IBIS_TYPE_CLIENT,
		G_TYPE_STRV);

	/**
	 * IbisCapabilities::del:
	 * @capabilities: the instance
	 * @client: the client instance
	 * @removed: the capabilities that were removed
	 *
	 * Emitted when the server sends the `CAP DEL` command. @removed is a
	 * [type@GLib.Strv] of the capabilities that the server removed.
	 *
	 * There are two approaches to how you can use this signal. You can check
	 * each item in @removed for the values you care about, or you can call
	 * [method@Capabilities.lookup] to see if the capabilities you're
	 * interested in have been removed.
	 *
	 * Since: 0.1
	 */
	signals[SIG_DEL] = g_signal_new_class_handler(
		"del",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		2,
		IBIS_TYPE_CLIENT,
		G_TYPE_STRV);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
IbisCapabilities *
ibis_capabilities_new(void) {
	return g_object_new(IBIS_TYPE_CAPABILITIES, NULL);
}

void
ibis_capabilities_start(IbisCapabilities *capabilities, IbisClient *client) {
	IbisMessage *message = NULL;

	g_return_if_fail(IBIS_IS_CAPABILITIES(capabilities));
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(IBIS_IS_CLIENT(capabilities->client) || capabilities->negotiating) {
		g_warning("capabilities %p is already started.", capabilities);

		return;
	}

	capabilities->negotiating = TRUE;
	capabilities->client = g_object_ref(client);
	g_signal_connect_object(capabilities->client, "message::" IBIS_MSG_CAP,
	                        G_CALLBACK(ibis_capabilities_message_cb),
	                        capabilities, G_CONNECT_DEFAULT);

	message = ibis_message_new(IBIS_MSG_CAP);
	ibis_message_set_params(message, "LS", IBIS_CAPABILITY_CAP_LS_VERSION,
	                        NULL);
	ibis_client_write(capabilities->client, message);
}

void
ibis_capabilities_stop(IbisCapabilities *capabilities) {
	g_return_if_fail(IBIS_IS_CAPABILITIES(capabilities));

	if(!IBIS_IS_CLIENT(capabilities->client)) {
		return;
	}

	g_signal_handlers_disconnect_by_func(capabilities->client,
	                                     G_CALLBACK(ibis_capabilities_message_cb),
	                                     capabilities);

	g_clear_object(&capabilities->client);
}

void
ibis_capabilities_request(IbisCapabilities *capabilities,
                          const char *capability)
{
	IbisMessage *message = NULL;

	g_return_if_fail(IBIS_IS_CAPABILITIES(capabilities));
	g_return_if_fail(capability != NULL);

	g_ptr_array_add(capabilities->requests, g_strdup(capability));

	message = ibis_message_new(IBIS_MSG_CAP);
	ibis_message_set_params(message, "REQ", capability, NULL);
	ibis_client_write(capabilities->client, message);
}

const char *
ibis_capabilities_lookup(IbisCapabilities *capabilities, const char *name,
                         gboolean *found)
{
	gpointer value = NULL;
	gboolean real_found = FALSE;

	g_return_val_if_fail(IBIS_IS_CAPABILITIES(capabilities), NULL);
	g_return_val_if_fail(name != NULL, NULL);

	real_found = g_hash_table_lookup_extended(capabilities->caps, name, NULL,
	                                          &value);

	if(found != NULL) {
		*found = real_found;
	}

	return value;
}

gboolean
ibis_capabilities_lookup_and_request(IbisCapabilities *capabilities,
                                     const char *name)
{
	gboolean found = FALSE;

	g_return_val_if_fail(IBIS_IS_CAPABILITIES(capabilities), FALSE);
	g_return_val_if_fail(name != NULL, FALSE);

	ibis_capabilities_lookup(capabilities, name, &found);
	if(found) {
		ibis_capabilities_request(capabilities, name);
	}

	return found;
}

void
ibis_capabilities_add_wait(IbisCapabilities *capabilities) {
	g_return_if_fail(IBIS_IS_CAPABILITIES(capabilities));

	if(capabilities->negotiating) {
		g_atomic_ref_count_inc(&capabilities->wait_counters);
	}
}

void
ibis_capabilities_remove_wait(IbisCapabilities *capabilities) {
	g_return_if_fail(IBIS_IS_CAPABILITIES(capabilities));

	if(capabilities->negotiating) {
		if(g_atomic_ref_count_dec(&capabilities->wait_counters)) {
			ibis_capabilities_finish(capabilities);
		}
	}
}
