#include "pycall_internal.h"

static PyMemberDef PyRuby_members[] = {
  {"ruby_object_ptr", Py_T_PYSSIZET, offsetof(PyRubyObject, ruby_object), Py_READONLY},
  {NULL} /* sentinel */
};

static void PyRuby_dealloc(PyRubyObject *);
static PyObject * PyRuby_repr(PyRubyObject *);
static PyObject * PyRuby_call(PyRubyObject *, PyObject *, PyObject *);
static PyObject * PyRuby_getattro(PyRubyObject *, PyObject *);

PyTypeObject PyRuby_Type = {
  PyVarObject_HEAD_INIT(NULL, 0)
  "PyCall.ruby_object", /* tp_name */
  sizeof(PyRubyObject), /* tp_basicsize */
  0, /* tp_itemsize */
  (destructor)PyRuby_dealloc,    /* tp_dealloc */
  0,                             /* tp_print */
  0,                             /* tp_getattr */
  0,                             /* tp_setattr */
  0,                             /* tp_reserved */
  (reprfunc)PyRuby_repr,         /* tp_repr */
  0,                             /* tp_as_number */
  0,                             /* tp_as_sequence */
  0,                             /* tp_as_mapping */
  {0},                           /* tp_hash  */
  (ternaryfunc)PyRuby_call,      /* tp_call */
  0,                             /* tp_str */
  (getattrofunc)PyRuby_getattro, /* tp_getattro */
  0,                             /* tp_setattro */
  0,                             /* tp_as_buffer */
  Py_TPFLAGS_BASETYPE,           /* tp_flags */
  "ruby object wrapper",         /* tp_doc */
  0,                             /*tp_traverse*/
  0,                             /*tp_clear*/
  0,                             /*tp_richcompare*/
  0,                             /*tp_weaklistoffset*/
  0,                             /*tp_iter*/
  0,                             /*tp_iternext*/
  0,                             /*tp_methods*/
  PyRuby_members,                /*tp_members*/
};

PyObject *
PyRuby_New(VALUE ruby_object)
{
  PyRubyObject *op;

  op = (PyRubyObject *)Py_API(_PyObject_New)(&PyRuby_Type);
  op->ruby_object = ruby_object;
  pycall_gcguard_register_pyrubyobj((PyObject *)op);
  return (PyObject *)op;
}

static VALUE
funcall_id2ref(VALUE object_id)
{
  VALUE rb_mObjSpace;
  object_id = rb_check_to_integer(object_id, "to_int");
  rb_mObjSpace = rb_const_get(rb_cObject, rb_intern("ObjectSpace"));
  return rb_funcall(rb_mObjSpace, rb_intern("_id2ref"), 1, object_id);
}

static VALUE
protect_id2ref(VALUE object_id)
{
  VALUE obj;
  int state;

  obj = rb_protect((VALUE (*)(VALUE))funcall_id2ref, object_id, &state);
  if (state)
    return Qundef;

  return obj;
}

static VALUE
protect_id2ref_and_set_pyerr(VALUE object_id)
{
  VALUE obj = protect_id2ref(object_id);
  if (obj != Qundef)
    return obj;

  obj = rb_errinfo();
  if (RTEST(rb_obj_is_kind_of(obj, rb_eRangeError))) {
    Py_API(PyErr_SetString)(Py_API(PyExc_RuntimeError), "[BUG] referenced object was garbage-collected");
  }
  else {
    VALUE emesg = rb_check_funcall(obj, rb_intern("message"), 0, 0);
    Py_API(PyErr_Format)(Py_API(PyExc_RuntimeError),
        "[BUG] Unable to obtain ruby object from ID: %s (%s)",
        StringValueCStr(emesg), rb_class2name(CLASS_OF(obj)));
  }
  return Qundef;
}

