/*
 * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#define G_LOG_DOMAIN "FuCommon"

#include "config.h"

#include <string.h>

#include "fwupd-enums.h"
#include "fwupd-error.h"

#include "fu-version-common.h"

#define FU_COMMON_VERSION_DECODE_BCD(val) ((((val) >> 4) & 0x0f) * 10 + ((val)&0x0f))

gchar *
fu_common_version_ensure_semver(const gchar *version);

/**
 * fu_version_from_uint64:
 * @val: a raw version number
 * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_QUAD
 *
 * Returns a dotted decimal version string from a 64 bit number.
 *
 * Returns: a version number, e.g. `1.2.3.4`, or %NULL if not supported
 *
 * Since: 1.8.2
 **/
gchar *
fu_version_from_uint64(guint64 val, FwupdVersionFormat kind)
{
	if (kind == FWUPD_VERSION_FORMAT_QUAD) {
		/* AABB.CCDD.EEFF.GGHH */
		return g_strdup_printf("%" G_GUINT64_FORMAT "."
				       "%" G_GUINT64_FORMAT "."
				       "%" G_GUINT64_FORMAT "."
				       "%" G_GUINT64_FORMAT "",
				       (val >> 48) & 0xffff,
				       (val >> 32) & 0xffff,
				       (val >> 16) & 0xffff,
				       val & 0xffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_PAIR) {
		/* AABBCCDD.EEFFGGHH */
		return g_strdup_printf("%" G_GUINT64_FORMAT ".%" G_GUINT64_FORMAT "",
				       (val >> 32) & 0xffffffff,
				       val & 0xffffffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
		/* AABBCCDD */
		return g_strdup_printf("%" G_GUINT64_FORMAT, val);
	}
	if (kind == FWUPD_VERSION_FORMAT_HEX) {
		/* 0xAABBCCDDEEFFGGHH */
		return g_strdup_printf("0x%08x%08x",
				       (guint32)(val >> 32),
				       (guint32)(val & 0xffffffff));
	}
	g_critical("failed to convert version format %s: %" G_GUINT64_FORMAT "",
		   fwupd_version_format_to_string(kind),
		   val);
	return NULL;
}

/**
 * fu_version_from_uint32:
 * @val: a uint32le version number
 * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
 *
 * Returns a dotted decimal version string from a 32 bit number.
 *
 * Returns: a version number, e.g. `1.0.3`, or %NULL if not supported
 *
 * Since: 1.8.2
 **/
gchar *
fu_version_from_uint32(guint32 val, FwupdVersionFormat kind)
{
	if (kind == FWUPD_VERSION_FORMAT_QUAD) {
		/* AA.BB.CC.DD */
		return g_strdup_printf("%u.%u.%u.%u",
				       (val >> 24) & 0xff,
				       (val >> 16) & 0xff,
				       (val >> 8) & 0xff,
				       val & 0xff);
	}
	if (kind == FWUPD_VERSION_FORMAT_TRIPLET) {
		/* AA.BB.CCDD */
		return g_strdup_printf("%u.%u.%u",
				       (val >> 24) & 0xff,
				       (val >> 16) & 0xff,
				       val & 0xffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_PAIR) {
		/* AABB.CCDD */
		return g_strdup_printf("%u.%u", (val >> 16) & 0xffff, val & 0xffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
		/* AABBCCDD */
		return g_strdup_printf("%" G_GUINT32_FORMAT, val);
	}
	if (kind == FWUPD_VERSION_FORMAT_BCD) {
		/* AA.BB.CC.DD, but BCD */
		return g_strdup_printf("%u.%u.%u.%u",
				       FU_COMMON_VERSION_DECODE_BCD(val >> 24),
				       FU_COMMON_VERSION_DECODE_BCD(val >> 16),
				       FU_COMMON_VERSION_DECODE_BCD(val >> 8),
				       FU_COMMON_VERSION_DECODE_BCD(val));
	}
	if (kind == FWUPD_VERSION_FORMAT_INTEL_ME) {
		/* aaa+11.bbbbb.cccccccc.dddddddddddddddd */
		return g_strdup_printf("%u.%u.%u.%u",
				       ((val >> 29) & 0x07) + 0x0b,
				       (val >> 24) & 0x1f,
				       (val >> 16) & 0xff,
				       val & 0xffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_INTEL_ME2) {
		/* A.B.CC.DDDD */
		return g_strdup_printf("%u.%u.%u.%u",
				       (val >> 28) & 0x0f,
				       (val >> 24) & 0x0f,
				       (val >> 16) & 0xff,
				       val & 0xffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_SURFACE_LEGACY) {
		/* 10b.12b.10b */
		return g_strdup_printf("%u.%u.%u",
				       (val >> 22) & 0x3ff,
				       (val >> 10) & 0xfff,
				       val & 0x3ff);
	}
	if (kind == FWUPD_VERSION_FORMAT_SURFACE) {
		/* 8b.16b.8b */
		return g_strdup_printf("%u.%u.%u",
				       (val >> 24) & 0xff,
				       (val >> 8) & 0xffff,
				       val & 0xff);
	}
	if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS) {
		/* BB.CC.DD */
		return g_strdup_printf("%u.%u.%u",
				       (val >> 16) & 0xff,
				       (val >> 8) & 0xff,
				       val & 0xff);
	}
	if (kind == FWUPD_VERSION_FORMAT_HEX) {
		/* 0xAABBCCDD */
		return g_strdup_printf("0x%08x", val);
	}
	g_critical("failed to convert version format %s: %u",
		   fwupd_version_format_to_string(kind),
		   val);
	return NULL;
}

/**
 * fu_version_from_uint24:
 * @val: a uint24le version number
 * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
 *
 * Returns a dotted decimal version string from a 24 bit number.
 *
 * Returns: a version number, e.g. `1.0.3`, or %NULL if not supported
 *
 * Since: 1.8.9
 **/
gchar *
fu_version_from_uint24(guint32 val, FwupdVersionFormat kind)
{
	if (kind == FWUPD_VERSION_FORMAT_TRIPLET) {
		/* BB.CC.DD */
		return g_strdup_printf("%u.%u.%u",
				       (val >> 24) & 0xff,
				       (val >> 16) & 0xff,
				       val & 0xffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_PAIR) {
		/* BB.CCDD */
		return g_strdup_printf("%u.%u", (val >> 16) & 0xff, val & 0xffff);
	}
	if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
		/* BBCCDD */
		return g_strdup_printf("%" G_GUINT32_FORMAT, val);
	}
	if (kind == FWUPD_VERSION_FORMAT_HEX) {
		/* 0xBBCCDD */
		return g_strdup_printf("0x%06x", val);
	}
	g_critical("failed to convert version format %s: %u",
		   fwupd_version_format_to_string(kind),
		   val);
	return NULL;
}

/**
 * fu_version_from_uint16:
 * @val: a uint16le version number
 * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
 *
 * Returns a dotted decimal version string from a 16 bit number.
 *
 * Returns: a version number, e.g. `1.3`, or %NULL if not supported
 *
 * Since: 1.8.2
 **/
gchar *
fu_version_from_uint16(guint16 val, FwupdVersionFormat kind)
{
	if (kind == FWUPD_VERSION_FORMAT_BCD) {
		return g_strdup_printf("%i.%i",
				       FU_COMMON_VERSION_DECODE_BCD(val >> 8),
				       FU_COMMON_VERSION_DECODE_BCD(val));
	}
	if (kind == FWUPD_VERSION_FORMAT_PAIR) {
		return g_strdup_printf("%u.%u", (guint)(val >> 8) & 0xff, (guint)val & 0xff);
	}
	if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) {
		return g_strdup_printf("%" G_GUINT16_FORMAT, val);
	}
	if (kind == FWUPD_VERSION_FORMAT_HEX) {
		/* 0xAABB */
		return g_strdup_printf("0x%04x", val);
	}
	g_critical("failed to convert version format %s: %u",
		   fwupd_version_format_to_string(kind),
		   val);
	return NULL;
}

static gint
fu_version_compare_char(gchar chr1, gchar chr2)
{
	if (chr1 == chr2)
		return 0;
	if (chr1 == '~')
		return -1;
	if (chr2 == '~')
		return 1;
	return chr1 < chr2 ? -1 : 1;
}

static gint
fu_version_compare_chunk(const gchar *str1, const gchar *str2)
{
	guint i;

	/* trivial */
	if (g_strcmp0(str1, str2) == 0)
		return 0;
	if (str1 == NULL)
		return 1;
	if (str2 == NULL)
		return -1;

	/* check each char of the chunk */
	for (i = 0; str1[i] != '\0' && str2[i] != '\0'; i++) {
		gint rc = fu_version_compare_char(str1[i], str2[i]);
		if (rc != 0)
			return rc;
	}
	return fu_version_compare_char(str1[i], str2[i]);
}

static gboolean
_g_ascii_is_digits(const gchar *str)
{
	g_return_val_if_fail(str != NULL, FALSE);
	for (gsize i = 0; str[i] != '\0'; i++) {
		if (!g_ascii_isdigit(str[i]))
			return FALSE;
	}
	return TRUE;
}

static guint
fu_version_format_number_sections(FwupdVersionFormat fmt)
{
	if (fmt == FWUPD_VERSION_FORMAT_PLAIN || fmt == FWUPD_VERSION_FORMAT_NUMBER ||
	    fmt == FWUPD_VERSION_FORMAT_HEX)
		return 1;
	if (fmt == FWUPD_VERSION_FORMAT_PAIR || fmt == FWUPD_VERSION_FORMAT_BCD)
		return 2;
	if (fmt == FWUPD_VERSION_FORMAT_TRIPLET || fmt == FWUPD_VERSION_FORMAT_SURFACE_LEGACY ||
	    fmt == FWUPD_VERSION_FORMAT_SURFACE || fmt == FWUPD_VERSION_FORMAT_DELL_BIOS)
		return 3;
	if (fmt == FWUPD_VERSION_FORMAT_QUAD || fmt == FWUPD_VERSION_FORMAT_INTEL_ME ||
	    fmt == FWUPD_VERSION_FORMAT_INTEL_ME2)
		return 4;
	return 0;
}

/**
 * fu_version_ensure_semver:
 * @version: (nullable): a version number, e.g. ` V1.2.3 `
 * @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
 *
 * Builds a semver from the possibly crazy version number. Depending on the @semver value
 * the string will be split and a string in the correct format will be returned.
 *
 * Returns: a version number, e.g. `1.2.3`, or %NULL if the version was not valid
 *
 * Since: 1.8.2
 */
gchar *
fu_version_ensure_semver(const gchar *version, FwupdVersionFormat fmt)
{
	guint sections_actual;
	guint sections_expected = fu_version_format_number_sections(fmt);
	g_autofree gchar *tmp = NULL;
	g_auto(GStrv) split = NULL;
	g_autoptr(GString) str = g_string_new(NULL);

	/* split into all sections */
	tmp = fu_common_version_ensure_semver(version);
	if (tmp == NULL)
		return NULL;
	if (fmt == FWUPD_VERSION_FORMAT_UNKNOWN)
		return g_steal_pointer(&tmp);
	split = g_strsplit(tmp, ".", -1);
	sections_actual = g_strv_length(split);

	/* add zero sections as required */
	if (sections_actual < sections_expected) {
		for (guint i = 0; i < sections_expected - sections_actual; i++) {
			if (str->len > 0)
				g_string_append(str, ".");
			g_string_append(str, "0");
		}
	}

	/* only add enough sections for the format */
	for (guint i = 0; i < sections_actual && i < sections_expected; i++) {
		if (str->len > 0)
			g_string_append(str, ".");
		g_string_append(str, split[i]);
	}

	/* success */
	return g_string_free(g_steal_pointer(&str), FALSE);
}

/**
 * fu_common_version_ensure_semver:
 * @version: (nullable): a version number, e.g. ` V1.2.3 `
 *
 * Builds a semver from the possibly crazy version number.
 *
 * Returns: a version number, e.g. `1.2.3`, or %NULL if the version was not valid
 */
gchar *
fu_common_version_ensure_semver(const gchar *version)
{
	gboolean dot_valid = FALSE;
	guint digit_cnt = 0;
	g_autoptr(GString) version_safe = g_string_new(NULL);

	/* invalid */
	if (version == NULL)
		return NULL;

	/* hex prefix */
	if (g_str_has_prefix(version, "0x")) {
		return fu_version_parse_from_format(version, FWUPD_VERSION_FORMAT_TRIPLET);
	}

	/* make sane */
	for (guint i = 0; version[i] != '\0'; i++) {
		if (g_ascii_isdigit(version[i])) {
			g_string_append_c(version_safe, version[i]);
			digit_cnt++;
			dot_valid = TRUE;
			continue;
		}
		if (version[i] == '-' || version[i] == '~') {
			g_string_append_c(version_safe, '.');
			dot_valid = FALSE;
			continue;
		}
		if (version[i] == '.' && dot_valid && version[i + 1] != '\0') {
			g_string_append_c(version_safe, version[i]);
			dot_valid = FALSE;
			continue;
		}
	}

	/* remove any trailing dot */
	if (version_safe->len > 0 && version_safe->str[version_safe->len - 1] == '.')
		g_string_truncate(version_safe, version_safe->len - 1);

	/* found no digits */
	if (digit_cnt == 0)
		return NULL;
	return g_string_free(g_steal_pointer(&version_safe), FALSE);
}

/**
 * fu_version_parse_from_format
 * @version: (nullable): a version number
 * @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET
 *
 * Returns a dotted decimal version string from a version string using @fmt.
 * The supported formats are:
 *
 * - Dotted decimal, e.g. `1.2.3`
 * - Base 16, a hex number *with* a 0x prefix, e.g. `0x10203`
 * - Base 10, a string containing just [0-9], e.g. `66051`
 * - Date in YYYYMMDD format, e.g. `20150915`
 *
 * Anything with a `.` or that doesn't match `[0-9]` or `0x[a-f,0-9]` is considered
 * a string and returned without modification.
 *
 * Returns: a version number, e.g. `1.0.3`, or %NULL on error
 *
 * Since: 1.8.2
 */
gchar *
fu_version_parse_from_format(const gchar *version, FwupdVersionFormat fmt)
{
	const gchar *version_noprefix = version;
	gchar *endptr = NULL;
	guint64 tmp;
	guint base;

	/* sanity check */
	if (version == NULL)
		return NULL;

	/* already dotted decimal */
	if (g_strstr_len(version, -1, ".") != NULL)
		return g_strdup(version);

	/* is a date */
	if (g_str_has_prefix(version, "20") && strlen(version) == 8)
		return g_strdup(version);

	/* convert 0x prefixed strings to dotted decimal */
	if (g_str_has_prefix(version, "0x")) {
		version_noprefix += 2;
		base = 16;
	} else {
		/* for non-numeric content, just return the string */
		if (!_g_ascii_is_digits(version))
			return g_strdup(version);
		base = 10;
	}

	/* convert */
	tmp = g_ascii_strtoull(version_noprefix, &endptr, base);
	if (endptr != NULL && endptr[0] != '\0')
		return g_strdup(version);
	if (tmp == 0)
		return g_strdup(version);
	return fu_version_from_uint32((guint32)tmp, fmt);
}

/**
 * fu_version_guess_format:
 * @version: (nullable): a version number, e.g. `1.2.3`
 *
 * Guesses the version format from the version number. This is only a heuristic
 * and plugins and components should explicitly set the version format whenever
 * possible.
 *
 * If the version format cannot be guessed with any degree of accuracy, the
 * %FWUPD_VERSION_FORMAT_UNKNOWN constant is returned.
 *
 * Returns: a version format, e.g. %FWUPD_VERSION_FORMAT_QUAD
 *
 * Since: 1.8.2
 */
FwupdVersionFormat
fu_version_guess_format(const gchar *version)
{
	guint sz;
	g_auto(GStrv) split = NULL;

	/* nothing to use */
	if (version == NULL || version[0] == '\0')
		return FWUPD_VERSION_FORMAT_UNKNOWN;

	/* no dots, assume just text */
	split = g_strsplit(version, ".", -1);
	sz = g_strv_length(split);
	if (sz == 1) {
		if (g_str_has_prefix(version, "0x") || _g_ascii_is_digits(version))
			return FWUPD_VERSION_FORMAT_NUMBER;
		return FWUPD_VERSION_FORMAT_PLAIN;
	}

	/* check for only-digit semver version */
	for (guint i = 0; split[i] != NULL; i++) {
		/* check sections are plain numbers */
		if (!_g_ascii_is_digits(split[i]))
			return FWUPD_VERSION_FORMAT_PLAIN;
	}

	/* the most common formats */
	if (sz == 2)
		return FWUPD_VERSION_FORMAT_PAIR;
	if (sz == 3)
		return FWUPD_VERSION_FORMAT_TRIPLET;
	if (sz == 4)
		return FWUPD_VERSION_FORMAT_QUAD;

	/* unknown! */
	return FWUPD_VERSION_FORMAT_UNKNOWN;
}

static FwupdVersionFormat
fu_version_format_convert_base(FwupdVersionFormat fmt)
{
	if (fmt == FWUPD_VERSION_FORMAT_INTEL_ME || fmt == FWUPD_VERSION_FORMAT_INTEL_ME2)
		return FWUPD_VERSION_FORMAT_QUAD;
	if (fmt == FWUPD_VERSION_FORMAT_DELL_BIOS)
		return FWUPD_VERSION_FORMAT_TRIPLET;
	if (fmt == FWUPD_VERSION_FORMAT_BCD)
		return FWUPD_VERSION_FORMAT_PAIR;
	if (fmt == FWUPD_VERSION_FORMAT_HEX)
		return FWUPD_VERSION_FORMAT_NUMBER;
	return fmt;
}

/**
 * fu_version_verify_format:
 * @version: (not nullable): a string, e.g. `0x1234`
 * @fmt: a version format
 * @error: (nullable): optional return location for an error
 *
 * Verifies if a version matches the input format.
 *
 * Returns: TRUE or FALSE
 *
 * Since: 1.8.2
 **/
gboolean
fu_version_verify_format(const gchar *version, FwupdVersionFormat fmt, GError **error)
{
	FwupdVersionFormat fmt_base = fu_version_format_convert_base(fmt);
	FwupdVersionFormat fmt_guess;

	g_return_val_if_fail(version != NULL, FALSE);
	g_return_val_if_fail(error == NULL || *error == NULL, FALSE);

	/* don't touch */
	if (fmt == FWUPD_VERSION_FORMAT_PLAIN)
		return TRUE;

	/* nothing we can check for */
	if (fmt == FWUPD_VERSION_FORMAT_UNKNOWN)
		return TRUE;

	/* check the base format */
	fmt_guess = fu_version_guess_format(version);
	if (fmt_guess != fmt_base) {
		g_set_error(error,
			    G_IO_ERROR,
			    G_IO_ERROR_INVALID_DATA,
			    "%s is not a valid %s (guessed %s)",
			    version,
			    fwupd_version_format_to_string(fmt),
			    fwupd_version_format_to_string(fmt_guess));
		return FALSE;
	}
	return TRUE;
}

static gint
fu_version_compare_safe(const gchar *version_a, const gchar *version_b)
{
	guint longest_split;
	g_auto(GStrv) split_a = NULL;
	g_auto(GStrv) split_b = NULL;

	/* sanity check */
	if (version_a == NULL || version_b == NULL)
		return G_MAXINT;

	/* optimization */
	if (g_strcmp0(version_a, version_b) == 0)
		return 0;

	/* split into sections, and try to parse */
	split_a = g_strsplit(version_a, ".", -1);
	split_b = g_strsplit(version_b, ".", -1);
	longest_split = MAX(g_strv_length(split_a), g_strv_length(split_b));
	for (guint i = 0; i < longest_split; i++) {
		gchar *endptr_a = NULL;
		gchar *endptr_b = NULL;
		gint64 ver_a;
		gint64 ver_b;

		/* we lost or gained a dot */
		if (split_a[i] == NULL)
			return -1;
		if (split_b[i] == NULL)
			return 1;

		/* compare integers */
		ver_a = g_ascii_strtoll(split_a[i], &endptr_a, 10);
		ver_b = g_ascii_strtoll(split_b[i], &endptr_b, 10);
		if (ver_a < ver_b)
			return -1;
		if (ver_a > ver_b)
			return 1;

		/* compare strings */
		if ((endptr_a != NULL && endptr_a[0] != '\0') ||
		    (endptr_b != NULL && endptr_b[0] != '\0')) {
			gint rc = fu_version_compare_chunk(endptr_a, endptr_b);
			if (rc < 0)
				return -1;
			if (rc > 0)
				return 1;
		}
	}

	/* we really shouldn't get here */
	return 0;
}

/**
 * fu_version_compare:
 * @version_a: (nullable): the semver release version, e.g. `1.2.3`
 * @version_b: (nullable): the semver release version, e.g. `1.2.3.1`
 * @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_PLAIN
 *
 * Compares version numbers for sorting taking into account the version format
 * if required.
 *
 * Returns: -1 if a < b, +1 if a > b, 0 if they are equal, and %G_MAXINT on error
 *
 * Since: 1.8.2
 */
gint
fu_version_compare(const gchar *version_a, const gchar *version_b, FwupdVersionFormat fmt)
{
	if (fmt == FWUPD_VERSION_FORMAT_PLAIN)
		return g_strcmp0(version_a, version_b);
	if (fmt == FWUPD_VERSION_FORMAT_HEX) {
		g_autofree gchar *hex_a = NULL;
		g_autofree gchar *hex_b = NULL;
		hex_a = fu_version_parse_from_format(version_a, fmt);
		hex_b = fu_version_parse_from_format(version_b, fmt);
		return fu_version_compare_safe(hex_a, hex_b);
	}
	return fu_version_compare_safe(version_a, version_b);
}
