/*
 * _inotify.c - Python extension interfacing to the Linux inotify subsystem
 *
 * Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of version 2.1 of the GNU Lesser General
 * Public License, incorporated herein by reference.
 */

#include <Python.h>
#include <alloca.h>
#include <sys/inotify.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <unistd.h>

static PyObject *init(PyObject *self, PyObject *args)
{
    PyObject *ret = NULL;
    int fd = -1;

     if (!PyArg_ParseTuple(args, ":init"))
	goto bail;

    Py_BEGIN_ALLOW_THREADS
    fd = inotify_init();
    Py_END_ALLOW_THREADS

    if (fd == -1) {
	PyErr_SetFromErrno(PyExc_OSError);
	goto bail;
    }

    ret = PyInt_FromLong(fd);
    if (ret == NULL)
	goto bail;

    goto done;

bail:
    if (fd != -1)
	close(fd);

    Py_CLEAR(ret);

done:
    return ret;
}

PyDoc_STRVAR(
    init_doc,
    "init() -> fd\n"
    "\n"
    "Initialise an inotify instance.\n"
    "Return a file descriptor associated with a new inotify event queue.");

static PyObject *add_watch(PyObject *self, PyObject *args)
{
    PyObject *ret = NULL;
    uint32_t mask;
    int wd = -1;
    char *path;
    int fd;

    if (!PyArg_ParseTuple(args, "isI:add_watch", &fd, &path, &mask))
	goto bail;

    Py_BEGIN_ALLOW_THREADS
    wd = inotify_add_watch(fd, path, mask);
    Py_END_ALLOW_THREADS

    if (wd == -1) {
	PyErr_SetFromErrnoWithFilename(PyExc_OSError, path);
	goto bail;
    }

    ret = PyInt_FromLong(wd);
    if (ret == NULL)
	goto bail;

    goto done;

bail:
    if (wd != -1)
	inotify_rm_watch(fd, wd);

    Py_CLEAR(ret);

done:
    return ret;
}

PyDoc_STRVAR(
    add_watch_doc,
    "add_watch(fd, path, mask) -> wd\n"
    "\n"
    "Add a watch to an inotify instance, or modify an existing watch.\n"
    "\n"
    "        fd: file descriptor returned by init()\n"
    "        path: path to watch\n"
    "        mask: mask of events to watch for\n"
    "\n"
    "Return a unique numeric watch descriptor for the inotify instance\n"
    "mapped by the file descriptor.");

static PyObject *remove_watch(PyObject *self, PyObject *args)
{
    PyObject *ret = NULL;
    uint32_t wd;
    int fd;
    int r;

    if (!PyArg_ParseTuple(args, "iI:remove_watch", &fd, &wd))
	goto bail;

    Py_BEGIN_ALLOW_THREADS
    r = inotify_rm_watch(fd, wd);
    Py_END_ALLOW_THREADS

    if (r == -1) {
	PyErr_SetFromErrno(PyExc_OSError);
	goto bail;
    }

    Py_INCREF(Py_None);

    goto done;

bail:
    Py_CLEAR(ret);

done:
    return ret;
}

PyDoc_STRVAR(
    remove_watch_doc,
    "remove_watch(fd, wd)\n"
    "\n"
    "        fd: file descriptor returned by init()\n"
    "        wd: watch descriptor returned by add_watch()\n"
    "\n"
    "Remove a watch associated with the watch descriptor wd from the\n"
    "inotify instance associated with the file descriptor fd.\n"
    "\n"
    "Removing a watch causes an IN_IGNORED event to be generated for this\n"
    "watch descriptor.");

#define bit_name(x) {x, #x}

static struct {
    int bit;
    const char *name;
    PyObject *pyname;
} bit_names[] = {
    bit_name(IN_ACCESS),
    bit_name(IN_MODIFY),
    bit_name(IN_ATTRIB),
    bit_name(IN_CLOSE_WRITE),
    bit_name(IN_CLOSE_NOWRITE),
    bit_name(IN_OPEN),
    bit_name(IN_MOVED_FROM),
    bit_name(IN_MOVED_TO),
    bit_name(IN_CREATE),
    bit_name(IN_DELETE),
    bit_name(IN_DELETE_SELF),
    bit_name(IN_MOVE_SELF),
    bit_name(IN_UNMOUNT),
    bit_name(IN_Q_OVERFLOW),
    bit_name(IN_IGNORED),
    bit_name(IN_ONLYDIR),
    bit_name(IN_DONT_FOLLOW),
    bit_name(IN_MASK_ADD),
    bit_name(IN_ISDIR),
    bit_name(IN_ONESHOT),
    {0}
};

