/*
 * 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 {
	SIG_CHANGED,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

struct _IbisFeatures {
	GObject parent;

	GHashTable *features;
};

/******************************************************************************
 * Helpers
 *****************************************************************************/
static 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 *feature,
                       guint uvalue)
{
	GQuark quark = g_quark_from_string(feature);
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features,
	                                     GINT_TO_POINTER(quark));
	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, GINT_TO_POINTER(quark), value);

	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
}

static void
ibis_features_set_string(IbisFeatures *features, const char *feature,
                         const char *svalue)
{
	GQuark quark = g_quark_from_string(feature);
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features,
	                                     GINT_TO_POINTER(quark));
	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, GINT_TO_POINTER(quark), value);

	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
}

static void
ibis_features_set_char(IbisFeatures *features, const char *feature,
                       char cvalue)
{
	GQuark quark = g_quark_from_string(feature);
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features,
	                                     GINT_TO_POINTER(quark));
	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, GINT_TO_POINTER(quark), value);

	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
}

static void
ibis_features_set_boolean(IbisFeatures *features, const char *feature,
                          gboolean bvalue)
{
	GQuark quark = g_quark_from_string(feature);
	GValue *existing_value = NULL;
	GValue *value = NULL;

	existing_value = g_hash_table_lookup(features->features,
	                                     GINT_TO_POINTER(quark));
	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, GINT_TO_POINTER(quark), value);

	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
}

/**
 * ibis_features_parse_prefix: (skip)
 * @features: The instance.
 * @value: The value of the PREFIX feature.
 *
 * Parses the value of the PREFIX feature and adds the synthetic MODES and
 * PREFIXES features.
 */
static void
ibis_features_parse_prefix(IbisFeatures *features, const char *prefix) {
	GRegex *regex = NULL;
	GMatchInfo *info = NULL;
	gboolean matches = FALSE;

	regex = g_regex_new("^\\((?P<modes>.+)\\)(?P<prefixes>.+)$",
	                    G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT,
	                    NULL);

	matches = g_regex_match(regex, prefix, G_REGEX_MATCH_DEFAULT, &info);
	if(matches) {
		char *modes = NULL;
		char *prefixes = NULL;

		modes = g_match_info_fetch_named(info, "modes");
		prefixes = g_match_info_fetch_named(info, "prefixes");

		if(strlen(modes) == strlen(prefixes)) {
			ibis_features_set_string(features, IBIS_FEATURE_PREFIX_MODES, modes);

			ibis_features_set_string(features, IBIS_FEATURE_PREFIX_PREFIXES,
			                         prefixes);
		} else {
			g_warning("format of prefix '%s' is invalid", prefix);
		}

		g_free(modes);
		g_free(prefixes);
	}

	g_clear_pointer(&info, g_match_info_unref);
	g_clear_pointer(&regex, g_regex_unref);
}

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));
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CASEMAPPING)) {
		ibis_features_set_string(features, IBIS_FEATURE_CASEMAPPING, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANLIMIT)) {
		ibis_features_set_string(features, IBIS_FEATURE_CHANLIMIT, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANMODES)) {
		ibis_features_set_string(features, IBIS_FEATURE_CHANMODES, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANNELLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_CHANNELLEN,
		                       ibis_str_to_uint(value));
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANTYPES)) {
		ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_CLIENTTAGDENY)) {
		ibis_features_set_string(features, IBIS_FEATURE_CLIENTTAGDENY, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_ELIST)) {
		ibis_features_set_string(features, IBIS_FEATURE_ELIST, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_EXCEPTS)) {
		if(strlen(value) == 1) {
			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, value[0]);
		} else {
			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, 'e');
		}
	} else if(ibis_str_equal(feature, IBIS_FEATURE_EXTBAN)) {
		ibis_features_set_string(features, IBIS_FEATURE_EXTBAN, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_HOSTLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_HOSTLEN,
		                       ibis_str_to_uint(value));
	} else if(ibis_str_equal(feature, IBIS_FEATURE_INVEX)) {
		if(strlen(value) == 1) {
			ibis_features_set_char(features, IBIS_FEATURE_INVEX, value[0]);
		} else {
			ibis_features_set_char(features, IBIS_FEATURE_INVEX, 'I');
		}
	} else if(ibis_str_equal(feature, IBIS_FEATURE_KICKLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_KICKLEN,
		                       ibis_str_to_uint(value));
	} else if(ibis_str_equal(feature, IBIS_FEATURE_MAXLIST)) {
		ibis_features_set_string(features, IBIS_FEATURE_MAXLIST, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_MAXTARGETS)) {
		ibis_features_set_uint(features, IBIS_FEATURE_MAXTARGETS,
		                       ibis_str_to_uint(value));
	} 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);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_NETWORK)) {
		ibis_features_set_string(features, IBIS_FEATURE_NETWORK, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_NICKLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_NICKLEN,
		                       ibis_str_to_uint(value));
	} else if(ibis_str_equal(feature, IBIS_FEATURE_PREFIX)) {
		ibis_features_set_string(features, IBIS_FEATURE_PREFIX, value);
		ibis_features_parse_prefix(features, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_SAFELIST)) {
		ibis_features_set_boolean(features, IBIS_FEATURE_SAFELIST, TRUE);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_SILENCE)) {
		ibis_features_set_uint(features, IBIS_FEATURE_SILENCE,
		                       ibis_str_to_uint(value));
	} else if(ibis_str_equal(feature, IBIS_FEATURE_STATUSMSG)) {
		ibis_features_set_string(features, IBIS_FEATURE_STATUSMSG, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_TARGMAX)) {
		ibis_features_set_string(features, IBIS_FEATURE_TARGMAX, value);
	} else if(ibis_str_equal(feature, IBIS_FEATURE_TOPICLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_TOPICLEN,
		                       ibis_str_to_uint(value));
	} else if(ibis_str_equal(feature, IBIS_FEATURE_USERLEN)) {
		ibis_features_set_uint(features, IBIS_FEATURE_USERLEN,
		                       ibis_str_to_uint(value));
	} else {
		GRegex *regex = NULL;

		/* 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)) {
			ibis_features_set_uint(features, feature, ibis_str_to_uint(value));
		} else if(!ibis_str_is_empty(value)) {
			ibis_features_set_string(features, feature, value);
		} else {
			ibis_features_set_boolean(features, feature, TRUE);
		}

		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_init(IbisFeatures *features) {
	features->features = g_hash_table_new_full(g_direct_hash, g_direct_equal,
	                                           NULL,
	                                           ibis_features_free_gvalue);

	ibis_features_clear(features);
}

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

	obj_class->finalize = ibis_features_finalize;

	/**
	 * IbisFeatures::changed:
	 * @features: the instance
	 * @feature: the name of the feature that changed
	 *
	 * Emitted when a feature changes.
	 *
	 * This signal supports *details* so you can connect to it with something
	 * like `changed::chantypes` so your callback is only called if the
	 * `chantypes` feature changed.
	 *
	 * Note: This will also be called when a feature is removed.
	 *
	 * Since: 0.4
	 */
	signals[SIG_CHANGED] = g_signal_new_class_handler(
		"changed",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		1,
		G_TYPE_STRING);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
void
ibis_features_clear(IbisFeatures *features) {
	GHashTableIter iter;
	gpointer key = NULL;
	const char *default_prefix = "(ov)@+";

	g_return_if_fail(IBIS_IS_FEATURES(features));

	g_hash_table_iter_init(&iter, features->features);
	while(g_hash_table_iter_next(&iter, &key, NULL)) {
		GQuark detail = GPOINTER_TO_INT(key);

		g_hash_table_iter_remove(&iter);

		g_signal_emit(features, signals[SIG_CHANGED], detail,
		              g_quark_to_string(detail));
	}

	/* Set the default values specified at
	 * https://modern.ircdocs.horse/#rplisupport-parameters
	 */
	ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, "#");
	ibis_features_set_string(features, IBIS_FEATURE_PREFIX, default_prefix);
	ibis_features_parse_prefix(features, default_prefix);
}

