/*
 * ministream - minimal (subset) appstream metadata parser
 * Copyright (c) 2026  Florian Leander Singer <sp1rit@disroot.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include "ministream-priv.h"

static const gchar* ms_find_attr(const gchar* needle, const gchar** names, const gchar** values, const gchar* fallback) {
	for (gsize i = 0; names[i]; i++)
		if (g_strcmp0(needle, names[i]) == 0)
			return values[i];
	return fallback;
}

typedef struct {
	gchar* string;
	guint score;
} MsLocalizedString;

typedef struct _MsParserData MsParserData;

typedef struct {
	void(*child)(MsParserData* pdata, const gchar* name, const gchar** attr_names, const gchar** attr_values, GError** error);
	void(*child_end)(MsParserData* pdata, const gchar* name, GError** error);
	void(*text)(MsParserData* pdata, const gchar* text, gsize len, GError** error);
	void(*fin)(MsParserData* pdata, const gchar* name, GError** error);

	void(*destroy)(gpointer instance_data);
} MsParserClass;

typedef struct _MsParserClassNode MsParserClassNode;
struct _MsParserClassNode {
	const MsParserClass* class;
	const GSList* to;
	MsParserClassNode* parent;

	gpointer instance_data;
};

struct _MsParserData {
	GMarkupParseContext* ctx;

	gchar* locale;
	MsParserClassNode* parser;

	MsComponent* component;
};

static void ms_push_parser(MsParserData* pdata, const MsParserClass* class, gpointer instance_data) {
	MsParserClassNode node = {
		.class = class,
		.to = g_markup_parse_context_get_element_stack(pdata->ctx),
		.parent = pdata->parser,
		.instance_data = instance_data
	};
	pdata->parser = g_memdup2(&node, sizeof node);
}

static void
ms_start_element_cb(GMarkupParseContext* context,
                    const gchar* element_name,
                    const gchar** attr_names,
                    const gchar** attr_values,
                    gpointer user_data,
                    GError** error)
{
	MsParserData* pdata = user_data;
	pdata->parser->class->child(pdata, element_name, attr_names, attr_values, error);
}

static void
ms_end_element_cb (GMarkupParseContext* context,
                   const gchar* element_name,
                   gpointer user_data,
                   GError** error)
{
	MsParserData* pdata = user_data;

	if (pdata->parser->to == g_markup_parse_context_get_element_stack(context)) {
		if (pdata->parser->class->fin)
			pdata->parser->class->fin(pdata, element_name, error);
		MsParserClassNode* current = pdata->parser;
		pdata->parser = current->parent;
		if (current->class->destroy)
			current->class->destroy(current->instance_data);
		g_free(current);
	} else if (pdata->parser->class->child_end) {
		pdata->parser->class->child_end(pdata, element_name, error);
	}
}

static void
ms_text_cb (GMarkupParseContext* context,
            const gchar* text, gsize len,
            gpointer user_data,
            GError** error)
{
	MsParserData* pdata = user_data;
	if (pdata->parser->class->text)
		pdata->parser->class->text(pdata, text, len, error);
}


static void ms_skip_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {}
static const MsParserClass ms_skip_parser = {
	.child = ms_skip_parser_child
};


typedef struct {
	gchar** loc;
	GString* string;
} MsStringParser;
static void ms_string_reader_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	g_warning("Found <%s> while attempting to read string, ignoring", node);
	ms_push_parser(pdata, &ms_skip_parser, NULL);
}
static void ms_string_reader_parser_text(MsParserData* pdata, const gchar* text, gsize len, GError** error) {
	MsStringParser* data = pdata->parser->instance_data;
	g_string_append_len(data->string, text, len);
}
static void ms_string_reader_parser_fin(MsParserData* pdata, const gchar* node, GError** error) {
	MsStringParser* data = pdata->parser->instance_data;
	if (*data->loc)
		g_free(*data->loc);
	*data->loc = g_string_free(g_steal_pointer(&data->string), FALSE);

}
static void ms_string_reader_parser_destroy(gpointer instance_data) {
	MsStringParser* data = (MsStringParser*)instance_data;
	if (data->string)
		g_string_free(data->string, TRUE);
	g_free(data);
}
static const MsParserClass ms_string_parser = {
	.child = ms_string_reader_parser_child,
	.text = ms_string_reader_parser_text,
	.fin = ms_string_reader_parser_fin,
	.destroy = ms_string_reader_parser_destroy
};
static void ms_string_parser_push(MsParserData* pdata, gchar** loc) {
	g_assert(loc != NULL);
	MsStringParser data = {
		.string = g_string_new(NULL),
		.loc = loc
	};
	ms_push_parser(pdata, &ms_string_parser, g_memdup2(&data, sizeof data));
}

// ---

typedef struct {
	GString* markup;
	gboolean lastwasspace;
	gchar* suffix;
} MsMarkupParser;
static void ms_markup_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	MsMarkupParser* data = (MsMarkupParser*)pdata->parser->instance_data;

	if (g_strcmp0(node, "em") == 0 || g_strcmp0(node, "code") == 0) {
		g_string_append_printf(data->markup, "<%s>", node);
	} else {
		g_warning("Got invalid node <%s> in markup, ignoring", node);
		ms_push_parser(pdata, &ms_skip_parser, NULL);
	}
}
static void ms_markup_parser_child_end(MsParserData* pdata, const gchar* node, GError** error) {
	MsMarkupParser* data = (MsMarkupParser*)pdata->parser->instance_data;
	if (g_strcmp0(node, "em") == 0 || g_strcmp0(node, "code") == 0) {
		g_string_append_printf(data->markup, "</%s>", node);
		return;
	}

	g_warn_if_reached();
}
static void ms_markup_parser_text(MsParserData* pdata, const gchar* text, gsize len, GError** error) {
	MsMarkupParser* data = (MsMarkupParser*)pdata->parser->instance_data;
	GString* tmp = g_string_sized_new(len);
	for (gsize i = 0; i < len; i++) {
		if (text[i] == '\n' || text[i] == '\t' || text[i] == ' ') {
			if (data->lastwasspace)
				continue;
			g_string_append_c(tmp, ' ');
			data->lastwasspace = TRUE;
			continue;
		}

		g_string_append_c(tmp, text[i]);
		data->lastwasspace = FALSE;
	}

	gchar* escaped = g_markup_escape_text(tmp->str, tmp->len);
	g_string_append(data->markup, escaped);
	g_free(escaped);
	g_string_free(tmp, TRUE);
}
static void ms_markup_parser_fin(MsParserData* pdata, const gchar* node, GError** error) {
	MsMarkupParser* data = (MsMarkupParser*)pdata->parser->instance_data;
	if (data->suffix)
		g_string_append(data->markup, data->suffix);
}
static void ms_markup_parser_destroy(gpointer instance_data) {
	MsMarkupParser* data = (MsMarkupParser*)instance_data;
	if (data->suffix)
		g_free(data->suffix);
	g_free(data);
}
static const MsParserClass ms_markup_parser = {
	.child = ms_markup_parser_child,
	.child_end = ms_markup_parser_child_end,
	.text = ms_markup_parser_text,
	.fin = ms_markup_parser_fin,
	.destroy = ms_markup_parser_destroy
};

typedef struct {
	gchar** loc;

	GHashTable* desc;
	// 'o' for ordered, 'u' for unordered or 0
	gchar in_list;
} MsDescriptionParser;
static void ms_description_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	MsDescriptionParser* data = (MsDescriptionParser*)pdata->parser->instance_data;

	if (g_strcmp0(node, "p") == 0 || (data->in_list && g_strcmp0(node, "li") == 0)) {
		const gchar* lang = ms_find_attr("xml:lang", attr_names, attr_values, "C");
		guint score = ms_locale_score(pdata->locale, lang);

		if (score == 0 && g_strcmp0(lang, "C") != 0) {
			/* this locale is not for us */
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}

		GString* markup = g_hash_table_lookup(data->desc, lang);
		if (markup == NULL) {
			markup = g_string_new(NULL);
			g_hash_table_insert(data->desc, g_strdup(lang), markup);

			/* if we are within a list and the language is new, add the listing tag first */
			if (data->in_list)
				g_string_append_printf(markup, "<%cl>\n", data->in_list);
		}

		g_string_append_printf(markup, "<%s>", node);
		MsMarkupParser markup_data = { .markup = markup, .lastwasspace = TRUE, .suffix = g_strdup_printf("</%s>\n", node) };
		ms_push_parser(pdata, &ms_markup_parser, g_memdup2(&markup_data, sizeof markup_data));
	} else if (!data->in_list && (g_strcmp0(node, "ul") == 0 || g_strcmp0(node, "ol") == 0)) {
		/* append listing node tag to every locale string */
		GHashTableIter iter;
		gpointer value;
		g_hash_table_iter_init(&iter, data->desc);
		while (g_hash_table_iter_next(&iter, NULL, &value))
			g_string_append_printf(value, "<%s>\n", node);

		data->in_list = node[0];
	} else {
		g_warning("Unexpected tag <%s> in description, ignoring", node);
		ms_push_parser(pdata, &ms_skip_parser, NULL);
	}
}
static void ms_description_parser_child_end(MsParserData* pdata, const gchar* node, GError** error) {
	MsDescriptionParser* data = (MsDescriptionParser*)pdata->parser->instance_data;

	if (!data->in_list || (g_strcmp0(node, "ul") != 0 && g_strcmp0(node, "ol") != 0) || data->in_list != node[0]) {
		g_warning("got unxpected node </%s> in <description>", node);
		return;
	}

	/* close listing tags */
	GHashTableIter iter;
	gpointer value;
	g_hash_table_iter_init(&iter, data->desc);
	while (g_hash_table_iter_next(&iter, NULL, &value))
		g_string_append_printf(value, "</%s>\n", node);
	data->in_list = 0;
}
static void ms_description_parser_fin(MsParserData* pdata, const gchar* node, GError** error) {
	MsDescriptionParser* data = (MsDescriptionParser*)pdata->parser->instance_data;

	guint score = 0;
	GString* markup = g_hash_table_lookup(data->desc, "C");

	GHashTableIter iter;
	gpointer key, value;
	g_hash_table_iter_init(&iter, data->desc);
	while (g_hash_table_iter_next(&iter, &key, &value)) {
		guint nscore = ms_locale_score(pdata->locale, key);
		if (nscore > score) {
			score = nscore;
			markup = value;
		}
	}

	if (*data->loc)
		g_free(*data->loc);
	*data->loc = markup ? g_strdup(markup->str) : NULL;
}
static void ms_description_parser_destroy(gpointer instance_data) {
	MsDescriptionParser* data = (MsDescriptionParser*)instance_data;
	g_hash_table_unref(data->desc);
	g_free(data);
}
static const MsParserClass ms_description_parser = {
	.child = ms_description_parser_child,
	.child_end = ms_description_parser_child_end,
	.fin = ms_description_parser_fin,
	.destroy = ms_description_parser_destroy
};
static void ms_description_string_free(GString* string) {
	g_string_free(string, TRUE);
}
static void ms_description_parser_push(MsParserData* pdata, gchar** loc) {
	g_assert(loc != NULL);
	MsDescriptionParser data = {
		.loc = loc,
		.desc = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)ms_description_string_free),
		.in_list = 0
	};
	ms_push_parser(pdata, &ms_description_parser, g_memdup2(&data, sizeof data));
}

