#include <string.h>
#include <glib.h>

/**
 * SECTION:shared-data
 * @title: Shared Data
 * @short_description: reference counted data
 *
 * Shared data provide an easy path to reference counted types without
 * the difficulties or overhead of GObject while allowing you to
 * preserve compatibility with existing software.
 *
 * Instead of using g_malloc() (or something similar) to allocate
 * storage for a struct, you can use one of the functions to allocate
 * a new shared buffer, such as g_shared_alloc().  Reference counting
 * is then handled by g_ref() and g_unref(), and when the reference
 * count drops to zero a callback is invoked to clean up the contents
 * of the struct before the associated memory is released.
 **/


// Public API
typedef gpointer (*GRefFunc) (gpointer data);
typedef void (*GUnrefFunc) (gpointer data);

gpointer g_ref (gpointer data);
void g_unref (gpointer data);

gpointer g_ref_alloc (gsize size, GRefFunc ref_func, GUnrefFunc unref_func);
gpointer g_ref_alloc0 (gsize size, GRefFunc ref_func, GUnrefFunc unref_func);
#define g_ref_new(type,ref_func,unref_func) \
  ((type*) g_ref_alloc(sizeof(type),ref_func,unref_func))
#define g_ref_new0(type,ref_func,unref_func) \
  ((type*) g_ref_alloc0(sizeof(type),ref_func,unref_func))

gpointer g_ref_alloc_slice (gsize size, GRefFunc ref_func, GUnrefFunc unref_func);
gpointer g_ref_alloc_slice0 (gsize size, GRefFunc ref_func, GUnrefFunc unref_func);
#define g_ref_new_slice(type,ref_func,unref_func) \
  ((type*) g_ref_alloc(sizeof(type),ref_func,unref_func))
#define g_ref_new_slice0(type,ref_func,unref_func) \
  ((type*) g_ref_alloc_slice0(sizeof(type),ref_func,unref_func))

void g_ref_free (gpointer data);

gpointer g_shared_alloc (gsize size, GDestroyNotify notify);
gpointer g_shared_alloc0 (gsize size, GDestroyNotify notify);
#define g_shared_new(type,notify) \
  ((type*) g_shared_alloc(sizeof(type),notify))
#define g_shared_new0(type,notify) \
  ((type*) g_shared_alloc0(sizeof(type),notify))

gpointer g_shared_alloc_slice (gsize size, GDestroyNotify notify);
gpointer g_shared_alloc_slice0 (gsize size, GDestroyNotify notify);
#define g_shared_new_slice(type,notify) \
  ((type*) g_shared_alloc_slice(sizeof(type),notify))
#define g_shared_new_slice0(type,notify) \
  ((type*) g_shared_alloc_slice0(sizeof(type),notify))

typedef struct _GSharedPtr GSharedPtr;

struct _GSharedPtr {
  gpointer data;
};

GSharedPtr* g_shared_ptr_new (gpointer data, GDestroyNotify notify);
// End public API



struct GRef {
  GRefFunc ref_func;
  GUnrefFunc unref_func;
  /* If we're using the slice allocator this should be > 0, otherwise
     we know to free with g_free. */
  gsize slice_size;

  /* The data is actually at the &fake_data, not fake_data.  This is
     here so we can use G_STRUCT_OFFSET to calculate how much room we
     need in addition to what was requested.  I decided to do it this
     way instead of just omitting fake_data and using sizeof(struct
     GShared) because I think C compilers will be more likely to add
     some padding when necessary for better alignment. */
  gpointer fake_data;
};

struct GShared {
  volatile gint ref_count;
  GDestroyNotify notify;
  struct GRef ref;
};

#define G_REF_HEADER_OFFSET G_STRUCT_OFFSET(struct GRef, fake_data)
#define G_REF_DATA_TO_HEADER(data) \
  ((struct GRef*) (((guint8*) data) - G_REF_HEADER_OFFSET))
#define G_REF_HEADER_TO_DATA(header) (&(header->fake_data))


static void
g_ref_init (struct GRef* ref, gsize size, GRefFunc ref_func, GUnrefFunc unref_func)
{
  ref->slice_size = size;
  ref->ref_func = ref_func;
  ref->unref_func = unref_func;
}

