/*
 * Copyright © 2015 Canonical Limited
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Ryan Lortie <desrt@desrt.ca>
 */

#include "config.h"

#include "gcontextspecificgroup.h"

#include <glib-object.h>

#include "glib-private.h"

typedef struct
{
    GSource source;

    GMutex lock;
    gpointer instance;
    GQueue pending;
} GContextSpecificSource;

static gboolean g_context_specific_source_dispatch(GSource* source,
                                                   GSourceFunc callback,
                                                   gpointer user_data)
{
    GContextSpecificSource* css = (GContextSpecificSource*)source;
    guint signal_id;

    g_mutex_lock(&css->lock);

    g_assert(!g_queue_is_empty(&css->pending));
    signal_id = GPOINTER_TO_UINT(g_queue_pop_head(&css->pending));

    if (g_queue_is_empty(&css->pending))
        g_source_set_ready_time(source, -1);

    g_mutex_unlock(&css->lock);

    g_signal_emit(css->instance, signal_id, 0);

    return TRUE;
}

static void g_context_specific_source_finalize(GSource* source)
{
    GContextSpecificSource* css = (GContextSpecificSource*)source;

    g_mutex_clear(&css->lock);
    g_queue_clear(&css->pending);
}

static GContextSpecificSource* g_context_specific_source_new(const gchar* name,
                                                             gpointer instance)
{
    static GSourceFuncs source_funcs = {NULL, NULL,
                                        g_context_specific_source_dispatch,
                                        g_context_specific_source_finalize};
    GContextSpecificSource* css;
    GSource* source;

    source = g_source_new(&source_funcs, sizeof(GContextSpecificSource));
    css = (GContextSpecificSource*)source;

    g_source_set_name(source, name);

    g_mutex_init(&css->lock);
    g_queue_init(&css->pending);
    css->instance = instance;

    return css;
}

static gboolean g_context_specific_group_change_state(gpointer user_data)
{
    GContextSpecificGroup* group = user_data;

    g_mutex_lock(&group->lock);

    if (group->requested_state != group->effective_state)
    {
        (*group->requested_func)();

        group->effective_state = group->requested_state;
        group->requested_func = NULL;

        g_cond_broadcast(&group->cond);
    }

    g_mutex_unlock(&group->lock);

    return FALSE;
}

/* this is not the most elegant way to deal with this, but it's probably
 * the best.  there are only two other things we could do, really:
 *
 *  - run the start function (but not the stop function) from the user's
 *    thread under some sort of lock.  we don't run the stop function
 *    from the user's thread to avoid the destroy-while-emitting problem
 *
 *  - have some check-and-compare functionality similar to what
 *    gsettings does where we send an artificial event in case we notice
 *    a change during the potential race period (using stat, for
 *    example)
 */
static void g_context_specific_group_request_state(GContextSpecificGroup* group,
                                                   gboolean requested_state,
                                                   GCallback requested_func)
{
    if (requested_state != group->requested_state)
    {
        if (group->effective_state != group->requested_state)
        {
            /* abort the currently pending state transition */
            g_assert(group->effective_state == requested_state);

            group->requested_state = requested_state;
            group->requested_func = NULL;
        }
        else
        {
            /* start a new state transition */
            group->requested_state = requested_state;
            group->requested_func = requested_func;

            g_main_context_invoke(GLIB_PRIVATE_CALL(g_get_worker_context)(),
                                  g_context_specific_group_change_state, group);
        }
    }

    /* we only block for positive transitions */
    if (requested_state)
    {
        while (group->requested_state != group->effective_state)
            g_cond_wait(&group->cond, &group->lock);

        /* there is no way this could go back to FALSE because the object
         * that we just created in this thread would have to have been
         * destroyed again (from this thread) before that could happen.
         */
        g_assert(group->effective_state);
    }
}

gpointer g_context_specific_group_get(GContextSpecificGroup* group, GType type,
                                      goffset context_offset,
                                      GCallback start_func)
{
    GContextSpecificSource* css;
    GMainContext* context;

    context = g_main_context_get_thread_default();
    if (!context)
        context = g_main_context_default();

    g_mutex_lock(&group->lock);

    if (!group->table)
        group->table = g_hash_table_new(NULL, NULL);

    css = g_hash_table_lookup(group->table, context);

    if (!css)
    {
        gpointer instance;

        instance = g_object_new(type, NULL);
        css = g_context_specific_source_new(g_type_name(type), instance);
        G_STRUCT_MEMBER(GMainContext*, instance, context_offset) =
            g_main_context_ref(context);
        g_source_attach((GSource*)css, context);

        g_hash_table_insert(group->table, context, css);
    }
    else
        g_object_ref(css->instance);

    if (start_func)
        g_context_specific_group_request_state(group, TRUE, start_func);

    g_mutex_unlock(&group->lock);

    return css->instance;
}

void g_context_specific_group_remove(GContextSpecificGroup* group,
                                     GMainContext* context, gpointer instance,
                                     GCallback stop_func)
{
    GContextSpecificSource* css;

    if (!context)
    {
        g_critical("Removing %s with NULL context.  This object was probably "
                   "directly constructed from a "
                   "dynamic language.  This is not a valid use of the API.",
                   G_OBJECT_TYPE_NAME(instance));
        return;
    }

    g_mutex_lock(&group->lock);
    css = g_hash_table_lookup(group->table, context);
    g_hash_table_remove(group->table, context);
    g_assert(css);

    /* stop only if we were the last one */
    if (stop_func && g_hash_table_size(group->table) == 0)
        g_context_specific_group_request_state(group, FALSE, stop_func);

    g_mutex_unlock(&group->lock);

    g_assert(css->instance == instance);

    g_source_destroy((GSource*)css);
    g_source_unref((GSource*)css);
    g_main_context_unref(context);
}

void g_context_specific_group_emit(GContextSpecificGroup* group,
                                   guint signal_id)
{
    g_mutex_lock(&group->lock);

    if (group->table)
    {
        GHashTableIter iter;
        gpointer value;
        gpointer ptr;

        ptr = GUINT_TO_POINTER(signal_id);

        g_hash_table_iter_init(&iter, group->table);
        while (g_hash_table_iter_next(&iter, NULL, &value))
        {
            GContextSpecificSource* css = value;

            g_mutex_lock(&css->lock);

            g_queue_remove(&css->pending, ptr);
            g_queue_push_tail(&css->pending, ptr);

            g_source_set_ready_time((GSource*)css, 0);

            g_mutex_unlock(&css->lock);
        }
    }

    g_mutex_unlock(&group->lock);
}