static void ms_release_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	MsRelease* release = (MsRelease*)pdata->parser->instance_data;

	if (g_strcmp0(node, "description") == 0) {
		if (release->description) {
			g_warning("Found <release> with multiple <description> nodes, ignoring all but the first one");
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}

		ms_description_parser_push(pdata, &release->description);
	} else {
		g_debug("Skipping unhandled release element %s", node);
		ms_push_parser(pdata, &ms_skip_parser, NULL);
	}
}
static const MsParserClass ms_release_parser = {
	.child = ms_release_parser_child
};

static void ms_releases_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	GPtrArray* releases = (GPtrArray*)pdata->parser->instance_data;

	if (g_strcmp0(node, "release") != 0) {
		g_warning("Found unexpected node <%s> in <releases>, ignoring", node);
		ms_push_parser(pdata, &ms_skip_parser, NULL);
		return;
	}

	const gchar* version = ms_find_attr("version", attr_names, attr_values, NULL);
	MsRelease* rel = g_new0(MsRelease, 1);
	if (version)
		rel->version = g_strdup(version);
	ms_push_parser(pdata, &ms_release_parser, rel);
	g_ptr_array_add(releases, rel);
}
static const MsParserClass ms_releases_parser = {
	.child = ms_releases_parser_child
};

typedef struct {
	gchar* id;
	MsLocalizedString name;
	gchar* homepage_uri;
	gchar* bugtracker_uri;
	gchar* help_uri;
	GPtrArray* desktop_launchables;
	GPtrArray* releases;
	gchar* project_license;
	MsLocalizedString developer_name;
} MsComponentParser;

