/*
 *  $Id: serializable-utils.c 29750 2026-04-07 16:18:14Z yeti-dn $
 *  Copyright (C) 2009-2026 David Nečas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program 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 General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>
#include <stdarg.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/gwyddion.h"
#include "libgwyddion/serializable-internal.h"

static void add_invalid_value_error (GwyErrorList **error_list,
                                     const gchar *item_name,
                                     const gchar *type_name,
                                     const gchar *fmt,
                                     ...)                              G_GNUC_PRINTF(4, 5);
static void add_wrong_type_error    (GwyErrorList **error_list,
                                     const GwySerializableItem *model,
                                     gsize n_items,
                                     const GwySerializableItem *item,
                                     const gchar *type_name);
static void add_duplicate_item_error(GwyErrorList **error_list,
                                     const gchar *name,
                                     GwySerializableCType ctype,
                                     const gchar *type_name);

static gboolean
validate_by_pspec(GParamSpec *pspec, const GwySerializableItem *item, const gchar *type_name,
                  GwyErrorList **error_list)
{
    GwySerializableCType ctype = item->ctype;

    if (ctype == GWY_SERIALIZABLE_BOOLEAN) {
        /* Nothing to validate. Classes may use pspecs also for booleans, to get the name or something. */
        if (G_IS_PARAM_SPEC_BOOLEAN(pspec))
            return TRUE;
        g_critical("Item %s of object %s serializes as boolean and tries to use GParamSpec of type %s.",
                   item->name, type_name, G_PARAM_SPEC_TYPE_NAME(pspec));
        return FALSE;
    }
    if (ctype == GWY_SERIALIZABLE_DOUBLE) {
        if (G_IS_PARAM_SPEC_DOUBLE(pspec)) {
            gdouble v = item->value.v_double;
            GParamSpecDouble *itempspec = G_PARAM_SPEC_DOUBLE(pspec);
            if (v < itempspec->minimum || v > itempspec->maximum) {
                add_invalid_value_error(error_list, item->name, type_name, "%g", v);
                return FALSE;
            }
            return TRUE;
        }
        g_critical("Item %s of object %s serializes as double and tries to use GParamSpec of type %s.",
                   item->name, type_name, G_PARAM_SPEC_TYPE_NAME(pspec));
        return FALSE;
    }
    if (ctype == GWY_SERIALIZABLE_INT32) {
        if (G_IS_PARAM_SPEC_INT(pspec)) {
            gint32 v = item->value.v_int32;
            GParamSpecInt *itempspec = G_PARAM_SPEC_INT(pspec);
            if (v < itempspec->minimum || v > itempspec->maximum) {
                add_invalid_value_error(error_list, item->name, type_name, "%d", v);
                return FALSE;
            }
            return TRUE;
        }
        if (G_IS_PARAM_SPEC_UINT(pspec)) {
            guint32 v = item->value.v_uint32;
            GParamSpecUInt *itempspec = G_PARAM_SPEC_UINT(pspec);
            if (v < itempspec->minimum || v > itempspec->maximum) {
                add_invalid_value_error(error_list, item->name, type_name, "%u", v);
                return FALSE;
            }
            return TRUE;
        }
        if (G_IS_PARAM_SPEC_ENUM(pspec)) {
            gint32 v = item->value.v_int32;
            GParamSpecEnum *itempspec = G_PARAM_SPEC_ENUM(pspec);
            GEnumClass *enum_class = itempspec->enum_class;
            if (v < enum_class->minimum || v > enum_class->maximum) {
                add_invalid_value_error(error_list, item->name, type_name, "%d", v);
                return FALSE;
            }
            guint k, n_values = enum_class->n_values;
            for (k = 0; k < n_values; k++) {
                if (enum_class->values[k].value == v)
                    break;
            }
            if (k == n_values) {
                add_invalid_value_error(error_list, item->name, type_name, "%d", v);
                return FALSE;
            }
            return TRUE;
        }
        g_critical("Item %s of object %s serializes as int32 and tries to use GParamSpec of type %s.",
                   item->name, type_name, G_PARAM_SPEC_TYPE_NAME(pspec));
        return FALSE;
    }
    if (ctype == GWY_SERIALIZABLE_INT64) {
        if (G_IS_PARAM_SPEC_INT64(pspec)) {
            gint64 v = item->value.v_int64;
            GParamSpecInt64 *itempspec = G_PARAM_SPEC_INT64(pspec);
            if (v < itempspec->minimum || v > itempspec->maximum) {
                add_invalid_value_error(error_list, item->name, type_name, "%" G_GINT64_FORMAT, v);
                return FALSE;
            }
            return TRUE;
        }
        if (G_IS_PARAM_SPEC_UINT64(pspec)) {
            guint64 v = item->value.v_uint64;
            GParamSpecUInt64 *itempspec = G_PARAM_SPEC_UINT64(pspec);
            if (v < itempspec->minimum || v > itempspec->maximum) {
                add_invalid_value_error(error_list, item->name, type_name, "%" G_GUINT64_FORMAT, v);
                return FALSE;
            }
            return TRUE;
        }
        g_critical("Item %s of object %s serializes as int64 and tries to use GParamSpec of type %s.",
                   item->name, type_name, G_PARAM_SPEC_TYPE_NAME(pspec));
        return FALSE;
    }
    if (ctype == GWY_SERIALIZABLE_FLOAT) {
        if (G_IS_PARAM_SPEC_FLOAT(pspec)) {
            gfloat v = item->value.v_float;
            GParamSpecFloat *itempspec = G_PARAM_SPEC_FLOAT(pspec);
            if (v < itempspec->minimum || v > itempspec->maximum) {
                add_invalid_value_error(error_list, item->name, type_name, "%g", v);
                return FALSE;
            }
            return TRUE;
        }
        g_critical("Item %s of object %s serializes as float and tries to use GParamSpec of type %s.",
                   item->name, type_name, G_PARAM_SPEC_TYPE_NAME(pspec));
        return FALSE;
    }
    g_assert_not_reached();
    return FALSE;
}

