#include "pycall_internal.h"

static ID id_gcguard_table;

struct gcguard {
  st_table *guarded_objects;
};

static int
gcguard_mark_i(st_data_t key, st_data_t val, st_data_t arg)
{
  VALUE obj = (VALUE)val;
  rb_gc_mark(obj);
  return ST_CONTINUE;
}

static void
gcguard_mark(void* ptr)
{
  struct gcguard *gg = (struct gcguard *)ptr;
  st_foreach(gg->guarded_objects, gcguard_mark_i, 0);
}

static void
gcguard_free(void* ptr)
{
  struct gcguard *gg = (struct gcguard *)ptr;
  st_free_table(gg->guarded_objects);
  rb_ivar_set(mPyCall, id_gcguard_table, Qnil);
}

static size_t
gcguard_memsize(const void* ptr)
{
  const struct gcguard *gg = (const struct gcguard *)ptr;
  return st_memsize(gg->guarded_objects);
}

static rb_data_type_t gcguard_data_type = {
  "PyCall::gcguard",
  {
    gcguard_mark,
    gcguard_free,
    gcguard_memsize,
  },
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
  0, 0, RUBY_TYPED_FREE_IMMEDIATELY
#endif
};

static void
gcguard_aset(VALUE gcguard, PyObject *pyptr, VALUE rbobj)
{
  struct gcguard *gg;
  TypedData_Get_Struct(gcguard, struct gcguard, &gcguard_data_type, gg);

  st_insert(gg->guarded_objects, (st_data_t)pyptr, (st_data_t)rbobj);
}

static void
gcguard_delete(VALUE gcguard, PyObject *pyptr)
{
  if (rb_typeddata_is_kind_of(gcguard, &gcguard_data_type)) {
    /* This check is necessary to avoid error on the process finalization phase */
    struct gcguard *gg;
    st_data_t key, val;

    TypedData_Get_Struct(gcguard, struct gcguard, &gcguard_data_type, gg);

    key = (st_data_t)pyptr;
    st_delete(gg->guarded_objects, &key, &val);
  }
}

static PyObject *weakref_callback_pyobj;
static PyObject *gcguard_weakref_destroyed(PyObject *self, PyObject *weakref);

PyMethodDef gcguard_weakref_callback_def = {
  "_gcguard_weakref_destroyed", (PyCFunction) gcguard_weakref_destroyed, Py_METH_O
};

static PyObject *
gcguard_weakref_destroyed(PyObject *self, PyObject *weakref)
{
  pycall_gcguard_delete(weakref);
  Py_API(Py_DecRef)(weakref);

  Py_API(Py_IncRef)(Py_API(_Py_NoneStruct));
  return Py_API(_Py_NoneStruct);
}

void
pycall_gcguard_aset(PyObject *pyobj, VALUE rbobj)
{
  VALUE gcguard = rb_ivar_get(mPyCall, id_gcguard_table);
  gcguard_aset(gcguard, pyobj, rbobj);
}

void
pycall_gcguard_delete(PyObject *pyobj)
{
  VALUE gcguard = rb_ivar_get(mPyCall, id_gcguard_table);
  if (!NIL_P(gcguard)) {
    gcguard_delete(gcguard, pyobj);
  }
}

void
pycall_gcguard_register_pyrubyobj(PyObject *pyobj)
{
  VALUE rbobj;

  if (!PyRuby_Check(pyobj)) {
    rb_raise(rb_eTypeError, "wrong type of python object %s (expect PyCall.ruby_object)", Py_TYPE(pyobj)->tp_name);
  }

  rbobj = PyRuby_get_ruby_object(pyobj);
  pycall_gcguard_aset(pyobj, rbobj);
}

void
pycall_gcguard_unregister_pyrubyobj(PyObject *pyobj)
{
  if (!PyRuby_Check(pyobj)) {
    rb_raise(rb_eTypeError, "wrong type of python object %s (expect PyCall.ruby_object)", Py_TYPE(pyobj)->tp_name);
  }

  pycall_gcguard_delete(pyobj);
}

void
pycall_gcguard_register(PyObject *pyobj, VALUE obj)
{
  PyObject *wref;

  wref = Py_API(PyWeakref_NewRef)(pyobj, weakref_callback_pyobj);
  pycall_gcguard_aset(wref, obj);
}

static VALUE
gcguard_new(void)
{
  struct gcguard *gg;
  VALUE obj = TypedData_Make_Struct(0, struct gcguard, &gcguard_data_type, gg);
  gg->guarded_objects = st_init_numtable();

  return obj;
}

void
pycall_init_gcguard(void)
{
  id_gcguard_table = rb_intern("gcguard_table");
  rb_ivar_set(mPyCall, id_gcguard_table, gcguard_new());

  weakref_callback_pyobj = Py_API(PyCFunction_NewEx)(&gcguard_weakref_callback_def, NULL, NULL);
}
