/*
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * Copyright (c) 1999-2001 Apple Computer, Inc.  All Rights Reserved. The
 * contents of this file constitute Original Code as defined in and are
 * subject to the Apple Public Source License Version 1.2 (the 'License').
 * You may not use this file except in compliance with the License.  Please
 * obtain a copy of the License at http://www.apple.com/publicsource and
 * read it before using this file.
 *
 * This Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.  Please
 * see the License for the specific language governing rights and
 * limitations under the License.
 *
 *
 * @APPLE_LICENSE_HEADER_END@
 *
 */
/*
	File:		ev.cpp

	Contains:	POSIX select implementation of MacOS X event queue functions.


	

*/

#define EV_DEBUGGING 0 //Enables a lot of printfs

	#include <sys/time.h>
	#include <sys/types.h>

#ifndef __MACOS__
	#include <sys/select.h>
#endif

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/errno.h>

#include "ev.h"
#include "OS.h"
#include "OSHeaders.h"
#include "MyAssert.h"
#include "OSThread.h"
#include "OSMutex.h"

static fd_set 	sReadSet;
static fd_set 	sWriteSet;
static fd_set 	sReturnedReadSet;
static fd_set 	sReturnedWriteSet;
static void** 	sCookieArray = NULL;
static int*		sFDsToCloseArray = NULL;
static int sPipes[2];

static int sCurrentFDPos = 0;
static int sMaxFDPos = 0;
static bool sInReadSet = true;
static int sNumFDsBackFromSelect = 0;
static UInt32 sNumFDsProcessed = 0;
static OSMutex sMaxFDPosMutex;


static bool selecthasdata();
static int constructeventreq(struct eventreq* req, int fd, int event);


void select_startevents()
{
	FD_ZERO(&sReadSet);
	FD_ZERO(&sWriteSet);
	FD_ZERO(&sReturnedReadSet);
	FD_ZERO(&sReturnedWriteSet);

	//We need to associate cookies (void*)'s with our file descriptors.
	//We do so by storing cookies in this cookie array. Because an fd_set is
	//a big array of bits, we should have as many entries in the array as
	//there are bits in the fd set	
	sCookieArray = new void*[sizeof(fd_set) * 8];
	::memset(sCookieArray, 0, sizeof(void *) * sizeof(fd_set) * 8);
	
	//We need to close all fds from the select thread. Once an fd is passed into
	//removeevent, its added to this array so it may be deleted from the select thread
	sFDsToCloseArray = new int[sizeof(fd_set) * 8];
	for (int i = 0; i < (int) (sizeof(fd_set) * 8); i++)
		sFDsToCloseArray[i] = -1;
	
	//We need to wakeup select when the masks have changed. In order to do this,
	//we create a pipe that gets written to from modwatch, and read when select returns
	int theErr = ::pipe((int*)&sPipes);
	Assert(theErr == 0);
	
	//Add the read end of the pipe to the read mask
	FD_SET(sPipes[0], &sReadSet);
	sMaxFDPos = sPipes[0];
}

int select_removeevent(int which)
{

	{
		//Manipulating sMaxFDPos is not pre-emptive safe, so we have to wrap it in a mutex
		//I believe this is the only variable that is not preemptive safe....
		OSMutexLocker locker(&sMaxFDPosMutex);
		
	//Clear this fd out of both sets
		FD_CLR(which, &sWriteSet);
		FD_CLR(which, &sReadSet);
		
		FD_CLR(which, &sReturnedReadSet);
		FD_CLR(which, &sReturnedWriteSet);
	
		sCookieArray[which] = NULL; // Clear out the cookie
		
		if (which == sMaxFDPos)
		{
			//We've just deleted the highest numbered fd in our set,
			//so we need to recompute what the highest one is.
			while (!FD_ISSET(sMaxFDPos, &sReadSet) && !FD_ISSET(sMaxFDPos, &sWriteSet) &&
				(sMaxFDPos > 0))
				{
#if EV_DEBUGGING				  
					 printf("removeevent: reset MaxFDPos = %d to %d\n", sMaxFDPos , sMaxFDPos -1);
#endif				  
					sMaxFDPos--;
				}
		}

		//We also need to keep the mutex locked during any manipulation of the
		//sFDsToCloseArray, because it's definitely not preemptive safe.
			
		//put this fd into the fd's to close array, so that when select wakes up, it will
		//close the fd
		UInt32 theIndex = 0;
		while ((sFDsToCloseArray[theIndex] != -1) && (theIndex < sizeof(fd_set) * 8))
			theIndex++;
		Assert(sFDsToCloseArray[theIndex] == -1);
		sFDsToCloseArray[theIndex] = which;
#if EV_DEBUGGING
	printf("removeevent: Disabled %d \n", which);
#endif
	}
	
	//write to the pipe so that select wakes up and registers the new mask
	int theErr = ::write(sPipes[1], "p", 1);
	Assert(theErr == 1);

	return 0;
}

int select_watchevent(struct eventreq *req, int which)
{
	return select_modwatch(req, which);
}