/**
 * gwy_deserialize_filter_items:
 * @model: List of expected items.  Generally, the values should be set to defaults (%NULL for non-atomic types).
 * @n_items: The number of items in the template.
 * @group: Item list passed to construct().
 * @type_name: Name of the deserialised type for error messages.
 * @error_list: (nullable): Location to store the errors occuring, %NULL to ignore.
 *              Only non-fatal error %GWY_DESERIALIZE_ERROR_ITEM can occur.
 *
 * Fills the template of expected items with values from received item list.
 *
 * This is a helper function for use in #GwySerializable.construct() method.
 *
 * Expected values are moved from @group to @model. It means their ownership is transferred from @group to @model.
 * Unexpected items are left in @group. The owner of @group frees such items. It normally means you do not need to
 * concern yourself with them because @group is owned by the deserialisation function.
 *
 * The owner of @model (normally the object being constructed) is, however, responsible for consuming or freeing all
 * data moved there. Often it means it just takes over the arrays/objects and uses them in its own data structures.
 * Do not forget to free the data also in the case of failure.
 *
 * An item template is identified by its name and type  Multiple items of the same name are permitted in @model as
 * long as their types differ (this can be useful e.g. to accept old serialised representations for compatibility).
 *
 * The concrete type of boxed types is not part of the identification, i.e. only one boxed item of a specific name can
 * be given in @model and the type, if specified using the @aux field, must match exactly.
 **/