/**
 * g_ref_alloc:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate basic reference counting buffer using g_malloc().
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_ref_alloc (gsize size, GRefFunc ref_func, GUnrefFunc unref_func)
{
  struct GRef* ref;

  g_return_val_if_fail (size > 0, NULL);
  g_return_val_if_fail (ref_func != NULL, NULL);
  g_return_val_if_fail (unref_func != NULL, NULL);

  ref = g_malloc (size + G_REF_HEADER_OFFSET);
  g_return_val_if_fail (ref != NULL, NULL);

  g_ref_init (ref, 0, ref_func, unref_func);

  return G_REF_HEADER_TO_DATA (ref);
}

/**
 * g_ref_alloc0:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate a basic reference counting buffer using g_malloc() and
 * initialize the returned memory to 0.
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_ref_alloc0 (gsize size, GRefFunc ref_func, GUnrefFunc unref_func)
{
  gpointer data = g_ref_alloc (size, ref_func, unref_func);

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

  memset (data, 0, size);

  return data;
}

/**
 * g_ref_alloc_slice:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate basic reference counting buffer using the slice allocator.
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_ref_alloc_slice (gsize size, GRefFunc ref_func, GUnrefFunc unref_func)
{
  struct GRef* ref;

  g_return_val_if_fail (size > 0, NULL);
  g_return_val_if_fail (ref_func != NULL, NULL);
  g_return_val_if_fail (unref_func != NULL, NULL);

  ref = g_slice_alloc (size + G_REF_HEADER_OFFSET);
  g_return_val_if_fail (ref != NULL, NULL);

  g_ref_init (ref, size, ref_func, unref_func);

  return G_REF_HEADER_TO_DATA (ref);
}

/**
 * g_ref_alloc_slice0:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate basic reference counting buffer using the slice allocator
 * and initialize the returned memory to 0.
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_ref_alloc_slice0 (gsize size, GRefFunc ref_func, GUnrefFunc unref_func)
{
  gpointer data = g_ref_alloc_slice (size, ref_func, unref_func);

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

  memset (data, 0, size);

  return data;
}

/**
 * g_ref_free:
 * @data: buffer to free
 *
 * Destroy a reference counted buffer and free the associated memory.
 * This function should only be called from within a #GUnrefFunc
 * callback.
 */
void
g_ref_free (gpointer data)
{
  struct GRef* ref;

  g_return_if_fail (data != NULL);

  ref = G_REF_DATA_TO_HEADER(data);
  if (ref->slice_size == 0)
    {
      g_free (ref);
    }
  else
    {
      g_slice_free1 (ref->slice_size, ref);
    }
}

/**
 * GRefFunc:
 * @data: the pointer to increase the reference count of
 *
 * A callback for incrementing a reference count.
 *
 * Returns: @data with its reference count increased
 */

/**
 * GUnrefFunc:
 * @data: the pointer to decrease the reference count of
 *
 * A callback for decrementing a reference count.
 */

/**
 * g_ref:
 * @data: data
 *
 * Invokes the registered #GRefFunc on @data, ostensibly increasing
 * the reference count.
 *
 * Returns: (transfer full): @data, with its reference count increased
 * by one.
 */
gpointer
g_ref (gpointer data)
{
  struct GRef* ref;

  g_return_val_if_fail (data != NULL, NULL);

  ref = G_REF_DATA_TO_HEADER(data);
  if (ref->ref_func != NULL)
    {
      return ref->ref_func (data);
    }

  return NULL;
}

/**
 * g_unref:
 * @data: (transfer full): data
 *
 * Invokes the registered #GUnrefFunc on @data, ostensibly decreasing its
 * reference count.
 */
void
g_unref (gpointer data)
{
  struct GRef* ref;

  g_return_if_fail (data != NULL);

  ref = G_REF_DATA_TO_HEADER(data);
  if (ref->unref_func != NULL)
    {
      ref->unref_func (data);
    }
}

#define G_SHARED_HEADER_OFFSET \
  (G_STRUCT_OFFSET(struct GShared, ref) + G_REF_HEADER_OFFSET)
