/*
 * Ibis - IRCv3 Library
 * Copyright (C) 2022-2024 Ibis Developers <devel@pidgin.im>
 *
 * 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 <glib/gi18n-lib.h>

#include <birb.h>

#include "ibisclient.h"

#include "ibisconstants.h"
#include "ibissource.h"
#include "ibisstring.h"
#include "ibisnormalize.h"

enum {
	PROP_0,
	PROP_ACTIVE_NICK,
	PROP_ALT_NICK,
	PROP_CANCELLABLE,
	PROP_CAPABILITIES,
	PROP_CONNECTED,
	PROP_ERROR,
	PROP_FEATURES,
	PROP_NETWORK,
	PROP_NICK,
	PROP_REALNAME,
	PROP_REGISTERED,
	PROP_USERNAME,
	PROP_HASL_CONTEXT,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

enum {
	SIG_MESSAGE,
	SIG_WRITING_MESSAGE,
	SIG_WROTE_MESSAGE,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

struct _IbisClient {
	GObject parent;

	GCancellable *cancellable;
	IbisCapabilities *capabilities;
	gboolean connected;
	gboolean registered;
	GError *error;
	IbisFeatures *features;

	GIOStream *stream;
	GDataInputStream *input;
	GOutputStream *output;

	char *network;
	char *nick;
	char *alt_nick;
	char *realname;
	char *username;

	/* This keeps track of whether or not the message tags capability has been
	 * negotiated.
	 */
	gboolean message_tags_negotiated;

	/* The name of the server detected from the source of the CAP or
	 * RPL_WELCOME messages.
	 */
	char *server_name;

	HaslContext *hasl_context;

	/* The nick name that we have on the server. This is separate from nick and
	 * alt_nick above because the user might want to restore their nick later.
	 */
	char *active_nick;
};

/******************************************************************************
 * Helpers
 *****************************************************************************/

/*
 * ibis_client_set_active_nick:
 * @active_nick: the new active nick.
 *
 * Sets active nick.
 *
 * This is a helper function to set the active nick since it can come from a
 * few places.
 *
 * Since: 0.11
 */
static void
ibis_client_set_active_nick(IbisClient *client, const char *active_nick) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(g_set_str(&client->active_nick, active_nick)) {
		g_object_notify_by_pspec(G_OBJECT(client),
		                         properties[PROP_ACTIVE_NICK]);
	}
}

static const char *
ibis_client_get_usable_realname(IbisClient *client) {
	if(!ibis_str_is_empty(client->realname)) {
		return client->realname;
	}

	if(!ibis_str_is_empty(client->nick)) {
		return client->nick;
	}

	if(!ibis_str_is_empty(client->alt_nick)) {
		return client->alt_nick;
	}

	return _("unknown");
}

static const char *
ibis_client_get_usable_username(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	if(!ibis_str_is_empty(client->username)) {
		return client->username;
	}

	if(!ibis_str_is_empty(client->nick)) {
		return client->nick;
	}

	if(!ibis_str_is_empty(client->alt_nick)) {
		return client->alt_nick;
	}

	return _("unknown");
}

static void
ibis_client_set_connected(IbisClient *client, gboolean connected) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(client->connected != connected) {
		client->connected = connected;

		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_CONNECTED]);
	}
}

static void
ibis_client_set_registered(IbisClient *client, gboolean registered) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(client->registered != registered) {
		client->registered = registered;

		g_object_notify_by_pspec(G_OBJECT(client),
		                         properties[PROP_REGISTERED]);
	}
}

static void
ibis_client_set_error(IbisClient *client, GError *error) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(client->error == NULL && error == NULL) {
		return;
	}

	if(error != NULL) {
		if(!g_error_matches(client->error, error->domain, error->code)) {
			/* If this is a different error, set it and notify. */
			g_clear_error(&client->error);
			g_propagate_error(&client->error, error);

			g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ERROR]);
		} else {
			/* The error was the same, but this is transfer full, so we need
			 * to remove the reference.
			 */
			g_error_free(error);
		}
	} else if(client->error != NULL) {
		/* We're clearing the air, so do that and notify. */
		g_clear_error(&client->error);

		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ERROR]);
	}
}

static void
ibis_client_set_network(IbisClient *client, const char *network) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(g_set_str(&client->network, network)) {
		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_NETWORK]);
	}
}

