/*
 *  $Id: resource.c 28798 2025-11-05 11:40:26Z yeti-dn $
 *  Copyright (C) 2003-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@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 <errno.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <sys/stat.h>
#include <sys/types.h>
#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/inventory.h"
#include "libgwyddion/resource.h"

#define MAGIC_HEADER "Gwyddion resource "

#define CONSTRUCT_GPARAM (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)

enum {
    SGNL_DATA_CHANGED,
    NUM_SIGNALS
};

enum {
    PROP_0,
    PROP_NAME,
    PROP_CONST,
    PROP_MANAGED,
    PROP_PREFERRED,
    NUM_PROPERTIES
};

struct _GwyResourcePrivate {
    GString *name;

    gboolean is_const : 8;
    gboolean is_modified : 8;
    gboolean is_preferred : 8;
};

/* Use a static propery -> trait map.  We could do it generically, too. g_param_spec_pool_list() is ugly and slow is
 * the minor problem, the major is that it does g_hash_table_foreach() so we would get different orders on different
 * invocations and have to sort it. */
/* Threads: only modified in class init functon.  */
static GType gwy_resource_trait_types[NUM_PROPERTIES-1];          /* Threads: OK */
static const gchar *gwy_resource_trait_names[NUM_PROPERTIES-1];   /* Threads: OK */
static guint gwy_resource_ntraits = 0;                       /* Threads: OK */

static void         finalize            (GObject *object);
static void         set_property        (GObject *object,
                                         guint prop_id,
                                         const GValue *value,
                                         GParamSpec *pspec);
static void         get_property        (GObject *object,
                                         guint prop_id,
                                         GValue *value,
                                         GParamSpec *pspec);
static gboolean     item_is_const       (gconstpointer item);
static const gchar* item_get_name       (gpointer item);
static gint         item_compare        (gconstpointer item1,
                                         gconstpointer item2);
static void         item_rename         (gpointer item,
                                         const gchar *new_name);
static const GType* item_get_traits     (gint *ntraits);
static const gchar* item_get_trait_name (gint i);
static void         item_get_trait_value(gpointer item,
                                         gint i,
                                         GValue *value);
static GwyResource* parse               (const gchar *text,
                                         GType expected_type);
static void         was_modified        (GwyResource *resource);
static void         load_class_dir      (const gchar *path,
                                         GwyResourceClass *klass,
                                         gboolean is_system);
static void         save_if_modified    (gpointer key,
                                         gpointer value,
                                         gpointer user_data);
static gboolean     save                (GwyResource *resource,
                                         GError **error);

static guint signals[NUM_SIGNALS];
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GObjectClass *parent_class = NULL;

G_LOCK_DEFINE_STATIC(all_resources);
static GArray *all_resources = NULL;          /* Threads: protected by lock */

static const GwyInventoryItemType gwy_resource_item_type = {
    0,
    "data-changed",
    item_is_const,
    item_get_name,
    item_compare,
    item_rename,
    NULL,
    NULL,  /* copy needs particular class */
    item_get_traits,
    item_get_trait_name,
    item_get_trait_value,
};

G_DEFINE_ABSTRACT_TYPE_WITH_CODE(GwyResource, gwy_resource, G_TYPE_OBJECT,
                                 G_ADD_PRIVATE(GwyResource))