static VALUE
PyRuby_get_ruby_object_and_set_pyerr(PyObject *pyobj)
{
  VALUE obj_id;
  if (!PyRuby_Check(pyobj))
    return Qundef;
  obj_id = rb_obj_id(PyRuby_get_ruby_object(pyobj));
  return protect_id2ref_and_set_pyerr(obj_id);
}

static void
PyRuby_dealloc(PyRubyObject *pyro)
{
  VALUE obj;

  obj = PyRuby_get_ruby_object((PyObject *)pyro);

#ifdef PYCALL_DEBUG_DUMP_REFCNT
  fprintf(stderr, "PyRuby_dealloc(%p), ruby_object=%"PRI_LL_PREFIX"d\n", pyro, NUM2LL(rb_obj_id(obj)));
#endif /* PYCALL_DEBUG_DUMP_REFCNT */

  if (obj == Qundef)
    return;

  pycall_gcguard_unregister_pyrubyobj((PyObject *)pyro);
}

static PyObject *
PyRuby_repr(PyRubyObject *pyro)
{
  VALUE obj, str;
  PyObject *res;

  obj = PyRuby_get_ruby_object_and_set_pyerr((PyObject *)pyro);
  if (obj == Qundef)
    return NULL;

  str = rb_inspect(obj);
  res = pycall_pystring_from_format("<PyCall.ruby_object %s>", StringValueCStr(str));
  return res;
}

#if SIZEOF_SSIZE_T < 8
/* int64to32hash from src/support/hashing.c in Julia */
static inline uint32_t
int64to32hash(uint64_t key)
{
    key = (~key) + (key << 18); // key = (key << 18) - key - 1;
    key =   key  ^ (key >> 31);
    key = key * 21;             // key = (key + (key << 2)) + (key << 4);
    key = key ^ (key >> 11);
    key = key + (key << 6);
    key = key ^ (key >> 22);
    return (uint32_t)key;
}
#endif

static long
PyRuby_hash_long(PyRubyObject *pyro)
{
  VALUE obj, rbhash;
  long h;

  obj = PyRuby_get_ruby_object_and_set_pyerr((PyObject *)pyro);
  if (obj == Qundef)
    return -1;

  rbhash = rb_hash(obj);
  h = FIX2LONG(rbhash); /* Ruby's hash value is a Fixnum */

  return h == -1 ? pycall_hash_salt : h;
}

static Py_hash_t
PyRuby_hash_hash_t(PyRubyObject *pyro)
{
  VALUE obj, rbhash;
  Py_hash_t h;

  obj = PyRuby_get_ruby_object_and_set_pyerr((PyObject *)pyro);
  if (obj == Qundef)
    return -1;

  rbhash = rb_hash(obj);
#if SIZEOF_PY_HASH_T == SIZEOF_LONG
  /* In this case, we can assume sizeof(Py_hash_t) == sizeof(long) */
  h = NUM2SSIZET(rbhash);
  return h == -1 ? (Py_hash_t)pycall_hash_salt : h;
#else
  /* In this case, we can assume sizeof(long) == 4 and sizeof(Py_hash_t) == 8 */
  h = (pycall_hash_salt_32 << 32) | FIX2LONG(rbhash);
  return h == -1 ? ((pycall_hash_salt << 32) | pycall_hash_salt) : h;
#endif
}

struct call_rb_funcallv_params {
  VALUE recv;
  ID meth;
  int argc;
  VALUE *argv;
};

static VALUE
call_rb_funcallv(struct call_rb_funcallv_params *params)
{
  return rb_funcallv(params->recv, params->meth, params->argc, params->argv);
}

static VALUE
rb_protect_funcallv(VALUE recv, ID meth, int argc, VALUE *argv, int *pstate)
{
  struct call_rb_funcallv_params params;
  VALUE res;
  int state;

  params.recv = recv;
  params.meth = meth;
  params.argc = argc;
  params.argv = argv;

  res = rb_protect((VALUE (*)(VALUE))call_rb_funcallv, (VALUE)&params, &state);
  if (pstate) *pstate = state;
  if (state) return Qnil;
  return res;
}

