/* cc-object-storage.h
 *
 * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#define G_LOG_DOMAIN "cc-object-storage"

#include "cc-object-storage.h"

struct _CcObjectStorage {
    GObject parent_instance;

    GHashTable *id_to_object;
};

G_DEFINE_FINAL_TYPE (CcObjectStorage, cc_object_storage, G_TYPE_OBJECT)

/* Singleton instance */
static CcObjectStorage *_instance = NULL;

/* GTask API to create a new D-Bus proxy */
typedef struct {
    GBusType bus_type;
    GDBusProxyFlags flags;
    gchar *name;
    gchar *path;
    gchar *interface;
    gboolean cached;
} TaskData;

static TaskData *
task_data_new (GBusType bus_type, GDBusProxyFlags flags, const gchar *name, const gchar *path, const gchar *interface)
{
    TaskData *data = g_slice_new (TaskData);
    data->bus_type = bus_type;
    data->flags = flags;
    data->name = g_strdup (name);
    data->path = g_strdup (path);
    data->interface = g_strdup (interface);
    data->cached = FALSE;

    return data;
}

static void
task_data_free (TaskData *data)
{
    g_free (data->name);
    g_free (data->path);
    g_free (data->interface);
    g_slice_free (TaskData, data);
}

static void
create_dbus_proxy_in_thread_cb (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable)
{
    g_autoptr(GDBusProxy) proxy = NULL;
    g_autoptr(GError) local_error = NULL;
    TaskData *data = task_data;

    proxy = g_dbus_proxy_new_for_bus_sync (data->bus_type, data->flags, NULL, data->name, data->path, data->interface,
                                           cancellable, &local_error);

    if (local_error) {
        g_task_return_error (task, g_steal_pointer (&local_error));
        return;
    }

    g_task_return_pointer (task, g_object_ref (g_steal_pointer (&proxy)), g_object_unref);
}