static void
gwy_resource_class_init(GwyResourceClass *klass)
{
    static const guint trait_props[] = { PROP_NAME, PROP_PREFERRED, PROP_CONST };
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    all_resources = g_array_new(FALSE, FALSE, sizeof(GType));

    parent_class = gwy_resource_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->get_property = get_property;
    gobject_class->set_property = set_property;

    klass->item_type = gwy_resource_item_type;
    klass->item_type.type = G_TYPE_FROM_CLASS(klass);
    klass->data_changed = was_modified;

    /* What is the default value good for? */
    properties[PROP_NAME] = g_param_spec_string("name", NULL, "Resource name",
                                                NULL,
                                                CONSTRUCT_GPARAM);
    properties[PROP_CONST] = g_param_spec_boolean("const", NULL,
                                                  "Whether a resource is constant (system)",
                                                  FALSE,
                                                  CONSTRUCT_GPARAM);
    /* FIXME: We are currently not able to emit notify on this property, treating it as never changing. Usually it
     * should not change except during initial resource data filling, but it may. */
    properties[PROP_MANAGED] = g_param_spec_boolean("managed", NULL,
                                                    "Whether a resource is managed in the inventory",
                                                    FALSE,
                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    properties[PROP_PREFERRED] = g_param_spec_boolean("preferred", NULL,
                                                      "Whether a resource is preferred (shown in shortlists)",
                                                      FALSE,
                                                      GWY_GPARAM_RWE);
    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);

    for (guint i = 0; i < G_N_ELEMENTS(trait_props); i++) {
        GParamSpec *pspec = properties[trait_props[i]];
        gwy_resource_trait_types[i] = pspec->value_type;
        gwy_resource_trait_names[i] = pspec->name;
    }
    gwy_resource_ntraits = G_N_ELEMENTS(trait_props);

    /**
     * GwyResource::data-changed:
     * @gwyresource: The #GwyResource which received the signal.
     *
     * The ::data-changed signal is emitted when resource data changes.
     */
    signals[SGNL_DATA_CHANGED] = g_signal_new("data-changed", type,
                                              G_SIGNAL_RUN_FIRST,
                                              G_STRUCT_OFFSET(GwyResourceClass, data_changed),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__VOID,
                                              G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_DATA_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);
}

static void
gwy_resource_init(GwyResource *resource)
{
    GwyResourcePrivate *priv;

    priv = resource->priv = gwy_resource_get_instance_private(resource);
    priv->name = g_string_new(NULL);
}

static void
finalize(GObject *object)
{
    GwyResourcePrivate *priv = GWY_RESOURCE(object)->priv;

    g_string_free(priv->name, TRUE);

    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyResource *resource = GWY_RESOURCE(object);
    GwyResourcePrivate *priv = resource->priv;

    /* NB: G_PARAM_CONSTRUCT_ONLY implies G_PARAM_CONSTRUCT. It is not just a restriction, GLib will always set
     * the property upon upon construction. Everything except PREFERRED can only be set upon construction so we
     * do not emit any signals. */
    switch (prop_id) {
        case PROP_NAME:
        gwy_assign_gstring(priv->name, g_value_get_string(value));
        break;

        case PROP_PREFERRED:
        gwy_resource_set_preferred(resource, g_value_get_boolean(value));
        break;

        case PROP_CONST:
        priv->is_const = g_value_get_boolean(value);
        priv->is_modified = !priv->is_const;
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyResourcePrivate *priv = GWY_RESOURCE(object)->priv;

    switch (prop_id) {
        case PROP_NAME:
        g_value_set_string(value, priv->name->str);
        break;

        case PROP_CONST:
        g_value_set_boolean(value, priv->is_const);
        break;

        case PROP_PREFERRED:
        g_value_set_boolean(value, priv->is_preferred);
        break;

        case PROP_MANAGED:
        g_value_set_boolean(value, gwy_resource_is_managed(GWY_RESOURCE(object)));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static const gchar*
item_get_name(gpointer item)
{
    GwyResource *resource = (GwyResource*)item;
    return resource->priv->name->str;
}

static gboolean
item_is_const(gconstpointer item)
{
    GwyResource *resource = (GwyResource*)item;
    return resource->priv->is_const;
}

static gint
item_compare(gconstpointer item1, gconstpointer item2)
{
    GwyResource *resource1 = (GwyResource*)item1;
    GwyResource *resource2 = (GwyResource*)item2;

    return strcmp(resource1->priv->name->str, resource2->priv->name->str);
}

static void
item_rename(gpointer item, const gchar *new_name)
{
    GwyResource *resource = (GwyResource*)item;
    GwyResourcePrivate *priv = resource->priv;

    g_return_if_fail(!priv->is_const);

    g_string_assign(priv->name, new_name);
    g_object_notify_by_pspec(G_OBJECT(item), properties[PROP_NAME]);
}

static const GType*
item_get_traits(gint *ntraits)
{
    if (ntraits)
        *ntraits = gwy_resource_ntraits;

    return gwy_resource_trait_types;
}

static const gchar*
item_get_trait_name(gint i)
{
    g_return_val_if_fail(i >= 0 && i < gwy_resource_ntraits, NULL);
    return gwy_resource_trait_names[i];
}

static void
item_get_trait_value(gpointer item,
                     gint i,
                     GValue *value)
{
    g_return_if_fail(i >= 0 && i < gwy_resource_ntraits);
    g_value_init(value, gwy_resource_trait_types[i]);
    g_object_get_property(G_OBJECT(item), gwy_resource_trait_names[i], value);
}

/**
 * gwy_resource_get_name:
 * @resource: A resource.
 *
 * Returns resource name.
 *
 * Returns: Name of @resource.  The string is owned by @resource and must not be modfied or freed.
 **/
const gchar*
gwy_resource_get_name(GwyResource *resource)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), NULL);
    return resource->priv->name->str;
}

