#include "parts.h"

static PyObject*
test_gc_control(PyObject *self, PyObject *Py_UNUSED(ignored))
{
    int orig_enabled = PyGC_IsEnabled();
    const char* msg = "ok";
    int old_state;

    old_state = PyGC_Enable();
    msg = "Enable(1)";
    if (old_state != orig_enabled) {
        goto failed;
    }
    msg = "IsEnabled(1)";
    if (!PyGC_IsEnabled()) {
        goto failed;
    }

    old_state = PyGC_Disable();
    msg = "disable(2)";
    if (!old_state) {
        goto failed;
    }
    msg = "IsEnabled(2)";
    if (PyGC_IsEnabled()) {
        goto failed;
    }

    old_state = PyGC_Enable();
    msg = "enable(3)";
    if (old_state) {
        goto failed;
    }
    msg = "IsEnabled(3)";
    if (!PyGC_IsEnabled()) {
        goto failed;
    }

    if (!orig_enabled) {
        old_state = PyGC_Disable();
        msg = "disable(4)";
        if (old_state) {
            goto failed;
        }
        msg = "IsEnabled(4)";
        if (PyGC_IsEnabled()) {
            goto failed;
        }
    }

    Py_RETURN_NONE;

failed:
    /* Try to clean up if we can. */
    if (orig_enabled) {
        PyGC_Enable();
    } else {
        PyGC_Disable();
    }
    PyErr_Format(PyExc_ValueError, "GC control failed in %s", msg);
    return NULL;
}

static PyObject *
without_gc(PyObject *Py_UNUSED(self), PyObject *obj)
{
    PyTypeObject *tp = (PyTypeObject*)obj;
    if (!PyType_Check(obj) || !PyType_HasFeature(tp, Py_TPFLAGS_HEAPTYPE)) {
        return PyErr_Format(PyExc_TypeError, "heap type expected, got %R", obj);
    }
    if (PyType_IS_GC(tp)) {
        // Don't try this at home, kids:
        tp->tp_flags -= Py_TPFLAGS_HAVE_GC;
        tp->tp_free = PyObject_Free;
        tp->tp_traverse = NULL;
        tp->tp_clear = NULL;
    }
    assert(!PyType_IS_GC(tp));
    return Py_NewRef(obj);
}

static void
slot_tp_del(PyObject *self)
{
    PyObject *del, *res;

    /* Temporarily resurrect the object. */
    assert(Py_REFCNT(self) == 0);
    Py_SET_REFCNT(self, 1);

    /* Save the current exception, if any. */
    PyObject *exc = PyErr_GetRaisedException();

    PyObject *tp_del = PyUnicode_InternFromString("__tp_del__");
    if (tp_del == NULL) {
        PyErr_FormatUnraisable("Exception ignored while deallocating");
        PyErr_SetRaisedException(exc);
        return;
    }
    /* Execute __del__ method, if any. */
    del = _PyType_LookupRef(Py_TYPE(self), tp_del);
    Py_DECREF(tp_del);
    if (del != NULL) {
        res = PyObject_CallOneArg(del, self);
        Py_DECREF(del);
        if (res == NULL) {
            PyErr_FormatUnraisable("Exception ignored while calling "
                                   "deallocator %R", del);
        }
        else {
            Py_DECREF(res);
        }
    }

    /* Restore the saved exception. */
    PyErr_SetRaisedException(exc);

    /* Undo the temporary resurrection; can't use DECREF here, it would
     * cause a recursive call.
     */
    assert(Py_REFCNT(self) > 0);
    Py_SET_REFCNT(self, Py_REFCNT(self) - 1);
    if (Py_REFCNT(self) == 0) {
        /* this is the normal path out */
        return;
    }

    /* __del__ resurrected it!  Make it look like the original Py_DECREF
     * never happened.
     */
    {
        _Py_ResurrectReference(self);
    }
    assert(!PyType_IS_GC(Py_TYPE(self)) || PyObject_GC_IsTracked(self));
}

static PyObject *
with_tp_del(PyObject *self, PyObject *args)
{
    PyObject *obj;
    PyTypeObject *tp;

    if (!PyArg_ParseTuple(args, "O:with_tp_del", &obj))
        return NULL;
    tp = (PyTypeObject *) obj;
    if (!PyType_Check(obj) || !PyType_HasFeature(tp, Py_TPFLAGS_HEAPTYPE)) {
        PyErr_Format(PyExc_TypeError,
                     "heap type expected, got %R", obj);
        return NULL;
    }
    tp->tp_del = slot_tp_del;
    return Py_NewRef(obj);
}


struct gc_visit_state_basic {
    PyObject *target;
    int found;
};

static int
gc_visit_callback_basic(PyObject *obj, void *arg)
{
    struct gc_visit_state_basic *state = (struct gc_visit_state_basic *)arg;
    if (obj == state->target) {
        state->found = 1;
        return 0;
    }
    return 1;
}