void
gwy_deserialize_filter_items(GwySerializableItem *model,
                             gsize n_items,
                             GwySerializableGroup *group,
                             const gchar *type_name,
                             GwyErrorList **error_list)
{
    guint8 *seen = g_new0(guint8, n_items);
    gsize i;

    /* XXX: This is quadratic. Usually the number of items in fixed-items objects is a couple of dozens at most. */
    for (i = 0; i < group->n; i++) {
        GwySerializableItem *item = group->items + i;
        const GwySerializableCType ctype = item->ctype;
        const gchar *name = item->name;
        gsize j;

        for (j = 0; j < n_items; j++) {
            if (model[j].ctype == ctype && gwy_strequal(model[j].name, name))
                break;
        }

        if (j == n_items) {
            add_wrong_type_error(error_list, model, n_items, item, type_name);
            /* At this moment unconsumed items are OK. We may be deserialising a parent class and will still get to
             * the child items or something. We check at the very end. */
            continue;
        }
        if (G_UNLIKELY(seen[j])) {
            // Report the first duplicate.
            if (seen[j] == 1) {
                add_duplicate_item_error(error_list, name, ctype, type_name);
                seen[j]++;
            }
            continue;
        }
        seen[j]++;

        /* Boxed types must give the boxed type and can be also filtered using the type. */
        if (ctype == GWY_SERIALIZABLE_BOXED) {
            g_assert(model[j].aux.boxed_type);
            if (G_UNLIKELY(item->aux.boxed_type != model[j].aux.boxed_type)) {
                gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_ITEM,
                                   _("Item ‘%s’ in the representation of object ‘%s’ has type ‘%s’ instead of "
                                     "expected ‘%s’. It was ignored."),
                                   name ? name : "(nil)", type_name,
                                   g_type_name(item->aux.boxed_type),
                                   g_type_name(model[j].aux.boxed_type));
                continue;
            }
        }

        /* Objects may give the type and if they do they can be also filtered using the type. */
        if (ctype == GWY_SERIALIZABLE_OBJECT || ctype == GWY_SERIALIZABLE_OBJECT_ARRAY) {
            GType object_type = model[j].aux.object_type;
            if (object_type && !gwy_check_object_component(item, type_name, object_type, error_list))
                continue;
        }

        /* Atomic types may give a GParamSpec and if they do we can weed out invalid values. */
        GParamSpec *pspec = model[j].aux.pspec;
        if (model[j].aux.pspec && (ctype == GWY_SERIALIZABLE_DOUBLE
                                   || ctype == GWY_SERIALIZABLE_FLOAT
                                   || ctype == GWY_SERIALIZABLE_INT32
                                   || ctype == GWY_SERIALIZABLE_INT64
                                   || ctype == GWY_SERIALIZABLE_BOOLEAN)) {
            if (!validate_by_pspec(pspec, item, type_name, error_list))
                continue;
        }
        else if (ctype == GWY_SERIALIZABLE_DOUBLE) {
            gdouble v = item->value.v_double;
            if (!gwy_isnormal(v)) {
                add_invalid_value_error(error_list, item->name, type_name, "%g", v);
                continue;
            }
        }
        else if (ctype == GWY_SERIALIZABLE_FLOAT) {
            gfloat v = item->value.v_float;
            if (!gwy_isnormal(v)) {
                add_invalid_value_error(error_list, item->name, type_name, "%g", v);
                continue;
            }
        }
        else if (pspec && (ctype == GWY_SERIALIZABLE_INT8 || ctype == GWY_SERIALIZABLE_STRING)) {
            g_warning("GParamSpec check for %s in object %s not implemented.", item->name, type_name);
            /* XXX: For arrays we do not implement it at all. */
        }

        /* Give ownership to template. */
        memcpy(&model[j].value, &item->value, sizeof(GwySerializableValue));
        item->value.v_uint8_array = NULL;
        item->ctype = GWY_SERIALIZABLE_CONSUMED;

        model[j].array_size = item->array_size;
        item->array_size = 0;
    }

    g_free(seen);
}

/**
 * gwy_fill_serializable_defaults_pspec:
 * @items: (array length=n_items): List of serialisation items.
 * @n_items: The number of items in @group.
 * @names_may_differ: Pass %TRUE if items and properties both have names and they can differ.
 *
 * Fills serializable template item values using param specs.
 *
 * The item type must be compatible with the param spec type. For instance, a 32bit integer item may have param spec
 * of signed or unsigned integer type or it can be an enum.
 *
 * If both items and properties have names set they should be equal (the function prints a warning unless
 * @names_may_differ is %TRUE). Items that do not have names but have a param spec will have their names set to match
 * the corresponding properties. In such case the property name must be a static string.
 *
 * Only atomic items which have @aux.pspec set have the values set.
 **/