#define G_SHARED_DATA_TO_HEADER(data) \
  ((struct GShared*) (((guint8*) data) - G_SHARED_HEADER_OFFSET))
#define G_SHARED_HEADER_TO_DATA(header) (&(header->ref.fake_data))

static gpointer
g_shared_ref (gpointer data)
{
  struct GShared* shared;

  g_return_val_if_fail (data != NULL, NULL);

  shared = G_SHARED_DATA_TO_HEADER (data);
  g_atomic_int_inc (&(shared->ref_count));

  return data;
}

static void
g_shared_unref (gpointer data)
{
  struct GShared* shared;

  g_return_if_fail (data != NULL);

  shared = G_SHARED_DATA_TO_HEADER (data);

  if (g_atomic_int_dec_and_test (&(shared->ref_count)))
    {
      if (shared->notify != NULL)
        {
          shared->notify (data);
          shared->notify = NULL;
        }

      g_free (shared);
    }
}

static void
g_shared_init (struct GShared* shared, gsize size, GDestroyNotify notify)
{
  shared->ref_count = 1;
  shared->notify = notify;
  g_ref_init (&(shared->ref), size, g_shared_ref, g_shared_unref);
}

/**
 * g_shared_alloc:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate a reference counting buffer using g_malloc() with default
 * #GRefFunc and #GUnrefFunc implementations.
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_shared_alloc (gsize size, GDestroyNotify notify)
{
  struct GShared* shared;

  g_return_val_if_fail (size > 0, NULL);

  shared = g_malloc (sizeof(struct GShared) + G_SHARED_HEADER_OFFSET);
  if (G_UNLIKELY(shared == NULL))
    {
      return NULL;
    }

  g_shared_init (shared, 0, notify);

  return G_SHARED_HEADER_TO_DATA (shared);
}

/**
 * g_shared_alloc0:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate a reference counting buffer using g_malloc() with default
 * #GRefFunc and #GUnrefFunc implementations and initialize the
 * returned memory to 0.
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_shared_alloc0 (gsize size, GDestroyNotify notify)
{
  gpointer data = g_shared_alloc (size, notify);

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

  memset (data, 0, size);

  return data;
}

/**
 * g_shared_alloc_slice:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate a reference counting buffer using the slice
 * allocator with default #GRefFunc and #GUnrefFunc implementations.
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_shared_alloc_slice (gsize size, GDestroyNotify notify)
{
  struct GShared* shared;

  g_return_val_if_fail (size > 0, NULL);

  shared = g_slice_alloc (sizeof(struct GShared) + G_SHARED_HEADER_OFFSET);
  if (G_UNLIKELY(shared == NULL))
    {
      return NULL;
    }

  g_shared_init (shared, size, notify);

  return G_SHARED_HEADER_TO_DATA (shared);
}

/**
 * g_shared_alloc_slice0:
 * @size: size in bytes to allocate
 * @notify: callback to be invoked when reference count drops to zero
 *
 * Allocate a reference counting buffer using the slice allocator with
 * default #GRefFunc and #GUnrefFunc implementations and initialize
 * the returned memory to 0.
 *
 * Returns: (transfer full): a new shared pointer
 */
gpointer
g_shared_alloc_slice0 (gsize size, GDestroyNotify notify)
{
  gpointer data = g_shared_alloc_slice (size, notify);

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

  memset (data, 0, size);

  return data;
}

/**
 * GSharedPtr:
 * @data: the content
 *
 * A generic container for a single container which is used to layer
 * reference counting on top of pointers for which you have no control
 * over the allocator and, therefore, cannot cannot be allocated with
 * g_shared_alloc().
 *
 * Use g_ref() and g_unref() to ref and unref instances.
 */

typedef struct _GSharedPtrReal {
  gpointer data;
  GDestroyNotify notify;
} GSharedPtrReal;

static void
g_shared_ptr_destroy_notify (GSharedPtr* ptr)
{
  GSharedPtrReal* real_ptr = (GSharedPtrReal*) ptr;

  g_return_if_fail (ptr != NULL);

  if (real_ptr->notify != NULL)
    {
      real_ptr->notify (real_ptr->data);
    }
}