gboolean
ibis_features_client_tag_denied(IbisFeatures *features, const char *tag) {
	const char *clienttagdenied = NULL;
	gboolean denied = FALSE;
	GStrv denied_tags = NULL;
	guint n_denied_tags = 0;
	const char *queried_tag = NULL;

	g_return_val_if_fail(IBIS_IS_FEATURES(features), FALSE);
	g_return_val_if_fail(!ibis_str_is_empty(tag), FALSE);

	clienttagdenied = ibis_features_get_string(features,
	                                           IBIS_FEATURE_CLIENTTAGDENY);
	/* An empty or missing CLIENTTAGDENY matches the default case and indicates
	 * that all client-only tags are allowed.
	 */
	if(ibis_str_is_empty(clienttagdenied)) {
		return FALSE;
	}

	/* An asterisk * (0x2A) indicates that all client-only tags are blocked. */
	if(ibis_str_equal(clienttagdenied, "*")) {
		return TRUE;
	}

	/* The CLIENTTAGDENY token value is a comma , (0x2C) separated list of blocked
	 * client-only tags. The client-only prefix (+) is omitted when a tag appears
	 * in this list.
	 */
	if(tag[0] == '+') {
		queried_tag = tag + 1;
	} else {
		queried_tag = tag;
	}
	denied_tags = g_strsplit(clienttagdenied, ",", 0);
	n_denied_tags = g_strv_length(denied_tags);

	if(n_denied_tags == 1) {
		denied = ibis_str_equal(denied_tags[0], queried_tag);
		g_strfreev(denied_tags);

		return denied;
	}

	if(ibis_str_equal(denied_tags[0], "*")) {
		/* If the first denied tag is '*', subsequent tags are expected to start with
		 * a hyphen to exempt particular tags from the catch-all.
		 */
		denied = TRUE;

		for(guint i = 1; i < n_denied_tags; i++) {
			const char *allowed_tag = NULL;

			if(denied_tags[i][0] != '-') {
				continue;
			}

			allowed_tag = denied_tags[i] + 1;
			if(ibis_str_equal(allowed_tag, queried_tag)) {
				denied = FALSE;
				break;
			}
		}
	} else {
		denied = FALSE;

		for(guint i = 0; i < n_denied_tags; i++) {
			if(ibis_str_equal(denied_tags[i], queried_tag)) {
				denied = TRUE;
				break;
			}
		}
	}

	g_strfreev(denied_tags);

	return denied;
}

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;
	}

	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);

	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);
}

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

	return ibis_features_get_string(features, IBIS_FEATURE_PREFIX_MODES);
}

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

	return ibis_features_get_string(features, IBIS_FEATURE_PREFIX_PREFIXES);
}

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) {
	GQuark quark = 0;
	GValue *value = NULL;

	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	quark = g_quark_from_string(parameter);
	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));

	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) {
	GQuark quark = 0;
	GValue *value = NULL;

	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);

	quark = g_quark_from_string(parameter);
	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));

	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) {
	GQuark quark = 0;
	GValue *value = NULL;

	g_return_val_if_fail(IBIS_IS_FEATURES(features), FALSE);

	quark = g_quark_from_string(parameter);
	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));

	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) {
	GQuark quark = 0;
	GValue *value = NULL;

	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);

	quark = g_quark_from_string(parameter);
	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));

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

	return g_value_get_schar(value);
}