void
gwy_fill_serializable_defaults_pspec(GwySerializableItem *items, gsize n_items,
                                     gboolean names_may_differ)
{
    for (guint i = 0; i < n_items; i++) {
        GwySerializableItem *it = items + i;
        GParamSpec *pspec = it->aux.pspec;
        GwySerializableCType ctype = it->ctype;
        gboolean handle_name = FALSE;
        if (!pspec)
            continue;

        /* NB: Must not check the name before the type because non-atomic types may have different things in aux. */
        if (ctype == GWY_SERIALIZABLE_INT32) {
            handle_name = TRUE;
            if (G_IS_PARAM_SPEC_INT(pspec))
                it->value.v_int32 = G_PARAM_SPEC_INT(pspec)->default_value;
            else if (G_IS_PARAM_SPEC_UINT(pspec))
                it->value.v_uint32 = G_PARAM_SPEC_UINT(pspec)->default_value;
            else if (G_IS_PARAM_SPEC_ENUM(pspec))
                it->value.v_int32 = G_PARAM_SPEC_ENUM(pspec)->default_value;
            else if (G_IS_PARAM_SPEC_FLAGS(pspec))
                it->value.v_uint32 = G_PARAM_SPEC_FLAGS(pspec)->default_value;
            else {
                g_critical("Unimplemented param spec %s for int32 item %s.", G_PARAM_SPEC_TYPE_NAME(pspec), it->name);
            }
        }
        else if (ctype == GWY_SERIALIZABLE_INT64) {
            handle_name = TRUE;
            if (G_IS_PARAM_SPEC_INT64(pspec))
                it->value.v_int64 = G_PARAM_SPEC_INT64(pspec)->default_value;
            else if (G_IS_PARAM_SPEC_UINT64(pspec))
                it->value.v_uint64 = G_PARAM_SPEC_UINT64(pspec)->default_value;
            else {
                g_critical("Unimplemented param spec %s for int64 item %s.", G_PARAM_SPEC_TYPE_NAME(pspec), it->name);
            }
        }
        else if (ctype == GWY_SERIALIZABLE_DOUBLE) {
            handle_name = TRUE;
            if (G_IS_PARAM_SPEC_DOUBLE(pspec))
                it->value.v_double = G_PARAM_SPEC_DOUBLE(pspec)->default_value;
            else {
                g_critical("Unimplemented param spec %s for double item %s.", G_PARAM_SPEC_TYPE_NAME(pspec), it->name);
            }
        }
        else if (ctype == GWY_SERIALIZABLE_FLOAT) {
            handle_name = TRUE;
            if (G_IS_PARAM_SPEC_FLOAT(pspec))
                it->value.v_float = G_PARAM_SPEC_FLOAT(pspec)->default_value;
            else {
                g_critical("Unimplemented param spec %s for float item %s.", G_PARAM_SPEC_TYPE_NAME(pspec), it->name);
            }
        }
        else if (ctype == GWY_SERIALIZABLE_BOOLEAN) {
            handle_name = TRUE;
            /* This is kind of overkill but a class may do it for consistency. */
            if (G_IS_PARAM_SPEC_BOOLEAN(pspec))
                it->value.v_boolean = G_PARAM_SPEC_BOOLEAN(pspec)->default_value;
            else {
                g_critical("Unimplemented param spec %s for boolean item %s.", G_PARAM_SPEC_TYPE_NAME(pspec), it->name);
            }
        }

        if (handle_name) {
            if (it->name)
                g_warn_if_fail(names_may_differ || gwy_strequal(pspec->name, it->name));
            else {
                g_warn_if_fail(pspec->flags & G_PARAM_STATIC_NAME);
                it->name = pspec->name;
            }
        }
    }
}

