/*
 * Support for callback functions/structs in the FSEvents frameework.
 */
#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_GET_ITEM((PyObject*)clientCallbackInfo, 0);
    if (PyObjC_PythonToObjC(
            @encode(FSEventStreamCreateFlags), v, &flags) < 0) {
        PyObjCErr_ToObjCWithGILState(&state);
    }

    info = PyTuple_GET_ITEM((PyObject*)clientCallbackInfo, 1);
    callback = PyTuple_GET_ITEM((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;

    PyObjC_DURING
        stream = FSEventStreamCreate(
            allocator,
            m_FSEVentStreamCallback,
            &context,
            pathsToWatch,
            sinceWhen,
            latency,
            flags);

    PyObjC_HANDLER
        stream = NULL;
        PyObjCErr_FromObjC(localException);

    PyObjC_ENDHANDLER

    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;

    PyObjC_DURING
        stream = FSEventStreamCreateRelativeToDevice(
                allocator,
                m_FSEVentStreamCallback,
                &context,
                deviceToWatch,
                pathsToWatch,
                sinceWhen,
                latency,
                flags);
    PyObjC_HANDLER
        stream = NULL;
        PyObjCErr_FromObjC(localException);

    PyObjC_ENDHANDLER

    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, }
};

PyObjC_MODULE_INIT(_callbacks)
{
    PyObject* m = PyObjC_MODULE_CREATE(_callbacks);
    if (m == NULL) {
        PyObjC_INITERROR();
    }

    if (PyObjC_ImportAPI(m) < 0) {
        PyObjC_INITERROR();
    }

    PyObjC_INITDONE();
}