static void
ibis_client_attempt_nick(IbisClient *client) {
	const char *candidate_nick = NULL;

	/* This should only be called by the read callback, but we still make sure
	 * registration hasn't completed before we attempt to automatically change
	 * the user's nickname.
	 */
	if(client->registered) {
		return;
	}

	if(ibis_str_is_empty(client->active_nick)) {
		candidate_nick = client->nick;
	}

	if(ibis_str_is_empty(candidate_nick) &&
	   ibis_str_equal(client->active_nick, client->nick))
	{
		candidate_nick = client->alt_nick;
	}

	if(!ibis_str_is_empty(candidate_nick)) {
		IbisMessage *message = NULL;

		ibis_client_set_active_nick(client, candidate_nick);

		message = ibis_message_new(IBIS_MSG_NICK);
		ibis_message_set_params(message, candidate_nick, NULL);
		ibis_client_write(client, message);
	} else {
		GError *error = g_error_new_literal(IBIS_CLIENT_ERROR, 0,
		                                    _("no more nicks to try"));

		ibis_client_stop(client, error);
	}
}


static void
ibis_client_register(IbisClient *client, const char *password) {
	IbisMessage *message = NULL;

	if(client->registered) {
		return;
	}

	if(!ibis_str_is_empty(password)) {
		message = ibis_message_new(IBIS_MSG_PASS);
		ibis_message_set_params(message, password, NULL);
		ibis_client_write(client, message);
	}

	message = ibis_message_new(IBIS_MSG_USER);
	ibis_message_set_params(message,
	                        ibis_client_get_usable_username(client),
	                        "0",
	                        "*",
	                        ibis_client_get_usable_realname(client),
	                        NULL);
	ibis_client_write(client, message);

	ibis_client_attempt_nick(client);
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
ibis_client_write_cb(GObject *source, GAsyncResult *result, gpointer data) {
	IbisMessage *message = data;
	IbisClient *client = g_object_get_data(G_OBJECT(message), "ibis-client");
	BirbQueuedOutputStream *stream = BIRB_QUEUED_OUTPUT_STREAM(source);
	GError *error = NULL;
	gboolean success = FALSE;

	success = birb_queued_output_stream_push_bytes_finish(stream, result,
	                                                      &error);

	if(!success) {
		birb_queued_output_stream_clear_queue(stream);

		/* Ignore cancellations as they get set in _stop. */
		if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			g_prefix_error(&error, "%s", _("Lost connection: "));

			ibis_client_stop(client, error);
		}
	} else {
		g_signal_emit(client, signals[SIG_WROTE_MESSAGE],
		              ibis_message_get_command_quark(message), message);
	}

	g_clear_object(&message);
}

static void
ibis_client_read_cb(GObject *source, GAsyncResult *result, gpointer data) {
	IbisClient *client = data;
	IbisMessage *message = NULL;
	GDataInputStream *input = G_DATA_INPUT_STREAM(source);
	GError *error = NULL;
	gchar *line = NULL;
	gsize length;

	line = g_data_input_stream_read_line_finish(input, result, &length,
	                                            &error);
	if(line == NULL || error != NULL) {
		if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			if(IBIS_IS_CLIENT(client)) {
				if(error == NULL) {
					g_set_error_literal(&error, IBIS_CLIENT_ERROR, 0,
					                    _("Server closed the connection"));
				} else {
					g_prefix_error(&error, "%s",
					               _("Lost connection with server: "));
				}

				ibis_client_stop(client, error);
			}
		}

		/* In the off chance that line was returned, make sure we free it. */
		g_free(line);

		g_clear_object(&client);

		return;
	}

	message = ibis_message_parse(line, &error);
	if(error != NULL) {
		g_warning("failed to handle '%s': %s", line,
		          error != NULL ? error->message : "unknown error");

		g_clear_error(&error);
	} else if(IBIS_IS_MESSAGE(message)) {
		GQuark quark = 0;
		const char *command = NULL;
		const char *source = NULL;
		gboolean handled = FALSE;

		/* We use the quark as we know it's been normalized. */
		quark = ibis_message_get_command_quark(message);
		command = g_quark_to_string(quark);

		/* Grab the source. We save it if it's a CAP or RPL_WELCOME and use it
		 * when the server doesn't provide a source.
		 */
		source = ibis_message_get_source(message);

		if(client->server_name == NULL && source != NULL) {
			if(ibis_str_equal(command, IBIS_MSG_CAP) ||
			   ibis_str_equal(command, IBIS_RPL_WELCOME))
			{
				g_set_str(&client->server_name, source);
			}
		}

		if(ibis_str_equal(command, IBIS_RPL_WELCOME)) {
			ibis_client_set_registered(client, TRUE);
		}

		if(source == NULL) {
			ibis_message_set_source(message, client->server_name);
		}

		if(!client->registered) {
			if(ibis_str_equal(command, IBIS_ERR_ERRONEUSNICKNAME) ||
			   ibis_str_equal(command, IBIS_ERR_NICKNAMEINUSE) ||
			   ibis_str_equal(command, IBIS_ERR_NONICKNAMEGIVEN))
			{
				ibis_client_attempt_nick(client);
			}
		}

		g_signal_emit(client, signals[SIG_MESSAGE], quark, command, message,
		              &handled);

		if(!handled) {
			g_warning("unhandled message: '%s'", line);
		}
	} else {
		g_warning("parsing failed to generate a message or error for '%s'",
		          line);
	}

	g_clear_pointer(&line, g_free);
	g_clear_object(&message);

	/* Call read_line_async again to continue reading lines. */
	if(!ibis_client_get_error(client)) {
		g_data_input_stream_read_line_async(client->input,
		                                    G_PRIORITY_DEFAULT,
		                                    client->cancellable,
		                                    ibis_client_read_cb,
		                                    client);
	} else {
		/* Otherwise, drop the ref used to keep the client alive for this
		 * callback, since it won't be used again. */
		g_clear_object(&client);
	}
}