/**
 * gwy_resource_is_modifiable:
 * @resource: A resource.
 *
 * Returns whether a resource is modifiable.
 *
 * Resources loaded from user directories and unmanaged resources are generally modifiable. System resources are not.
 *
 * Returns: %TRUE if resource is modifiable, %FALSE if it is a fixed (system) resource.
 **/
gboolean
gwy_resource_is_modifiable(GwyResource *resource)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), FALSE);
    return !resource->priv->is_const;
}

/**
 * gwy_resource_is_modified:
 * @resource: A resource.
 *
 * Returns whether a resource has been modified.
 *
 * Resources loaded from user directories and unmanaged resources are generally modifiable. System resources are not.
 *
 * Returns: %TRUE if resource has been modified since last save or loading from disk, %FALSE if it has not been
 *          modified.
 **/
gboolean
gwy_resource_is_modified(GwyResource *resource)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), FALSE);
    return !!resource->priv->is_modified;
}

/**
 * gwy_resource_is_managed:
 * @resource: A resource.
 *
 * Returns whether a resource is managed.
 *
 * Managed resources are in the class inventory returned by gwy_resource_class_get_inventory() (or class-specific
 * functions). They are usualy backed up by on disk files. New are created using inventory functions.
 *
 * Unmanaged resources are standalone objects, not in the class inventory and not saved to disk.
 *
 * Returns: %TRUE if resource is managed, %FALSE if it is not.
 **/
gboolean
gwy_resource_is_managed(GwyResource *resource)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), FALSE);
    GwyInventory *inventory = GWY_RESOURCE_GET_CLASS(resource)->inventory;
    if (!inventory)
        return FALSE;

    GwyResource *maybe_self = gwy_inventory_get_item(inventory, resource->priv->name->str);
    return maybe_self == resource;
}

/**
 * gwy_resource_get_preferred:
 * @resource: A resource.
 *
 * Returns whether a resource is preferred.
 *
 * Returns: %TRUE if resource is preferred, %FALSE otherwise.
 **/
gboolean
gwy_resource_get_preferred(GwyResource *resource)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), FALSE);
    return !!resource->priv->is_preferred;
}

/**
 * gwy_resource_set_preferred:
 * @resource: A resource.
 * @is_preferred: %TRUE to make @resource preferred, %FALSE to make it not preferred.
 *
 * Sets preferability of a resource.
 *
 * Preferred resources are offered more readily. For instance short lists can offer only preferred resources and the
 * user may have to go to a full list to access the non-preferred.
 **/
void
gwy_resource_set_preferred(GwyResource *resource,
                           gboolean is_preferred)
{
    g_return_if_fail(GWY_IS_RESOURCE(resource));
    GwyResourcePrivate *priv = resource->priv;
    if (!priv->is_preferred == !is_preferred)
        return;
    priv->is_preferred = !!is_preferred;
    g_object_notify_by_pspec(G_OBJECT(resource), properties[PROP_PREFERRED]);
}

/**
 * gwy_resource_class_get_name:
 * @klass: A resource class.
 *
 * Gets the name of resource class.
 *
 * This is an simple identifier usable for example as directory name.
 *
 * Returns: Resource class name, as a constant string that must not be modified nor freed.
 **/