static void
cc_object_storage_finalize (GObject *object)
{
    CcObjectStorage *self = (CcObjectStorage *) object;

    g_debug ("Destroying cached objects");

    g_clear_pointer (&self->id_to_object, g_hash_table_destroy);

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

static void
cc_object_storage_class_init (CcObjectStorageClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->finalize = cc_object_storage_finalize;
}

static void
cc_object_storage_init (CcObjectStorage *self)
{
    self->id_to_object = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
}

/**
 * cc_object_storage_has_object:
 * @key: the unique string identifier of the object
 *
 * Checks whether there is an object associated with @key.
 *
 * Returns: %TRUE if the object is stored, %FALSE otherwise.
 */
gboolean
cc_object_storage_has_object (const gchar *key)
{
    g_assert (CC_IS_OBJECT_STORAGE (_instance));
    g_assert (key != NULL);

    return g_hash_table_contains (_instance->id_to_object, key);
}

/**
 * cc_object_storage_add_object:
 * @key: the unique string identifier of the object
 * @object: (type GObject): the object to be stored
 *
 * Adds @object to the object storage. It is a programming error to try to
 * add an object that was already added.
 *
 * @object must be a GObject.
 *
 * Always check if the object is stored with cc_object_storage_has_object()
 * before calling this function.
 */
void
cc_object_storage_add_object (const gchar *key, gpointer object)
{
    /* Trying to add an object that was already added is a hard error. Each
     * object must be added once, and only once, over the entire lifetime
     * of the application.
     */
    g_assert (CC_IS_OBJECT_STORAGE (_instance));
    g_assert (key != NULL);
    g_assert (G_IS_OBJECT (object));
    g_assert (!g_hash_table_contains (_instance->id_to_object, key));

    g_debug ("Adding object %s (%s → %p) to the storage", g_type_name (G_OBJECT_TYPE (object)), key, object);

    g_hash_table_insert (_instance->id_to_object, g_strdup (key), g_object_ref (object));
}

/**
 * cc_object_storage_get_object:
 * @key: the unique string identifier of the object
 *
 * Retrieves the object associated with @key. It is a programming error to
 * try to retrieve an object before adding it.
 *
 * Always check if the object is stored with cc_object_storage_has_object()
 * before calling this function.
 *
 * Returns: (transfer full): the GObject associated with @key.
 */
gpointer
cc_object_storage_get_object (const gchar *key)
{
    /* Trying to peek an object that was not yet added is a hard error. Users
     * of this API need to first check if the object is available with
     * cc_object_storage_has_object().
     */
    g_assert (CC_IS_OBJECT_STORAGE (_instance));
    g_assert (key != NULL);
    g_assert (g_hash_table_contains (_instance->id_to_object, key));

    return g_object_ref (g_hash_table_lookup (_instance->id_to_object, key));
}

/**
 * cc_object_storage_create_dbus_proxy_sync:
 * @name: the D-Bus name
 * @flags: the D-Bus proxy flags
 * @path: the D-Bus object path
 * @interface: the D-Bus interface name
 * @cancellable: (nullable): #GCancellable to cancel the operation
 * @error: (nullable): return location for a #GError
 *
 * Synchronously create a #GDBusProxy with @name, @path and @interface,
 * stores it in the cache, and returns the newly created proxy.
 *
 * If a proxy with that signature is already created, it will be used
 * instead of creating a new one.
 *
 * Returns: (transfer full)(nullable): the new #GDBusProxy.
 */
gpointer
cc_object_storage_create_dbus_proxy_sync (GBusType bus_type, GDBusProxyFlags flags, const gchar *name,
                                          const gchar *path, const gchar *interface, GCancellable *cancellable,
                                          GError **error)
{
    g_autoptr(GDBusProxy) proxy = NULL;
    g_autoptr(GError) local_error = NULL;
    g_autofree gchar *key = NULL;

    g_assert (CC_IS_OBJECT_STORAGE (_instance));
    g_assert (name && *name);
    g_assert (path && *path);
    g_assert (interface && *interface);
    g_assert (!error || !*error);

    key = g_strdup_printf ("CcObjectStorage::dbus-proxy(%s,%s,%s)", name, path, interface);

    g_debug ("Creating D-Bus proxy for %s", key);

    /* Check if a DBus proxy with that signature is already available; if it is,
     * return that instead of a new one.
     */
    if (g_hash_table_contains (_instance->id_to_object, key))
        return cc_object_storage_get_object (key);

    proxy = g_dbus_proxy_new_for_bus_sync (bus_type, flags, NULL, name, path, interface, cancellable, &local_error);

    if (local_error) {
        g_propagate_error (error, g_steal_pointer (&local_error));
        return NULL;
    }

    /* Store the newly created D-Bus proxy */
    cc_object_storage_add_object (key, proxy);

    return g_steal_pointer (&proxy);
}

/**
 * cc_object_storage_create_dbus_proxy:
 * @name: the D-Bus name
 * @flags: the D-Bus proxy flags
 * @path: the D-Bus object path
 * @interface: the D-Bus interface name
 * @cancellable: (nullable): #GCancellable to cancel the operation
 * @callback: callback for when the async operation is finished
 * @user_data: user data for @callback
 *
 * Asynchronously create a #GDBusProxy with @name, @path and @interface.
 *
 * If a proxy with that signature is already created, it will be used instead of
 * creating a new one.
 *
 * It is a programming error to create the an identical proxy while asynchronously
 * creating one. Not cancelling this operation will result in an assertion failure
 * when calling cc_object_storage_create_dbus_proxy_finish().
 */
void
cc_object_storage_create_dbus_proxy (GBusType bus_type, GDBusProxyFlags flags, const gchar *name, const gchar *path,
                                     const gchar *interface, GCancellable *cancellable, GAsyncReadyCallback callback,
                                     gpointer user_data)
{
    g_autoptr(GTask) task = NULL;
    g_autofree gchar *key = NULL;
    TaskData *data = NULL;

    g_assert (CC_IS_OBJECT_STORAGE (_instance));
    g_assert (name && *name);
    g_assert (path && *path);
    g_assert (interface && *interface);
    g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));

    data = task_data_new (bus_type, flags, name, path, interface);

    task = g_task_new (_instance, cancellable, callback, user_data);
    g_task_set_source_tag (task, cc_object_storage_create_dbus_proxy);
    g_task_set_task_data (task, data, (GDestroyNotify) task_data_free);

    /* Check if the D-Bus proxy is already created */
    key = g_strdup_printf ("CcObjectStorage::dbus-proxy(%s,%s,%s)", name, path, interface);

    g_debug ("Asynchronously creating D-Bus proxy for %s", key);

    if (g_hash_table_contains (_instance->id_to_object, key)) {
        /* Mark this GTask as already cached, so we can call the right assertions
         * on the callback
         * */
        data->cached = TRUE;

        g_debug ("Found in cache the D-Bus proxy %s", key);

        g_task_return_pointer (task, cc_object_storage_get_object (key), g_object_unref);
        return;
    }

    g_task_run_in_thread (task, create_dbus_proxy_in_thread_cb);
}