static void
ibis_client_connect_cb(GObject *source, GAsyncResult *result, gpointer data) {
	IbisClient *client = data;
	GError *error = NULL;
	GSocketClient *socket_client = G_SOCKET_CLIENT(source);
	GSocketConnection *connection = NULL;

	connection = g_socket_client_connect_to_host_finish(socket_client, result,
	                                                    &error);
	if(error != NULL) {
		if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			g_prefix_error_literal(&error, _("Unable to connect: "));
			ibis_client_stop(client, error);
		}
	} else {
		const char *password = NULL;

		password = g_object_get_data(G_OBJECT(client), "ibis-password");

		ibis_client_start(client, G_IO_STREAM(connection), password,
		                  client->cancellable);
	}

	g_object_set_data(G_OBJECT(client), "ibis-password", NULL);

	g_clear_object(&socket_client);
	g_clear_object(&connection);
	g_clear_object(&client);
}

/******************************************************************************
 * Default handlers
 *****************************************************************************/
static gboolean
ibis_client_handle_nick(IbisClient *client, IbisMessage *message) {
	const char *source = NULL;
	char *nick = NULL;

	source = ibis_message_get_source(message);
	ibis_source_parse(source, &nick, NULL, NULL);

	/* The server tells us of our nick changing by sending a nick message from
	 * our old nick with a parameter of the new nick.
	 */
	if(ibis_str_equal(client->active_nick, nick)) {
		GStrv params = ibis_message_get_params(message);

		if(g_strv_length(params) == 1) {
			ibis_client_set_active_nick(client, params[0]);
		}
	}

	g_free(nick);

	return TRUE;
}

static gboolean
ibis_client_handle_ping(IbisClient *client, IbisMessage *message) {
	IbisMessage *pong = NULL;
	GStrv params = NULL;

	pong = ibis_message_new(IBIS_MSG_PONG);

	params = ibis_message_get_params(message);
	if(g_strv_length(params) >= 1) {
		ibis_message_set_params(pong, params[0], NULL);
	}

	ibis_client_write(client, pong);

	return TRUE;
}

static gboolean
ibis_client_handle_rpl_isupport(IbisClient *client, IbisMessage *message) {
	ibis_features_parse(client->features, message);

	return TRUE;
}

static gboolean
ibis_client_handle_rpl_welcome(IbisClient *client, IbisMessage *message) {
	GStrv params = NULL;

	/* Per https://modern.ircdocs.horse/#connection-registration the first
	 * parameter of RPL_WELCOME is the nick that the server assigned us.
	 */
	params = ibis_message_get_params(message);
	if(params != NULL && g_strv_length(params) > 0) {
		ibis_client_set_active_nick(client, params[0]);
	}

	return TRUE;
}

