/*
 * Copyright (C) 2023 António Fernandes <antoniof@gnome.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include "nautilus-fd-holder.h"

/**
 * NautilusFdHolder:
 *
 * A helper object which wraps the process of getting, keeping, and releasing
 * file descriptor references.
 *
 * It can be reused for multiple locations, but only one at a time.
 *
 * The original use case is for #NautilusWindowSlot to keep autofs locations
 * from getting unmounted while we are displaying them.
 *
 * For convenience, we use GFileEnumerator as the real file descriptor holder.
 * This relies on the assumption that its implementation for local files calls
 * `opendir()` on creation and `closedir()` on destruction.
 */
struct _NautilusFdHolder
{
    GObject parent_instance;

    GFile *location;

    GFileEnumerator *enumerator;
    GCancellable *enumerator_cancellable;
};

G_DEFINE_FINAL_TYPE (NautilusFdHolder, nautilus_fd_holder, G_TYPE_OBJECT)

enum
{
    PROP_0,
    PROP_LOCATION,
    N_PROPS
};

static GParamSpec *properties[N_PROPS];

static GHashTable *location_enumerator_map = NULL;

static inline void
ensure_location_enumerator_map (void)
{
    if (G_UNLIKELY (location_enumerator_map == NULL))
    {
        location_enumerator_map = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal,
                                                         g_object_unref, NULL);
    }
}

static void
on_enumerator_destroyed (gpointer  data,
                         GObject  *where_the_object_was)
{
    GFile *location = G_FILE (data);

    if (location_enumerator_map != NULL &&
        where_the_object_was == g_hash_table_lookup (location_enumerator_map, location))
    {
        g_hash_table_remove (location_enumerator_map, location);
    }
}

static void
on_enumerator_ready (GObject      *source_object,
                     GAsyncResult *res,
                     gpointer      data)
{
    GFile *location = G_FILE (source_object);
    g_autoptr (GError) error = NULL;
    g_autoptr (GFileEnumerator) enumerator = g_file_enumerate_children_finish (location, res, &error);

    if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
    {
        return;
    }

    NautilusFdHolder *self = NAUTILUS_FD_HOLDER (data);

    g_return_if_fail (g_file_equal (location, self->location));
    g_warn_if_fail (self->enumerator == NULL);

    ensure_location_enumerator_map ();
    GFileEnumerator *existing = g_hash_table_lookup (location_enumerator_map, location);
    if (G_LIKELY (existing == NULL))
    {
        g_hash_table_insert (location_enumerator_map, g_object_ref (location), enumerator);
        g_object_weak_ref (G_OBJECT (enumerator), on_enumerator_destroyed, location);
    }
    else
    {
        /* Cope with the rare case of `g_file_enumerate_children_async() being
         * called twice for the same location before the first callback by using
         * the first one instead. */
        g_set_object (&enumerator, existing);
    }

    g_set_object (&self->enumerator, enumerator);
}

static void
update_fd_holder (NautilusFdHolder *self)
{
    g_cancellable_cancel (self->enumerator_cancellable);
    g_clear_object (&self->enumerator_cancellable);
    g_clear_object (&self->enumerator);

    if (self->location == NULL || !g_file_is_native (self->location))
    {
        return;
    }

    if (G_LIKELY (location_enumerator_map != NULL))
    {
        GFileEnumerator *existing = g_hash_table_lookup (location_enumerator_map,
                                                         self->location);
        if (existing != NULL)
        {
            self->enumerator = g_object_ref (existing);
            return;
        }
    }

    self->enumerator_cancellable = g_cancellable_new ();
    g_file_enumerate_children_async (self->location,
                                     G_FILE_ATTRIBUTE_STANDARD_NAME,
                                     G_FILE_QUERY_INFO_NONE,
                                     G_PRIORITY_LOW,
                                     self->enumerator_cancellable,
                                     on_enumerator_ready,
                                     self);
}

NautilusFdHolder *
nautilus_fd_holder_new (void)
{
    return g_object_new (NAUTILUS_TYPE_FD_HOLDER, NULL);
}

static void
nautilus_fd_holder_finalize (GObject *object)
{
    NautilusFdHolder *self = (NautilusFdHolder *) object;

    g_cancellable_cancel (self->enumerator_cancellable);
    g_clear_object (&self->enumerator_cancellable);
    g_clear_object (&self->enumerator);

    if (location_enumerator_map != NULL && g_hash_table_size (location_enumerator_map) == 0)
    {
        g_clear_pointer (&location_enumerator_map, g_hash_table_destroy);
    }

    g_clear_object (&self->location);

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

static void
nautilus_fd_holder_get_property (GObject    *object,
                                 guint       prop_id,
                                 GValue     *value,
                                 GParamSpec *pspec)
{
    NautilusFdHolder *self = NAUTILUS_FD_HOLDER (object);

    switch (prop_id)
    {
        case PROP_LOCATION:
        {
            g_value_set_object (value, self->location);
        }
        break;

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

static void
nautilus_fd_holder_set_property (GObject      *object,
                                 guint         prop_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
    NautilusFdHolder *self = NAUTILUS_FD_HOLDER (object);

    switch (prop_id)
    {
        case PROP_LOCATION:
        {
            if (g_set_object (&self->location, g_value_get_object (value)))
            {
                update_fd_holder (self);
                g_object_notify_by_pspec (object, properties[PROP_LOCATION]);
            }
        }
        break;

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

static void
nautilus_fd_holder_class_init (NautilusFdHolderClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->finalize = nautilus_fd_holder_finalize;
    object_class->get_property = nautilus_fd_holder_get_property;
    object_class->set_property = nautilus_fd_holder_set_property;

    properties[PROP_LOCATION] =
        g_param_spec_object ("location", NULL, NULL,
                             G_TYPE_FILE,
                             G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
    g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
nautilus_fd_holder_init (NautilusFdHolder *self)
{
}

void
nautilus_fd_holder_set_location (NautilusFdHolder *self,
                                 GFile            *location)
{
    g_return_if_fail (NAUTILUS_IS_FD_HOLDER (self));
    g_return_if_fail (location == NULL || G_IS_FILE (location));

    g_object_set (self, "location", location, NULL);
}

static gboolean
close_if_within_mount (GFile           *mount_root,
                       GFile           *location,
                       GFileEnumerator *enumerator)
{
    g_return_val_if_fail (G_IS_FILE (location), FALSE);

    if (g_file_equal (location, mount_root) || g_file_has_prefix (location, mount_root))
    {
        g_return_val_if_fail (G_IS_FILE_ENUMERATOR (enumerator), TRUE);
        g_file_enumerator_close (enumerator, NULL, NULL);
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

void
nautilus_fd_holders_release_for_mount (GMount *mount)
{
    g_return_if_fail (G_IS_MOUNT (mount));

    g_autoptr (GFile) mount_root = g_mount_get_root (mount);
    GHashTableIter iter;
    GFile *location;
    GFileEnumerator *enumerator;

    if (G_UNLIKELY (location_enumerator_map == NULL))
    {
        return;
    }

    g_hash_table_iter_init (&iter, location_enumerator_map);
    while (g_hash_table_iter_next (&iter, (gpointer *) &location, (gpointer *) &enumerator))
    {
        if (close_if_within_mount (mount_root, location, enumerator))
        {
            g_hash_table_iter_remove (&iter);
        }
    }
}
