/*
 * ministream - minimal (subset) appstream metadata parser
 *
 * Code in this file was taken more or less as-is from
 * appstream:src/as-vercmp.c, licensed under LGPL-2.1 or later.
 *
 * Copyright (c) 2026  Florian Leander Singer <sp1rit@disroot.org>
 * Copyright (C) 2020-2022 Matthias Klumpp <mak@debian.org>
 * Copyright (C) 2010 Julian Andres Klode <jak@debian.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"

typedef struct {
	const gchar* epoch;
	const gchar* version;
	const gchar* version_end;
	const gchar* revision;
	const gchar* revision_end;
} MsVersion;

static void
ms_version_init_from_str(MsVersion* version, const gchar* v) {
	const gchar* epoch_end = strchr(v, ':');
	const gchar* version_end = strrchr(v, '-');
	const gchar* complete_end = v + strlen(v);

	version->epoch = (epoch_end == NULL) ? "" : v;
	version->version = (epoch_end == NULL) ? v : epoch_end + 1;
	version->version_end = (version_end == NULL) ? complete_end : version_end;
	version->revision = (version_end == NULL) ? "0" : version_end + 1;
	version->revision_end = ((version_end == NULL) ? version->revision + 1 : complete_end);
}

static gint ms_version_cmp_number(const gchar* a, const gchar* b, const gchar** pa, const gchar** pb) {
	gint res = 0;
	if (*a == '\0' && *b == '\0')
		return 0;
	for (; *a == '0'; a++)
		;
	for (; *b == '0'; b++)
		;
	for (; g_ascii_isdigit(*a) && g_ascii_isdigit(*b); a++, b++) {
		if (res == 0 && *a != *b)
			res = *a < *b ? -1 : 1;
	}
	if (g_ascii_isdigit(*a)) {
		if (!g_ascii_isdigit(*b))
			res = 1;
	} else if (g_ascii_isdigit(*b)) {
		res = -1;
	}
	if (pa != NULL) {
		g_assert(pb != NULL);
		*pa = a;
		*pb = b;
	}
	return res;
}

static gint ms_version_cmp_part(const gchar* a, const gchar* a_end, const gchar* b, const gchar* b_end) {
	while (a != a_end || b != b_end) {
		int cmpres;
		for (; !g_ascii_isdigit(*a) || !g_ascii_isdigit(*b); a++, b++) {
			if (a == a_end && b == b_end)
				return 0;
			else if (*a == *b && a != a_end && b != b_end)
				continue;
			/* Tilde always sorts first; i.e., the string with tilde loses */
			else if (*a == '~' || *b == '~')
				return (*a == '~') ? -1 : 1;
			/* One string is empty, other is a number -> go into number mode */
			else if ((a == a_end && *b == '0') || (b == b_end && *a == '0'))
				return ms_version_cmp_number(a, b, NULL, NULL);
			/* One string is empty, other is not a number -> other wins */
			else if (a == a_end || b == b_end)
				return (a == a_end) ? -1 : 1;
			/* One non-digit part is shorter than the other one */
			else if (g_ascii_isdigit(*a) != g_ascii_isdigit(*b))
				return g_ascii_isdigit(*a) ? -1 : 1;
			/* Alpha looses against not alpha */
			else if (g_ascii_isalpha(*a) != g_ascii_isalpha(*b))
				return g_ascii_isalpha (*a) ? -1 : 1;
			/* Standard ASCII comparison */
			else
				return *a < *b ? -1 : 1;
		}

		/* Now compare numbers */
		cmpres = ms_version_cmp_number(a, b, &a, &b);
		if (cmpres != 0 || (a == a_end && b == b_end))
			return cmpres;
	}

	return 0;
}

gint ms_vercmp(const gchar* lhs, const gchar* rhs) {
	if (lhs == NULL && rhs == NULL)
		return 0;
	if (lhs == NULL)
		return -1;
	if (rhs == NULL)
		return 1;

	/* check if a and b are the same pointer */
	if (lhs == rhs)
		return 0;

	/* Optimize the case of differing single digit epochs. */
	if (*lhs != *rhs && *lhs != '\0' && *rhs != '\0' && lhs[1] == ':' && rhs[1] == ':')
		return *lhs < *rhs ? -1 : 1;

	/* easy comparison to see if versions are identical */
	if (g_strcmp0(lhs, rhs) == 0)
		return 0;

	MsVersion ver_lhs, ver_rhs;
	ms_version_init_from_str(&ver_lhs, lhs);
	ms_version_init_from_str(&ver_rhs, rhs);

	gint res = 0;
	if (G_UNLIKELY(ver_lhs.epoch != ver_rhs.epoch) &&
	    (res = ms_version_cmp_number(ver_lhs.epoch, ver_rhs.epoch, NULL, NULL)) != 0)
		goto out;
	else if ((res = ms_version_cmp_part(ver_lhs.version,
				            ver_lhs.version_end,
				            ver_rhs.version,
				            ver_rhs.version_end)) != 0)
		goto out;
	/* Optimizes for native version numbers (where revision is a string literal) */
	else if (G_LIKELY(ver_lhs.revision != ver_rhs.revision) &&
		 (res = ms_version_cmp_part(ver_lhs.revision,
				            ver_lhs.revision_end,
				            ver_rhs.revision,
				            ver_rhs.revision_end)) != 0)
		goto out;

out:
	return res;
}