static gboolean
ibis_client_default_message_handler(IbisClient *client, const char *command,
                                    IbisMessage *message)
{
	if(ibis_str_equal(command, IBIS_MSG_PING)) {
		return ibis_client_handle_ping(client, message);
	} else if(ibis_str_equal(command, IBIS_MSG_NICK)) {
		return ibis_client_handle_nick(client, message);
	} else if(ibis_str_equal(command, IBIS_RPL_WELCOME)) {
		return ibis_client_handle_rpl_welcome(client, message);
	} else if(ibis_str_equal(command, IBIS_RPL_ISUPPORT)) {
		return ibis_client_handle_rpl_isupport(client, message);
	}

	return FALSE;
}

static void
ibis_client_ack_message_tags_cb(G_GNUC_UNUSED IbisCapabilities *capabilities,
                                IbisClient *client,
                                G_GNUC_UNUSED const char *name,
                                G_GNUC_UNUSED gpointer data)
{
	client->message_tags_negotiated = TRUE;
}

static void
ibis_client_network_feature_cb(IbisFeatures *features, const char *name,
                               gpointer data)
{
	IbisClient *client = data;
	const char *value = NULL;

	/* This is a detailed signal, so name should always be the value of
	 * IBIS_FEATURE_NETWORK.
	 */
	value = ibis_features_get_string(features, name);
	ibis_client_set_network(client, value);
}

/******************************************************************************
 * IbisClient default handlers
 *****************************************************************************/
