#include <Python.h>

#ifndef PyObject_TypeCheck /* new in Python 2.2 */
#define PyObject_TypeCheck(ob, tp) ((ob)->ob_type == (tp))
#endif

#ifdef MS_WINDOWS
#include <windows.h>
#endif

/* abstract differences between Python versions */
#if PY_MAJOR_VERSION == 1 && PY_MINOR_VERSION < 6
#define PyObject_New PyObject_NEW
#define PyObject_Del PyMem_Del
#endif

/* Global variable holding the exception type for errors detected
   by this module (but not argument type or memory errors, etc.). */

static PyObject *PyEvent_Error;

#ifdef MS_WINDOWS
typedef struct {
  PyObject_HEAD
  PyObject *ev_dict;
  char * ev_name;
  HANDLE ev_event;  /* handle to event object */
} PyEventObject;

/* forwards */

staticforward PyTypeObject PyEvent_Type;
static void event_dealloc(PyEventObject *self);

#define PyEvent_EVENT(ob) (((PyEventObject*)(ob))->ev_event)

#ifdef MS_WINDOWS
static PyObject *PyEvent_Err(void)
{
  LPVOID lpMsgBuf;
  DWORD lastError = GetLastError();
  PyObject *error = NULL;
  FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                NULL, lastError,
                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
                (LPTSTR) &lpMsgBuf, 0, NULL);
  error = Py_BuildValue("(is)", lastError, lpMsgBuf);
  LocalFree(lpMsgBuf);
  if (error != NULL)
  {
    PyErr_SetObject(PyEvent_Error, error);
    Py_DECREF(error);
  }
  return NULL;
}
#else
#define PyEvent_Err() PyErr_SetFromErrno(PyEvent_Error)
#endif

static char *make_path(char *name)
{
  char *cp;
  /* Windows does have a strdup */
  char *path = strdup(name);
  if (!path)
  {
    PyErr_NoMemory();
    return NULL;
  }

  for (cp = path; *cp; cp++)
  {
    if (*cp == '\\') *cp = '#';
  }

  return path;
}

static PyObject *PyEventObject_New(char *name, int create)
{
  PyEventObject *self;
  PyObject *dict;
  char *path;
  HANDLE event;

  path = make_path(name);
  if (!path) return NULL;

  /* if the event exists, CreateEvent returns a handle to it */
  if (create) {
    event = CreateEvent(NULL,  /* security attributes */
			TRUE,  /* manually reset */
			FALSE, /* initial state */
			path); /* pointer to event-object name */
  } 
  else {
    event = OpenEvent(EVENT_ALL_ACCESS, /* access flag */
		      FALSE,            /* inherit flag */
		      path);            /* pointer to event-object name */
  }

  free(path);
  if (event == NULL)
  {
    return PyEvent_Err();
  }
  
  dict = PyDict_New();
  if (dict == NULL)
  {
    CloseHandle(event);
    return NULL;
  }

  PyEvent_Type.ob_type = &PyType_Type;
  self = PyObject_New(PyEventObject, &PyEvent_Type);
  if (self == NULL) {
    Py_DECREF(dict);
    CloseHandle(event);
    return NULL;
  }

  /* setup our object */
  self->ev_name = strdup(name);
  self->ev_event = event;
  self->ev_dict = dict;
  return (PyObject*)self;
}

/* PyEventObject methods */

static void event_dealloc(PyEventObject *self)
{
  Py_DECREF(self->ev_dict);
  free(self->ev_name);
  CloseHandle(self->ev_event);
  PyObject_Del(self);
}

static char event_set__doc__[] =
"E.set()\n\
\n\
Set the internal flag to true.";

static PyObject *event_set(PyObject *self, PyObject *args)
{
  if (!PyArg_ParseTuple(args, ":set"))
    return NULL;

  SetEvent(PyEvent_EVENT(self));

  Py_INCREF(Py_None);
  return Py_None;
}

static char event_clear__doc__[] =
"E.clear()\n\
\n\
Reset the internal flag to false.";

static PyObject *event_clear(PyObject *self, PyObject *args)
{
  if (!PyArg_ParseTuple(args, ":clear"))
    return NULL;

  ResetEvent(PyEvent_EVENT(self));

  Py_INCREF(Py_None);
  return Py_None;
}