int select_modwatch(struct eventreq *req, int which)
{
	{
		//Manipulating sMaxFDPos is not pre-emptive safe, so we have to wrap it in a mutex
		//I believe this is the only variable that is not preemptive safe....
		OSMutexLocker locker(&sMaxFDPosMutex);

		//Add or remove this fd from the specified sets
		if (which & EV_RE)
		{
	#if EV_DEBUGGING
			printf("modwatch: Enabling %d in readset\n", req->er_handle);
	#endif
			FD_SET(req->er_handle, &sReadSet);
		}
		else
		{
	#if EV_DEBUGGING
			printf("modwatch: Disbling %d in readset\n", req->er_handle);
	#endif
			FD_CLR(req->er_handle, &sReadSet);
		}
		if (which & EV_WR)
		{
	#if EV_DEBUGGING
			printf("modwatch: Enabling %d in writeset\n", req->er_handle);
	#endif
			FD_SET(req->er_handle, &sWriteSet);
		}
		else
		{
	#if EV_DEBUGGING
			printf("modwatch: Disabling %d in writeset\n", req->er_handle);
	#endif
			FD_CLR(req->er_handle, &sWriteSet);
		}

		if (req->er_handle > sMaxFDPos)
			sMaxFDPos = req->er_handle;

#if EV_DEBUGGING
		printf("modwatch: MaxFDPos=%d\n", sMaxFDPos);
#endif
		//
		// Also, modifying the cookie is not preemptive safe. This must be
		// done atomically wrt setting the fd in the set. Otherwise, it is
		// possible to have a NULL cookie on a fd.
		Assert(req->er_handle < (int)(sizeof(fd_set) * 8));
		Assert(req->er_data != NULL);
		sCookieArray[req->er_handle] = req->er_data;
	}
	
	//write to the pipe so that select wakes up and registers the new mask
	int theErr = ::write(sPipes[1], "p", 1);
	Assert(theErr == 1);

	return 0;
}

int constructeventreq(struct eventreq* req, int fd, int event)
{
	req->er_handle = fd;
	req->er_eventbits = event;
	Assert(fd < (int)(sizeof(fd_set) * 8));
	req->er_data = sCookieArray[fd];
	sCurrentFDPos++;
	sNumFDsProcessed++;
	
	//don't want events on this fd until modwatch is called.
	FD_CLR(fd, &sWriteSet);
	FD_CLR(fd, &sReadSet);
	
	return 0;
}

