/*
 * 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 "ibisfeatures.h"
#include "ibisconstants.h"
#include "ibisstring.h"

enum {
	PROP_0,
	PROP_AWAYLEN,
	PROP_CASEMAPPING,
	PROP_CHANLIMIT,
	PROP_CHANMODES,
	PROP_CHANNELLEN,
	PROP_CHANTYPES,
	PROP_ELIST,
	PROP_EXCEPTS,
	PROP_EXTBAN,
	PROP_HOSTLEN,
	PROP_INVEX,
	PROP_KICKLEN,
	PROP_MAXLIST,
	PROP_MAXTARGETS,
	PROP_MODES,
	PROP_NETWORK,
	PROP_NICKLEN,
	PROP_PREFIX,
	PROP_SAFELIST,
	PROP_SILENCE,
	PROP_STATUSMSG,
	PROP_TARGMAX,
	PROP_TOPICLEN,
	PROP_USERLEN,
	N_PROPERTIES,
};

static GParamSpec *properties[N_PROPERTIES] = {NULL, };

struct _IbisFeatures {
	GObject parent;

	GHashTable *features;
};

/******************************************************************************
 * Helpers
 *****************************************************************************/
void
ibis_features_free_gvalue(gpointer data) {
	GValue* value = data;
	g_value_unset(value);
	g_free(value);
}

static void
ibis_features_set_uint(IbisFeatures *features, const char *name,
                       guint uvalue, GParamSpec *pspec)
{
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features, name);
	if(existing_value != NULL &&
	   g_value_get_uint(existing_value) == uvalue) {
		return;
	}

	value = g_new0(GValue, 1);
	g_value_init(value, G_TYPE_UINT);
	g_value_set_uint(value, uvalue);

	g_hash_table_insert(features->features, g_strdup(name), value);

	if(pspec != NULL) {
		g_object_notify_by_pspec(G_OBJECT(features), pspec);
	}
}

static void
ibis_features_set_string(IbisFeatures *features, const char *name,
                         const char *svalue, GParamSpec *pspec)
{
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features, name);
	if(existing_value != NULL &&
	   ibis_str_equal(g_value_get_string(existing_value), svalue)) {
		return;
	}

	value = g_new0(GValue, 1);
	g_value_init(value, G_TYPE_STRING);
	g_value_set_string(value, svalue);

	g_hash_table_insert(features->features, g_strdup(name), value);

	if(pspec != NULL) {
		g_object_notify_by_pspec(G_OBJECT(features), pspec);
	}
}

static void
ibis_features_set_char(IbisFeatures *features, const char *name,
                       char cvalue, GParamSpec *pspec)
{
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features, name);
	if(existing_value != NULL &&
	   g_value_get_schar(existing_value) == cvalue) {
		return;
	}

	value = g_new0(GValue, 1);
	g_value_init(value, G_TYPE_CHAR);
	g_value_set_schar(value, cvalue);

	g_hash_table_insert(features->features, g_strdup(name), value);

	if(pspec != NULL) {
		g_object_notify_by_pspec(G_OBJECT(features), pspec);
	}
}

static void
ibis_features_set_boolean(IbisFeatures *features, const char *name,
                          gboolean bvalue, GParamSpec *pspec)
{
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features, name);
	if(existing_value != NULL &&
	   g_value_get_boolean(existing_value) == bvalue) {
		return;
	}

	value = g_new0(GValue, 1);
	g_value_init(value, G_TYPE_BOOLEAN);
	g_value_set_boolean(value, bvalue);

	g_hash_table_insert(features->features, g_strdup(name), value);

	if(pspec != NULL) {
		g_object_notify_by_pspec(G_OBJECT(features), pspec);
	}
}