static PyObject *
PyRuby_call(PyRubyObject *pyro, PyObject *pyobj_args, PyObject *pyobj_kwargs)
{
  ID id_call;
  VALUE obj, args, kwargs, res;
  PyObject *pyobj_res;
  int state;

  obj = PyRuby_get_ruby_object_and_set_pyerr((PyObject *)pyro);
  if (obj == Qundef)
    return NULL;

  id_call = rb_intern("call");
  if (!rb_respond_to(obj, id_call)) {
    Py_API(PyErr_SetString)(Py_API(PyExc_TypeError), "non-callable ruby object");
    return NULL;
  }

  args = pycall_pyobject_to_a(pyobj_args);
  if (pyobj_kwargs) {
    kwargs = pycall_pyobject_to_ruby(pyobj_kwargs);
    rb_ary_push(args, kwargs);
  }

  res = rb_protect_funcallv(obj, id_call, (int)RARRAY_LEN(args), RARRAY_PTR(args), &state);
  if (state) {
    /* TODO: pyerr set */
  }

  pyobj_res = pycall_pyobject_from_ruby(res);
  return pyobj_res;
}

static PyObject *
PyRuby_getattro(PyRubyObject *pyro, PyObject *pyobj_name)
{
  VALUE obj, name, res;
  char const *name_cstr;
  ID name_id;
  PyObject *pyobj_res;

  obj = PyRuby_get_ruby_object_and_set_pyerr((PyObject *)pyro);
  if (obj == Qundef)
    return NULL;

  name = pycall_pyobject_to_ruby(pyobj_name);
  name_cstr = StringValueCStr(name);
  name_id = rb_intern(name_cstr);

  /* TODO: should handle exception */
  if (strncmp(name_cstr, "__name__", 8) == 0 ||
      strncmp(name_cstr, "func_name", 9) == 0) {
    if (rb_respond_to(obj, rb_intern("name"))) {
      res = rb_funcall(obj, rb_intern("name"), 0);
    }
    else {
      res = rb_any_to_s(obj);
    }
  }
  else if (strncmp(name_cstr, "__doc__", 7) == 0 ||
           strncmp(name_cstr, "func_doc", 8) == 0) {
    /* TODO: support docstring */
    res = Qnil;
  }
  else if (strncmp(name_cstr, "__module__", 10) == 0) {
    res = Qnil;
  }
  else if (strncmp(name_cstr, "__defaults__", 12) == 0 ||
           strncmp(name_cstr, "func_defaults", 13) == 0) {
    res = Qnil;
  }
  else if (strncmp(name_cstr, "__closure__", 11) == 0 ||
           strncmp(name_cstr, "func_closure", 12) == 0) {
    res = Qnil;
  }
  else if (name_cstr[0] == '_' && name_cstr[1] == '_') {
    /* name.start_with? "__" */
    /* TODO: handle `__code__` and `func_code` */
    return Py_API(PyObject_GenericGetAttr)((PyObject *)pyro, pyobj_name);
  }
  else {
    /* TODO: handle `__code__` and `func_code` */
    if (rb_respond_to(obj, name_id)) {
      VALUE method = rb_obj_method(obj, name);
      return PyRuby_New(method);
    }
    return Py_API(PyObject_GenericGetAttr)((PyObject *)pyro, pyobj_name);
  }

  pyobj_res = pycall_pyobject_from_ruby(res);
  return pyobj_res;
}

/* ==== PyCall::PyRubyPtr ==== */

VALUE cPyRubyPtr;

const rb_data_type_t pycall_pyrubyptr_data_type = {
  "PyCall::PyRubyPtr",
  { 0, pycall_pyptr_free, pycall_pyptr_memsize, },
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
  &pycall_pyptr_data_type, 0, RUBY_TYPED_FREE_IMMEDIATELY
#endif
};

