/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple 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 <birb.h>

#include "purpletags.h"

enum {
	SIG_ADDED,
	SIG_REMOVED,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

struct _PurpleTags {
	GObject parent;

	GPtrArray *tags;
};

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
purple_tags_real_add(PurpleTags *tags, const char *tag, const char *name,
                     const char *value)
{
	/* If this tag exists, remove it. */
	purple_tags_remove(tags, tag);

	/* Add the new tag. */
	g_ptr_array_add(tags->tags, birb_string_object_new(tag));

	/* Finally emit the signal. */
	g_signal_emit(tags, signals[SIG_ADDED], 0, tag, name, value);
	g_list_model_items_changed(G_LIST_MODEL(tags), tags->tags->len - 1, 0, 1);
}

static gboolean
purple_tags_real_remove(PurpleTags *tags, const char *tag, const char *name,
                        const char *value)
{
	/* Walk through the tags looking for the one that was passed in. */
	for(guint i = 0; i < tags->tags->len; i++) {
		BirbStringObject *etag = NULL;
		const char *existing = NULL;

		etag = g_ptr_array_index(tags->tags, i);
		existing = birb_string_object_get_string(etag);

		/* If we found it, remove it and exit early. */
		if(birb_str_equal(existing, tag)) {
			g_ptr_array_remove_index(tags->tags, i);

			g_signal_emit(tags, signals[SIG_REMOVED], 0, tag, name, value);
			g_list_model_items_changed(G_LIST_MODEL(tags), i, 1, 0);

			return TRUE;
		}
	}

	return FALSE;
}

/******************************************************************************
 * GListModel Implementation
 *****************************************************************************/
static GType
purple_tags_get_item_type(G_GNUC_UNUSED GListModel *model) {
	return BIRB_TYPE_STRING_OBJECT;
}

static guint
purple_tags_get_n_items(GListModel *list) {
	PurpleTags *tags = PURPLE_TAGS(list);

	return tags->tags->len;
}

static gpointer
purple_tags_get_item(GListModel *list, guint position) {
	PurpleTags *tags = PURPLE_TAGS(list);
	BirbStringObject *tag = NULL;

	if(position < tags->tags->len) {
		tag = g_ptr_array_index(tags->tags, position);
		g_object_ref(tag);
	}

	return tag;
}

static void
purple_tags_list_model_init(GListModelInterface *iface) {
	iface->get_item_type = purple_tags_get_item_type;
	iface->get_n_items = purple_tags_get_n_items;
	iface->get_item = purple_tags_get_item;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleTags, purple_tags, G_TYPE_OBJECT,
                              G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
                                                    purple_tags_list_model_init));