static PyObject *decode_mask(int mask)
{
    PyObject *ret = PyList_New(0);
    int i;

    if (ret == NULL)
	goto bail;

    for (i = 0; bit_names[i].bit; i++) {
	if (mask & bit_names[i].bit) {
	    if (bit_names[i].pyname == NULL) {
		bit_names[i].pyname = PyString_FromString(bit_names[i].name);
		if (bit_names[i].pyname == NULL)
		    goto bail;
	    }
	    Py_INCREF(bit_names[i].pyname);
	    if (PyList_Append(ret, bit_names[i].pyname) == -1)
		goto bail;
	}
    }

    goto done;

bail:
    Py_CLEAR(ret);

done:
    return ret;
}

static PyObject *pydecode_mask(PyObject *self, PyObject *args)
{
    int mask;

    if (!PyArg_ParseTuple(args, "i:decode_mask", &mask))
	return NULL;

    return decode_mask(mask);
}

PyDoc_STRVAR(
    decode_mask_doc,
    "decode_mask(mask) -> list_of_strings\n"
    "\n"
    "Decode an inotify mask value into a list of strings that give the\n"
    "name of each bit set in the mask.");

static char doc[] = "Low-level inotify interface wrappers.";

static void define_const(PyObject *dict, const char *name, uint32_t val)
{
    PyObject *pyval = PyInt_FromLong(val);
    PyObject *pyname = PyString_FromString(name);

    if (!pyname || !pyval)
	goto bail;

    PyDict_SetItem(dict, pyname, pyval);

bail:
    Py_XDECREF(pyname);
    Py_XDECREF(pyval);
}

static void define_consts(PyObject *dict)
{
    define_const(dict, "IN_ACCESS", IN_ACCESS);
    define_const(dict, "IN_MODIFY", IN_MODIFY);
    define_const(dict, "IN_ATTRIB", IN_ATTRIB);
    define_const(dict, "IN_CLOSE_WRITE", IN_CLOSE_WRITE);
    define_const(dict, "IN_CLOSE_NOWRITE", IN_CLOSE_NOWRITE);
    define_const(dict, "IN_OPEN", IN_OPEN);
    define_const(dict, "IN_MOVED_FROM", IN_MOVED_FROM);
    define_const(dict, "IN_MOVED_TO", IN_MOVED_TO);

    define_const(dict, "IN_CLOSE", IN_CLOSE);
    define_const(dict, "IN_MOVE", IN_MOVE);

    define_const(dict, "IN_CREATE", IN_CREATE);
    define_const(dict, "IN_DELETE", IN_DELETE);
    define_const(dict, "IN_DELETE_SELF", IN_DELETE_SELF);
    define_const(dict, "IN_MOVE_SELF", IN_MOVE_SELF);
    define_const(dict, "IN_UNMOUNT", IN_UNMOUNT);
    define_const(dict, "IN_Q_OVERFLOW", IN_Q_OVERFLOW);
    define_const(dict, "IN_IGNORED", IN_IGNORED);

    define_const(dict, "IN_ONLYDIR", IN_ONLYDIR);
    define_const(dict, "IN_DONT_FOLLOW", IN_DONT_FOLLOW);
    define_const(dict, "IN_MASK_ADD", IN_MASK_ADD);
    define_const(dict, "IN_ISDIR", IN_ISDIR);
    define_const(dict, "IN_ONESHOT", IN_ONESHOT);
    define_const(dict, "IN_ALL_EVENTS", IN_ALL_EVENTS);
}

struct event {
    PyObject_HEAD
    PyObject *wd;
    PyObject *mask;
    PyObject *cookie;
    PyObject *name;
};

static PyObject *event_wd(PyObject *self, void *x)
{
    struct event *evt = (struct event *) self;
    Py_INCREF(evt->wd);
    return evt->wd;
}

static PyObject *event_mask(PyObject *self, void *x)
{
    struct event *evt = (struct event *) self;
    Py_INCREF(evt->mask);
    return evt->mask;
}

static PyObject *event_cookie(PyObject *self, void *x)
{
    struct event *evt = (struct event *) self;
    Py_INCREF(evt->cookie);
    return evt->cookie;
}