static void ms_developer_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	MsComponentParser* data = (MsComponentParser*)pdata->parser->instance_data;

	if (g_strcmp0(node, "name") == 0) {
		const gchar* lang = ms_find_attr("xml:lang", attr_names, attr_values, "C");
		guint score = ms_locale_score(pdata->locale, lang) + 1024; /* significantly shift the <developer> score, in order to always prefer <developer> over <developer_name> */
		if (score > data->developer_name.score && (g_strcmp0(lang, "C") == 0 || score > 1024)) {
			data->developer_name.score = score;
			ms_string_parser_push(pdata, &data->developer_name.string);
		} else {
			ms_push_parser(pdata, &ms_skip_parser, NULL);
		}
	} else if (g_strcmp0(node, "url") == 0) {
		g_debug("Skipping unhandled url in developer");
		ms_push_parser(pdata, &ms_skip_parser, NULL);
	} else {
		g_warning("Unexpected tag <%s> in <developer>, ignoring", node);
		ms_push_parser(pdata, &ms_skip_parser, NULL);
	}
}
static const MsParserClass ms_developer_parser = {
	.child = ms_developer_parser_child
};

static void ms_component_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	MsComponentParser* data = (MsComponentParser*)pdata->parser->instance_data;

	if (g_strcmp0(node, "id") == 0) {
		if (data->id) {
			g_warning("Found multiple <id> nodes, ignoring all but the first one");
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}
		ms_string_parser_push(pdata, &data->id);
	} else if (g_strcmp0(node, "name") == 0) {
		const gchar* lang = ms_find_attr("xml:lang", attr_names, attr_values, "C");
		guint score = ms_locale_score(pdata->locale, lang);
		if ((!data->name.string && g_strcmp0(lang, "C") == 0) || score > data->name.score) {
			data->name.score = score;
			ms_string_parser_push(pdata, &data->name.string);
		} else {
			ms_push_parser(pdata, &ms_skip_parser, NULL);
		}
	} else if (g_strcmp0(node, "url") == 0) {
		const gchar* type = ms_find_attr("type", attr_names, attr_values, NULL);
		if (!type) {
			g_warning("Found <url> without type attribute, ignoring");
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}
		if (g_strcmp0(type, "homepage") == 0)
			ms_string_parser_push(pdata, &data->homepage_uri);
		else if (g_strcmp0(type, "bugtracker") == 0)
			ms_string_parser_push(pdata, &data->bugtracker_uri);
		else if (g_strcmp0(type, "help") == 0)
			ms_string_parser_push(pdata, &data->help_uri);
		else {
			g_debug("Skipping unhandled <url> kind %s", type);
			ms_push_parser(pdata, &ms_skip_parser, NULL);
		}
	} else if (g_strcmp0(node, "launchable") == 0) {
		const gchar* type = ms_find_attr("type", attr_names, attr_values, NULL);
		if (!type) {
			g_warning("Found <launchable> without type attribute, ignoring");
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}
		if (g_strcmp0(type, "desktop-id") != 0) {
			g_debug("Skipping unhandled <launchable> kind %s", type);
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}

		if (!data->desktop_launchables)
			data->desktop_launchables = g_ptr_array_new_with_free_func(g_free);
		guint idx = data->desktop_launchables->len;
		g_ptr_array_add (data->desktop_launchables, NULL);
		// This should be fine, as desktop_launchables won't be realloced until the string parser is popped
		ms_string_parser_push(pdata, (gchar**)&g_ptr_array_index(data->desktop_launchables, idx));
	} else if (g_strcmp0(node, "releases") == 0) {
		if (data->releases) {
			g_warning("Found multiple <releases> nodes, ignoring all but the first one");
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}
		data->releases = g_ptr_array_new_with_free_func((GDestroyNotify)ms_release_free);
		ms_push_parser(pdata, &ms_releases_parser, data->releases);
	} else if (g_strcmp0(node, "project_license") == 0) {
		if (data->project_license) {
			g_warning("Found multiple <project_license> nodes, ignoring all but the first one");
			ms_push_parser(pdata, &ms_skip_parser, NULL);
			return;
		}
		ms_string_parser_push(pdata, &data->project_license);
	} else if (g_strcmp0(node, "developer") == 0) {
		ms_push_parser(pdata, &ms_developer_parser, data);
	} else if (g_strcmp0(node, "developer_name") == 0) {
		const gchar* lang = ms_find_attr("xml:lang", attr_names, attr_values, "C");
		guint score = ms_locale_score(pdata->locale, lang);
		if ((!data->developer_name.string && g_strcmp0(lang, "C") == 0) || score > data->developer_name.score) {
			data->developer_name.score = score;
			ms_string_parser_push(pdata, &data->developer_name.string);
		} else {
			ms_push_parser(pdata, &ms_skip_parser, NULL);
		}
	} else {
		g_debug("Skipping unhandled element %s", node);
		ms_push_parser(pdata, &ms_skip_parser, NULL);
	}
}
static gint ms_release_cmp_desc(MsRelease* lhs, MsRelease* rhs) {
	return -ms_vercmp(ms_release_get_version(lhs), ms_release_get_version(rhs));
}
static void ms_component_parser_fin(MsParserData* pdata, const gchar* node, GError** error) {
	MsComponentParser* data = (MsComponentParser*)pdata->parser->instance_data;

	if (!data->id) {
		g_set_error_literal(error, MS_ERROR, MS_ERROR_NO_IDENTIFIER, "Component did not contain <id> node");
		return;
	}

	if (data->desktop_launchables)
		g_ptr_array_add(data->desktop_launchables, NULL);
	if (data->releases)
		g_ptr_array_sort_values(data->releases, (GCompareFunc)ms_release_cmp_desc);

	MsComponent component = {
		.id = g_steal_pointer(&data->id),
		.name = g_steal_pointer(&data->name.string),
		.homepage_uri = g_steal_pointer(&data->homepage_uri),
		.bugtracker_uri = g_steal_pointer(&data->bugtracker_uri),
		.help_uri = g_steal_pointer(&data->help_uri),
		.desktop_launchables = g_steal_pointer(&data->desktop_launchables),
		.releases = g_steal_pointer(&data->releases),
		.project_license = g_steal_pointer(&data->project_license),
		.developer_name = g_steal_pointer(&data->developer_name.string)
	};
	pdata->component = g_memdup2(&component, sizeof component);
	g_atomic_ref_count_init(&pdata->component->rc);
}
static void ms_component_parser_destroy(gpointer instance_data) {
	MsComponentParser* data = (MsComponentParser*)instance_data;

	if (data->id)
		g_free(data->id);
	if (data->name.string)
		g_free(data->name.string);
	if (data->homepage_uri)
		g_free(data->homepage_uri);
	if (data->bugtracker_uri)
		g_free(data->bugtracker_uri);
	if (data->help_uri)
		g_free(data->help_uri);
	if (data->desktop_launchables)
		g_ptr_array_unref(data->desktop_launchables);
	if (data->releases)
		g_ptr_array_unref(data->releases);
	if (data->project_license)
		g_free(data->project_license);
	if(data->developer_name.string)
		g_free(data->developer_name.string);

	g_free(data);
}