static void
purple_tags_finalize(GObject *obj) {
	PurpleTags *tags = PURPLE_TAGS(obj);

	g_clear_pointer(&tags->tags, g_ptr_array_unref);

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

static void
purple_tags_init(PurpleTags *tags) {
	tags->tags = g_ptr_array_new_full(0, g_object_unref);
}

static void
purple_tags_class_init(PurpleTagsClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = purple_tags_finalize;

	/**
	 * PurpleTags::added:
	 * @tags: The instance.
	 * @tag: The tag value.
	 * @name: The name of the tag.
	 * @value: The value of the tag.
	 *
	 * Emitted when a tag is added. The tag as well as its name and value are
	 * provided to be as flexible as possible.
	 *
	 * > NOTE: When a duplicate tag is added, the original one will be removed
	 * > which will emit the [signal@Tags::removed] signal and then the new tag
	 * > will be added which will emit [signal@Tags::added].
	 *
	 * Since: 3.0
	 */
	signals[SIG_ADDED] = g_signal_new_class_handler(
		"added",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		3,
		G_TYPE_STRING,
		G_TYPE_STRING,
		G_TYPE_STRING);

	/**
	 * PurpleTags::removed:
	 * @tags: The instance.
	 * @tag: The tag value.
	 * @name: The name of the tag.
	 * @value: The value of the tag.
	 *
	 * Emitted when a tag is removed. The tag as well as its name and value are
	 * provided to be as flexible as possible.
	 *
	 * > NOTE: When a duplicate tag is added, the original one will be removed
	 * > which will emit the [signal@Tags::removed] signal and then the new tag
	 * > will be added which will emit [signal@Tags::added].
	 *
	 * Since: 3.0
	 */
	signals[SIG_REMOVED] = g_signal_new_class_handler(
		"removed",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		3,
		G_TYPE_STRING,
		G_TYPE_STRING,
		G_TYPE_STRING);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
PurpleTags *
purple_tags_new(void) {
	return g_object_new(PURPLE_TYPE_TAGS, NULL);
}

gboolean
purple_tags_exists(PurpleTags *tags, const char *tag) {
	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
	g_return_val_if_fail(!birb_str_is_empty(tag), FALSE);

	for(guint i = 0; i < tags->tags->len; i++) {
		BirbStringObject *obj = NULL;
		const char *existing = NULL;

		obj = g_ptr_array_index(tags->tags, i);
		existing = birb_string_object_get_string(obj);

		if(birb_str_equal(existing, tag)) {
			return TRUE;
		}
	}

	return FALSE;
}

const char *
purple_tags_lookup(PurpleTags *tags, const char *name, gboolean *found) {
	size_t name_len = 0;

	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
	g_return_val_if_fail(name != NULL, FALSE);

	/* Assume we're going to find the tag, if we don't we set found to false
	 * before we return. This sounds silly, but it saves some additional logic
	 * below.
	 */
	if(found) {
		*found = TRUE;
	}

	name_len = strlen(name);

	for(guint i = 0; i < tags->tags->len; i++) {
		BirbStringObject *obj = g_ptr_array_index(tags->tags, i);
		const char *tag = birb_string_object_get_string(obj);

		if(g_str_has_prefix(tag, name)) {
			const char *value = tag + name_len;

			if(*value == '\0') {
				return NULL;
			} else if(*value == ':') {
				return value+1;
			}
		}
	}

	/* We didn't find the tag, so set found to false if necessary. */
	if(found) {
		*found = FALSE;
	}

	return NULL;
}

const char *
purple_tags_get(PurpleTags *tags, const char *name) {
	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
	g_return_val_if_fail(name != NULL, NULL);

	return purple_tags_lookup(tags, name, NULL);
}

void
purple_tags_add(PurpleTags *tags, const char *tag) {
	char *name = NULL;
	char *value = NULL;

	g_return_if_fail(PURPLE_IS_TAGS(tags));
	g_return_if_fail(tag != NULL);

	purple_tag_split(tag, &name, &value);
	purple_tags_real_add(tags, tag, name, value);
	g_clear_pointer(&name, g_free);
	g_clear_pointer(&value, g_free);
}

void
purple_tags_add_with_value(PurpleTags *tags, const char *name,
                           const char *value)
{
	char *tag = NULL;

	g_return_if_fail(PURPLE_IS_TAGS(tags));
	g_return_if_fail(name != NULL);

	if(value != NULL) {
		tag = g_strdup_printf("%s:%s", name, value);
	} else {
		tag = g_strdup(name);
	}

	purple_tags_real_add(tags, tag, name, value);

	g_free(tag);
}

gboolean
purple_tags_remove(PurpleTags *tags, const char *tag) {
	char *name = NULL;
	char *value = NULL;
	gboolean ret = FALSE;

	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
	g_return_val_if_fail(tag != NULL, FALSE);

	purple_tag_split(tag, &name, &value);
	ret = purple_tags_real_remove(tags, tag, name, value);
	g_clear_pointer(&name, g_free);
	g_clear_pointer(&value, g_free);

	return ret;
}

gboolean
purple_tags_remove_with_value(PurpleTags *tags, const char *name, const char *value) {
	char *tag = NULL;
	gboolean ret = FALSE;

	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
	g_return_val_if_fail(name != NULL, FALSE);

	/* If there's no value, the tag and name are the same so we can avoid an
	 * unnecessary allocation.
	 */
	if(value == NULL) {
		return purple_tags_real_remove(tags, name, name, NULL);
	}

	tag = g_strdup_printf("%s:%s", name, value);
	ret = purple_tags_real_remove(tags, tag, name, value);
	g_free(tag);

	return ret;
}

void
purple_tags_remove_all(PurpleTags *tags) {
	guint len = 0;

	g_return_if_fail(PURPLE_IS_TAGS(tags));

	len = tags->tags->len;
	if(len > 0) {
		g_ptr_array_remove_range(tags->tags, 0, len);
		g_list_model_items_changed(G_LIST_MODEL(tags), 0, len, 0);
	}
}

PurpleTags *
purple_tags_get_all_with_name(PurpleTags *tags, const char *name) {
	PurpleTags *filtered = NULL;
	size_t name_len = 0;

	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);
	g_return_val_if_fail(!birb_str_is_empty(name), NULL);

	name_len = strlen(name);
	filtered = purple_tags_new();

	for(guint i = 0; i < tags->tags->len; i++) {
		BirbStringObject *obj = NULL;
		const char *str = NULL;

		obj = g_ptr_array_index(tags->tags, i);
		str = birb_string_object_get_string(obj);

		if(g_str_has_prefix(str, name)) {
			const char *value = str + name_len;

			/* Make sure this is a tag with no value and that we didn't get a
			* partial match on the name.
			*/
			if(*value == '\0' || *value == ':') {
				g_ptr_array_add(filtered->tags, g_object_ref(obj));
			}
		}
	}

	return filtered;
}

char *
purple_tags_to_string(PurpleTags *tags, const char *separator) {
	GString *value = NULL;

	g_return_val_if_fail(PURPLE_IS_TAGS(tags), NULL);

	value = g_string_new("");

	for(guint i = 0; i < tags->tags->len; i++) {
		BirbStringObject *obj = NULL;
		const char *tag = NULL;

		obj = g_ptr_array_index(tags->tags, i);
		tag = birb_string_object_get_string(obj);

		g_string_append(value, tag);

		if(separator != NULL && i < tags->tags->len - 1) {
			g_string_append(value, separator);
		}
	}

	return g_string_free_and_steal(value);
}

void
purple_tag_split(const char *tag, char **name, char **value) {
	const char *colon = NULL;

	g_return_if_fail(tag != NULL);

	colon = g_strstr_len(tag, -1, ":");
	if(colon == NULL) {
		if(name != NULL) {
			*name = g_strdup(tag);
		}
		if(value != NULL) {
			*value = NULL;
		}
	} else {
		if(name != NULL) {
			*name = g_strndup(tag, colon - tag);
		}
		if(value != NULL) {
			*value = g_strdup(colon + 1);
		}
	}
}

gboolean
purple_tags_contains(PurpleTags *tags, PurpleTags *needle) {
	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
	g_return_val_if_fail(PURPLE_IS_TAGS(needle), FALSE);

	for(guint i = 0; i < needle->tags->len; i++) {
		BirbStringObject *obj = NULL;
		const char *needle_tag = NULL;

		obj = g_ptr_array_index(needle->tags, i);
		needle_tag = birb_string_object_get_string(obj);

		if(!purple_tags_exists(tags, needle_tag)) {
			return FALSE;
		}
	}

	return TRUE;
}
