/*
 * Copyright 2018 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#define G_LOG_DOMAIN "XbSilo"

#include "config.h"

#include <gio/gio.h>
#include <string.h>

#include "xb-string-private.h"

/**
 * xb_string_replace: (skip)
 * @str: The #GString to operate on
 * @search: The text to search for
 * @replace: The text to use for substitutions
 *
 * Performs multiple search and replace operations on the given string.
 *
 * Returns: the number of replacements done, or 0 if @search is not found.
 **/
guint
xb_string_replace(GString *str, const gchar *search, const gchar *replace)
{
	gchar *tmp;
	guint count = 0;
	gsize search_idx = 0;
	gsize replace_len;
	gsize search_len;

	g_return_val_if_fail(str != NULL, 0);
	g_return_val_if_fail(search != NULL, 0);
	g_return_val_if_fail(replace != NULL, 0);

	/* nothing to do */
	if (str->len == 0)
		return 0;

	search_len = strlen(search);
	replace_len = strlen(replace);

	do {
		tmp = g_strstr_len(str->str + search_idx, -1, search);
		if (tmp == NULL)
			break;

		/* advance the counter in case @replace contains @search */
		search_idx = (gsize)(tmp - str->str);

		/* reallocate the string if required */
		if (search_len > replace_len) {
			g_string_erase(str, (gssize)search_idx, (gssize)(search_len - replace_len));
			memcpy(tmp, replace, replace_len);
		} else if (search_len < replace_len) {
			g_string_insert_len(str,
					    (gssize)search_idx,
					    replace,
					    (gssize)(replace_len - search_len));
			/* we have to treat this specially as it could have
			 * been reallocated when the insertion happened */
			memcpy(str->str + search_idx, replace, replace_len);
		} else {
			/* just memcmp in the new string */
			memcpy(tmp, replace, replace_len);
		}
		search_idx += replace_len;
		count++;
	} while (TRUE);

	return count;
}

/**
 * xb_string_append_union:
 * @xpath: The #GString to operate on
 * @fmt: The format string
 * @...: varargs for @fmt
 *
 * Appends an XPath query into the string, automatically adding the union
 * operator (`|`) if required.
 *
 * Since: 0.1.2
 **/
void
xb_string_append_union(GString *xpath, const gchar *fmt, ...)
{
	va_list args;

	g_return_if_fail(xpath != NULL);
	g_return_if_fail(fmt != NULL);

	if (xpath->len > 0)
		g_string_append(xpath, "|");
	va_start(args, fmt);
#pragma clang diagnostic ignored "-Wformat-nonliteral"
	g_string_append_vprintf(xpath, fmt, args);
#pragma clang diagnostic pop
	va_end(args);
}

/**
 * xb_string_contains: (skip)
 * @text: The source string
 * @search: The text to search for
 *
 * Searches for a substring match.
 *
 * Returns: %TRUE if the string @search is contained in @text.
 **/
gboolean
xb_string_contains(const gchar *text, const gchar *search)
{
	guint search_sz;
	guint text_sz;

	/* can't possibly match */
	if (text == NULL || search == NULL)
		return FALSE;

	/* sanity check */
	text_sz = strlen(text);
	search_sz = strlen(search);
	if (search_sz > text_sz)
		return FALSE;
	for (guint i = 0; i < text_sz - search_sz + 1; i++) {
		if (strncmp(text + i, search, search_sz) == 0)
			return TRUE;
	}
	return FALSE;
}

/**
 * xb_string_search: (skip)
 * @text: The source string
 * @search: The text to search for
 *
 * Searches for a fuzzy search match, ignoring search matches that are not at
 * the start of the token.
 *
 * For example, `foo` and `baz` would match `foobar baz` but `bar` would not
 * match the same string.
 *
 * Returns: %TRUE if the string @search is contained in @text.
 *
 * Since: 0.3.0
 **/
gboolean
xb_string_search(const gchar *text, const gchar *search)
{
	guint search_sz;
	guint text_sz;
	gboolean is_sow = TRUE;

	/* can't possibly match */
	if (text == NULL || text[0] == '\0')
		return FALSE;
	if (search == NULL || search[0] == '\0')
		return FALSE;

	/* sanity check */
	text_sz = strlen(text);
	search_sz = strlen(search);
	if (search_sz > text_sz)
		return FALSE;
	for (guint i = 0; i < text_sz - search_sz + 1; i++) {
		if (!g_ascii_isalnum(text[i])) {
			is_sow = TRUE;
			continue;
		}
		if (!is_sow)
			continue;
		if (g_ascii_strncasecmp(text + i, search, search_sz) == 0)
			return TRUE;
		/* no longer the start of the word */
		is_sow = FALSE;
	}
	return FALSE;
}