/**
 * gwy_check_object_component:
 * @item: Serialised item.
 * @type_name: Name of the type being deserialised (used for error messages only).
 * @component_type: Expected component type.
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 *
 * Checks if objects in a deserialised item list match the expected type.
 *
 * The item must be of type %GWY_SERIALIZABLE_OBJECT or %GWY_SERIALIZABLE_OBJECT_ARRAY. In the latter case all
 * elements are checked.
 *
 * If a single object is %NULL (i.e. the corresponding component was not present in the file) the function return
 * %FALSE but does not report any error. This is usually the most useful behaviour. If you need to distinguish missing
 * and invalid objects, simply check @value.v_object.
 *
 * An array of objects cannot have holes. So either the array exists and is full of objects or the entire array is
 * missing. The function returns %TRUE only if the array exists and all objects match the type. When the array is
 * %NULL, %FALSE is returned, but no errors are set.
 *
 * Returns: %TRUE if the types match, %FALSE if they do not.
 **/
gboolean
gwy_check_object_component(const GwySerializableItem *item,
                           const gchar *type_name,
                           GType component_type,
                           GwyErrorList **error_list)
{
    if (item->ctype == GWY_SERIALIZABLE_OBJECT) {
        GObject *object = item->value.v_object;
        if (!object)
            return FALSE;
        if (G_LIKELY(G_TYPE_CHECK_INSTANCE_TYPE(object, component_type)))
            return TRUE;
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                           // TRANSLATORS: Error message.
                           _("Component ‘%s’ of object %s is of type %s instead of %s."),
                           item->name,
                           type_name,
                           G_OBJECT_TYPE_NAME(object),
                           g_type_name(component_type));
        return FALSE;
    }
    if (item->ctype == GWY_SERIALIZABLE_OBJECT_ARRAY) {
        if (!item->value.v_object_array)
            return FALSE;
        for (gsize i = 0; i < item->array_size; i++) {
            GObject *iobject = item->value.v_object_array[i];
            if (G_UNLIKELY(!G_TYPE_CHECK_INSTANCE_TYPE(iobject, component_type))) {
                gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                                   // TRANSLATORS: Error message.
                                   _("Object %lu in component ‘%s’ of object %s is of type %s instead of %s."),
                                   (gulong)i,
                                   item->name,
                                   type_name,
                                   G_OBJECT_TYPE_NAME(iobject),
                                   g_type_name(component_type));
                return FALSE;
            }
        }
        return TRUE;
    }
    g_return_val_if_reached(FALSE);
}

/* TODO: Also implement checking double arrays to see if they are completely normal or contain weird values. */
/**
 * gwy_check_double_component:
 * @item: Serialised item for a floating point value.
 * @type_name: Name of the type being deserialised (used for error messages only).
 * @min: The minimum allowed value.
 * @max: The maximum allowed value.
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 *
 * Checks if a floating point value in a deserialised item list is normal and within allowed range.
 *
 * The item must be of type %GWY_SERIALIZABLE_DOUBLE.
 *
 * Returns: %TRUE if the value is normal and within range [@min,@max] (inclusive), %FALSE if it is not.
 **/
gboolean
gwy_check_double_component(const GwySerializableItem *item,
                           const gchar *type_name,
                           gdouble min,
                           gdouble max,
                           GwyErrorList **error_list)
{
    if (item->ctype == GWY_SERIALIZABLE_DOUBLE) {
        gdouble v = item->value.v_double;
        if (gwy_isnormal(v) && v >= min && v <= max)
            return TRUE;
        add_invalid_value_error(error_list, item->name, type_name, "%g", v);
        return FALSE;
    }
    g_return_val_if_reached(FALSE);
}

/**
 * gwy_check_float_component:
 * @item: Serialised item for a floating point value.
 * @type_name: Name of the type being deserialised (used for error messages only).
 * @min: The minimum allowed value.
 * @max: The maximum allowed value.
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 *
 * Checks if a floating point value in a deserialised item list is normal and within allowed range.
 *
 * The item must be of type %GWY_SERIALIZABLE_DOUBLE.
 *
 * Returns: %TRUE if the value is normal and within range [@min,@max] (inclusive), %FALSE if it is not.
 **/
