/*
 * 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 "ibismessage.h"

#include "ibisconstants.h"
#include "ibisctcpmessage.h"
#include "ibisstring.h"

enum {
	PROP_0,
	PROP_COMMAND,
	PROP_COMMAND_QUARK,
	PROP_SOURCE,
	PROP_PARAMS,
	PROP_RAW_MESSAGE,
	PROP_TAGS,
	PROP_ERROR,
	PROP_CTCP,
	PROP_CTCP_MESSAGE,
	PROP_STANDARD_REPLY,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

struct _IbisMessage {
	GObject parent;

	char *raw_message;

	char *command;
	GQuark command_quark;

	GStrv params;
	char *source;
	IbisTags *tags;
	GError *error;
	IbisCTCPMessage *ctcp_message;
	IbisStandardReply *standard_reply;
};

/******************************************************************************
 * Serialization
 *****************************************************************************/
static inline void
ibis_message_serialize_add_delimiter(GString *str) {
	if(str->len > 0) {
		g_string_append(str, " ");
	}
}

static void
ibis_message_serialize_params(IbisMessage *message, GString *str) {
	gboolean first = TRUE;

	for(guint i = 0; message->params[i] != NULL; i++) {
		if(!first) {
			g_string_append(str, " ");
		}

		/* If this is the last param we may need to prefix it with a :. */
		if(message->params[i + 1] == NULL) {
			const char *param = message->params[i];
			gboolean prefix = FALSE;

			/* We only need the prefix if the parameter is empty, starts
			 * with a :, or contains a space.
			 */
			if(param[0] == '\0' || param[0] == ':') {
				prefix = TRUE;
			} else if(g_strstr_len(param, -1, " ") != NULL) {
				prefix = TRUE;
			}

			if(prefix) {
				g_string_append_c(str, ':');
			}

			g_string_append_printf(str, "%s", param);
		} else {
			g_string_append(str, message->params[i]);
		}

		if(first) {
			first = FALSE;
		}
	}
}

static GString *
ibis_message_serialize_internal(IbisMessage *message, gboolean include_tags) {
	GString *str = g_string_new("");

	if(include_tags && IBIS_IS_TAGS(message->tags)) {
		char *tag_data = ibis_tags_serialize(message->tags);

		g_string_append(str, tag_data);
		g_free(tag_data);
	}

	if(!ibis_str_is_empty(message->source)) {
		ibis_message_serialize_add_delimiter(str);
		g_string_append_printf(str, ":%s", message->source);
	}

	if(!ibis_str_is_empty(message->command)) {
		ibis_message_serialize_add_delimiter(str);
		g_string_append(str, message->command);
	}

	if(message->params != NULL) {
		ibis_message_serialize_add_delimiter(str);
		ibis_message_serialize_params(message, str);
	}

	if(IBIS_IS_CTCP_MESSAGE(message->ctcp_message)) {
		char *body = NULL;

		ibis_message_serialize_add_delimiter(str);

		body = ibis_ctcp_message_serialize(message->ctcp_message);
		g_string_append_printf(str, ":%s", body);
		g_free(body);
	}

	/* Finally add the trailing \r\n if the string isn't empty. */
	if(str->len > 0) {
		g_string_append(str, "\r\n");
	}

	return str;
}

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
ibis_message_set_command(IbisMessage *message, const char *command) {
	char *upper_command = NULL;

	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(command != NULL) {
		upper_command = g_utf8_strup(command, -1);
	}

	if(g_set_str(&message->command, upper_command)) {
		GObject *obj = G_OBJECT(message);

		message->command_quark = g_quark_from_string(upper_command);

		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_COMMAND]);
		g_object_notify_by_pspec(obj, properties[PROP_COMMAND_QUARK]);
		g_object_thaw_notify(obj);
	}

	g_clear_pointer(&upper_command, g_free);
}

static void
ibis_message_set_raw_message(IbisMessage *message, const char *raw_message) {
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(g_set_str(&message->raw_message, raw_message)) {
		g_object_notify_by_pspec(G_OBJECT(message),
		                         properties[PROP_RAW_MESSAGE]);
	}
}

static guint
ibis_message_extract_params(GStrvBuilder *builder, const char *str) {
	char *ptr = NULL;
	guint count = 0;

	/* Loop through str finding each space separated string. */
	while(str != NULL && *str != '\0') {
		/* Look for a space. */
		ptr = strchr(str, ' ');

		/* If we found one, set it to null terminator and add the string to our
		 * builder.
		 */
		if(ptr != NULL) {
			*ptr = '\0';
			g_strv_builder_add(builder, str);

			/* Move str to the next character as we know there's another
			 * character which might be another null terminator.
			 */
			str = ptr + 1;

			/* And don't forget to increment the count... ah ah ah! */
			count++;
		} else {
			/* Add the remaining string. */
			g_strv_builder_add(builder, str);

			/* Give the count another one, ah ah ah! */
			count++;

			/* Finally break out of the loop. */
			break;
		}
	}

	return count;
}