static PyObject *event_name(PyObject *self, void *x)
{
    struct event *evt = (struct event *) self;
    Py_INCREF(evt->name);
    return evt->name;
}

static struct PyGetSetDef event_getsets[] = {
    {"wd", event_wd, NULL,
     "watch descriptor"},
    {"mask", event_mask, NULL,
     "event mask"},
    {"cookie", event_cookie, NULL,
     "rename cookie, if rename-related event"},
    {"name", event_name, NULL,
     "file name"},
    {NULL}
};

PyDoc_STRVAR(
    event_doc,
    "event: Structure describing an inotify event.");

static PyObject *event_new(PyTypeObject *t, PyObject *a, PyObject *k)
{
    return (*t->tp_alloc)(t, 0);
}

static void event_dealloc(struct event *evt)
{
    Py_XDECREF(evt->wd);
    Py_XDECREF(evt->mask);
    Py_XDECREF(evt->cookie);
    Py_XDECREF(evt->name);

    (*evt->ob_type->tp_free)(evt);
}

static PyObject *event_repr(struct event *evt)
{
    int wd = PyInt_AsLong(evt->wd);
    int cookie = evt->cookie == Py_None ? -1 : PyInt_AsLong(evt->cookie);
    PyObject *ret = NULL, *pymasks = NULL, *pymask = NULL;
    PyObject *join = NULL;
    char *maskstr;

    join = PyString_FromString("|");
    if (join == NULL)
	goto bail;

    pymasks = decode_mask(PyInt_AsLong(evt->mask));
    if (pymasks == NULL)
	goto bail;

    pymask = _PyString_Join(join, pymasks);
    if (pymask == NULL)
	goto bail;

    maskstr = PyString_AsString(pymask);

    if (evt->name != Py_None) {
	PyObject *pyname = PyString_Repr(evt->name, 1);
	char *name = pyname ? PyString_AsString(pyname) : "???";

	if (cookie == -1)
	    ret = PyString_FromFormat("event(wd=%d, mask=%s, name=%s)",
				      wd, maskstr, name);
	else
	    ret = PyString_FromFormat("event(wd=%d, mask=%s, "
				      "cookie=0x%x, name=%s)",
				      wd, maskstr, cookie, name);

	Py_XDECREF(pyname);
    } else {
	if (cookie == -1)
	    ret = PyString_FromFormat("event(wd=%d, mask=%s)",
				      wd, maskstr);
	else {
	    ret = PyString_FromFormat("event(wd=%d, mask=%s, cookie=0x%x)",
				      wd, maskstr, cookie);
	}
    }

    goto done;
bail:
    Py_CLEAR(ret);

done:
    Py_XDECREF(pymask);
    Py_XDECREF(pymasks);
    Py_XDECREF(join);

    return ret;
}

static PyTypeObject event_type = {
    PyObject_HEAD_INIT(NULL)
    0,                         /*ob_size*/
    "_inotify.event",             /*tp_name*/
    sizeof(struct event), /*tp_basicsize*/
    0,                         /*tp_itemsize*/
    (destructor)event_dealloc, /*tp_dealloc*/
    0,                         /*tp_print*/
    0,                         /*tp_getattr*/
    0,                         /*tp_setattr*/
    0,                         /*tp_compare*/
    (reprfunc)event_repr,      /*tp_repr*/
    0,                         /*tp_as_number*/
    0,                         /*tp_as_sequence*/
    0,                         /*tp_as_mapping*/
    0,                         /*tp_hash */
    0,                         /*tp_call*/
    0,                         /*tp_str*/
    0,                         /*tp_getattro*/
    0,                         /*tp_setattro*/
    0,                         /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
    event_doc,           /* tp_doc */
    0,                         /* tp_traverse */
    0,                         /* tp_clear */
    0,                         /* tp_richcompare */
    0,                         /* tp_weaklistoffset */
    0,                         /* tp_iter */
    0,                         /* tp_iternext */
    0,                         /* tp_methods */
    0,                         /* tp_members */
    event_getsets,      /* tp_getset */
    0,                         /* tp_base */
    0,                         /* tp_dict */
    0,                         /* tp_descr_get */
    0,                         /* tp_descr_set */
    0,                         /* tp_dictoffset */
    0,                         /* tp_init */
    0,                         /* tp_alloc */
    event_new,          /* tp_new */
};