/**
 * g_shared_ptr_new:
 * @data: data
 * @notify: destroy notify
 *
 * Creates a new #GSharedPtr
 */
GSharedPtr*
g_shared_ptr_new (gpointer data, GDestroyNotify notify)
{
  GSharedPtrReal* real_ptr;

  real_ptr = g_shared_new (GSharedPtrReal,
                           (GDestroyNotify) g_shared_ptr_destroy_notify);

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

  real_ptr->data = data;
  real_ptr->notify = notify;

  return (GSharedPtr*) real_ptr;
}













/******** Tests / Examples ********/

typedef struct _Foo {
  int one;
  int two;
  int three;
} Foo;

void
foo_destroy (Foo* foo)
{
  memset (foo, 0, sizeof (Foo));
}

Foo*
foo_new ()
{
  Foo* foo = g_shared_new (Foo, (GDestroyNotify) foo_destroy);
  foo->one = 1;
  foo->two = 2;
  foo->three = 3;
  return foo;
}

void
foo_weak_notify (Foo* foo, gpointer user_data)
{
  g_debug ("Destroy notify for Foo %p { %d, %d, %d }", foo, foo->one, foo->two, foo->three);
}

void
foo_test ()
{
  Foo* foo = foo_new ();
  g_debug ("Got a new Foo %p { %d, %d, %d }", foo, foo->one, foo->two, foo->three);

  g_ref (foo);
  g_debug ("Ref a Foo %p { %d, %d, %d }", foo, foo->one, foo->two, foo->three);
  g_unref (foo);
  g_debug ("Unref a Foo %p { %d, %d, %d }", foo, foo->one, foo->two, foo->three);
  g_unref (foo);
  g_debug ("Unref (hopefully destroyed) a Foo %p", foo);
}





typedef struct _Bar {
  gint ref_count;
} Bar;

static Bar*
bar_ref (Bar* bar)
{
  // Notice this isn't atomic
  bar->ref_count++;
}

static void
bar_unref (Bar* bar)
{
  if (--bar->ref_count == 0)
    {
      g_debug ("Destroying Bar %p", bar);
      g_ref_free (bar);
    }
}

Bar* bar_new ()
{
  Bar* bar = g_ref_new_slice (Bar, (GRefFunc) bar_ref, (GUnrefFunc) bar_unref);
  bar->ref_count = 1;
  return bar;
}

void
bar_test ()
{
  Bar* bar = bar_new ();
  g_debug ("Created Bar %p", bar);
  g_ref (bar);
  g_unref (bar);
  g_debug ("Bar %p should still be alive", bar);
  g_unref (bar);
  g_debug ("Bar %p should be dead", bar);
}





typedef struct _Callbacks {
  GFunc foo;
  GSharedPtr* foo_user_data;
  GFunc bar;
  GSharedPtr* bar_user_data;
  GFunc baz;
  GSharedPtr* baz_user_data;
} Callbacks;

void
callbacks_invoke_foo (Callbacks* callbacks, gpointer data)
{
  if (callbacks->foo != NULL)
    callbacks->foo (data, callbacks->foo_user_data->data);
}

void
callbacks_invoke_bar (Callbacks* callbacks, gpointer data)
{
  if (callbacks->bar != NULL)
    callbacks->bar (data, callbacks->bar_user_data->data);
}

void
callbacks_invoke_baz (Callbacks* callbacks, gpointer data)
{
  if (callbacks->baz != NULL)
    callbacks->baz (data, callbacks->baz_user_data->data);
}

void
callbacks_set_foo (Callbacks* callbacks, GFunc foo, gpointer user_data, GDestroyNotify notify)
{
  if (callbacks->foo_user_data != NULL)
    callbacks->foo_user_data = (g_unref (callbacks->foo_user_data), NULL);
  callbacks->foo = foo;
  if (user_data != NULL)
    callbacks->foo_user_data = g_shared_ptr_new (user_data, notify);
}

void
callbacks_set_bar (Callbacks* callbacks, GFunc bar, gpointer user_data, GDestroyNotify notify)
{
  if (callbacks->bar_user_data != NULL)
    callbacks->bar_user_data = (g_unref (callbacks->bar_user_data), NULL);
  callbacks->bar = bar;
  if (user_data != NULL)
    callbacks->bar_user_data = g_shared_ptr_new (user_data, notify);
}