static GStrv
ibis_message_build_params(const char *middle, const char *coda,
                          const char *trailing, guint *n_params)
{
	GStrvBuilder *builder = g_strv_builder_new();
	GStrv result = NULL;

	ibis_message_extract_params(builder, middle);

	if(*coda != '\0') {
		g_strv_builder_add(builder, trailing);
	}

	result = g_strv_builder_end(builder);

	g_strv_builder_unref(builder);

	if(result != NULL && n_params != NULL) {
		*n_params = g_strv_length(result);
	}

	return result;
}

static void
ibis_message_set_standard_reply(IbisMessage *message, IbisStandardReply *reply)
{
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(g_set_object(&message->standard_reply, reply)) {
		g_object_notify_by_pspec(G_OBJECT(message),
		                         properties[PROP_STANDARD_REPLY]);
	}
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(IbisMessage, ibis_message, G_TYPE_OBJECT)

static void
ibis_message_finalize(GObject *obj) {
	IbisMessage *message = IBIS_MESSAGE(obj);

	g_clear_pointer(&message->command, g_free);
	g_clear_pointer(&message->params, g_strfreev);
	g_clear_pointer(&message->raw_message, g_free);
	g_clear_pointer(&message->source, g_free);
	g_clear_object(&message->tags);
	g_clear_error(&message->error);
	g_clear_object(&message->ctcp_message);
	g_clear_object(&message->standard_reply);

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

static void
ibis_message_get_property(GObject *obj, guint param_id, GValue *value,
                          GParamSpec *pspec)
{
	IbisMessage *message = IBIS_MESSAGE(obj);

	switch(param_id) {
	case PROP_COMMAND:
		g_value_set_string(value, ibis_message_get_command(message));
		break;
	case PROP_COMMAND_QUARK:
		g_value_set_uint(value, ibis_message_get_command_quark(message));
		break;
	case PROP_PARAMS:
		g_value_set_boxed(value, ibis_message_get_params(message));
		break;
	case PROP_RAW_MESSAGE:
		g_value_set_string(value, ibis_message_get_raw_message(message));
		break;
	case PROP_SOURCE:
		g_value_set_string(value, ibis_message_get_source(message));
		break;
	case PROP_TAGS:
		g_value_set_object(value, ibis_message_get_tags(message));
		break;
	case PROP_ERROR:
		g_value_set_boxed(value, ibis_message_get_error(message));
		break;
	case PROP_CTCP:
		g_value_set_boolean(value, ibis_message_get_ctcp(message));
		break;
	case PROP_CTCP_MESSAGE:
		g_value_set_object(value, ibis_message_get_ctcp_message(message));
		break;
	case PROP_STANDARD_REPLY:
		g_value_set_object(value, ibis_message_get_standard_reply(message));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
ibis_message_set_property(GObject *obj, guint param_id, const GValue *value,
                          GParamSpec *pspec)
{
	IbisMessage *message = IBIS_MESSAGE(obj);

	switch(param_id) {
	case PROP_COMMAND:
		ibis_message_set_command(message, g_value_get_string(value));
		break;
	case PROP_PARAMS:
		ibis_message_set_paramsv(message, g_value_get_boxed(value));
		break;
	case PROP_RAW_MESSAGE:
		ibis_message_set_raw_message(message, g_value_get_string(value));
		break;
	case PROP_SOURCE:
		ibis_message_set_source(message, g_value_get_string(value));
		break;
	case PROP_TAGS:
		G_GNUC_BEGIN_IGNORE_DEPRECATIONS
		ibis_message_set_tags(message, g_value_get_object(value));
		G_GNUC_END_IGNORE_DEPRECATIONS
		break;
	case PROP_ERROR:
		ibis_message_set_error(message, g_value_get_boxed(value));
		break;
	case PROP_CTCP:
		G_GNUC_BEGIN_IGNORE_DEPRECATIONS
		ibis_message_set_ctcp(message, g_value_get_boolean(value));
		G_GNUC_END_IGNORE_DEPRECATIONS
		break;
	case PROP_CTCP_MESSAGE:
		ibis_message_set_ctcp_message(message, g_value_get_object(value));
		break;
	case PROP_STANDARD_REPLY:
		ibis_message_set_standard_reply(message, g_value_get_object(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
ibis_message_init(IbisMessage *message) {
	message->tags = ibis_tags_new();
}

static void
ibis_message_class_init(IbisMessageClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = ibis_message_finalize;
	obj_class->get_property = ibis_message_get_property;
	obj_class->set_property = ibis_message_set_property;

	/**
	 * IbisMessage:command:
	 *
	 * The command of this message.
	 *
	 * This could be something like JOIN or a server reply numeric like 005.
	 *
	 * As of version 0.4, this will now be automatically noramlized to
	 * uppercase to help with comparison.
	 *
	 * Since: 0.1
	 */
	properties[PROP_COMMAND] = g_param_spec_string(
		"command", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:command-quark:
	 *
	 * The #GQuark for the command of this message.
	 *
	 * This is typically only used for emitting signals like
	 * [signal@Client::message].
	 *
	 * Since: 0.4
	 */
	properties[PROP_COMMAND_QUARK] = g_param_spec_string(
		"command-quark", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:params:
	 *
	 * The parameters of the message.
	 *
	 * When serialized, the last item will be prefixed with a :.
	 *
	 * Since: 0.1
	 */
	properties[PROP_PARAMS] = g_param_spec_boxed(
		"params", NULL, NULL,
		G_TYPE_STRV,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:raw-message:
	 *
	 * The raw message that was parsed.
	 *
	 * This property will only be non %NULL if the message was created with
	 * [func@Message.parse].
	 *
	 * Since: 0.1
	 */
	properties[PROP_RAW_MESSAGE] = g_param_spec_string(
		"raw-message", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:source:
	 *
	 * The source of the message.
	 *
	 * This could be a nickname, a full nick!ident@server, a server name, or
	 * %NULL.
	 *
	 * Since: 0.1
	 */
	properties[PROP_SOURCE] = g_param_spec_string(
		"source", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:tags:
	 *
	 * The [ircv3 message tags](https://ircv3.net/specs/extensions/message-tags)
	 * for the message.
	 *
	 * Since: 0.1
	 */
	properties[PROP_TAGS] = g_param_spec_object(
		"tags", NULL, NULL,
		IBIS_TYPE_TAGS,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:error:
	 *
	 * An error that was encountered when processing the message.
	 *
	 * Since: 0.1
	 */
	properties[PROP_ERROR] = g_param_spec_boxed(
		"error", NULL, NULL,
		G_TYPE_ERROR,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:ctcp:
	 *
	 * Whether or not the message is a CTCP message.
	 *
	 * This property should only be valid for messages whose command is
	 * `PRIVMSG` or `NOTICE`.
	 *
	 * Since: 0.1
	 */
	properties[PROP_CTCP] = g_param_spec_boolean(
		"ctcp", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:ctcp-message:
	 *
	 * The `CTCP` message that makes up the body of this message.
	 *
	 * Since: 0.5
	 */
	properties[PROP_CTCP_MESSAGE] = g_param_spec_object(
		"ctcp-message", NULL, NULL,
		IBIS_TYPE_CTCP_MESSAGE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisMessage:standard-reply:
	 *
	 * The [class@StandardReply] if the message is a standard reply.
	 *
	 * Since: 0.8
	 */
	properties[PROP_STANDARD_REPLY] = g_param_spec_object(
		"standard-reply", NULL, NULL,
		IBIS_TYPE_STANDARD_REPLY,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
IbisMessage *
ibis_message_new(const char *command) {
	g_return_val_if_fail(!ibis_str_is_empty(command), NULL);

	return g_object_new(
		IBIS_TYPE_MESSAGE,
		"command", command,
		NULL);
}

const char *
ibis_message_get_command(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->command;
}

GQuark
ibis_message_get_command_quark(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), 0);

	return message->command_quark;
}

const char *
ibis_message_get_source(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->source;
}

void
ibis_message_set_source(IbisMessage *message, const char *source) {
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(g_set_str(&message->source, source)) {
		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_SOURCE]);
	}
}

IbisTags *
ibis_message_get_tags(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->tags;
}

void
ibis_message_set_tags(IbisMessage *message, IbisTags *tags) {
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(g_set_object(&message->tags, tags)) {
		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_TAGS]);
	}
}

GDateTime *
ibis_message_get_timestamp(IbisMessage *message) {
	GDateTime *dt = NULL;
	const char *timestamp = NULL;

	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	if(!IBIS_IS_TAGS(message->tags)) {
		return NULL;
	}

	timestamp = ibis_tags_lookup(message->tags, IBIS_TAG_TIME);
	if(!ibis_str_is_empty(timestamp)) {
		GTimeZone *tz = g_time_zone_new_utc();

		dt = g_date_time_new_from_iso8601(timestamp, tz);

		g_time_zone_unref(tz);
	}

	return dt;
}

GStrv
ibis_message_get_params(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->params;
}

void
ibis_message_set_params(IbisMessage *message, ...) {
	GStrv params = NULL;
	GStrvBuilder *builder = NULL;
	va_list args;
	const char *param = NULL;

	g_return_if_fail(IBIS_IS_MESSAGE(message));

	builder = g_strv_builder_new();

	va_start(args, message);
	while((param = va_arg(args, const char *)) != NULL) {
		g_strv_builder_add(builder, param);
	}
	va_end(args);

	params = g_strv_builder_end(builder);
	g_clear_pointer(&builder, g_strv_builder_unref);

	ibis_message_set_paramsv(message, params);
	g_strfreev(params);
}

void
ibis_message_set_paramsv(IbisMessage *message, GStrv params) {
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(message->params != params) {
		g_clear_pointer(&message->params, g_strfreev);

		if(params != NULL) {
			message->params = g_strdupv(params);
		}

		g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_PARAMS]);
	}
}

GBytes *
ibis_message_serialize(IbisMessage *message, gboolean include_tags) {
	GString *str = NULL;

	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	str = ibis_message_serialize_internal(message, include_tags);

	return g_string_free_to_bytes(str);
}

char *
ibis_message_serialize_to_string(IbisMessage *message, gboolean include_tags) {
	GString *str = NULL;

	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	str = ibis_message_serialize_internal(message, include_tags);

	return g_string_free_and_steal(str);
}

GError *
ibis_message_get_error(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->error;
}

void
ibis_message_set_error(IbisMessage *message, GError *error) {
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	g_clear_error(&message->error);
	if(error != NULL) {
		message->error = g_error_copy(error);
	}

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

void
ibis_message_take_error(IbisMessage *message, GError *error) {
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	g_clear_error(&message->error);
	message->error = error;

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

IbisMessage *
ibis_message_parse(const char *buffer, GError **error) {
	IbisMessage *message = NULL;
	GError *local_error = NULL;
	GMatchInfo *info = NULL;
	GRegex *regex = NULL;
	GStrv params = NULL;
	char *coda = NULL;
	char *command = NULL;
	char *middle = NULL;
	char *source = NULL;
	char *tags_string = NULL;
	char *trailing = NULL;
	char *validated = NULL;
	guint n_params = 0;

	g_return_val_if_fail(buffer != NULL, FALSE);

	validated = g_utf8_make_valid(buffer, -1);
	regex = g_regex_new("(?:@(?<tags>[^ ]+) )?"
	                    "(?::(?<source>[^ ]+) +)?"
	                    "(?<command>[^ :]+)"
	                    "(?: +(?<middle>(?:[^ :][^ ]*(?: +[^ :][^ ]*)*)))*"
	                    "(?<coda> +:(?<trailing>.*)?)?",
	                    0, 0, NULL);

	/* Check if the buffer matches our regex for messages. */
	if(!g_regex_match(regex, validated, 0, &info)) {
		g_match_info_unref(info);
		g_clear_pointer(&validated, g_free);

		g_set_error(error, IBIS_MESSAGE_PARSE_ERROR, 0,
		            "failed to parser buffer '%s'", buffer);

		g_clear_pointer(&regex, g_regex_unref);

		return NULL;
	}

	/* We can *NOT* free validated here as the string is not copied into the
	 * GMatchInfo. We can only free it when the GMatchInfo is destroyed.
	 */

	/* Extract the command from the buffer, so we can create the message. */
	command = g_match_info_fetch_named(info, "command");
	message = g_object_new(
		IBIS_TYPE_MESSAGE,
		"command", command,
		"raw-message", buffer,
		NULL);

	tags_string = g_match_info_fetch_named(info, "tags");
	if(!ibis_str_is_empty(tags_string)) {
		IbisTags *tags = ibis_message_get_tags(message);

		ibis_tags_parse_string(tags, tags_string, &local_error);
		if(local_error != NULL) {
			g_propagate_error(error, local_error);

			g_free(tags_string);
			g_free(command);
			g_clear_object(&message);
			g_match_info_unref(info);
			g_clear_pointer(&validated, g_free);

			g_clear_pointer(&regex, g_regex_unref);

			return NULL;
		}
	}
	g_free(tags_string);

	source = g_match_info_fetch_named(info, "source");
	if(!ibis_str_is_empty(source)) {
		ibis_message_set_source(message, source);
	}
	g_free(source);

	middle = g_match_info_fetch_named(info, "middle");
	coda = g_match_info_fetch_named(info, "coda");
	trailing = g_match_info_fetch_named(info, "trailing");
	params = ibis_message_build_params(middle, coda, trailing, &n_params);

	/* If the command is PRIVMSG or NOTICE, we need to check if the body, the
	 * last parameter, starts with \001 which would make it a CTCP message. If
	 * so, we need to remove the leading and optional trailing \001 from the
	 * body and set the ctcp-message property of the message.
	 */
	if(ibis_str_equal(command, IBIS_MSG_PRIVMSG) ||
	   ibis_str_equal(command, IBIS_MSG_NOTICE))
	{
		if(n_params >= 1) {
			IbisCTCPMessage *ctcp_message = NULL;
			char *body = NULL;
			guint last = n_params - 1;

			body = params[last];
			ctcp_message = ibis_ctcp_message_parse(body);
			if(IBIS_IS_CTCP_MESSAGE(ctcp_message)) {
				GStrvBuilder *builder = NULL;
				GStrv adjusted = NULL;

				/* Set the CTCP message. */
				ibis_message_set_ctcp_message(message, ctcp_message);
				g_clear_object(&ctcp_message);

				/* Finally set params to be all of the params except for the
				 * last one that we used for CTCP.
				 */
				builder = g_strv_builder_new();
				for(guint i = 0; i < last; i++) {
					g_strv_builder_add(builder, params[i]);
				}
				adjusted = g_strv_builder_end(builder);
				g_strv_builder_unref(builder);

				ibis_message_set_paramsv(message, adjusted);
				g_strfreev(adjusted);
			}
		}
	}

	if(params != NULL) {
		ibis_message_set_paramsv(message, params);
	}

	/* Once we've parsed the full message we can check if it's a standard reply
	 * and parse that if it is.
	 */
	if(ibis_str_equal(command, IBIS_MSG_FAIL) ||
	   ibis_str_equal(command, IBIS_MSG_NOTE) ||
	   ibis_str_equal(command, IBIS_MSG_WARN))
	{
		IbisStandardReply *reply = NULL;

		reply = ibis_standard_reply_parse(message);
		ibis_message_set_standard_reply(message, reply);
		g_clear_object(&reply);
	}

	/* Clean up everything */
	g_free(command);
	g_free(middle);
	g_free(coda);
	g_free(trailing);
	g_strfreev(params);

	/* Cleanup the left overs. */
	g_match_info_unref(info);
	g_clear_pointer(&validated, g_free);
	g_clear_pointer(&regex, g_regex_unref);

	return message;
}

const char *
ibis_message_get_raw_message(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->raw_message;
}

gboolean
ibis_message_get_ctcp(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), FALSE);

	return IBIS_IS_CTCP_MESSAGE(message->ctcp_message);
}

void
ibis_message_set_ctcp(IbisMessage *message, gboolean ctcp) {
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(ctcp) {
		IbisCTCPMessage *ctcp_message = NULL;

		ctcp_message = ibis_ctcp_message_new("UNKNOWN");
		ibis_message_set_ctcp_message(message, ctcp_message);
		g_clear_object(&ctcp_message);
	} else {
		ibis_message_set_ctcp_message(message, NULL);
	}
}

IbisCTCPMessage *
ibis_message_get_ctcp_message(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->ctcp_message;
}

void
ibis_message_set_ctcp_message(IbisMessage *message,
                              IbisCTCPMessage *ctcp_message)
{
	g_return_if_fail(IBIS_IS_MESSAGE(message));

	if(!ibis_str_equal(message->command, IBIS_MSG_PRIVMSG) &&
	   !ibis_str_equal(message->command, IBIS_MSG_NOTICE))
	{
		return;
	}

	if(g_set_object(&message->ctcp_message, ctcp_message)) {
		GObject *obj = G_OBJECT(message);

		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_CTCP]);
		g_object_notify_by_pspec(obj, properties[PROP_CTCP_MESSAGE]);
		g_object_thaw_notify(obj);
	}
}

gboolean
ibis_message_is_command(IbisMessage *message, const char *command) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), FALSE);

	return ibis_str_equal(message->command, command);
}

IbisStandardReply *
ibis_message_get_standard_reply(IbisMessage *message) {
	g_return_val_if_fail(IBIS_IS_MESSAGE(message), NULL);

	return message->standard_reply;
}
