/*
 * Support for callback functions/structs in the FSEvents frameework.
 */
#define Py_LIMITED_API 0x03060000
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include "pyobjc-api.h"

#import <CoreServices/CoreServices.h>

static const void*
m_retain_python(const void* value)
{
    PyGILState_STATE state = PyGILState_Ensure();

    Py_XINCREF((PyObject*)value);

    PyGILState_Release(state);

    return value;
}

static void
m_release_python(const void* value)
{
    PyGILState_STATE state = PyGILState_Ensure();

    Py_XDECREF((PyObject*)value);

    PyGILState_Release(state);
}

static CFStringRef
m_copyDescription_python(const void* value)
{
    CFStringRef result;
    PyObject*   description;
    int         r;

    PyGILState_STATE state = PyGILState_Ensure();

    description = PyObject_Repr((PyObject*)value);
    if (description == NULL) {
        PyObjCErr_ToObjCWithGILState(&state);
    }

    r = PyObjC_PythonToObjC(@encode(CFStringRef), description, &result);
    Py_DECREF(description);
    if (r == -1) {
        PyObjCErr_ToObjCWithGILState(&state);
    }

    /* description is autoreleased, we should donate a reference to
     * our caller
     */
    CFRetain(result);

    PyGILState_Release(state);
    return result;
}

static FSEventStreamContext m_python_context_template = {
    0, NULL, m_retain_python, m_release_python, m_copyDescription_python};

static void
m_FSEVentStreamCallback(ConstFSEventStreamRef streamRef, void* clientCallbackInfo,
                        size_t numEvents, void* eventPaths,
                        const FSEventStreamEventFlags eventFlags[],
                        const FSEventStreamEventId    eventIds[])
{
    PyGILState_STATE         state = PyGILState_Ensure();
    FSEventStreamCreateFlags flags;
    PyObject*                callback;
    PyObject*                info;
    PyObject*                v;
    PyObject*                paths;

    v = PyTuple_GetItem((PyObject*)clientCallbackInfo, 0);
    if (PyObjC_PythonToObjC(@encode(FSEventStreamCreateFlags), v, &flags) < 0) {
        PyObjCErr_ToObjCWithGILState(&state);
    }

    info     = PyTuple_GetItem((PyObject*)clientCallbackInfo, 1);
    callback = PyTuple_GetItem((PyObject*)clientCallbackInfo, 2);

    if (flags & kFSEventStreamCreateFlagUseCFTypes) {
        /* The evenPaths are an CFArray */
        paths = PyObjC_ObjCToPython(@encode(CFArrayRef), &eventPaths);
        if (paths == NULL) {
            PyObjCErr_ToObjCWithGILState(&state);
        }
    } else {
        /* The evenPaths are a CArray of C strings */
        paths = PyObjC_CArrayToPython(@encode(char*), eventPaths, numEvents);
        if (paths == NULL) {
            PyObjCErr_ToObjCWithGILState(&state);
        }
    }

    PyObject* py_streamRef =
        PyObjC_ObjCToPython(@encode(ConstFSEventStreamRef), &streamRef);
    if (py_streamRef == NULL) {
        Py_DECREF(paths);
        PyObjCErr_ToObjCWithGILState(&state);
    }
    PyObject* py_eventFlags = PyObjC_CArrayToPython(@encode(FSEventStreamCreateFlags),
                                                    (void*)eventFlags, numEvents);
    if (py_eventFlags == NULL) {
        Py_DECREF(paths);
        Py_DECREF(py_streamRef);
        PyObjCErr_ToObjCWithGILState(&state);
    }
    PyObject* py_eventIds =
        PyObjC_CArrayToPython(@encode(FSEventStreamEventId), (void*)eventIds, numEvents);
    if (py_eventIds == NULL) {
        Py_DECREF(paths);
        Py_DECREF(py_streamRef);
        Py_DECREF(py_eventFlags);
        PyObjCErr_ToObjCWithGILState(&state);
    }

    PyObject* result =
        PyObject_CallFunction(callback, "OOnOOO", py_streamRef, info, numEvents, paths,
                              py_eventFlags, py_eventIds);
    Py_DECREF(paths);
    Py_DECREF(py_streamRef);
    Py_DECREF(py_eventFlags);
    Py_DECREF(py_eventIds);
    if (result == NULL) {
        PyObjCErr_ToObjCWithGILState(&state);
    }
    Py_DECREF(result);

    PyGILState_Release(state);
}

