/* 
 * tkUnixEvent.c --
 *
 *	This file implements an event source for X displays for the
 *	UNIX version of Tk.
 *
 * Copyright (c) 1995-1997 Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * SCCS: @(#) tkUnixEvent.c 1.17 97/09/11 12:51:04
 */

#include "tkInt.h"
#include "tkUnixInt.h"
#include <signal.h>

/*
 * The following static indicates whether this module has been initialized.
 */

static int initialized = 0;

/*
 * Prototypes for procedures that are referenced only in this file:
 */

static void		DisplayCheckProc _ANSI_ARGS_((ClientData clientData,
			    int flags));
static void		DisplayExitHandler _ANSI_ARGS_((
			    ClientData clientData));
static void		DisplayFileProc _ANSI_ARGS_((ClientData clientData,
			    int flags));
static void		DisplaySetupProc _ANSI_ARGS_((ClientData clientData,
			    int flags));

/*
 *----------------------------------------------------------------------
 *
 * TkCreateXEventSource --
 *
 *	This procedure is called during Tk initialization to create
 *	the event source for X Window events.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	A new event source is created.
 *
 *----------------------------------------------------------------------
 */

void
TkCreateXEventSource()
{
    if (!initialized) {
	initialized = 1;
	Tcl_CreateEventSource(DisplaySetupProc, DisplayCheckProc, NULL);
	Tcl_CreateExitHandler(DisplayExitHandler, NULL);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * DisplayExitHandler --
 *
 *	This function is called during finalization to clean up the
 *	display module.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static void
DisplayExitHandler(clientData)
    ClientData clientData;	/* Not used. */
{
    Tcl_DeleteEventSource(DisplaySetupProc, DisplayCheckProc, NULL);
    initialized = 0;
}

/*
 *----------------------------------------------------------------------
 *
 * TkpOpenDisplay --
 *
 *	Allocates a new TkDisplay, opens the X display, and establishes
 *	the file handler for the connection.
 *
 * Results:
 *	A pointer to a Tk display structure.
 *
 * Side effects:
 *	Opens a display.
 *
 *----------------------------------------------------------------------
 */

TkDisplay *
TkpOpenDisplay(display_name)
    char *display_name;
{
    TkDisplay *dispPtr;
    Display *display = XOpenDisplay(display_name);

    if (display == NULL) {
	return NULL;
    }
    dispPtr = (TkDisplay *) ckalloc(sizeof(TkDisplay));
    dispPtr->display = display;
    Tcl_CreateFileHandler(ConnectionNumber(display), TCL_READABLE,
	    DisplayFileProc, (ClientData) dispPtr);
    return dispPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * TkpCloseDisplay --
 *
 *	Cancels notifier callbacks and closes a display.  
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Deallocates the displayPtr.
 *
 *----------------------------------------------------------------------
 */

void
TkpCloseDisplay(displayPtr)
    TkDisplay *displayPtr;
{
    TkDisplay *dispPtr = (TkDisplay *) displayPtr;

    if (dispPtr->display != 0) {
        Tcl_DeleteFileHandler(ConnectionNumber(dispPtr->display));
	
        (void) XSync(dispPtr->display, False);
        (void) XCloseDisplay(dispPtr->display);
    }
    
    ckfree((char *) dispPtr);
}

/*
 *----------------------------------------------------------------------
 *
 * DisplaySetupProc --
 *
 *	This procedure implements the setup part of the UNIX X display
 *	event source.  It is invoked by Tcl_DoOneEvent before entering
 *	the notifier to check for events on all displays.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	If data is queued on a display inside Xlib, then the maximum
 *	block time will be set to 0 to ensure that the notifier returns
 *	control to Tcl even if there is no more data on the X connection.
 *
 *----------------------------------------------------------------------
 */

static void
DisplaySetupProc(clientData, flags)
    ClientData clientData;	/* Not used. */
    int flags;
{
    TkDisplay *dispPtr;
    static Tcl_Time blockTime = { 0, 0 };

    if (!(flags & TCL_WINDOW_EVENTS)) {
	return;
    }

    for (dispPtr = tkDisplayList; dispPtr != NULL;
	 dispPtr = dispPtr->nextPtr) {

	/*
	 * Flush the display. If data is pending on the X queue, set
	 * the block time to zero.  This ensures that we won't block
	 * in the notifier if there is data in the X queue, but not on
	 * the server socket.
	 */

	XFlush(dispPtr->display);
	if (XQLength(dispPtr->display) > 0) {
	    Tcl_SetMaxBlockTime(&blockTime);
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * DisplayCheckProc --
 *
 *	This procedure checks for events sitting in the X event
 *	queue.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Moves queued events onto the Tcl event queue.
 *
 *----------------------------------------------------------------------
 */

static void
DisplayCheckProc(clientData, flags)
    ClientData clientData;	/* Not used. */
    int flags;
{
    TkDisplay *dispPtr;
    XEvent event;
    int numFound;

    if (!(flags & TCL_WINDOW_EVENTS)) {
	return;
    }

    for (dispPtr = tkDisplayList; dispPtr != NULL;
	 dispPtr = dispPtr->nextPtr) {
	XFlush(dispPtr->display);
	numFound = XQLength(dispPtr->display);

	/*
	 * Transfer events from the X event queue to the Tk event queue.
	 */

	while (numFound > 0) {
	    XNextEvent(dispPtr->display, &event);
	    Tk_QueueWindowEvent(&event, TCL_QUEUE_TAIL);
	    numFound--;
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * DisplayFileProc --
 *
 *	This procedure implements the file handler for the X connection.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Makes entries on the Tcl event queue for all the events available
 *	from all the displays.
 *
 *----------------------------------------------------------------------
 */

static void
DisplayFileProc(clientData, flags)
    ClientData clientData;		/* The display pointer. */
    int flags;				/* Should be TCL_READABLE. */
{
    TkDisplay *dispPtr = (TkDisplay *) clientData;
    Display *display = dispPtr->display;
    XEvent event;
    int numFound;

    XFlush(display);
    numFound = XEventsQueued(display, QueuedAfterReading);
    if (numFound == 0) {
	
	/*
	 * Things are very tricky if there aren't any events readable
	 * at this point (after all, there was supposedly data
	 * available on the connection).  A couple of things could
	 * have occurred:
	 * 
	 * One possibility is that there were only error events in the
	 * input from the server.  If this happens, we should return
	 * (we don't want to go to sleep in XNextEvent below, since
	 * this would block out other sources of input to the
	 * process).
	 *
	 * Another possibility is that our connection to the server
	 * has been closed.  This will not necessarily be detected in
	 * XEventsQueued (!!), so if we just return then there will be
	 * an infinite loop.  To detect such an error, generate a NoOp
	 * protocol request to exercise the connection to the server,
	 * then return.  However, must disable SIGPIPE while sending
	 * the request, or else the process will die from the signal
	 * and won't invoke the X error function to print a nice (?!)
	 * message.
	 */
	
	void (*oldHandler)();
	
	oldHandler = (void (*)()) signal(SIGPIPE, SIG_IGN);
	XNoOp(display);
	XFlush(display);
	(void) signal(SIGPIPE, oldHandler);
    }
    
    /*
     * Transfer events from the X event queue to the Tk event queue.
     */

    while (numFound > 0) {
	XNextEvent(display, &event);
	Tk_QueueWindowEvent(&event, TCL_QUEUE_TAIL);
	numFound--;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TkUnixDoOneXEvent --
 *
 *	This routine waits for an X event to be processed or for
 *	a timeout to occur.  The timeout is specified as an absolute
 *	time.  This routine is called when Tk needs to wait for a
 *	particular X event without letting arbitrary events be
 *	processed.  The caller will typically call Tk_RestrictEvents
 *	to set up an event filter before calling this routine.  This
 *	routine will service at most one event per invocation.
 *
 * Results:
 *	Returns 0 if the timeout has expired, otherwise returns 1.
 *
 * Side effects:
 *	Can invoke arbitrary Tcl scripts.
 *
 *----------------------------------------------------------------------
 */

int
TkUnixDoOneXEvent(timePtr)
    Tcl_Time *timePtr;		/* Specifies the absolute time when the
				 * call should time out. */
{
    TkDisplay *dispPtr;
    static fd_mask readMask[MASK_SIZE];
    struct timeval blockTime, *timeoutPtr;
    Tcl_Time now;
    int fd, index, bit, numFound, numFdBits = 0;

    /*
     * Look for queued events first. 
     */

    if (Tcl_ServiceEvent(TCL_WINDOW_EVENTS)) {
	return 1;
    }

    /*
     * Compute the next block time and check to see if we have timed out.
     * Note that HP-UX defines tv_sec to be unsigned so we have to be
     * careful in our arithmetic.
     */

    if (timePtr) {
	TclpGetTime(&now);
	blockTime.tv_sec = timePtr->sec;
	blockTime.tv_usec = timePtr->usec - now.usec;
	if (blockTime.tv_usec < 0) {
	    now.sec += 1;
	    blockTime.tv_usec += 1000000;
	}
	if (blockTime.tv_sec < now.sec) {
	    blockTime.tv_sec = 0;
	    blockTime.tv_usec = 0;
	} else {
	    blockTime.tv_sec -= now.sec;
	}
	timeoutPtr = &blockTime;
    } else {
	timeoutPtr = NULL;
    }

    /*
     * Set up the select mask for all of the displays.  If a display has
     * data pending, then we want to poll instead of blocking.
     */

    memset((VOID *) readMask, 0, MASK_SIZE*sizeof(fd_mask));
    for (dispPtr = tkDisplayList; dispPtr != NULL;
	 dispPtr = dispPtr->nextPtr) {
	XFlush(dispPtr->display);
	if (XQLength(dispPtr->display) > 0) {
	    blockTime.tv_sec = 0;
	    blockTime.tv_usec = 0;
	}
	fd = ConnectionNumber(dispPtr->display);
	index = fd/(NBBY*sizeof(fd_mask));
	bit = 1 << (fd%(NBBY*sizeof(fd_mask)));
	readMask[index] |= bit;
	if (numFdBits <= fd) {
	    numFdBits = fd+1;
	}
    }

    numFound = select(numFdBits, (SELECT_MASK *) &readMask[0], NULL, NULL,
	    timeoutPtr);
    if (numFound <= 0) {
	/*
	 * Some systems don't clear the masks after an error, so
	 * we have to do it here.
	 */

	memset((VOID *) readMask, 0, MASK_SIZE*sizeof(fd_mask));
    }

    /*
     * Process any new events on the display connections.
     */

    for (dispPtr = tkDisplayList; dispPtr != NULL;
	 dispPtr = dispPtr->nextPtr) {
	fd = ConnectionNumber(dispPtr->display);
	index = fd/(NBBY*sizeof(fd_mask));
	bit = 1 << (fd%(NBBY*sizeof(fd_mask)));
	if ((readMask[index] & bit) || (XQLength(dispPtr->display) > 0)) {
	    DisplayFileProc((ClientData)dispPtr, TCL_READABLE);
	}
    }
    if (Tcl_ServiceEvent(TCL_WINDOW_EVENTS)) {
	return 1;
    }

    /*
     * Check to see if we timed out.
     */

    if (timePtr) {
	TclpGetTime(&now);
	if ((now.sec > timePtr->sec) || ((now.sec == timePtr->sec)
		&& (now.usec > timePtr->usec))) {
	    return 0;
	}
    }

    /*
     * We had an event but we did not generate a Tcl event from it. Behave
     * as though we dealt with it. (JYL&SS)
     */

    return 1;
}

/*
 *----------------------------------------------------------------------
 *
 * TkpSync --
 *
 *	This routine ensures that all pending X requests have been
 *	seen by the server, and that any pending X events have been
 *	moved onto the Tk event queue.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Places new events on the Tk event queue.
 *
 *----------------------------------------------------------------------
 */

void
TkpSync(display)
    Display *display;		/* Display to sync. */
{
    int numFound = 0;
    XEvent event;

    XSync(display, False);

    /*
     * Transfer events from the X event queue to the Tk event queue.
     */

    numFound = XQLength(display);
    while (numFound > 0) {
	XNextEvent(display, &event);
	Tk_QueueWindowEvent(&event, TCL_QUEUE_TAIL);
	numFound--;
    }
}