PyObject *read_events(PyObject *self, PyObject *args)
{
    PyObject *ctor_args = NULL;
    PyObject *pybufsize = NULL;
    PyObject *ret = NULL;
    int bufsize = 65536;
    char *buf = NULL;
    int nread, pos;
    int fd;

    if (!PyArg_ParseTuple(args, "i|O:read", &fd, &pybufsize))
	goto bail;

    if (pybufsize && pybufsize != Py_None)
	bufsize = PyInt_AsLong(pybufsize);

    ret = PyList_New(0);
    if (ret == NULL)
	goto bail;

    if (bufsize <= 0) {
	int r;

	Py_BEGIN_ALLOW_THREADS
	r = ioctl(fd, FIONREAD, &bufsize);
	Py_END_ALLOW_THREADS

	if (r == -1) {
	    PyErr_SetFromErrno(PyExc_OSError);
	    goto bail;
	}
	if (bufsize == 0)
	    goto done;
    }
    else {
	static long name_max;
	static long name_fd = -1;
	long min;

	if (name_fd != fd) {
	    name_fd = fd;
	    Py_BEGIN_ALLOW_THREADS
	    name_max = fpathconf(fd, _PC_NAME_MAX);
	    Py_END_ALLOW_THREADS
	}

	min = sizeof(struct inotify_event) + name_max + 1;

	if (bufsize < min) {
	    PyErr_Format(PyExc_ValueError, "bufsize must be at least %d",
			 (int) min);
	    goto bail;
	}
    }

    buf = alloca(bufsize);

    Py_BEGIN_ALLOW_THREADS
    nread = read(fd, buf, bufsize);
    Py_END_ALLOW_THREADS

    if (nread == -1) {
	PyErr_SetFromErrno(PyExc_OSError);
	goto bail;
    }

    ctor_args = PyTuple_New(0);

    if (ctor_args == NULL)
	goto bail;

    pos = 0;

    while (pos < nread) {
	struct inotify_event *in = (struct inotify_event *) (buf + pos);
	struct event *evt;
	PyObject *obj;

	obj = PyObject_CallObject((PyObject *) &event_type, ctor_args);

	if (obj == NULL)
	    goto bail;

	evt = (struct event *) obj;

	evt->wd = PyInt_FromLong(in->wd);
	evt->mask = PyInt_FromLong(in->mask);
	if (in->mask & IN_MOVE)
	    evt->cookie = PyInt_FromLong(in->cookie);
	else {
	    Py_INCREF(Py_None);
	    evt->cookie = Py_None;
	}
	if (in->len)
	    evt->name = PyString_FromString(in->name);
	else {
	    Py_INCREF(Py_None);
	    evt->name = Py_None;
	}

	if (!evt->wd || !evt->mask || !evt->cookie || !evt->name)
	    goto mybail;

	if (PyList_Append(ret, obj) == -1)
	    goto mybail;

	pos += sizeof(struct inotify_event) + in->len;
	continue;

    mybail:
	Py_CLEAR(evt->wd);
	Py_CLEAR(evt->mask);
	Py_CLEAR(evt->cookie);
	Py_CLEAR(evt->name);
	Py_DECREF(obj);

	goto bail;
    }

    goto done;

bail:
    Py_CLEAR(ret);

done:
    Py_XDECREF(ctor_args);

    return ret;
}

PyDoc_STRVAR(
    read_doc,
    "read(fd, bufsize[=65536]) -> list_of_events\n"
    "\n"
    "\nRead inotify events from a file descriptor.\n"
    "\n"
    "        fd: file descriptor returned by init()\n"
    "        bufsize: size of buffer to read into, in bytes\n"
    "\n"
    "Return a list of event objects.\n"
    "\n"
    "If bufsize is > 0, block until events are available to be read.\n"
    "Otherwise, immediately return all events that can be read without\n"
    "blocking.");


static PyMethodDef methods[] = {
    {"init", init, METH_VARARGS, init_doc},
    {"add_watch", add_watch, METH_VARARGS, add_watch_doc},
    {"remove_watch", remove_watch, METH_VARARGS, remove_watch_doc},
    {"read", read_events, METH_VARARGS, read_doc},
    {"decode_mask", pydecode_mask, METH_VARARGS, decode_mask_doc},
    {NULL},
};

void init_inotify(void)
{
    PyObject *mod, *dict;

    if (PyType_Ready(&event_type) == -1)
	return;

    mod = Py_InitModule3("_inotify", methods, doc);

    dict = PyModule_GetDict(mod);

    if (dict)
	define_consts(dict);
}