void
callbacks_set_baz (Callbacks* callbacks, GFunc baz, gpointer user_data, GDestroyNotify notify)
{
  if (callbacks->baz_user_data != NULL)
    callbacks->baz_user_data = (g_unref (callbacks->baz_user_data), NULL);
  callbacks->baz = baz;
  if (user_data != NULL)
    callbacks->baz_user_data = g_shared_ptr_new (user_data, notify);
}

void
callbacks_set_callbacks (Callbacks* callbacks,
                   GFunc foo,
                   GFunc bar,
                   GFunc baz,
                   gpointer user_data,
                   GDestroyNotify notify) {
  GSharedPtr* shared_user_data = g_shared_ptr_new (user_data, notify);

  if (callbacks->foo_user_data != NULL)
    g_unref (callbacks->foo_user_data);
  callbacks->foo = foo;
  callbacks->foo_user_data = shared_user_data;

  if (callbacks->bar_user_data != NULL)
    g_unref (callbacks->bar_user_data);
  callbacks->bar = bar;
  callbacks->bar_user_data = g_ref (shared_user_data);

  if (callbacks->baz_user_data != NULL)
    g_unref (callbacks->baz_user_data);
  callbacks->baz = baz;
  callbacks->baz_user_data = g_ref (shared_user_data);
}

void
callbacks_foo_cb (gpointer data, gpointer user_data)
{
  g_debug ("callbacks_foo_cb (\"%s\", \"%s\");", data, user_data);
}

void
callbacks_bar_cb (gpointer data, gpointer user_data)
{
  g_debug ("callbacks_bar_cb (\"%s\", \"%s\");", data, user_data);
}

void
callbacks_baz_cb (gpointer data, gpointer user_data)
{
  g_debug ("callbacks_baz_cb (\"%s\", \"%s\");", data, user_data);
}

void
vocal_g_free (gpointer user_data)
{
  g_debug ("g_free (%p: \"%s\")", user_data, user_data);
  g_free (user_data);
}

void
callbacks_destroy (Callbacks* callbacks)
{
  g_debug ("Destroying Callbacks %p", callbacks);

  if (callbacks->foo_user_data != NULL)
    callbacks->foo_user_data = (g_unref (callbacks->foo_user_data), NULL);
  if (callbacks->bar_user_data != NULL)
    callbacks->bar_user_data = (g_unref (callbacks->bar_user_data), NULL);
  if (callbacks->baz_user_data != NULL)
    callbacks->baz_user_data = (g_unref (callbacks->baz_user_data), NULL);
}

void
callbacks_test ()
{
  Callbacks callbacks = { 0, };

  g_debug ("Callbacks %p", &callbacks);

  gchar* user_data = g_strdup ("user data");
  g_debug ("callbacks_set_callbacks (%p, %p, %p, %p, %p, %p)", &callbacks, callbacks_foo_cb, callbacks_bar_cb, callbacks_baz_cb, user_data, vocal_g_free);
  callbacks_set_callbacks (&callbacks, callbacks_foo_cb, callbacks_bar_cb, callbacks_baz_cb, user_data, vocal_g_free);

  callbacks_invoke_foo (&callbacks, "Foo says");
  callbacks_invoke_bar (&callbacks, "Bar says");
  callbacks_invoke_baz (&callbacks, "Baz says");

  callbacks_set_foo (&callbacks, callbacks_foo_cb, g_strdup ("foo user data"), vocal_g_free);
  callbacks_set_bar (&callbacks, callbacks_bar_cb, g_strdup ("bar user data"), vocal_g_free);
  callbacks_set_baz (&callbacks, callbacks_baz_cb, g_strdup ("baz user data"), vocal_g_free);
  // user_data should have been freed

  callbacks_invoke_foo (&callbacks, "Foo says");
  callbacks_invoke_bar (&callbacks, "Bar says");
  callbacks_invoke_baz (&callbacks, "Baz says");

  callbacks_destroy (&callbacks);
}




int main (int argc, char** argv) {
  foo_test ();
  bar_test ();
  callbacks_test ();

  return 0;
}