static void
ibis_features_set_feature(IbisFeatures *features, const char *feature,
                          const char *value)
{
	g_return_if_fail(IBIS_IS_FEATURES(features));

	if(ibis_str_equal(feature, IBIS_FEATURE_AWAYLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_AWAYLEN,
		                       ibis_str_to_uint(value), properties[PROP_AWAYLEN]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CASEMAPPING)) {
		ibis_features_set_string(features, IBIS_FEATURE_CASEMAPPING, value,
		                         properties[PROP_CASEMAPPING]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANLIMIT)) {
		ibis_features_set_string(features, IBIS_FEATURE_CHANLIMIT, value,
		                         properties[PROP_CHANLIMIT]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANMODES)) {
		ibis_features_set_string(features, IBIS_FEATURE_CHANMODES, value,
		                         properties[PROP_CHANMODES]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANNELLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_CHANNELLEN,
		                       ibis_str_to_uint(value),
		                       properties[PROP_CHANNELLEN]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANTYPES)) {
		ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, value,
		                         properties[PROP_CHANTYPES]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_ELIST)) {
		ibis_features_set_string(features, IBIS_FEATURE_ELIST, value,
		                         properties[PROP_ELIST]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_EXCEPTS)) {
		if(strlen(value) == 1) {
			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, value[0],
			                       properties[PROP_EXCEPTS]);
		} else {
			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, 'e',
			                       properties[PROP_EXCEPTS]);
		}
	} else if(ibis_str_equal(feature, IBIS_FEATURE_EXTBAN)) {
		ibis_features_set_string(features, IBIS_FEATURE_EXTBAN, value,
		                         properties[PROP_EXTBAN]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_HOSTLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_HOSTLEN,
		                       ibis_str_to_uint(value), properties[PROP_HOSTLEN]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_INVEX)) {
		if(strlen(value) == 1) {
			ibis_features_set_char(features, IBIS_FEATURE_INVEX, value[0],
			                       properties[PROP_INVEX]);
		} else {
			ibis_features_set_char(features, IBIS_FEATURE_INVEX, 'I',
			                       properties[PROP_INVEX]);
		}
	} else if(ibis_str_equal(feature, IBIS_FEATURE_KICKLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_KICKLEN,
		                       ibis_str_to_uint(value), properties[PROP_KICKLEN]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_MAXLIST)) {
		ibis_features_set_string(features, IBIS_FEATURE_MAXLIST, value,
		                         properties[PROP_MAXLIST]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_MAXTARGETS)) {
		ibis_features_set_uint(features, IBIS_FEATURE_MAXTARGETS,
		                       ibis_str_to_uint(value),
		                       properties[PROP_MAXTARGETS]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_MODES)) {
		guint uvalue;
		if(strlen(value) > 0) {
			uvalue = ibis_str_to_uint(value);
		} else {
			uvalue = G_MAXUINT32;
		}
		ibis_features_set_uint(features, IBIS_FEATURE_MODES,
		                       uvalue, properties[PROP_MODES]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_NETWORK)) {
		ibis_features_set_string(features, IBIS_FEATURE_NETWORK, value,
		                         properties[PROP_NETWORK]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_NICKLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_NICKLEN,
		                       ibis_str_to_uint(value), properties[PROP_NICKLEN]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_PREFIX)) {
		ibis_features_set_string(features, IBIS_FEATURE_PREFIX, value,
		                         properties[PROP_PREFIX]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_SAFELIST)) {
		ibis_features_set_boolean(features, IBIS_FEATURE_SAFELIST, TRUE,
		                          properties[PROP_SAFELIST]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_SILENCE)) {
		ibis_features_set_uint(features, IBIS_FEATURE_SILENCE,
		                       ibis_str_to_uint(value), properties[PROP_SILENCE]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_STATUSMSG)) {
		ibis_features_set_string(features, IBIS_FEATURE_STATUSMSG, value,
		                         properties[PROP_STATUSMSG]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_TARGMAX)) {
		ibis_features_set_string(features, IBIS_FEATURE_TARGMAX, value,
		                         properties[PROP_TARGMAX]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_TOPICLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_TOPICLEN,
		                       ibis_str_to_uint(value),
		                       properties[PROP_TOPICLEN]);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_USERLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_USERLEN,
		                       ibis_str_to_uint(value), properties[PROP_USERLEN]);
	} else {
		GRegex *regex = NULL;
		GValue *variant_value = g_new0(GValue, 1);

		/* we're assuming we only get positive integers when receiving numeric
		 * values
		 */
		regex = g_regex_new("^[0-9]+$", 0, 0, NULL);

		if(g_regex_match(regex, value, 0, NULL)) {
			g_value_init(variant_value, G_TYPE_UINT);
			g_value_set_uint(variant_value, ibis_str_to_uint(value));
		} else if(!ibis_str_is_empty(value)) {
			g_value_init(variant_value, G_TYPE_STRING);
			g_value_set_string(variant_value, value);
		} else {
			g_value_init(variant_value, G_TYPE_BOOLEAN);
			g_value_set_boolean(variant_value, TRUE);
		}
		g_hash_table_insert(features->features, g_strdup(feature), variant_value);
		g_regex_unref(regex);
	}
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(IbisFeatures, ibis_features, G_TYPE_OBJECT)