static const MsParserClass ms_component_parser = {
	.child = ms_component_parser_child,
	.fin = ms_component_parser_fin,
	.destroy = ms_component_parser_destroy
};

static void ms_root_parser_child(MsParserData* pdata, const gchar* node, const gchar** attr_names, const gchar** attr_values, GError** error) {
	if (g_strcmp0(node, "component") != 0 && g_strcmp0(node, "application") != 0 /* legacy */) {
		g_warning("Unexpected node type %s as root node, ignoring", node);
		ms_push_parser(pdata, &ms_skip_parser, NULL);
		return;
	}

	if (pdata->component) {
		g_warning("Multiple components in a single appstream manifest are not allowed, ignoring all but the first one");
		ms_push_parser(pdata, &ms_skip_parser, NULL);
		return;
	}

	ms_push_parser(pdata, &ms_component_parser, g_new0(MsComponentParser, 1));
}
static const MsParserClass ms_root_parser = {
	.child = ms_root_parser_child
};

/**
 * ms_component_new_from_xml_data:
 * @data: (not nullable) (array length=len): the xml data
 * @len: the length of @data
 * @error: (out) (optional): detailed error reporting
 *
 * Parse an "AppStream Upstream Metadata" document into a
 * [struct@Ministream.Component].
 *
 * Returns: (transfer full): the component specified in the document
 */