PyDoc_STRVAR(m_FSEventStreamCreate_doc,
             "FSEventStreamCreate(allocator, callback, callback_info, \n"
             "   pathsToWatch, sinceWhen, latency, flags) -> stream\n"
             "\n"
             "NOTE: the callback info is passed directly, it is not a structure as\n"
             "it is in C");
static PyObject*
m_FSEventStreamCreate(PyObject* self __attribute__((__unused__)), PyObject* args)
{
    PyObject* py_allocator;
    PyObject* py_callback;
    PyObject* py_callback_info;
    PyObject* py_pathsToWatch;
    PyObject* py_sinceWhen;
    PyObject* py_latency;
    PyObject* py_flags;

    if (!PyArg_ParseTuple(args, "OOOOOOO", &py_allocator, &py_callback, &py_callback_info,
                          &py_pathsToWatch, &py_sinceWhen, &py_latency, &py_flags)) {

        return NULL;
    }

    CFAllocatorRef allocator;
    if (PyObjC_PythonToObjC(@encode(CFAllocatorRef), py_allocator, &allocator) < 0) {
        return NULL;
    }

    CFArrayRef pathsToWatch;
    if (PyObjC_PythonToObjC(@encode(CFArrayRef), py_pathsToWatch, &pathsToWatch) < 0) {
        return NULL;
    }

    FSEventStreamEventId sinceWhen;
    if (PyObjC_PythonToObjC(@encode(FSEventStreamEventId), py_sinceWhen, &sinceWhen)
        < 0) {
        return NULL;
    }

    CFTimeInterval latency;
    if (PyObjC_PythonToObjC(@encode(CFTimeInterval), py_latency, &latency) < 0) {
        return NULL;
    }

    FSEventStreamCreateFlags flags;
    if (PyObjC_PythonToObjC(@encode(FSEventStreamCreateFlags), py_flags, &flags) < 0) {
        return NULL;
    }

    /*
     * Build the actual callback info, which includes the flags because
     * the arguments passed to the callback vary based on the value of
     * flags.
     */
    PyObject* info = Py_BuildValue("OOO", py_flags, py_callback_info, py_callback);
    if (info == NULL) {
        return NULL;
    }

    FSEventStreamContext context = m_python_context_template;
    context.info                 = info;

    FSEventStreamRef stream = NULL;

    Py_BEGIN_ALLOW_THREADS
        @try {
            stream = FSEventStreamCreate(allocator, m_FSEVentStreamCallback, &context,
                                         pathsToWatch, sinceWhen, latency, flags);

        } @catch (NSException* localException) {
            stream = NULL;
            PyObjCErr_FromObjC(localException);
        }
    Py_END_ALLOW_THREADS

    Py_DECREF(info);

    if (stream == NULL && PyErr_Occurred()) {
        return NULL;
    }

    if (stream == NULL) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    PyObject* result = PyObjC_ObjCToPython(@encode(FSEventStreamRef), &stream);
    // FSEventStreamRef is not a CF type (AFAIK), hence the user is
    // responsible for maintaining the refcount.
    // FSEventStreamRelease(stream);
    return result;
}

PyDoc_STRVAR(m_FSEventStreamCreateRelativeToDevice_doc,
             "FSEventStreamCreate(allocator, callback, callback_info, \n"
             "    deviceToWatch, pathsToWatchRelativeToDevice, sinceWhen, \n"
             "    latency, flags) -> stream\n"
             "\n"
             "NOTE: the callback info is passed directly, it is not a structure as\n"
             "it is in C");