/**
 * cc_object_storage_create_dbus_proxy_finish:
 * @result:
 * @error: (nullable): return location for a #GError
 *
 * Finishes a D-Bus proxy creation started by cc_object_storage_create_dbus_proxy().
 *
 * Synchronously create a #GDBusProxy with @name, @path and @interface,
 * stores it in the cache, and returns the newly created proxy.
 *
 * If a proxy with that signature is already created, it will be used
 * instead of creating a new one.
 *
 * Returns: (transfer full)(nullable): the new #GDBusProxy.
 */
gpointer
cc_object_storage_create_dbus_proxy_finish (GAsyncResult *result, GError **error)
{
    g_autoptr(GDBusProxy) proxy = NULL;
    g_autoptr(GError) local_error = NULL;
    g_autofree gchar *key = NULL;
    TaskData *task_data;
    GTask *task;

    task = G_TASK (result);

    g_assert (task && G_TASK (result));
    g_assert (!error || !*error);

    task_data = g_task_get_task_data (task);
    g_assert (task_data != NULL);

    key = g_strdup_printf ("CcObjectStorage::dbus-proxy(%s,%s,%s)", task_data->name, task_data->path,
                           task_data->interface);

    g_debug ("Finished creating D-Bus proxy for %s", key);

    /* Retrieve the newly created proxy */
    proxy = g_task_propagate_pointer (task, &local_error);

    /* If the proxy is not cached, do the normal caching routine */
    if (local_error) {
        g_propagate_error (error, g_steal_pointer (&local_error));
        return NULL;
    }

    /* Either we have the object stored right when trying to create it - in which case,
     * task_data->cached == TRUE and cc_object_storage_has_object (key) == TRUE - or we
     * didn't have a cached proxy before, and we shouldn't have it now.
     *
     * This is to force consumers of this code to *never* try to create the same D-Bus
     * proxy asynchronously multiple times. Trying to do so is considered a programming
     * error.
     */
    g_assert (task_data->cached == cc_object_storage_has_object (key));

    /* If the proxy is already cached, destroy the newly created and used the cached proxy
     * instead.
     */
    if (cc_object_storage_has_object (key))
        return cc_object_storage_get_object (key);

    /* Store the newly created D-Bus proxy */
    cc_object_storage_add_object (key, proxy);

    return g_steal_pointer (&proxy);
}

/**
 * cc_object_storage_initialize:
 *
 * Initializes the single CcObjectStorage. This must be called only once,
 * and before every other method of this object.
 */
void
cc_object_storage_initialize (void)
{
    g_assert (_instance == NULL);

    if (g_once_init_enter (&_instance)) {
        CcObjectStorage *instance = g_object_new (CC_TYPE_OBJECT_STORAGE, NULL);

        g_debug ("Initializing object storage");

        g_once_init_leave (&_instance, instance);
    }
}

/**
 * cc_object_storage_destroy:
 *
 * Destroys the instance of #CcObjectStorage. This must be called only
 * once during the application lifetime. It is a programming error to
 * call this function multiple times
 */
void
cc_object_storage_destroy (void)
{
    g_assert (_instance != NULL);

    g_clear_object (&_instance);
}