const gchar*
gwy_resource_class_get_name(GwyResourceClass *klass)
{
    g_return_val_if_fail(GWY_IS_RESOURCE_CLASS(klass), NULL);
    return klass->name;
}

/**
 * gwy_resource_class_get_inventory:
 * @klass: A resource class.
 *
 * Gets inventory which holds resources of a resource class.
 *
 * Returns: (transfer none): Resource class inventory.
 **/
GwyInventory*
gwy_resource_class_get_inventory(GwyResourceClass *klass)
{
    g_return_val_if_fail(GWY_IS_RESOURCE_CLASS(klass), NULL);
    return klass->inventory;
}

/**
 * gwy_resource_class_get_item_type:
 * @klass: A resource class.
 *
 * Gets inventory item type for a resource class.
 *
 * Returns: Inventory item type.
 **/
const GwyInventoryItemType*
gwy_resource_class_get_item_type(GwyResourceClass *klass)
{
    g_return_val_if_fail(GWY_IS_RESOURCE_CLASS(klass), NULL);
    return &klass->item_type;
}

static GwyResource*
parse(const gchar *text, GType expected_type)
{
    if (!g_str_has_prefix(text, MAGIC_HEADER)) {
        g_warning("Wrong resource magic header");
        return NULL;
    }

    text += sizeof(MAGIC_HEADER) - 1;
    guint len = strspn(text, G_CSET_a_2_z G_CSET_A_2_Z G_CSET_DIGITS);
    gchar *name = g_strndup(text, len);
    text = strchr(text + len, '\n');
    if (!text) {
        g_warning("Truncated resource header");
        g_free(name);
        return NULL;
    }
    text++;

    GType type = g_type_from_name(name);
    if (!type
        || (expected_type && type != expected_type)
        || !g_type_is_a(type, GWY_TYPE_RESOURCE)
        || !G_TYPE_IS_INSTANTIATABLE(type)
        || G_TYPE_IS_ABSTRACT(type)) {
        g_warning("Wrong resource type `%s'", name);
        g_free(name);
        return NULL;
    }
    GwyResourceClass *klass = GWY_RESOURCE_CLASS(g_type_class_peek_static(type));
    GwyResource *resource = NULL;
    g_return_val_if_fail(klass, resource);

    if (klass->parse_with_type)
        resource = klass->parse_with_type(type, text);
    else if (klass->parse)
        resource = klass->parse(text);
    else {
        g_assert_not_reached();
    }

    g_free(name);

    return resource;
}

/**
 * gwy_resource_data_changed:
 * @resource: A resource.
 *
 * Emits signal "data-changed" on a resource.
 *
 * It can be called only on non-constant resources.  The default handler sets the modified flag on the resource which
 * can be queried with gwy_resource_is_modified().
 *
 * Mostly useful in resource implementation.
 **/
void
gwy_resource_data_changed(GwyResource *resource)
{
    g_return_if_fail(GWY_IS_RESOURCE(resource));
    g_signal_emit(resource, signals[SGNL_DATA_CHANGED], 0);
}

static void
was_modified(GwyResource *resource)
{
    GwyResourcePrivate *priv = resource->priv;
    if (priv->is_const)
        g_warning("Constant resource was modified");
    priv->is_modified = TRUE;
}

/**
 * gwy_resource_data_saved:
 * @resource: A resource.
 *
 * Clears the modified flag of a resource.
 **/
void
gwy_resource_data_saved(GwyResource *resource)
{
    g_return_if_fail(GWY_IS_RESOURCE(resource));
    GwyResourcePrivate *priv = resource->priv;
    if (priv->is_const)
        g_warning("Constant resource being passed to data_saved()");
    priv->is_modified = FALSE;
}

/**
 * gwy_resource_class_load:
 * @klass: A resource class.
 *
 * Loads resources of a resources class from disk.
 *
 * Resources are loaded from system directory (and marked constant) and from user directory (marked modifiable).
 **/