static PyObject*
m_FSEventStreamCreateRelativeToDevice(PyObject* self __attribute__((__unused__)),
                                      PyObject* args)
{
    PyObject* py_allocator;
    PyObject* py_callback;
    PyObject* py_callback_info;
    PyObject* py_pathsToWatch;
    PyObject* py_sinceWhen;
    PyObject* py_latency;
    PyObject* py_flags;
    PyObject* py_deviceToWatch;

    if (!PyArg_ParseTuple(args, "OOOOOOOO", &py_allocator, &py_callback,
                          &py_callback_info, &py_deviceToWatch, &py_pathsToWatch,
                          &py_sinceWhen, &py_latency, &py_flags)) {

        return NULL;
    }

    CFAllocatorRef allocator;
    if (PyObjC_PythonToObjC(@encode(CFAllocatorRef), py_allocator, &allocator) < 0) {
        return NULL;
    }

    dev_t deviceToWatch;
    if (PyObjC_PythonToObjC(@encode(dev_t), py_deviceToWatch, &deviceToWatch) < 0) {
        return NULL;
    }

    CFArrayRef pathsToWatch;
    if (PyObjC_PythonToObjC(@encode(CFArrayRef), py_pathsToWatch, &pathsToWatch) < 0) {
        return NULL;
    }

    FSEventStreamEventId sinceWhen;
    if (PyObjC_PythonToObjC(@encode(FSEventStreamEventId), py_sinceWhen, &sinceWhen)
        < 0) {
        return NULL;
    }

    CFTimeInterval latency;
    if (PyObjC_PythonToObjC(@encode(CFTimeInterval), py_latency, &latency) < 0) {
        return NULL;
    }

    FSEventStreamCreateFlags flags;
    if (PyObjC_PythonToObjC(@encode(FSEventStreamCreateFlags), py_flags, &flags) < 0) {
        return NULL;
    }

    /*
     * Build the actual callback info, which includes the flags because
     * the arguments passed to the callback vary based on the value of
     * flags.
     */
    PyObject* info = Py_BuildValue("OOO", py_flags, py_callback_info, py_callback);
    if (info == NULL) {
        return NULL;
    }

    FSEventStreamContext context = m_python_context_template;
    context.info                 = info;

    FSEventStreamRef stream = NULL;

    Py_BEGIN_ALLOW_THREADS
        @try {
            stream = FSEventStreamCreateRelativeToDevice(
                allocator, m_FSEVentStreamCallback, &context, deviceToWatch, pathsToWatch,
                sinceWhen, latency, flags);
        } @catch (NSException* localException) {
            stream = NULL;
            PyObjCErr_FromObjC(localException);
        }
    Py_END_ALLOW_THREADS

    Py_DECREF(info);

    if (stream == NULL && PyErr_Occurred()) {
        return NULL;
    }

    if (stream == NULL) {
        Py_INCREF(Py_None);
        return Py_None;
    }

    PyObject* result = PyObjC_ObjCToPython(@encode(FSEventStreamRef), &stream);
    // FSEventStreamRef is not a CF type (AFAIK), hence the user is
    // responsible for maintaining the refcount.
    // FSEventStreamRelease(stream);
    return result;
}

static PyMethodDef mod_methods[] = {
    {"FSEventStreamCreate", (PyCFunction)m_FSEventStreamCreate, METH_VARARGS,
     m_FSEventStreamCreate_doc},

    {"FSEventStreamCreateRelativeToDevice",
     (PyCFunction)m_FSEventStreamCreateRelativeToDevice, METH_VARARGS,
     m_FSEventStreamCreateRelativeToDevice_doc},

    {
        0,
        0,
        0,
    }};

static struct PyModuleDef mod_module = {
    PyModuleDef_HEAD_INIT, "_callbacks", NULL, 0, mod_methods, NULL, NULL, NULL, NULL};

PyObject* PyInit__callbacks(void);

PyObject* __attribute__((__visibility__("default"))) PyInit__callbacks(void)
{
    PyObject* m = PyModule_Create(&mod_module);
    if (m == NULL) {
        return NULL;
    }

    if (PyObjC_ImportAPI(m) < 0) {
        return NULL;
    }

    return m;
}