static inline int
is_pycall_pyrubyptr(VALUE obj)
{
  return rb_typeddata_is_kind_of(obj, &pycall_pyrubyptr_data_type);
}

static inline PyRubyObject*
get_pyrubyobj_ptr(VALUE obj)
{
  PyRubyObject *pyruby;
  TypedData_Get_Struct(obj, PyRubyObject, &pycall_pyrubyptr_data_type, pyruby);
  return pyruby;
}

static inline PyRubyObject*
try_get_pyrubyobj_ptr(VALUE obj)
{
  if (is_pycall_pyrubyptr(obj)) return NULL;
  return (PyRubyObject*)DATA_PTR(obj);
}

static inline PyRubyObject *
check_get_pyrubyobj_ptr(VALUE obj)
{
  PyRubyObject *pyrubyobj;
  if (!is_pycall_pyrubyptr(obj))
    rb_raise(rb_eTypeError, "unexpected type %s (expected PyCall::PyRubyPtr)", rb_class2name(CLASS_OF(obj)));

  pyrubyobj = get_pyrubyobj_ptr(obj);
  if (!PyRuby_Check(pyrubyobj))
    rb_raise(rb_eTypeError, "unexpected Python type %s (expected ruby object)", Py_TYPE(pyrubyobj)->tp_name);

  return pyrubyobj;
}

static VALUE
pycall_pyruby_allocate(VALUE klass)
{
  return TypedData_Wrap_Struct(klass, &pycall_pyrubyptr_data_type, NULL);
}

VALUE
pycall_pyrubyptr_new(PyObject *pyrubyobj)
{
  VALUE obj;

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

  obj = pycall_pyruby_allocate(cPyRubyPtr);
  DATA_PTR(obj) = pyrubyobj;
  return obj;
}

static VALUE
pycall_pyruby_get_ruby_object_id(VALUE obj)
{
  PyRubyObject *pyrubyobj;

  pyrubyobj = check_get_pyrubyobj_ptr(obj);
  return rb_obj_id(pyrubyobj->ruby_object);
}

VALUE
pycall_wrap_ruby_object(VALUE obj)
{
  PyObject *pyobj;
  pyobj = PyRuby_New(obj);
  return pycall_pyrubyptr_new(pyobj);
}

static VALUE
pycall_m_wrap_ruby_object(VALUE mod, VALUE obj)
{
  return pycall_wrap_ruby_object(obj);
}

void
pycall_init_ruby_wrapper(void)
{
  PyRuby_Type.ob_base.ob_type = Py_API(PyType_Type);
  PyRuby_Type.tp_flags |= pycall_default_tp_flags();
  PyRuby_Type.tp_new = Py_API(PyType_GenericNew);
  if (pycall_python_long_hash)
    PyRuby_Type.tp_hash._long = (hashfunc_long)PyRuby_hash_long;
  else
    PyRuby_Type.tp_hash._hash_t = (hashfunc_hash_t)PyRuby_hash_hash_t;

  if (Py_API(PyType_Ready)(&PyRuby_Type) < 0) {
    pycall_pyerror_fetch_and_raise("PyType_Ready in pycall_init_ruby_wrapper");
  }
  Py_API(Py_IncRef)((PyObject *)&PyRuby_Type);

  /* TODO */

  /* PyCall::PyRubyPtr */

  cPyRubyPtr = rb_define_class_under(mPyCall, "PyRubyPtr", cPyPtr);
  rb_define_alloc_func(cPyRubyPtr, pycall_pyruby_allocate);
  rb_define_method(cPyRubyPtr, "__ruby_object_id__", pycall_pyruby_get_ruby_object_id, 0);

  rb_define_module_function(mPyCall, "wrap_ruby_object", pycall_m_wrap_ruby_object, 1);
}