static char event_wait__doc__[] =
"E.wait([timeout]) -> int\n\
\n\
Block until the internal flag is true, or until the optional timeout\n\
occurs.  Returns the state of internal flag.\n\
\n\
  When the timeout argument is present and not None, it should be a\n\
  floating point number specifying a timeout for the operation in seconds\n\
  (or fractions thereof).\n\
";

static PyObject *event_wait(PyObject *self, PyObject *args)
{
  double timeout = INFINITE;
  DWORD milliseconds;
  DWORD rv;

  if (!PyArg_ParseTuple(args, "|d:wait", &timeout))
    return NULL;

  /* WaitForSingleObject uses milliseconds */
  if (timeout == INFINITE) milliseconds = INFINITE;
  else milliseconds = (DWORD)(timeout * 1000);

  Py_BEGIN_ALLOW_THREADS
  rv = WaitForSingleObject(PyEvent_EVENT(self), milliseconds);
  Py_END_ALLOW_THREADS
  return PyInt_FromLong((rv == WAIT_OBJECT_0) ? 1 : 0);
}

static PyMethodDef event_methods[] = {
  {"set",   event_set,   METH_VARARGS, event_set__doc__ },
  {"clear", event_clear, METH_VARARGS, event_clear__doc__ },
  {"wait",  event_wait,  METH_VARARGS, event_wait__doc__ },
  {NULL,    NULL}   /* sentinel */
};

static PyObject *event_getattr(PyEventObject *self, char *name)
{
  PyObject *v = PyDict_GetItemString(self->ev_dict, name);
  if (v != NULL)
  {
    Py_INCREF(v);
    return v;
  }
  return Py_FindMethod(event_methods, (PyObject *)self, name);
}

static int event_setattr(PyEventObject *self, char *name, PyObject *v)
{
  if (v == NULL)
  {
    int rv = PyDict_DelItemString(self->ev_dict, name);
    if (rv < 0)
    {
      /* remove an attribute from the instance dict */
      PyErr_Format(PyExc_AttributeError,
                   "%.50s instance has no attribute '%.400s'",
                   PyString_AS_STRING(self->ob_type->tp_name), name);
    }
    return rv;
  }
  else
  {
    return PyDict_SetItemString(self->ev_dict, name, v);
  }
}

static PyTypeObject PyEvent_Type = {
  PyObject_HEAD_INIT(NULL)
  0,      /*ob_size*/
  "event",     /*tp_name*/
  sizeof(PyEventObject), /*tp_basicsize*/
  0,      /*tp_itemsize*/
  /* methods */
  (destructor)event_dealloc, /*tp_dealloc*/
  0,      /*tp_print*/
  (getattrfunc)event_getattr, /*tp_getattr*/
  (setattrfunc)event_setattr, /*tp_setattr*/
  0,      /*tp_compare*/
  0,      /*tp_repr*/
  0,      /*tp_as_number*/
  0,      /*tp_as_sequence*/
  0,      /*tp_as_mapping*/
  0,      /*tp_hash*/
};

/*** Module Level Stuff ***/

static char PyEvent_event__doc__[] =
"Event(name) -> event object\n\
\n\
Creates a new named event object.";

static PyObject *PyEvent_event(PyObject *self, PyObject *args)
{
  char *name;

  if (!PyArg_ParseTuple(args, "s:Event", &name))
    return NULL;

  return PyEventObject_New(name, 1);
}

static char PyEvent_open__doc__[] =
"OpenEvent(name) -> event object\n\
\n\
Opens an existing named event object.";

static PyObject *PyEvent_open(PyObject *self, PyObject *args)
{
  char *name;

  if (!PyArg_ParseTuple(args, "s:OpenEvent", &name))
    return NULL;

  return PyEventObject_New(name, 0);
}

static char PyEvent_wait__doc__[] =
"WaitEvents(events[, timeout]) -> list of events\n\
\n\
Waits for any one of the events to become signaled or the timeout\n\
expires.";