static void
ibis_features_finalize(GObject *obj) {
	IbisFeatures *features = IBIS_FEATURES(obj);

	g_hash_table_destroy(features->features);

	G_OBJECT_CLASS(ibis_features_parent_class)->finalize(obj);
}
static void
ibis_features_get_property(GObject *obj, guint param_id, GValue *value,
                           GParamSpec *pspec)
{
	IbisFeatures *features = IBIS_FEATURES(obj);

	switch(param_id) {
	case PROP_AWAYLEN:
		g_value_set_uint(value, ibis_features_get_awaylen(features));
		break;
	case PROP_CASEMAPPING:
		g_value_set_string(value, ibis_features_get_casemapping(features));
		break;
	case PROP_CHANLIMIT:
		g_value_set_string(value, ibis_features_get_chanlimit(features));
		break;
	case PROP_CHANMODES:
		g_value_set_string(value, ibis_features_get_chanmodes(features));
		break;
	case PROP_CHANNELLEN:
		g_value_set_uint(value, ibis_features_get_channellen(features));
		break;
	case PROP_CHANTYPES:
		g_value_set_string(value, ibis_features_get_chantypes(features));
		break;
	case PROP_ELIST:
		g_value_set_string(value, ibis_features_get_elist(features));
		break;
	case PROP_EXCEPTS:
		g_value_set_schar(value, ibis_features_get_excepts(features));
		break;
	case PROP_EXTBAN:
		g_value_set_string(value, ibis_features_get_extban(features));
		break;
	case PROP_HOSTLEN:
		g_value_set_uint(value, ibis_features_get_hostlen(features));
		break;
	case PROP_INVEX:
		g_value_set_schar(value, ibis_features_get_invex(features));
		break;
	case PROP_KICKLEN:
		g_value_set_uint(value, ibis_features_get_kicklen(features));
		break;
	case PROP_MAXLIST:
		g_value_set_string(value, ibis_features_get_maxlist(features));
		break;
	case PROP_MAXTARGETS:
		g_value_set_uint(value, ibis_features_get_maxtargets(features));
		break;
	case PROP_MODES:
		g_value_set_uint(value, ibis_features_get_modes(features));
		break;
	case PROP_NETWORK:
		g_value_set_string(value, ibis_features_get_network(features));
		break;
	case PROP_NICKLEN:
		g_value_set_uint(value, ibis_features_get_nicklen(features));
		break;
	case PROP_PREFIX:
		g_value_set_string(value, ibis_features_get_prefix(features));
		break;
	case PROP_SAFELIST:
		g_value_set_boolean(value, ibis_features_get_safelist(features));
		break;
	case PROP_SILENCE:
		g_value_set_uint(value, ibis_features_get_silence(features));
		break;
	case PROP_STATUSMSG:
		g_value_set_string(value, ibis_features_get_statusmsg(features));
		break;
	case PROP_TARGMAX:
		g_value_set_string(value, ibis_features_get_targmax(features));
		break;
	case PROP_TOPICLEN:
		g_value_set_uint(value, ibis_features_get_topiclen(features));
		break;
	case PROP_USERLEN:
		g_value_set_uint(value, ibis_features_get_userlen(features));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
ibis_features_init(IbisFeatures *features) {
	features->features = g_hash_table_new_full(g_str_hash, g_str_equal,
	                                                 g_free,
	                                                 ibis_features_free_gvalue);
	/* set the default values specified at
	 * https://modern.ircdocs.horse/#rplisupport-parameters
	 */
	ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, "#", NULL);
	ibis_features_set_string(features, IBIS_FEATURE_PREFIX, "(ov)@+", NULL);
}

static void
ibis_features_class_init(IbisFeaturesClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->get_property = ibis_features_get_property;
	obj_class->finalize = ibis_features_finalize;

	/**
	 * IbisFeatures:awaylen:
	 *
	 * The [const@FEATURE_AWAYLEN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_AWAYLEN] = g_param_spec_uint(
		"awaylen", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:casemapping:
	 *
	 * The [const@FEATURE_CASEMAPPING] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_CASEMAPPING] = g_param_spec_string(
		"casemapping", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:chanlimit:
	 *
	 * The [const@FEATURE_CHANLIMIT] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_CHANLIMIT] = g_param_spec_string(
		"chanlimit", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:chanmodes:
	 *
	 * The [const@FEATURE_CHANMODES] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_CHANMODES] = g_param_spec_string(
		"chanmodes", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:channellen:
	 *
	 * The [const@FEATURE_CHANNELLEN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_CHANNELLEN] = g_param_spec_uint(
		"channellen", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:chantypes:
	 *
	 * The [const@FEATURE_CHANTYPES] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_CHANTYPES] = g_param_spec_string(
		"chantypes", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:elist:
	 *
	 * The [const@FEATURE_ELIST] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_ELIST] = g_param_spec_string(
		"elist", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:excepts:
	 *
	 * The [const@FEATURE_EXCEPTS] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_EXCEPTS] = g_param_spec_char(
		"excepts", NULL, NULL,
		0, G_MAXINT8, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:extban:
	 *
	 * The [const@FEATURE_EXTBAN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_EXTBAN] = g_param_spec_string(
		"extban", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:hostlen:
	 *
	 * The [const@FEATURE_HOSTLEN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_HOSTLEN] = g_param_spec_uint(
		"hostlen", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:invex:
	 *
	 * The [const@FEATURE_INVEX] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_INVEX] = g_param_spec_char(
		"invex", NULL, NULL,
		0, G_MAXINT8, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:kicklen:
	 *
	 * The [const@FEATURE_KICKLEN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_KICKLEN] = g_param_spec_uint(
		"kicklen", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:maxlist:
	 *
	 * The [const@FEATURE_MAXLIST] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_MAXLIST] = g_param_spec_string(
		"maxlist", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:maxtargets:
	 *
	 * The [const@FEATURE_MAXTARGETS] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_MAXTARGETS] = g_param_spec_uint(
		"maxtargets", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:modes:
	 *
	 * The [const@FEATURE_MODES] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_MODES] = g_param_spec_uint(
		"modes", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:network:
	 *
	 * The [const@FEATURE_NETWORK] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_NETWORK] = g_param_spec_string(
		"network", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:nicklen:
	 *
	 * The [const@FEATURE_NICKLEN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_NICKLEN] = g_param_spec_uint(
		"nicklen", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:prefix:
	 *
	 * The [const@FEATURE_PREFIX] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_PREFIX] = g_param_spec_string(
		"prefix", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:safelist:
	 *
	 * The [const@FEATURE_SAFELIST] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_SAFELIST] = g_param_spec_boolean(
		"safelist", NULL, NULL,
		FALSE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:silence:
	 *
	 * The [const@FEATURE_SILENCE] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_SILENCE] = g_param_spec_uint(
		"silence", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:statusmsg:
	 *
	 * The [const@FEATURE_STATUSMSG] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_STATUSMSG] = g_param_spec_string(
		"statusmsg", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:targmax:
	 *
	 * The [const@FEATURE_TARGMAX] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_TARGMAX] = g_param_spec_string(
		"targmax", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:topiclen:
	 *
	 * The [const@FEATURE_TOPICLEN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_TOPICLEN] = g_param_spec_uint(
		"topiclen", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * IbisFeatures:userlen:
	 *
	 * The [const@FEATURE_USERLEN] parameter of the features object.
	 *
	 * Since: 0.3
	 */
	properties[PROP_USERLEN] = g_param_spec_uint(
		"userlen", NULL, NULL,
		0, G_MAXUINT32, 0,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}


/******************************************************************************
 * Public API
 *****************************************************************************/
IbisFeatures *
ibis_features_new(void) {
	return g_object_new(IBIS_TYPE_FEATURES, NULL);
}

gboolean
ibis_features_parse(IbisFeatures *features, IbisMessage *message) {
	GStrv params = NULL;
	guint param_count = 0;
	guint i;
	GRegex *regex = NULL;
	GMatchInfo *info = NULL;
	gboolean matches = FALSE;
	const char *command = NULL;

	command = ibis_message_get_command(message);
	if(g_strcmp0(command, IBIS_RPL_ISUPPORT) != 0) {
		return FALSE;
	}

	/* Freeze notification since ibis_features_set_feature will notify for changed
	 * properties.
	 */
	g_object_freeze_notify(G_OBJECT(features));

	params = ibis_message_get_params(message);
	param_count = g_strv_length(params);
	regex = g_regex_new("(?<key>[A-Z0-9]+)(?:=(?<value>\\S+))?", 0, 0, NULL);

	for(i = 0; i < param_count; ++i) {
		matches = g_regex_match_full(regex, params[i], -1, 0, 0, &info, NULL);
		if (matches && g_match_info_matches(info)) {
			char *key = NULL;
			char *value = NULL;

			key = g_match_info_fetch_named(info, "key");
			value = g_match_info_fetch_named(info, "value");
			ibis_features_set_feature(features, key, value);
			g_free(key);
			g_free(value);
		}
		g_match_info_unref(info);
	}
	g_regex_unref(regex);

	g_object_thaw_notify(G_OBJECT(features));

	return TRUE;
}

guint
ibis_features_get_awaylen(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_AWAYLEN);
}

const char *
ibis_features_get_casemapping(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_CASEMAPPING);;
}