int select_waitevent(struct eventreq *req, void* /*onlyForMacOSX*/)
{
	//Check to see if we still have some select descriptors to process
	int theFDsProcessed = (int)sNumFDsProcessed;
	bool isSet = false;
	
	if (theFDsProcessed < sNumFDsBackFromSelect)
	{
		if (sInReadSet)
		{
			OSMutexLocker locker(&sMaxFDPosMutex);
#if EV_DEBUGGING
			printf("waitevent: Looping through readset starting at %d\n", sCurrentFDPos);
#endif
		 	while((!(isSet = FD_ISSET(sCurrentFDPos, &sReturnedReadSet))) && (sCurrentFDPos < sMaxFDPos)) 
            	sCurrentFDPos++;		

			if (isSet)
			{	
#if EV_DEBUGGING
				printf("waitevent: Found an fd: %d in readset max=%d\n", sCurrentFDPos, sMaxFDPos);
#endif
				FD_CLR(sCurrentFDPos, &sReturnedReadSet);
				return constructeventreq(req, sCurrentFDPos, EV_RE);
			}
			else
			{
#if EV_DEBUGGING
				printf("waitevent: Stopping traverse of readset at %d\n", sCurrentFDPos);
#endif
				sInReadSet = false;
				sCurrentFDPos = 0;
 			}
		}
		if (!sInReadSet)
		{
			OSMutexLocker locker(&sMaxFDPosMutex);
#if EV_DEBUGGING
			printf("waitevent: Looping through writeset starting at %d\n", sCurrentFDPos);
#endif
			while((!(isSet = FD_ISSET(sCurrentFDPos, &sReturnedWriteSet))) && (sCurrentFDPos < sMaxFDPos))
				sCurrentFDPos++;

			if (isSet)
			{
#if EV_DEBUGGING
				printf("waitevent: Found an fd: %d in writeset\n", sCurrentFDPos);
#endif
				FD_CLR(sCurrentFDPos, &sReturnedWriteSet);
				return constructeventreq(req, sCurrentFDPos, EV_WR);
			}
			else
			{
				// This can happen if another thread calls select_removeevent at just the right
				// time, setting sMaxFDPos lower than it was when select() was last called.
				// Becase sMaxFDPos is used as the place to stop iterating over the read & write
				// masks, setting it lower can cause file descriptors in the mask to get skipped.
				// If they are skipped, that's ok, because those file descriptors were removed
				// by select_removeevent anyway. We need to make sure to finish iterating over
				// the masks and call select again, which is why we set sNumFDsProcessed
				// artificially here.
				sNumFDsProcessed = sNumFDsBackFromSelect;
				Assert(sNumFDsBackFromSelect > 0);
			}
		}
	}
	
	if (sNumFDsProcessed > 0)
	{
		OSMutexLocker locker(&sMaxFDPosMutex);
#if DEBUG
		//
		// In a very bizarre circumstance (sMaxFDPos goes down & then back up again, these
		// asserts could hit.
		//
		//for (int x = 0; x < sMaxFDPos; x++)
		//	Assert(!FD_ISSET(x, &sReturnedReadSet));
		//for (int y = 0; y < sMaxFDPos; y++)
		//	Assert(!FD_ISSET(y, &sReturnedWriteSet));
#endif	
#if EV_DEBUGGING
		printf("waitevent: Finished with all fds in set. Stopped traverse of writeset at %d maxFD = %d\n", sCurrentFDPos,sMaxFDPos);
#endif
		//We've just cycled through one select result. Re-init all the counting states
		sNumFDsProcessed = 0;
		sNumFDsBackFromSelect = 0;
		sCurrentFDPos = 0;
		sInReadSet = true;
	}
	
	
	
	while(!selecthasdata())
	{
		{
			OSMutexLocker locker(&sMaxFDPosMutex);
			//Prepare to call select. Preserve the read and write sets by copying their contents
			//into the corresponding "returned" versions, and then pass those into select
			::memcpy(&sReturnedReadSet, &sReadSet, sizeof(fd_set));
			::memcpy(&sReturnedWriteSet, &sWriteSet, sizeof(fd_set));
		}

		SInt64	yieldDur = 0;
		SInt64	yieldStart;
		
		//Periodically time out the select call just in case we
		//are deaf for some reason
		// on platforw's where our threading is non-preemptive, just poll select

		struct timeval	tv;
		tv.tv_usec = 0;

	#if THREADING_IS_COOPERATIVE
		tv.tv_sec = 0;
		
		if ( yieldDur > 4 )
			tv.tv_usec = 0;
		else
			tv.tv_usec = 5000;
	#else
		tv.tv_sec = 15;
	#endif

#if EV_DEBUGGING
		printf("waitevent: about to call select\n");
#endif

		yieldStart = OS::Milliseconds();
		OSThread::ThreadYield();
		
		yieldDur = OS::Milliseconds() - yieldStart;
#if EV_DEBUGGING
		static SInt64	numZeroYields;
		
		if ( yieldDur > 1 )
		{
			printf( "select_waitevent time in OSThread::Yield() %i, numZeroYields %i\n", (long)yieldDur, (long)numZeroYields );
			numZeroYields = 0;
		}
		else
			numZeroYields++;

#endif

		sNumFDsBackFromSelect = ::select(sMaxFDPos+1, &sReturnedReadSet, &sReturnedWriteSet, NULL, &tv);

#if EV_DEBUGGING
		printf("waitevent: back from select. Result = %d\n", sNumFDsBackFromSelect);
#endif
	}
	

	if (sNumFDsBackFromSelect >= 0)
		return EINTR; 	//either we've timed out or gotten some events. Either way, force caller
						//to call waitevent again.
	return sNumFDsBackFromSelect;
}

bool selecthasdata()
{
	if (sNumFDsBackFromSelect < 0)
	{
		int err=OSThread::GetErrno();
		
#if EV_DEBUGGING
		if (err == ENOENT) 
		{
			 printf("selectHasdata: found error ENOENT==2 \n");
		}
#endif

		if ( 
#if __solaris__
			err == ENOENT || // this happens on Solaris when an HTTP fd is closed
#endif		
			err == EBADF || //this might happen if a fd is closed right before calling select
		    err == EINTR 
		   ) // this might happen if select gets interrupted
		     return false;
		return true;//if there is an error from select, we want to make sure and return to the caller
 	}
		
	if (sNumFDsBackFromSelect == 0)
		return false;//if select returns 0, we've simply timed out, so recall select
	
	if (FD_ISSET(sPipes[0], &sReturnedReadSet))
	{
#if EV_DEBUGGING
		printf("selecthasdata: Got some data on the pipe fd\n");
#endif
		//we've gotten data on the pipe file descriptor. Clear the data.
		char theBuffer[64];
		(void)::read(sPipes[0], &theBuffer[0], 64);
		FD_CLR(sPipes[0], &sReturnedReadSet);
		sNumFDsBackFromSelect--;
		
		{
			//Check the fds to close array, and if there are any in it, close those descriptors
			OSMutexLocker locker(&sMaxFDPosMutex);
			for (UInt32 theIndex = 0; ((sFDsToCloseArray[theIndex] != -1) && (theIndex < sizeof(fd_set) * 8)); theIndex++)
			{
				(void)::close(sFDsToCloseArray[theIndex]);
				sFDsToCloseArray[theIndex] = -1;
			}
		}
	}
	Assert(!FD_ISSET(sPipes[0], &sReturnedWriteSet));
	
	if (sNumFDsBackFromSelect == 0)
		return false;//if the pipe file descriptor is the ONLY data we've gotten, recall select
	else
		return true;//we've gotten a real event, return that to the caller
}