void
gwy_resource_class_load(GwyResourceClass *klass)
{
    g_return_if_fail(GWY_IS_RESOURCE_CLASS(klass));
    g_return_if_fail(klass->inventory);

    gwy_inventory_forget_order(klass->inventory);

    GType type = G_TYPE_FROM_CLASS(klass);
    G_LOCK(all_resources);
    guint i;
    for (i = 0; i < all_resources->len; i++) {
        if (g_array_index(all_resources, GType, i) == type)
            break;
    }
    if (i == all_resources->len)
        g_array_append_val(all_resources, type);
    G_UNLOCK(all_resources);

    gchar *path = gwy_find_self_path("data", klass->name, NULL);
    load_class_dir(path, klass, TRUE);
    g_free(path);

    path = g_build_filename(gwy_get_user_dir(), klass->name, NULL);
    load_class_dir(path, klass, FALSE);
    g_free(path);

    gwy_inventory_restore_order(klass->inventory);
}

static void
load_class_dir(const gchar *path, GwyResourceClass *klass, gboolean is_const)
{
    GDir *dir;
    if (!(dir = g_dir_open(path, 0, NULL)))
        return;

    const gchar *name;
    while ((name = g_dir_read_name(dir))) {
        if (gwy_filename_ignore(name))
            continue;

        if (gwy_inventory_get_item(klass->inventory, name)) {
            g_warning("Ignoring duplicate %s `%s'", klass->name, name);
            continue;
        }
        /* FIXME */
        gchar *filename = g_build_filename(path, name, NULL);

        GError *err = NULL;
        gchar *text;
        if (!g_file_get_contents(filename, &text, NULL, &err)) {
            g_warning("Cannot read `%s': %s", filename, err->message);
            g_clear_error(&err);
            g_free(filename);
            continue;
        }
        g_free(filename);

        GwyResource *resource = parse(text, G_TYPE_FROM_CLASS(klass));
        if (resource) {
            GwyResourcePrivate *priv = resource->priv;
            g_string_assign(priv->name, name);
            priv->is_const = is_const;
            priv->is_modified = FALSE;
            gwy_inventory_insert_item(klass->inventory, resource);
            g_object_unref(resource);
        }
        g_free(text);
    }

    g_dir_close(dir);
}

/**
 * gwy_resource_class_save_modified:
 * @klass: A resource class.
 *
 * Saves all modified resource of given class to disk.
 **/
void
gwy_resource_class_save_modified(GwyResourceClass *klass)
{
    g_return_if_fail(GWY_IS_RESOURCE_CLASS(klass));
    g_return_if_fail(klass->inventory);
    if (!gwy_resource_class_mkdir(klass)) {
        g_warning("Resource directory cannot be created.");
        return;
    }
    gwy_inventory_foreach(klass->inventory, save_if_modified, NULL);
}

static void
save_if_modified(gpointer key, gpointer value, G_GNUC_UNUSED gpointer user_data)
{
    G_GNUC_UNUSED guint i = GPOINTER_TO_UINT(key);
    GwyResource *resource = (GwyResource*)value;
    GwyResourcePrivate *priv = resource->priv;

    if (priv->is_modified && !priv->is_const)
        save(resource, NULL);
}

/**
 * gwy_resource_build_filename:
 * @resource: A resource.
 *
 * Builds file name a resource should be saved to.
 *
 * If the resource has not been newly created, renamed, or system it was probably loaded from file of the same name.
 *
 * Returns: Resource file name as a newly allocated string that must be freed by caller.
 **/
gchar*
gwy_resource_build_filename(GwyResource *resource)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), NULL);

    GwyResourcePrivate *priv = resource->priv;
    if (priv->is_const)
        g_warning("Filename of a constant resource `%s' should not be needed", priv->name->str);

    GwyResourceClass *klass = GWY_RESOURCE_GET_CLASS(resource);
    return g_build_filename(gwy_get_user_dir(), klass->name, priv->name->str, NULL);
}

/**
 * gwy_resource_delete:
 * @resource: A resource.
 *
 * Deletes a resource, including removal from disk.
 *
 * The method deletes the resource both in the inventory and on disk. Constant and unmanaged resources cannot be
 * removed (simply unref unmananged resources as any other object to destroy them).
 *
 * Returns: %TRUE if the removal succeeded.
 **/