const char *
ibis_features_get_chanlimit(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_CHANLIMIT);
}

const char *
ibis_features_get_chanmodes(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_CHANMODES);
}

const char *
ibis_features_get_chantypes(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_CHANTYPES);
}

guint
ibis_features_get_channellen(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_CHANNELLEN);
}

const char *
ibis_features_get_elist(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_ELIST);
}

char
ibis_features_get_excepts(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_char(features, IBIS_FEATURE_EXCEPTS);
}

const char *
ibis_features_get_extban(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_EXTBAN);
}

guint
ibis_features_get_hostlen(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_HOSTLEN);
}

char
ibis_features_get_invex(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_char(features, IBIS_FEATURE_INVEX);
}

guint
ibis_features_get_kicklen(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_KICKLEN);
}

const char *
ibis_features_get_maxlist(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_MAXLIST);
}

guint
ibis_features_get_maxtargets(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_MAXTARGETS);
}

guint
ibis_features_get_modes(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_MODES);
}

const char *
ibis_features_get_network(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_NETWORK);
}

guint
ibis_features_get_nicklen(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_NICKLEN);
}

const char *
ibis_features_get_prefix(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_PREFIX);
}

gboolean
ibis_features_get_safelist(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), FALSE);

	return ibis_features_get_boolean(features, IBIS_FEATURE_SAFELIST);
}