static gboolean
ibis_client_writing_message_default_handler(IbisClient *client,
                                            IbisMessage *message)
{
	const char *command = NULL;

	command = ibis_message_get_command(message);
	if(ibis_str_equal(command, IBIS_MSG_TAGMSG)) {
		if(!client->message_tags_negotiated) {
			return TRUE;
		}
	}

	return FALSE;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(IbisClient, ibis_client, G_TYPE_OBJECT)

static void
ibis_client_finalize(GObject *obj) {
	IbisClient *client = IBIS_CLIENT(obj);

	if(client->connected) {
		ibis_client_disconnect(client, NULL, NULL);
	}

	g_clear_object(&client->cancellable);
	g_clear_object(&client->capabilities);
	g_clear_error(&client->error);
	g_clear_object(&client->features);
	g_clear_object(&client->hasl_context);

	g_clear_pointer(&client->network, g_free);
	g_clear_pointer(&client->alt_nick, g_free);
	g_clear_pointer(&client->nick, g_free);
	g_clear_pointer(&client->realname, g_free);
	g_clear_pointer(&client->username, g_free);

	g_clear_pointer(&client->server_name, g_free);

	g_clear_pointer(&client->active_nick, g_free);

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

static void
ibis_client_get_property(GObject *obj, guint param_id, GValue *value,
                         GParamSpec *pspec)
{
	IbisClient *client = IBIS_CLIENT(obj);

	switch(param_id) {
	case PROP_ACTIVE_NICK:
		g_value_set_string(value, ibis_client_get_active_nick(client));
		break;
	case PROP_ALT_NICK:
		g_value_set_string(value, ibis_client_get_alt_nick(client));
		break;
	case PROP_CANCELLABLE:
		g_value_set_object(value, ibis_client_get_cancellable(client));
		break;
	case PROP_CAPABILITIES:
		g_value_set_object(value, ibis_client_get_capabilities(client));
		break;
	case PROP_CONNECTED:
		g_value_set_boolean(value, ibis_client_get_connected(client));
		break;
	case PROP_ERROR:
		g_value_set_boxed(value, ibis_client_get_error(client));
		break;
	case PROP_FEATURES:
		g_value_set_object(value, ibis_client_get_features(client));
		break;
	case PROP_NETWORK:
		g_value_set_string(value, ibis_client_get_network(client));
		break;
	case PROP_NICK:
		g_value_set_string(value, ibis_client_get_nick(client));
		break;
	case PROP_REALNAME:
		g_value_set_string(value, ibis_client_get_realname(client));
		break;
	case PROP_REGISTERED:
		g_value_set_boolean(value, ibis_client_get_registered(client));
		break;
	case PROP_USERNAME:
		g_value_set_string(value, ibis_client_get_username(client));
		break;
	case PROP_HASL_CONTEXT:
		g_value_set_object(value, ibis_client_get_hasl_context(client));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
ibis_client_set_property(GObject *obj, guint param_id, const GValue *value,
                         GParamSpec *pspec)
{
	IbisClient *client = IBIS_CLIENT(obj);

	switch(param_id) {
	case PROP_ALT_NICK:
		ibis_client_set_alt_nick(client, g_value_get_string(value));
		break;
	case PROP_NICK:
		ibis_client_set_nick(client, g_value_get_string(value));
		break;
	case PROP_REALNAME:
		ibis_client_set_realname(client, g_value_get_string(value));
		break;
	case PROP_USERNAME:
		ibis_client_set_username(client, g_value_get_string(value));
		break;
	case PROP_HASL_CONTEXT:
		ibis_client_set_hasl_context(client, g_value_get_object(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
ibis_client_init(IbisClient *client) {
	client->message_tags_negotiated = FALSE;

	client->capabilities = ibis_capabilities_new();
	g_signal_connect(client->capabilities,
	                 "ack::" IBIS_CAPABILITY_MESSAGE_TAGS,
	                 G_CALLBACK(ibis_client_ack_message_tags_cb),
	                 NULL);

	client->features = ibis_features_new();
	g_signal_connect_object(client->features, "changed::" IBIS_FEATURE_NETWORK,
	                        G_CALLBACK(ibis_client_network_feature_cb),
	                        client, G_CONNECT_DEFAULT);
}

static void
ibis_client_class_init(IbisClientClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = ibis_client_finalize;
	obj_class->get_property = ibis_client_get_property;
	obj_class->set_property = ibis_client_set_property;

	/**
	 * IbisClient:active-nick:
	 *
	 * The nick that the server has acknowledged.
	 *
	 * Since: 0.11
	 */
	properties[PROP_ACTIVE_NICK] = g_param_spec_string(
		"active-nick", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:alt-nick:
	 *
	 * The alternative nick to use if [property@Client:nick] is already in use.
	 *
	 * Since: 0.1
	 */
	properties[PROP_ALT_NICK] = g_param_spec_string(
		"alt-nick", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:cancellable:
	 *
	 * The [class@Gio.Cancellable] for the client.
	 *
	 * Since: 0.1
	 */
	properties[PROP_CANCELLABLE] = g_param_spec_object(
		"cancellable", NULL, NULL,
		G_TYPE_CANCELLABLE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:capabilities:
	 *
	 * The [class@Capabilities] that this client is using.
	 *
	 * Since: 0.1
	 */
	properties[PROP_CAPABILITIES] = g_param_spec_object(
		"capabilities", NULL, NULL,
		IBIS_TYPE_CAPABILITIES,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:connected:
	 *
	 * Set to whether or not the client is currently connected.
	 *
	 * Before 0.10 this was set to %TRUE after registration. In 0.10
	 * [property@Client:registered] was added to keep that behavior as this is
	 * now set to %TRUE in [method@Client.start].
	 *
	 * Since: 0.1
	 */
	properties[PROP_CONNECTED] = g_param_spec_boolean(
		"connected", NULL, NULL,
		FALSE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:error:
	 *
	 * A #GError that the client encountered.
	 *
	 * Since: 0.1
	 */
	properties[PROP_ERROR] = g_param_spec_boxed(
		"error", NULL, NULL,
		G_TYPE_ERROR,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:features:
	 *
	 * The [class@Features] for this connection.
	 *
	 * This property will be %NULL if the client is not currently connected.
	 *
	 * Since: 0.4
	 */
	properties[PROP_FEATURES] = g_param_spec_object(
		"features", NULL, NULL,
		IBIS_TYPE_FEATURES,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:network:
	 *
	 * The advertised name of the network from the server sending the
	 * [const@RPL_ISUPPORT] message.
	 *
	 * Since: 0.4
	 */
	properties[PROP_NETWORK] = g_param_spec_string(
		"network", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:nick:
	 *
	 * The primary nick to use.
	 *
	 * If the server tells us this nick is in use, an attempt to use
	 * [property@Client:alt-nick] will be made.
	 *
	 * Since: 0.1
	 */
	properties[PROP_NICK] = g_param_spec_string(
		"nick", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:realname:
	 *
	 * The realname to use when sending the USER command.
	 *
	 * If this is %NULL, the value of [property@Client:nick] will be returned.
	 *
	 * Since: 0.1
	 */
	properties[PROP_REALNAME] = g_param_spec_string(
		"realname", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:registered:
	 *
	 * Whether or not user registration has been completed.
	 *
	 * User registration must be completed at the beginning of a connection and
	 * no other commands may be sent while it is occurring.
	 *
	 * Since: 0.10
	 */
	properties[PROP_REGISTERED] = g_param_spec_boolean(
		"registered", NULL, NULL,
		FALSE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:username:
	 *
	 * The username to use when sending the USER command.
	 *
	 * If this is %NULL, the value of [property@Client:nick] will be returned.
	 *
	 * Since: 0.1
	 */
	properties[PROP_USERNAME] = g_param_spec_string(
		"username", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisClient:hasl-context:
	 *
	 * The [class@Hasl.Context] to use during SASL negotiation.
	 *
	 * If this is %NULL, SASL negotiation will not be attempted.
	 *
	 * Since: 0.1
	 */
	properties[PROP_HASL_CONTEXT] = g_param_spec_object(
		"hasl-context", NULL, NULL,
		HASL_TYPE_CONTEXT,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);

	/**
	 * IbisClient::message:
	 * @client: the instance
	 * @command: the command
	 * @message: the message
	 *
	 * Emitted when the client has received a new message.
	 *
	 * This signal supports *details* based on the upper case version of the
	 * command from the message. For example, `PRIVMSG`, `NOTICE`, etc.
	 *
	 * Returns: true to stop other handlers from being invoked; false otherwise.
	 *
	 * Since: 0.1
	 */
	signals[SIG_MESSAGE] = g_signal_new_class_handler(
		"message",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
		G_CALLBACK(ibis_client_default_message_handler),
		g_signal_accumulator_true_handled,
		NULL,
		NULL,
		G_TYPE_BOOLEAN,
		2,
		G_TYPE_STRING,
		IBIS_TYPE_MESSAGE);

	/**
	 * IbisClient::writing-message:
	 * @client: the instance
	 * @message: the message
	 *
	 * Emitted just before the client has queued @message to be written to the
	 * output stream to allow dropping or modification of messages.
	 *
	 * This signal supports *details* based on the normalized uppercase version
	 * of the command from the message. For example, `PRIVMSG`, `NOTICE`, etc.
	 *
	 * Returns: true to stop the message from being written; false otherwise.
	 *
	 * Since: 0.4
	 */
	signals[SIG_WRITING_MESSAGE] = g_signal_new_class_handler(
		"writing-message",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
		G_CALLBACK(ibis_client_writing_message_default_handler),
		g_signal_accumulator_true_handled,
		NULL,
		NULL,
		G_TYPE_BOOLEAN,
		1,
		IBIS_TYPE_MESSAGE);

	/**
	 * IbisClient::wrote-message:
	 * @client: the instance
	 * @message: the message
	 *
	 * Emitted when the client has successfully written @message to output
	 * stream.
	 *
	 * This signal supports *details* based on the normalized uppercase version
	 * of the command from the message. For example, `PRIVMSG`, `NOTICE`, etc.
	 *
	 * Since: 0.4
	 */
	signals[SIG_WROTE_MESSAGE] = g_signal_new_class_handler(
		"wrote-message",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		1,
		IBIS_TYPE_MESSAGE);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
void
ibis_client_connect(IbisClient *client, const char *hostname, guint16 port,
                    const char *password, gboolean tls,
                    GCancellable *cancellable,
                    GProxyResolver *proxy_resolver)
{
	GSocketClient *socket_client = NULL;

	g_return_if_fail(IBIS_IS_CLIENT(client));
	g_return_if_fail(!ibis_str_is_empty(hostname));

	socket_client = g_socket_client_new();
	g_socket_client_set_proxy_resolver(socket_client, proxy_resolver);
	g_socket_client_set_tls(socket_client, tls);

	if(!ibis_str_is_empty(password)) {
		g_object_set_data_full(G_OBJECT(client), "ibis-password",
		                       g_strdup(password), g_free);
	}

	g_set_object(&client->cancellable, cancellable);

	g_socket_client_connect_to_host_async(socket_client, hostname, port,
	                                      client->cancellable,
	                                      ibis_client_connect_cb,
	                                      g_object_ref(client));
}

void
ibis_client_disconnect(IbisClient *client, GError *error, const char *reason) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(client->connected) {
		IbisMessage *message = NULL;

		message = ibis_message_new(IBIS_MSG_QUIT);
		if(!ibis_str_is_empty(reason)) {
			ibis_message_set_params(message, reason, NULL);
		}

		ibis_client_write(client, message);
	}

	ibis_client_stop(client, error);
}

const char *
ibis_client_get_active_nick(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->active_nick;
}

const char *
ibis_client_get_alt_nick(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->alt_nick;
}

void
ibis_client_set_alt_nick(IbisClient *client, const char *alt_nick) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	if(g_set_str(&client->alt_nick, alt_nick)) {
		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ALT_NICK]);
	}
}

GCancellable *
ibis_client_get_cancellable(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->cancellable;
}

IbisCapabilities *
ibis_client_get_capabilities(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->capabilities;
}

gboolean
ibis_client_get_connected(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), FALSE);

	return client->connected;
}

GError *
ibis_client_get_error(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->error;
}

IbisFeatures *
ibis_client_get_features(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->features;
}

HaslContext *
ibis_client_get_hasl_context(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->hasl_context;
}

void
ibis_client_set_hasl_context(IbisClient *client, HaslContext *hasl_context) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

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

const char *
ibis_client_get_network(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->network;
}

const char *
ibis_client_get_nick(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->nick;
}

void
ibis_client_set_nick(IbisClient *client, const char *nick) {
	g_return_if_fail(IBIS_IS_CLIENT(client));
	g_return_if_fail(!ibis_str_is_empty(nick));

	if(g_set_str(&client->nick, nick)) {
		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_NICK]);
	}
}

const char *
ibis_client_get_realname(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->realname;
}

gboolean
ibis_client_get_registered(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), FALSE);

	return client->registered;
}

void
ibis_client_set_realname(IbisClient *client, const char *realname) {
	g_return_if_fail(IBIS_IS_CLIENT(client));
	g_return_if_fail(realname == NULL || realname[0] != '\0');

	if(g_set_str(&client->realname, realname)) {
		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_REALNAME]);
	}
}

char *
ibis_client_get_source_prefix(IbisClient *client, const char *source) {
	const char *prefixes = NULL;

	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
	g_return_val_if_fail(!ibis_str_is_empty(source), NULL);

	prefixes = ibis_features_get_prefix_prefixes(client->features);
	if(prefixes != NULL) {
		return ibis_source_get_prefix(source, prefixes);
	}

	return NULL;
}

const char *
ibis_client_get_username(IbisClient *client) {
	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	return client->username;
}

void
ibis_client_set_username(IbisClient *client, const char *username) {
	g_return_if_fail(IBIS_IS_CLIENT(client));
	g_return_if_fail(username == NULL || username[0] != '\0');

	if(g_set_str(&client->username, username)) {
		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_USERNAME]);
	}
}

gboolean
ibis_client_is_channel(IbisClient *client, const char *target) {
	const char *chantypes = NULL;

	g_return_val_if_fail(IBIS_IS_CLIENT(client), FALSE);
	g_return_val_if_fail(!ibis_str_is_empty(target), FALSE);

	chantypes = ibis_features_get_chantypes(client->features);
	if(ibis_str_is_empty(chantypes)) {
		return FALSE;
	}

	for(int i = 0; chantypes[i] != '\0'; i++) {
		if(chantypes[i] == target[0]) {
			return TRUE;
		}
	}

	return FALSE;
}

IbisClient *
ibis_client_new(void) {
	return g_object_new(IBIS_TYPE_CLIENT, NULL);
}

char *
ibis_client_normalize(IbisClient *client, const char *input) {
	const char *casemapping = NULL;

	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);

	casemapping = ibis_features_get_casemapping(client->features);

	if(ibis_str_equal(casemapping, "ascii")) {
		return ibis_normalize_ascii(input);
	} else if(ibis_str_equal(casemapping, "rfc1459")) {
		return ibis_normalize_rfc1459(input);
	} else if(ibis_str_equal(casemapping, "rfc1459-strict")) {
		return ibis_normalize_rfc1459_strict(input);
	}

	return g_strdup(input);
}

void
ibis_client_start(IbisClient *client, GIOStream *stream, const char *password,
                  GCancellable *cancellable)
{
	GInputStream *input = NULL;
	GOutputStream *output = NULL;

	g_return_if_fail(IBIS_IS_CLIENT(client));
	g_return_if_fail(G_IS_IO_STREAM(stream));
	g_return_if_fail(client->connected == FALSE);

	g_set_object(&client->cancellable, cancellable);
	if(!G_IS_CANCELLABLE(client->cancellable)) {
		client->cancellable = g_cancellable_new();
	}

	/* Clear the features object. */
	ibis_features_clear(client->features);

	/* Clear any previous error. */
	ibis_client_set_error(client, NULL);

	/* Store the stream for later use in disconnect. */
	client->stream = g_object_ref(stream);

	/* Wrap the output stream in a Birb QueuedOutputStream. */
	output = g_io_stream_get_output_stream(stream);
	client->output = birb_queued_output_stream_new(output);

	/* Setup the input stream. */
	input = g_io_stream_get_input_stream(G_IO_STREAM(stream));
	client->input = g_data_input_stream_new(input);
	g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(client->input),
	                                     G_DATA_STREAM_NEWLINE_TYPE_CR_LF);

	ibis_client_set_connected(client, TRUE);

	/* Start chunking the input. */
	g_data_input_stream_read_line_async(client->input,
	                                    G_PRIORITY_DEFAULT,
	                                    client->cancellable,
	                                    ibis_client_read_cb,
	                                    g_object_ref(client));

	ibis_capabilities_start(client->capabilities, client);
	ibis_client_register(client, password);
}

void
ibis_client_stop(IbisClient *client, GError *error) {
	g_return_if_fail(IBIS_IS_CLIENT(client));

	/* We set multiple properties while we're disconnecting but consumers
	 * shouldn't know about any of them changing until we've finished
	 * disconnecting.
	 */
	g_object_freeze_notify(G_OBJECT(client));

	/* Disable the capabilities handler. */
	ibis_capabilities_stop(client->capabilities);

	ibis_client_set_error(client, error);

	if(G_IS_IO_STREAM(client->stream)) {
		if(!g_io_stream_is_closed(client->stream)) {
			GError *close_error = NULL;
			gboolean success = FALSE;

			/* If closing the stream fails, the stream still gets closed, but
			 * we can update the error message if it wasn't already set.
			 */
			success = g_io_stream_close(client->stream, client->cancellable,
			                            &close_error);

			if(!success) {
				if(close_error != NULL) {
					g_warning("failed to close stream: %s",
					          close_error->message);
				} else {
					g_warning("failed to close stream: unknown error");
				}

				if(client->error == NULL) {
					g_warning("client error was null, setting '%s'", close_error->message);
					ibis_client_set_error(client, close_error);
				} else {
					g_clear_error(&close_error);
				}
			}
		}
	}

	ibis_client_set_connected(client, FALSE);
	ibis_client_set_registered(client, FALSE);

	ibis_features_clear(client->features);

	g_cancellable_cancel(client->cancellable);
	g_clear_object(&client->cancellable);
	g_clear_object(&client->input);
	g_clear_object(&client->output);

	g_clear_pointer(&client->server_name, g_free);

	g_object_thaw_notify(G_OBJECT(client));
}

char *
ibis_client_strip_source_prefix(IbisClient *client, const char *source) {
	const char *prefixes = NULL;

	g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL);
	g_return_val_if_fail(!ibis_str_is_empty(source), NULL);

	prefixes = ibis_features_get_prefix_prefixes(client->features);
	if(prefixes != NULL) {
		return ibis_source_strip_prefix(source, prefixes);
	}

	return g_strdup(source);
}

void
ibis_client_write(IbisClient *client, IbisMessage *message) {
	GBytes *bytes = NULL;
	gboolean cancelled = FALSE;

	g_return_if_fail(IBIS_IS_CLIENT(client));
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	g_signal_emit(client, signals[SIG_WRITING_MESSAGE],
	              ibis_message_get_command_quark(message), message, &cancelled);

	if(cancelled) {
		g_clear_object(&message);

		return;
	}

	bytes = ibis_message_serialize(message, client->message_tags_negotiated);

	g_object_set_data(G_OBJECT(message), "ibis-client", client);

	birb_queued_output_stream_push_bytes_async(BIRB_QUEUED_OUTPUT_STREAM(client->output),
	                                           bytes,
	                                           G_PRIORITY_DEFAULT,
	                                           client->cancellable,
	                                           ibis_client_write_cb,
	                                           message);

	g_bytes_unref(bytes);
}
