/*
 * Copyright (c) 2008, 2009, 2010, 2011
 *   Jonathan Schleifer <js@webkeks.org>
 *
 * All rights reserved.
 *
 * This file is part of ObjFW. It may be distributed under the terms of the
 * Q Public License 1.0, which can be found in the file LICENSE.QPL included in
 * the packaging of this file.
 *
 * Alternatively, it may be distributed under the terms of the GNU General
 * Public License, either version 2 or 3, which can be found in the file
 * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this
 * file.
 */

#include "config.h"

#include <string.h>
#include <assert.h>
#include <unistd.h>

#define __NO_EXT_QNX
#import "OFStreamSelectObserver.h"
#import "OFStream.h"
#import "OFArray.h"
#import "OFNumber.h"
#import "OFAutoreleasePool.h"

#ifdef _WIN32
# define close(sock) closesocket(sock)
#endif

enum {
	QUEUE_ADD = 0,
	QUEUE_REMOVE = 1,
	QUEUE_READ = 0,
	QUEUE_WRITE = 2
};

@implementation OFStreamSelectObserver
- init
{
	self = [super init];

	@try {
		FD_ZERO(&readFDs);
		FD_ZERO(&writeFDs);

		FD_SET(cancelFD[0], &readFDs);
		nFDs = cancelFD[0] + 1;
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)_addStream: (OFStream*)stream
	 withFDSet: (fd_set*)FDSet
{
	OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init];
	int fileDescriptor = [stream fileDescriptor];

	FD_SET(fileDescriptor, FDSet);
	FD_SET(fileDescriptor, &exceptFDs);

	if (fileDescriptor >= nFDs)
		nFDs = fileDescriptor + 1;

	[pool release];
}

- (void)_removeStream: (OFStream*)stream
	    withFDSet: (fd_set*)FDSet
	   otherFDSet: (fd_set*)otherFDSet
{
	int fileDescriptor = [stream fileDescriptor];

	FD_CLR(fileDescriptor, FDSet);

	if (!FD_ISSET(fileDescriptor, otherFDSet))
		FD_CLR(fileDescriptor, &exceptFDs);
}

- (void)_processQueue
{
	@synchronized (queue) {
		OFStream **queueCArray = [queue cArray];
		OFNumber **queueInfoCArray = [queueInfo cArray];
		size_t i, count = [queue count];

		for (i = 0; i < count; i++) {
			switch ([queueInfoCArray[i] intValue]) {
			case QUEUE_ADD | QUEUE_READ:
				[readStreams addObject: queueCArray[i]];

				[self _addStream: queueCArray[i]
				       withFDSet: &readFDs];

				break;
			case QUEUE_ADD | QUEUE_WRITE:
				[writeStreams addObject: queueCArray[i]];

				[self _addStream: queueCArray[i]
				       withFDSet: &writeFDs];

				break;
			case QUEUE_REMOVE | QUEUE_READ:
				[readStreams removeObjectIdenticalTo:
				    queueCArray[i]];

				[self _removeStream: queueCArray[i]
					  withFDSet: &readFDs
					 otherFDSet: &writeFDs];

				break;
			case QUEUE_REMOVE | QUEUE_WRITE:
				[writeStreams removeObjectIdenticalTo:
				    queueCArray[i]];

				[self _removeStream: queueCArray[i]
					  withFDSet: &writeFDs
					 otherFDSet: &readFDs];

				break;
			default:
				assert(0);
			}
		}

		[queue removeNObjects: count];
		[queueInfo removeNObjects: count];
	}
}

- (BOOL)observeWithTimeout: (int)timeout
{
	OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init];
	OFStream **cArray;
	fd_set readFDs_;
	fd_set writeFDs_;
	fd_set exceptFDs_;
	struct timeval time;
	size_t i, count;

	[self _processQueue];

	if ([self _processCache])
		return YES;

# ifdef FD_COPY
	FD_COPY(&readFDs, &readFDs_);
	FD_COPY(&writeFDs, &writeFDs_);
	FD_COPY(&exceptFDs, &exceptFDs_);
# else
	readFDs_ = readFDs;
	writeFDs_ = writeFDs;
	exceptFDs_ = exceptFDs;
# endif

	time.tv_sec = timeout / 1000;
	time.tv_usec = (timeout % 1000) * 1000;

	if (select(nFDs, &readFDs_, &writeFDs_, &exceptFDs_,
	    (timeout != -1 ? &time : NULL)) < 1)
		return NO;

	if (FD_ISSET(cancelFD[0], &readFDs_)) {
		char buffer;
#ifndef _WIN32
		assert(read(cancelFD[0], &buffer, 1) > 0);
#else
		assert(recvfrom(cancelFD[0], &buffer, 1, 0, NULL, NULL) > 0);
#endif
	}

	cArray = [readStreams cArray];
	count = [readStreams count];

	for (i = 0; i < count; i++) {
		int fileDescriptor = [cArray[i] fileDescriptor];

		if (FD_ISSET(fileDescriptor, &readFDs_)) {
			[delegate streamDidBecomeReadyForReading: cArray[i]];
			[pool releaseObjects];
		}

		if (FD_ISSET(fileDescriptor, &exceptFDs_)) {
			[delegate streamDidReceiveException: cArray[i]];
			[pool releaseObjects];

			/*
			 * Prevent calling it twice in case the FD is in both
			 * sets.
			 */
			FD_CLR(fileDescriptor, &exceptFDs_);
		}
	}

	cArray = [writeStreams cArray];
	count = [writeStreams count];

	for (i = 0; i < count; i++) {
		int fileDescriptor = [cArray[i] fileDescriptor];

		if (FD_ISSET(fileDescriptor, &writeFDs_)) {
			[delegate streamDidBecomeReadyForWriting: cArray[i]];
			[pool releaseObjects];
		}

		if (FD_ISSET(fileDescriptor, &exceptFDs_)) {
			[delegate streamDidReceiveException: cArray[i]];
			[pool releaseObjects];
		}
	}

	[pool release];

	return YES;
}
@end