MsComponent* ms_component_new_from_xml_data(const guchar* data, gsize len, GError** error) {
	MsParserClassNode root_node = {
		.class = &ms_root_parser,
		.to = NULL,
		.parent = NULL
	};
	MsParserData pdata = {
		.locale = ms_get_current_locale(),
		.parser = &root_node,
		.component = NULL
	};
	pdata.ctx = g_markup_parse_context_new(&(GMarkupParser){ ms_start_element_cb, ms_end_element_cb, ms_text_cb, NULL, NULL }, G_MARKUP_DEFAULT_FLAGS, &pdata, NULL);
	if (!g_markup_parse_context_parse(pdata.ctx, (const gchar*)data, len, error) ||
	    !g_markup_parse_context_end_parse(pdata.ctx, error)) {
		while (pdata.parser != &root_node) {
			MsParserClassNode* current = pdata.parser;
			pdata.parser = current->parent;
			if (current->class->destroy)
				current->class->destroy(current->instance_data);
			g_free(current);
		}
		goto fail;
	}
	g_assert(pdata.parser == &root_node);

	if (pdata.component == NULL) {
		g_set_error_literal(error, MS_ERROR, MS_ERROR_NO_COMPONENT, "Document did not contain <component> node");
		goto fail;
	}

	g_markup_parse_context_free(pdata.ctx);
	g_free(pdata.locale);

	return pdata.component;
fail:
	g_markup_parse_context_free(pdata.ctx);
	if (pdata.component)
		ms_component_unref(pdata.component);
	g_free(pdata.locale);
	return NULL;
}