gboolean
gwy_resource_delete(GwyResource *resource)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), FALSE);

    GwyResourcePrivate *priv = resource->priv;
    if (priv->is_const || !gwy_resource_is_managed(resource))
        return FALSE;

    GwyInventory *inventory = GWY_RESOURCE_GET_CLASS(resource)->inventory;
    gchar *filename = gwy_resource_build_filename(resource);
    gint retval = g_remove(filename);
    if (retval == 0)
        gwy_inventory_delete_item(inventory, priv->name->str);
    else {
        /* FIXME: GUIze this */
        g_warning("Cannot delete resource file: %s", filename);
    }
    g_free(filename);

    return retval == 0;
}

/**
 * gwy_resource_rename:
 * @resource: A resource.
 * @newname: New resource name.
 *
 * Renames a resource, including renaming it on disk.
 *
 * The method renames the resource both in the inventory and on disk. The renaming must not conflict with an existing
 * resource, constant resources cannot be renamed, etc.  It is OK to rename a resource to the same name (nothing
 * happens then).
 *
 * Constant (system) resources cannot be renamed. Unmanaged resources can be renamed using this function; it just
 * does not change anything on disk in such case.
 *
 * Returns: %TRUE if the renaming succeeded.
 **/
gboolean
gwy_resource_rename(GwyResource *resource,
                    const gchar *newname)
{
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), FALSE);
    g_return_val_if_fail(newname, FALSE);

    if (strchr(newname, '/') || strchr(newname, '\\')) {
        g_warning("Refusing to rename resource to name with special chars.");
        return FALSE;
    }

    GwyResourcePrivate *priv = resource->priv;
    if (gwy_strequal(newname, priv->name->str))
        return TRUE;
    if (priv->is_const)
        return FALSE;

    if (!gwy_resource_is_managed(resource)) {
        g_string_assign(priv->name, newname);
        g_object_notify_by_pspec(G_OBJECT(resource), properties[PROP_NAME]);
        return TRUE;
    }

    GwyInventory *inventory = GWY_RESOURCE_GET_CLASS(resource)->inventory;
    gpointer item = gwy_inventory_get_item(inventory, newname);
    if (item)
        return FALSE;

    gchar *oldname = g_strdup(priv->name->str);
    gchar *oldfilename = gwy_resource_build_filename(resource);
    gwy_inventory_rename_item(inventory, oldname, newname);
    gchar *newfilename = gwy_resource_build_filename(resource);

    gint retval = g_rename(oldfilename, newfilename);
    if (retval != 0) {
        /* FIXME: GUIze this */
        g_warning("Cannot rename resource file: %s to %s", oldfilename, newfilename);
        gwy_inventory_rename_item(inventory, newname, oldname);
    }
    g_free(newfilename);
    g_free(oldfilename);
    g_free(oldname);

    return retval == 0;
}

/**
 * gwy_resource_save:
 * @resource: A resource.
 * @error: Return location for a #GError.
 *
 * Saves a resource to disk.
 *
 * Constant resources cannot be saved. Unmanaged resources cannot be saved using this function because they have no
 * associated on-disk location.
 *
 * The file name is determined by gwy_resource_build_filename(). The resource data are saved even if the modified flag
 * is not set. Upon successful save, the modified flag is cleared.
 *
 * Instead of saving individual resources, consider also using gwy_resource_class_save_modified().
 *
 * Returns: %TRUE if resource was saved to disk, %FALSE otherwise.
 **/
gboolean
gwy_resource_save(GwyResource *resource,
                  GError **error)
{
    /* Return TRUE on assertions as we do not set the error. */
    g_return_val_if_fail(GWY_IS_RESOURCE(resource), TRUE);
    GwyResourcePrivate *priv = resource->priv;
    if (priv->is_const) {
        g_warning("Constant resource passed to gwy_resource_save()");
        return TRUE;
    }
    if (!gwy_resource_is_managed(resource)) {
        g_warning("Unmanage resource passed to gwy_resource_save()");
        return TRUE;
    }

    if (!gwy_resource_class_mkdir(GWY_RESOURCE_GET_CLASS(resource))) {
        g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_NOTDIR, _("Resource directory cannot be created."));
        return FALSE;
    }

    return save(resource, error);
}