static PyObject *
test_gc_visit_objects_basic(PyObject *Py_UNUSED(self),
                            PyObject *Py_UNUSED(ignored))
{
    PyObject *obj;
    struct gc_visit_state_basic state;

    obj = PyList_New(0);
    if (obj == NULL) {
        return NULL;
    }
    state.target = obj;
    state.found = 0;

    PyUnstable_GC_VisitObjects(gc_visit_callback_basic, &state);
    Py_DECREF(obj);
    if (!state.found) {
        PyErr_SetString(
             PyExc_AssertionError,
             "test_gc_visit_objects_basic: Didn't find live list");
         return NULL;
    }
    Py_RETURN_NONE;
}

static int
gc_visit_callback_exit_early(PyObject *obj, void *arg)
 {
    int *visited_i = (int *)arg;
    (*visited_i)++;
    if (*visited_i == 2) {
        return 0;
    }
    return 1;
}

static PyObject *
test_gc_visit_objects_exit_early(PyObject *Py_UNUSED(self),
                                 PyObject *Py_UNUSED(ignored))
{
    int visited_i = 0;
    PyUnstable_GC_VisitObjects(gc_visit_callback_exit_early, &visited_i);
    if (visited_i != 2) {
        PyErr_SetString(
            PyExc_AssertionError,
            "test_gc_visit_objects_exit_early: did not exit when expected");
    }
    Py_RETURN_NONE;
}

typedef struct {
    PyObject_HEAD
} ObjExtraData;

static PyObject *
obj_extra_data_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    size_t extra_size = sizeof(PyObject *);
    PyObject *obj = PyUnstable_Object_GC_NewWithExtraData(type, extra_size);
    if (obj == NULL) {
        return PyErr_NoMemory();
    }
    PyObject_GC_Track(obj);
    return obj;
}

static PyObject **
obj_extra_data_get_extra_storage(PyObject *self)
{
    return (PyObject **)((char *)self + Py_TYPE(self)->tp_basicsize);
}

static PyObject *
obj_extra_data_get(PyObject *self, void *Py_UNUSED(ignored))
{
    PyObject **extra_storage = obj_extra_data_get_extra_storage(self);
    PyObject *value = *extra_storage;
    if (!value) {
        Py_RETURN_NONE;
    }
    return Py_NewRef(value);
}

static int
obj_extra_data_set(PyObject *self, PyObject *newval, void *Py_UNUSED(ignored))
{
    PyObject **extra_storage = obj_extra_data_get_extra_storage(self);
    Py_CLEAR(*extra_storage);
    if (newval) {
        *extra_storage = Py_NewRef(newval);
    }
    return 0;
}

static PyGetSetDef obj_extra_data_getset[] = {
    {"extra", obj_extra_data_get, obj_extra_data_set, NULL},
    {NULL}
};

static int
obj_extra_data_traverse(PyObject *self, visitproc visit, void *arg)
{
    PyObject **extra_storage = obj_extra_data_get_extra_storage(self);
    PyObject *value = *extra_storage;
    Py_VISIT(value);
    return 0;
}

static int
obj_extra_data_clear(PyObject *self)
{
    PyObject **extra_storage = obj_extra_data_get_extra_storage(self);
    Py_CLEAR(*extra_storage);
    return 0;
}

static void
obj_extra_data_dealloc(PyObject *self)
{
    PyTypeObject *tp = Py_TYPE(self);
    PyObject_GC_UnTrack(self);
    obj_extra_data_clear(self);
    tp->tp_free(self);
    Py_DECREF(tp);
}

static PyType_Slot ObjExtraData_Slots[] = {
    {Py_tp_getset, obj_extra_data_getset},
    {Py_tp_dealloc, obj_extra_data_dealloc},
    {Py_tp_traverse, obj_extra_data_traverse},
    {Py_tp_clear, obj_extra_data_clear},
    {Py_tp_new, obj_extra_data_new},
    {Py_tp_free, PyObject_GC_Del},
    {0, NULL},
};

static PyType_Spec ObjExtraData_TypeSpec = {
    .name = "_testcapi.ObjExtraData",
    .basicsize = sizeof(ObjExtraData),
    .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .slots = ObjExtraData_Slots,
};

static PyMethodDef test_methods[] = {
    {"test_gc_control", test_gc_control, METH_NOARGS},
    {"test_gc_visit_objects_basic", test_gc_visit_objects_basic, METH_NOARGS, NULL},
    {"test_gc_visit_objects_exit_early", test_gc_visit_objects_exit_early, METH_NOARGS, NULL},
    {"without_gc", without_gc, METH_O, NULL},
    {"with_tp_del", with_tp_del, METH_VARARGS, NULL},
    {NULL}
};

int _PyTestCapi_Init_GC(PyObject *mod)
{
    if (PyModule_AddFunctions(mod, test_methods) < 0) {
        return -1;
    }
    if (PyModule_AddFunctions(mod, test_methods) < 0) {
        return -1;
    }

    PyObject *ObjExtraData_Type = PyType_FromModuleAndSpec(
        mod, &ObjExtraData_TypeSpec, NULL);
    if (ObjExtraData_Type == 0) {
        return -1;
    }
    int ret = PyModule_AddType(mod, (PyTypeObject*)ObjExtraData_Type);
    Py_DECREF(ObjExtraData_Type);
    if (ret < 0) {
        return ret;
    }

    return 0;
}
