/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2020-2022 Matthias Klumpp <mak@debian.org>
 * Copyright (C) 2010 Julian Andres Klode <jak@debian.org>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

#include "as-vercmp.h"

#include <config.h>
#include <glib.h>

#include "as-macros.h"

/**
 * SECTION:as-vercmp
 * @short_description: Version comparison functions.
 * @include: appstream.h
 *
 * Compare software version numbers.
 */

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

/**
 * as_version_parse:
 */
static void
as_version_parse (AsVersion *version, const gchar *v)
{
	const gchar *epoch_end = strchr (v, ':');
	const gchar *complete_end = v + strlen (v);
	const gchar *version_end;

	version->epoch = (epoch_end == NULL) ? "" : v;
	version->version = (epoch_end == NULL) ? v : epoch_end + 1;

	/* the revision separator has to be searched for *after* the epoch, otherwise a
	 * malformed version such as "1-a:x" (where the last '-' precedes the ':') would
	 * yield a version_end that lies before version, making the range invalid */
	version_end = strrchr (version->version, '-');

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

/**
 * cmp_number:
 * @a: The first number as a string.
 * @b: The second number as a string.
 * @pa: A pointer to a pointer which will be set to the first non-digit in @a.
 * @pb: A pointer to a pointer which will be set to the first non-digit in @b.
 *
 * Compares two numbers that are represented as strings
 * against each other. Compared to converting to an int
 * and comparing the integers, this has the advantage
 * that it does not cause overflow.
 */
static gint
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;
}

/**
 * cmp_part:
 *
 * Compare the ranges [@a, @a_end) and [@b, @b_end).
 *
 * The end pointers are compared with `>=` rather than `==` on purpose: should a caller
 * ever hand us a range whose end lies before its start, we still terminate at the first
 * check instead of walking off the end of the buffer.
 */
static gint
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 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 = cmp_number (a, b, &a, &b);
		if (cmpres != 0 || (a >= a_end && b >= b_end))
			return cmpres;
	}

	return 0;
}

/**
 * as_vercmp:
 * @a: First version number
 * @b: Second version number
 * @flags: Flags, e.g. %AS_VERCMP_FLAG_NONE
 *
 * Compare alpha and numeric segments of two software versions,
 * considering @flags.
 *
 * Returns: an integer > 0 if a is newer than b;
 *	    0 if a and b are the same version;
 *	    < 0 if b is newer than a
 */
gint
as_vercmp (const gchar *a, const gchar *b, AsVercmpFlags flags)
{
	AsVersion ver_a, ver_b;
	gint res = 0;

	if (a == 0 && b == 0)
		return 0;
	if (a == NULL)
		return -1;
	if (b == NULL)
		return 1;

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

	/* Optimize the case of differing single digit epochs. */
	if (!as_flags_contains (flags, AS_VERCMP_FLAG_IGNORE_EPOCH)) {
		if (*a != *b && *a != '\0' && *b != '\0' && a[1] == ':' && b[1] == ':')
			return *a < *b ? -1 : 1;
	}

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

	as_version_parse (&ver_a, a);
	as_version_parse (&ver_b, b);

	if (G_UNLIKELY (ver_a.epoch != ver_b.epoch) &&
	    (!as_flags_contains (flags, AS_VERCMP_FLAG_IGNORE_EPOCH)) &&
	    (res = cmp_number (ver_a.epoch, ver_b.epoch, NULL, NULL)) != 0)
		goto out;
	else if ((res = cmp_part (ver_a.version,
				  ver_a.version_end,
				  ver_b.version,
				  ver_b.version_end)) != 0)
		goto out;
	/* Optimizes for native version numbers (where revision is a string literal) */
	else if (G_LIKELY (ver_a.revision != ver_b.revision) &&
		 (res = cmp_part (ver_a.revision,
				  ver_a.revision_end,
				  ver_b.revision,
				  ver_b.revision_end)) != 0)
		goto out;

out:
	return res;
}

/**
 * as_vercmp_simple:
 * @a: First version number
 * @b: Second version number
 *
 * Compare alpha and numeric segments of two software versions.
 *
 * Returns: an integer > 0 if a is newer than b;
 *	    0 if a and b are the same version;
 *	    < 0 if b is newer than a
 */
gint
as_vercmp_simple (const gchar *a, const gchar *b)
{
	return as_vercmp (a, b, AS_VERCMP_FLAG_NONE);
}

/**
 * as_vercmp_test_match:
 * @ver1: first version number
 * @compare: the comparison operator
 * @ver2: second version number
 * @flags: the #AsVercmpFlags to use
 *
 * Compare two version numbers and check if the given version comparator matches.
 *
 * Returns: %TRUE if the version comparison matches, %FALSE otherwise.
 *
 * Since: 0.16.0
 */
gboolean
as_vercmp_test_match (const gchar *ver1,
		      AsRelationCompare compare,
		      const gchar *ver2,
		      AsVercmpFlags flags)
{
	gint rc;
	g_return_val_if_fail (compare != AS_RELATION_COMPARE_UNKNOWN, FALSE);

	rc = as_vercmp (ver1, ver2, flags);
	switch (compare) {
	case AS_RELATION_COMPARE_EQ:
		return rc == 0;
	case AS_RELATION_COMPARE_NE:
		return rc != 0;
	case AS_RELATION_COMPARE_LT:
		return rc < 0;
	case AS_RELATION_COMPARE_GT:
		return rc > 0;
	case AS_RELATION_COMPARE_LE:
		return rc <= 0;
	case AS_RELATION_COMPARE_GE:
		return rc >= 0;
	default:
		return FALSE;
	}
}
