/*
 * Copyright © 2015 Canonical Limited
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * 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,
    NULL, NULL
  };
  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);
        }
    }

  while (group->requested_state != group->effective_state)
    g_cond_wait (&group->cond, &group->lock);
}

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);
}