gboolean
gwy_check_float_component(const GwySerializableItem *item,
                          const gchar *type_name,
                          gfloat min,
                          gfloat max,
                          GwyErrorList **error_list)
{
    if (item->ctype == GWY_SERIALIZABLE_FLOAT) {
        gfloat v = item->value.v_float;
        /* XXX: Anything weird can probably happen here with -fast-math. We check the bit representation for doubles,
         * but -fast-math may break conversion of a float NaN to a double NaN. Do we need a separate function for
         * floats to ensure we are deserialising normal numbers? */
        if (gwy_isnormal(v) && v >= min && v <= max)
            return TRUE;
        add_invalid_value_error(error_list, item->name, type_name, "%g", v);
        return FALSE;
    }
    g_return_val_if_reached(FALSE);
}

/**
 * gwy_check_int32_component:
 * @item: Serialised item for a 32bit integer point value.
 * @type_name: Name of the type being deserialised (used for error messages only).
 * @min: The minimum allowed value.
 * @max: The maximum allowed value.
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 *
 * Checks if a 32bit integer value in a deserialised item list is within allowed range.
 *
 * The item must be of type %GWY_SERIALIZABLE_INT32.
 *
 * Returns: %TRUE if the value is within range [@min,@max] (inclusive), %FALSE if it is not.
 **/
gboolean
gwy_check_int32_component(const GwySerializableItem *item,
                          const gchar *type_name,
                          gint32 min,
                          gint32 max,
                          GwyErrorList **error_list)
{
    if (item->ctype == GWY_SERIALIZABLE_INT32) {
        gint32 v = item->value.v_int32;
        if (v >= min && v <= max)
            return TRUE;
        add_invalid_value_error(error_list, item->name, type_name, "%d", v);
        return FALSE;
    }
    g_return_val_if_reached(FALSE);
}

/**
 * gwy_check_enum_component:
 * @item: Serialised item for a 32bit integer point value.
 * @type_name: Name of the type being deserialised (used for error messages only).
 * @enum_type: Type of the enum the value must belong to.
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 *
 * Checks if an enumerated value in a deserialised item list is valid.
 *
 * The item must be of type %GWY_SERIALIZABLE_INT32.
 *
 * Returns: %TRUE if the value is among the enumerated values, %FALSE if it is not.
 **/
gboolean
gwy_check_enum_component(const GwySerializableItem *item,
                         const gchar *type_name,
                         GType enum_type,
                         GwyErrorList **error_list)
{
    if (item->ctype == GWY_SERIALIZABLE_INT32) {
        gint32 v = item->value.v_int32;
        if (v == gwy_enum_sanitize_value(v, enum_type))
            return TRUE;
        add_invalid_value_error(error_list, item->name, type_name, "%d", v);
        return FALSE;
    }
    g_return_val_if_reached(FALSE);
}

/**
 * gwy_check_data_length_multiple:
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 * @type_name: Name of the type being deserialised (used for error messages only).
 * @len: Data length.
 * @multiple: Positive number @len must be a multiple of.
 *
 * Checks if data length is a multiple of a fixed number.
 *
 * Zero length is considered valid.
 *
 * Returns: %TRUE if the data length is valid, %FALSE if it is not.
 **/
gboolean
gwy_check_data_length_multiple(GwyErrorList **error_list,
                               const gchar *type_name,
                               gsize len,
                               guint multiple)
{
    if (len % multiple == 0)
        return TRUE;

    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       // TRANSLATORS: Error message.
                       // TRANSLATORS: %s is a data type name, e.g. GwyCurve, GwyGradient.
                       _("Data length of ‘%s’ is %lu which is not a multiple of %u."),
                       type_name, (gulong)len, multiple);
    return FALSE;
}

// NB: Having to make an array of gsize values to pass logical_dims[] is a bit awkward. But it makes it very clear
// what you must do and prevenrs guint vs. gsize mix-ups. So keep it that way.
/**
 * gwy_check_data_dimension:
 * @error_list: Location to store the errors occuring, %NULL to ignore.
 * @type_name: Name of the type being deserialised (used for error messages only).
 * @n_dims: Number of dimensions.
 * @array_size: Physical array size (product of all the dimensions), if known. Pass 0 to not check the total size.
 * @logical_dims: @n_dims logical dimensions, i.e. numbers which were deserialised as dimensions of a possibly
 *                multi-dimensional array.
 *
 * Checks if deserialised data dimensions are valid.
 *
 * Any positive dimensions which do not multiply to an integer overflow are currently considered valid, unless
 * @array_size is also given. In such case they must multiply exactly to @array_size. Since individual zero
 * dimensions are invalid, zero is never a valid @array_size.
 *
 * A zero-dimensional array always has one element (a scalar). The empty product is equal to 1 by definition. However,
 * it is usually not meaningful to call this function with @n_dims=0.
 *
 * Returns: %TRUE if the dimensions are valid, %FALSE if they are not.
 **/