static PyObject *PyEvent_wait(PyObject *self, PyObject *args)
{
  PyObject *events;
  PyObject *result;
  double timeout = INFINITE;
  DWORD milliseconds, rv;
  HANDLE *waitlist;
  int i, count;
  PyObject *(*getitem)(PyObject *, int);

  if (!PyArg_ParseTuple(args, "O|d:WaitEvents", &events, &timeout))
    return NULL;

  if (PyList_Check(events)) {
    count = PyList_Size(events);
    getitem = PyList_GetItem;
  }
  else if (PyTuple_Check(events)) {
    count = PyTuple_Size(events);
    getitem = PyTuple_GetItem;
  }
  else {
    PyErr_SetString(PyExc_TypeError, "WaitEvents() arg 1 must be a tuple or list");
    return NULL;
  }

  if (count == 0)
  {
    PyErr_SetString(PyExc_ValueError, "WaitEvents() arg 1 must not be empty");
    return NULL;
  }

  waitlist = PyMem_NEW(HANDLE, count);
  for (i = 0; i < count; i++)
  {
    PyObject *event = (*getitem)(events, i);
    if (event == NULL) return NULL;
    if (!PyObject_TypeCheck(event, &PyEvent_Type))
    {
      PyErr_SetString(PyExc_TypeError, "WaitEvents() arg 1 must be a list of events");
      PyMem_DEL(waitlist);
      return NULL;
    }
    waitlist[i] = PyEvent_EVENT(event);
  }

  /* WaitForSingleObject uses milliseconds */
  if (timeout == INFINITE) milliseconds = INFINITE;
  else milliseconds = (DWORD)(timeout * 1000);

  Py_BEGIN_ALLOW_THREADS
  rv = WaitForMultipleObjectsEx(count, waitlist, FALSE, milliseconds, TRUE);
  Py_END_ALLOW_THREADS
  result = PyList_New(0);
  if ((WAIT_OBJECT_0 >= rv) && (rv < (WAIT_OBJECT_0 + count)))
  {
    for (i = 0; i < count; i++)
    {
      rv = WaitForSingleObject(waitlist[i], 0);
      if (rv == WAIT_OBJECT_0)
      {
        PyObject *event = (*getitem)(events, i);
        if (event == NULL)
        {
          PyMem_DEL(waitlist);
          Py_DECREF(result);
          return NULL;
        }
        PyList_Append(result, event);
      }
    }
  }
  return result;
}

#endif /* not Windows */

static PyMethodDef PyEvent_methods[] = {
#ifdef MS_WINDOWS
  { "Event",      PyEvent_event, METH_VARARGS, PyEvent_event__doc__ },
  { "OpenEvent",  PyEvent_open,  METH_VARARGS, PyEvent_open__doc__ },
  { "WaitEvents", PyEvent_wait,  METH_VARARGS, PyEvent_wait__doc__ },
#endif
  { NULL, NULL }
};

static char PyEvent__doc__[] =
"This module provides a multi-process enabled event.\n\
\n\
(*) - not available on all platforms\n\
\n\
Functions:\n\
\n\
Event() -- create a new event object (*)\n\
WaitEvents() -- wait for multiple events at once (*)\n\
\n\
Special objects:\n\
\n\
EventType -- type object for event objects (*)\n\
EventError -- exception raised for errors\n\
";

/* Initialize Module */
DL_EXPORT(void) init_event(void) {
  PyObject *m, *d;
  PyObject *mod_name;
  char *exc_name;

  m = Py_InitModule3("_event", PyEvent_methods, PyEvent__doc__);
  d = PyModule_GetDict(m);

  /* set global error class */
  /* dynamically create the class name based on the module name */
  /* too bad that Python cannot do this for us */
  mod_name = PyDict_GetItemString(d, "__name__"); /* borrowed */
  exc_name = (char *) malloc(PyString_Size(mod_name) + 12);
  sprintf(exc_name, "%s.%s", PyString_AS_STRING(mod_name), "EventError");
  PyEvent_Error = PyErr_NewException(exc_name, NULL, NULL);
  free(exc_name);
  if (PyEvent_Error == NULL)
    return;

#ifdef MS_WINDOWS
  /* make our type a valid Python type */
  PyEvent_Type.ob_type = &PyType_Type;
  Py_INCREF(&PyEvent_Type);

  /* add our globals to the module dict */
  PyDict_SetItemString(d, "EventError", PyEvent_Error);
  PyDict_SetItemString(d, "EventType", (PyObject *)&PyEvent_Type);
#endif
}