/**
 * xb_string_searchv: (skip)
 * @text: NULL-terminated source strings
 * @search: NULL-terminated text tokens to search for
 *
 * Searches for a fuzzy search match, ignoring search matches that are not at
 * the start of the token.
 *
 * For example `{"foo", "baz"}` would match with `{"wizz", "foobar"}` but
 * `{"baz"}` would not match the same set of strings.
 *
 * Returns: %TRUE if the string @search is contained in @text.
 *
 * Since: 0.3.0
 **/
gboolean
xb_string_searchv(const gchar **text, const gchar **search)
{
	if (text == NULL || text[0] == NULL || text[0][0] == '\0')
		return FALSE;
	if (search == NULL || search[0] == NULL || search[0][0] == '\0')
		return FALSE;
	for (guint j = 0; text[j] != NULL; j++) {
		for (guint i = 0; search[i] != NULL; i++) {
			if (g_str_has_prefix(text[j], search[i]))
				return TRUE;
		}
	}
	return FALSE;
}

/**
 * xb_string_token_valid: (skip)
 * @text: The potential token
 * @see_also: xb_builder_node_tokenize_text(), xb_builder_node_get_tokens()
 *
 * This is typically used to eliminate tokens which are not useful for search
 * matching.
 *
 * For instance, tokens like `in` and `at` are less than three characters in
 * size and should be rejected.
 *
 * Returns: %TRUE if the token should be used for searching.
 **/
gboolean
xb_string_token_valid(const gchar *text)
{
	if (text == NULL)
		return FALSE;
	if (text[0] == '\0' || text[1] == '\0' || text[2] == '\0')
		return FALSE;
	return TRUE;
}

/**
 * xb_string_escape:
 * @str: string, e.g. `app/org.gnome.ghex/x86_64/stable`
 *
 * Escapes XPath control sequences such as newlines, tabs, and forward slashes.
 *
 * Returns: (transfer full): new string that is safe to use for queries
 *
 * Since: 0.1.2
 **/
gchar *
xb_string_escape(const gchar *str)
{
	GString *tmp = g_string_new(str);
	xb_string_replace(tmp, "/", "\\/");
	xb_string_replace(tmp, "\t", "\\t");
	xb_string_replace(tmp, "\n", "\\n");
	return g_string_free(tmp, FALSE);
}

gchar *
xb_string_xml_escape(const gchar *str)
{
	GString *tmp = g_string_new(str);
	xb_string_replace(tmp, "&", "&amp;");
	xb_string_replace(tmp, "<", "&lt;");
	xb_string_replace(tmp, ">", "&gt;");
	xb_string_replace(tmp, "\"", "&quot;");
	return g_string_free(tmp, FALSE);
}

/* private */
gboolean
xb_string_isspace(const gchar *str, gssize strsz)
{
	gsize strsz_safe;
	if (str == NULL)
		return TRUE;
	strsz_safe = strsz >= 0 ? (gsize)strsz : strlen(str);
	for (gsize i = 0; i < strsz_safe; i++) {
		if (!g_ascii_isspace(str[i]))
			return FALSE;
	}
	return TRUE;
}

void
xb_guid_compute_for_data(XbGuid *out, const guint8 *buf, gsize bufsz)
{
	guint8 buf_tmp[20] = {0x0};
	gsize buf_tmpsz = sizeof(buf_tmp);
	g_autoptr(GChecksum) checksum = g_checksum_new(G_CHECKSUM_SHA1);
	if (buf != NULL && bufsz != 0)
		g_checksum_update(checksum, (const guchar *)buf, bufsz);
	g_checksum_get_digest(checksum, buf_tmp, &buf_tmpsz);
	memcpy(out, buf_tmp, sizeof(XbGuid));
}

gchar *
xb_guid_to_string(XbGuid *guid)
{
	return g_strdup_printf("%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x",
			       (guint)GUINT32_TO_BE(guid->tlo),
			       (guint)GUINT16_TO_BE(guid->tmi),
			       (guint)GUINT16_TO_BE(guid->thi),
			       (guint)GUINT16_TO_BE(guid->clo),
			       guid->nde[0],
			       guid->nde[1],
			       guid->nde[2],
			       guid->nde[3],
			       guid->nde[4],
			       guid->nde[5]);
}