gboolean
gwy_check_data_dimension(GwyErrorList **error_list,
                         const gchar *type_name,
                         guint n_dims,
                         gsize array_size,
                         const gsize *logical_dims)
{
    gboolean failed_total = FALSE;

    gsize total_size = 1;
    for (guint i = 0; i < n_dims; i++) {
        gsize dim = logical_dims[i];
        if (G_LIKELY(dim)) {
            if (G_MAXSIZE/dim >= total_size) {
                total_size *= dim;
                continue;
            }
        }
        goto fail;
    }

    if (array_size && total_size != array_size) {
        failed_total = TRUE;
        goto fail;
    }

    return TRUE;

fail:
    GString *str = g_string_new(NULL);
    if (n_dims) {
        for (guint j = 0; j < n_dims; j++) {
            if (j)
                g_string_append(str, "×");
            g_string_append_printf(str, "%" G_GSIZE_FORMAT, logical_dims[j]);
        }
    }
    else
        g_string_assign(str, "1");

    if (failed_total) {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_DATA,
                           // TRANSLATORS: Error message.
                           // TRANSLATORS: Second %s is a data type name, e.g. GwyField.
                           _("Dimension %s of ‘%s’ do not match expected total size %" G_GSIZE_FORMAT "."),
                           str->str, type_name, array_size);
    }
    else {
        gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                           // TRANSLATORS: Error message.
                           // TRANSLATORS: Second %s is a data type name, e.g. GwyField.
                           _("Dimension %s of ‘%s’ is invalid."),
                           str->str, type_name);
    }
    g_string_free(str, TRUE);
    return FALSE;
}

static void
add_invalid_value_error(GwyErrorList **error_list,
                        const gchar *item_name, const gchar *type_name,
                        const gchar *fmt, ...)
{
    if (!error_list)
        return;

    va_list ap;
    va_start(ap, fmt);
    gchar *s = g_strdup_vprintf(fmt, ap);
    va_end(ap);
    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       // TRANSLATORS: Error message.
                       _("Component ‘%s’ of object %s has invalid value %s."),
                       item_name, type_name, s);
    g_free(s);
}

static void
add_wrong_type_error(GwyErrorList **error_list,
                     const GwySerializableItem *model,
                     gsize n_items,
                     const GwySerializableItem *item,
                     const gchar *type_name)
{
    if (!error_list)
        return;

    // Look up the item with names only to give a more meaningful error message if the name matches but the type is
    // wrong. Do not add an error if the element is completely unknown as that can be actually OK (parent vs. child
    // class).
    gsize j = 0;
    for (j = 0; j < n_items; j++) {
        if (gwy_strequal(model[j].name, item->name))
            break;
    }
    if (j == n_items)
        return;

    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_ITEM,
                       // TRANSLATORS: Error message.
                       _("Item ‘%s’ in the representation of object ‘%s’ has type 0x%02x instead of expected 0x%02x. "
                         "It was ignored."),
                       item->name ? item->name : "(nil)",
                       type_name, item->ctype, model[j].ctype);
}

static void
add_duplicate_item_error(GwyErrorList **error_list,
                         const gchar *name,
                         GwySerializableCType ctype,
                         const gchar *type_name)
{
    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_ITEM,
                       // TRANSLATORS: Error message.
                       _("Item ‘%s’ of type 0x%02x is present multiple times in the representation "
                         "of object ‘%s’. Values other than the first were ignored."),
                       name ? name : "(nil)", ctype, type_name);
}

/**
 * SECTION: serializable-utils
 * @title: Serialisation utils
 * @short_description: Helper functions for classes implementing serialisation
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