guint
ibis_features_get_silence(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_SILENCE);
}

const char *
ibis_features_get_statusmsg(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_STATUSMSG);
}

const char *
ibis_features_get_targmax(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	return ibis_features_get_string(features, IBIS_FEATURE_TARGMAX);
}

guint
ibis_features_get_topiclen(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_TOPICLEN);
}

guint
ibis_features_get_userlen(IbisFeatures *features) {
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	return ibis_features_get_uint(features, IBIS_FEATURE_USERLEN);
}

guint
ibis_features_get_uint(IbisFeatures *features, const char *parameter) {
	GValue *value = NULL;
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	value = g_hash_table_lookup(features->features, parameter);

	if(value == NULL || !G_VALUE_HOLDS_UINT(value)) {
		return 0;
	}

	return g_value_get_uint(value);
}

const char *
ibis_features_get_string(IbisFeatures *features, const char *parameter) {
	GValue *value = NULL;
	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	value = g_hash_table_lookup(features->features, parameter);

	if(value == NULL || !G_VALUE_HOLDS_STRING(value)) {
		return NULL;
	}

	return g_value_get_string(value);
}

gboolean
ibis_features_get_boolean(IbisFeatures *features, const char *parameter) {
	GValue *value = NULL;
	g_return_val_if_fail(IBIS_IS_FEATURES(features), FALSE);

	value = g_hash_table_lookup(features->features, parameter);

	if(value == NULL || !G_VALUE_HOLDS_BOOLEAN(value)) {
		return FALSE;
	}

	return g_value_get_boolean(value);
}

char
ibis_features_get_char(IbisFeatures *features, const char *parameter) {
	GValue *value = NULL;
	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	value = g_hash_table_lookup(features->features, parameter);

	if(value == NULL || !G_VALUE_HOLDS_CHAR(value)) {
		return 0;
	}

	return g_value_get_schar(value);
}