static gboolean
save(GwyResource *resource, GError **error)
{
    GwyResourceClass *klass = GWY_RESOURCE_GET_CLASS(resource);
    g_return_val_if_fail(klass->dump, FALSE);

    GString *str = g_string_new(MAGIC_HEADER);
    g_string_append(str, G_OBJECT_TYPE_NAME(resource));
    g_string_append_c(str, '\n');
    klass->dump(resource, str);

    gchar *filename = gwy_resource_build_filename(resource);
    gboolean ok;
    if ((ok = g_file_set_contents(filename, str->str, str->len, error)))
        gwy_resource_data_saved(resource);

    g_string_free(str, TRUE);
    g_free(filename);

    return ok;
}

/**
 * gwy_resource_class_mkdir:
 * @klass: A resource class.
 *
 * Creates directory for user resources if it does not exist.
 *
 * Returns: %TRUE if the directory exists or has been successfully created. %FALSE if it doesn't exist and cannot be
 *          created, consult errno for reason.
 **/
gboolean
gwy_resource_class_mkdir(GwyResourceClass *klass)
{
    g_return_val_if_fail(GWY_IS_RESOURCE_CLASS(klass), FALSE);

    gchar *path = g_build_filename(gwy_get_user_dir(), klass->name, NULL);
    if (g_file_test(path, G_FILE_TEST_IS_DIR)) {
        g_free(path);
        return TRUE;
    }

    gint ok = !g_mkdir(path, 0700);
    g_free(path);

    return ok;
}

/**
 * gwy_resource_classes_finalize:
 *
 * Destroys the inventories of all resource classes.
 *
 * This function makes the affected resource classes unusable.  Its purpose is to faciliate reference leak debugging
 * by destroying a large number of objects that normally live forever.
 *
 * Note static resource classes that never called gwy_resource_class_load() are excluded.
 **/
void
gwy_resource_classes_finalize(void)
{
    G_LOCK(all_resources);
    for (guint i = 0; i < all_resources->len; i++) {
        GwyResourceClass *klass = g_type_class_ref(g_array_index(all_resources, GType, i));
        g_clear_object(&klass->inventory);
        g_type_class_unref(klass);
    }
    g_array_set_size(all_resources, 0);
    G_UNLOCK(all_resources);
}

/**
 * SECTION: resource
 * @title: GwyResource
 * @short_description: Built-in and/or user supplied application resources
 * @see_also: #GwyInventory
 *
 * #GwyResource is a base class for various application resources.  It defines common interface: questioning resource
 * name (gwy_resource_get_name()), modifiability (gwy_resource_is_modifiable()), loading resources from files and
 * saving them.
 *
 * Resources of one type are typically managed in an inventory which is filled during initialisation. Resource objects
 * are created, deleted and modified using #GwyResource and #GwyInventory methods. You can create standalone
 * (unmanaged) resource objects outside the inventory using g_object_new() or class-specific means and it is useful
 * sometimes for private resources, but program-wide resources are handled through the inventory.
 **/

/**
 * GwyResourceClass:
 * @inventory: Inventory with resources.
 * @name: Resource class name, usable as resource directory name for on-disk resources.
 * @item_type: Inventory item type.  Most fields are pre-filled, but namely @type and @copy must be filled by
 *             particular resource type.
 * @data_changed: "data-changed" signal method.
 * @dump: Format resource data to text. Only the data itself is formatted; the envelope is handled by #GwyResource.
 * @parse: Fill resource data from a string. The function only gets the resource data itself, the envelope is handled
 *         by #GwyResource. This is the usual method as the parser normally knows its type.
 * @parse_with_type: Alternative construction from a string, with the type to construct explicitly passed. It takes
 *                   precedence over @parse if both are defined.
 * @setup_builtins: Method putting predefined constant resources in the class inventory.
 *
 * Resource class.
 *
 * Method @setup_builtins is mostly only useful for resources defined within Gwyddion itself. An initialisation
 * function, such as gwy_init(), must know that the class exists in the first place to call its @setup_builtins. By
 * the time gwy_init() is called, third-party classes are usually not registered yet. Therefore, you can use
 * @setup_builtins in other classes if you find having class methods convenient. However, they will not be called by
 * gwy_init(); you need to call them yourself.
 **/

/* 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 : */
