/* sys.c, liboop, copyright 1999 Dan Egnor

   This is free software; you can redistribute it and/or modify it under the
   terms of the GNU Lesser General Public License, version 2.1 or later.
   See the file COPYING for details. */

#include "oop.h"

#include <errno.h>
#include <assert.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>

#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif

#define MAGIC 0x9643

struct sys_time {
	struct sys_time *next;
	struct timeval tv;
	oop_call_time *f;
	void *v;
};

struct sys_signal_handler {
	struct sys_signal_handler *next;
	oop_call_signal *f;
	void *v;
};

struct sys_signal {
	struct sys_signal_handler *list,*ptr;
	struct sigaction old;
	int active;
};

struct sys_file_handler {
	oop_call_fd *f;
	void *v;
};

typedef struct sys_file_handler sys_file[OOP_NUM_EVENTS];

struct oop_source_sys {
	oop_source oop;
	int magic;
	int in_run;
	int num_events;

	/* Timeout queue */
	struct sys_time *time_queue,*time_run;

	/* Signal handling */
	struct sys_signal sig[OOP_NUM_SIGNALS];
	sigjmp_buf env;
	int do_jmp,sig_active;

	/* File descriptors */
	int num_files;
	sys_file *files;
};

struct oop_source_sys *sys_sig_owner[OOP_NUM_SIGNALS];

static oop_source_sys *verify_source(oop_source *source) {
	oop_source_sys *sys = (oop_source_sys *) source;
	assert(MAGIC == sys->magic && "corrupt oop_source structure");
	return sys;
}

static void sys_on_fd(oop_source *source,int fd,oop_event ev,
                      oop_call_fd *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	if (fd >= sys->num_files) {
		int i,j,num_files = 1 + fd;
		sys_file *files = oop_malloc(num_files * sizeof(sys_file));
		if (NULL == files) return; /* ugh */
		memcpy(files,sys->files,sizeof(sys_file) * sys->num_files);
		for (i = sys->num_files; i < num_files; ++i)
			for (j = 0; j < OOP_NUM_EVENTS; ++j)
				files[i][j].f = NULL;

		oop_free(sys->files);
		sys->files = files;
		sys->num_files = num_files;
	}

	assert(NULL == sys->files[fd][ev].f && "multiple handlers registered for a file event");
	sys->files[fd][ev].f = f;
	sys->files[fd][ev].v = v;
	++sys->num_events;
}

static void sys_cancel_fd(oop_source *source,int fd,oop_event ev) {
	oop_source_sys *sys = verify_source(source);
	if (fd < sys->num_files && NULL != sys->files[fd][ev].f) {
		sys->files[fd][ev].f = NULL;
		sys->files[fd][ev].v = NULL;
		--sys->num_events;
	}
}

static void sys_on_time(oop_source *source,struct timeval tv,
                        oop_call_time *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	struct sys_time **p = &sys->time_queue;
	struct sys_time *time = oop_malloc(sizeof(struct sys_time));
	if (NULL == time) return; /* ugh */
	time->tv = tv;
	time->f = f;
	time->v = v;

	while (NULL != *p
	&&    ((*p)->tv.tv_sec < tv.tv_sec
	||    ((*p)->tv.tv_sec == tv.tv_sec 
	&&     (*p)->tv.tv_usec <= tv.tv_usec))) p = &(*p)->next;
	time->next = *p;
	*p = time;

	++sys->num_events;
}

static int sys_remove_time(oop_source_sys *sys,
                           struct sys_time **p,struct timeval tv,
                           oop_call_time *f,void *v) {
	while (NULL != *p
	&&    ((*p)->tv.tv_sec < tv.tv_sec
	||    ((*p)->tv.tv_sec == tv.tv_sec 
	&&     (*p)->tv.tv_usec < tv.tv_usec))) p = &(*p)->next;
	while (NULL != *p
	&&     (*p)->tv.tv_sec == tv.tv_sec
	&&     (*p)->tv.tv_usec == tv.tv_usec
	&&    ((*p)->f != f || (*p)->v != v)) p = &(*p)->next;
	if (NULL != *p 
	&& (*p)->tv.tv_sec == tv.tv_sec && (*p)->tv.tv_usec == tv.tv_usec) {
		struct sys_time *time = *p;
		assert(f == time->f);
		assert(v == time->v);
		*p = time->next;
		oop_free(time);
		--sys->num_events;
		return 1;
	}
	return 0;
}

static void sys_cancel_time(oop_source *source,struct timeval tv,
                            oop_call_time *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	if (!sys_remove_time(sys,&sys->time_run,tv,f,v))
		sys_remove_time(sys,&sys->time_queue,tv,f,v);
}

static void sys_signal_handler(int sig) {
	oop_source_sys *sys = sys_sig_owner[sig];
	struct sigaction act;
	assert(NULL != sys);

	/* Reset the handler, in case this is needed. */
	sigaction(sig,NULL,&act);
	act.sa_handler = sys_signal_handler;
	sigaction(sig,&act,NULL);

	assert(NULL != sys->sig[sig].list);
	sys->sig[sig].active = 1;
	sys->sig_active = 1;

	/* Break out of select() loop, if necessary. */
	if (sys->do_jmp) siglongjmp(sys->env,1);
}

static void sys_on_signal(oop_source *source,int sig,
                          oop_call_signal *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	struct sys_signal_handler *handler = oop_malloc(sizeof(*handler));
	if (NULL == handler) return; /* ugh */

	assert(sig > 0 && sig < OOP_NUM_SIGNALS && "invalid signal number");

	handler->f = f;
	handler->v = v;
	handler->next = sys->sig[sig].list;
	sys->sig[sig].list = handler;
	++sys->num_events;

	if (NULL == handler->next) {
		struct sigaction act;

		assert(NULL == sys_sig_owner[sig]);
		sys_sig_owner[sig] = sys;

		assert(0 == sys->sig[sig].active);
		sigaction(sig,NULL,&act);
		sys->sig[sig].old = act;
		act.sa_handler = sys_signal_handler;
#ifdef SA_NODEFER /* BSD/OS doesn't have this, for one. */
		act.sa_flags &= ~SA_NODEFER;
#endif
		sigaction(sig,&act,NULL);
	}
}

static void sys_cancel_signal(oop_source *source,int sig,
                              oop_call_signal *f,void *v) {
	oop_source_sys *sys = verify_source(source);
	struct sys_signal_handler **pp = &sys->sig[sig].list;

	assert(sig > 0 && sig < OOP_NUM_SIGNALS && "invalid signal number");

	while (NULL != *pp && ((*pp)->f != f || (*pp)->v != v))
		pp = &(*pp)->next;

	if (NULL != *pp) {
		struct sys_signal_handler *p = *pp;

		if (NULL == p->next && &sys->sig[sig].list == pp) {
			sigaction(sig,&sys->sig[sig].old,NULL);
			sys->sig[sig].active = 0;
			sys_sig_owner[sig] = NULL;
		}

		*pp = p->next;
		if (sys->sig[sig].ptr == p) sys->sig[sig].ptr = *pp;
		--sys->num_events;
		oop_free(p);
	}
}

oop_source_sys *oop_sys_new(void) {
	oop_source_sys *source = oop_malloc(sizeof(oop_source_sys));
	int i;
	if (NULL == source) return NULL;
	source->oop.on_fd = sys_on_fd;
	source->oop.cancel_fd = sys_cancel_fd;
	source->oop.on_time = sys_on_time;
	source->oop.cancel_time = sys_cancel_time;
	source->oop.on_signal = sys_on_signal;
	source->oop.cancel_signal = sys_cancel_signal;
	source->magic = MAGIC;
	source->in_run = 0;
	source->num_events = 0;
	source->time_queue = source->time_run = NULL;

	source->do_jmp = 0;
	source->sig_active = 0;
	for (i = 0; i < OOP_NUM_SIGNALS; ++i) {
		source->sig[i].list = NULL;
		source->sig[i].ptr = NULL;
		source->sig[i].active = 0;
	}

	source->num_files = 0;
	source->files = NULL;

	return source;
}

static void *sys_time_run(oop_source_sys *sys) {
	void *ret = OOP_CONTINUE;
	while (OOP_CONTINUE == ret && NULL != sys->time_run) {
		struct sys_time *p = sys->time_run;
		sys->time_run = sys->time_run->next;
		--sys->num_events;
		ret = p->f(&sys->oop,p->tv,p->v); /* reenter! */
		oop_free(p);
	}
	return ret;
}

void *oop_sys_run(oop_source_sys *sys) {
	void * volatile ret = OOP_CONTINUE;
	assert(!sys->in_run && "oop_sys_run is not reentrant");
	sys->in_run = 1;

	while (0 != sys->num_events && OOP_CONTINUE == ret) {
		struct timeval * volatile ptv = NULL;
		struct timeval tv;
		fd_set rfd,wfd,xfd;
		int i,rv;

		if (NULL != sys->time_run) {
			/* interrupted, restart */
			ptv = &tv;
			tv.tv_sec = 0;
			tv.tv_usec = 0;
		} else if (NULL != sys->time_queue) {
			ptv = &tv;
			gettimeofday(ptv,NULL);
			if (sys->time_queue->tv.tv_usec < tv.tv_usec) {
				tv.tv_usec -= 1000000;
				tv.tv_sec ++;
			}
			tv.tv_sec = sys->time_queue->tv.tv_sec - tv.tv_sec;
			tv.tv_usec = sys->time_queue->tv.tv_usec - tv.tv_usec;
			if (tv.tv_sec < 0) {
				tv.tv_sec = 0;
				tv.tv_usec = 0;
			}
		}

		if (!sys->sig_active) sys->do_jmp = !sigsetjmp(sys->env,1);
		if (sys->sig_active) {
			/* Still perform select(), but don't block. */
			ptv = &tv;
			tv.tv_sec = 0;
			tv.tv_usec = 0;
		}

		FD_ZERO(&rfd);
		FD_ZERO(&wfd);
		FD_ZERO(&xfd);
		for (i = 0; i < sys->num_files; ++i) {
			if (NULL != sys->files[i][OOP_READ].f) FD_SET(i,&rfd);
			if (NULL != sys->files[i][OOP_WRITE].f) FD_SET(i,&wfd);
			if (NULL != sys->files[i][OOP_EXCEPTION].f) FD_SET(i,&xfd);
		}

		do
			rv = select(sys->num_files,&rfd,&wfd,&xfd,ptv);
		while (0 > rv && EINTR == errno);

		sys->do_jmp = 0;

		if (0 > rv) { /* Error in select(). */
			ret = OOP_ERROR;
			break; 
		}

		if (sys->sig_active) {
			sys->sig_active = 0;
			for (i = 0; OOP_CONTINUE == ret && i < OOP_NUM_SIGNALS; ++i) {
				if (sys->sig[i].active) {
					sys->sig[i].active = 0;
					sys->sig[i].ptr = sys->sig[i].list;
				}
				while (OOP_CONTINUE == ret && NULL != sys->sig[i].ptr) {
					struct sys_signal_handler *h;
					h = sys->sig[i].ptr;
					sys->sig[i].ptr = h->next;
					ret = h->f(&sys->oop,i,h->v);
				}
			}
			if (OOP_CONTINUE != ret) {
				sys->sig_active = 1; /* come back */
				break;
			}
		}

		if (0 < rv) {
			for (i = 0; i < sys->num_files && OOP_CONTINUE == ret; ++i)
				if (FD_ISSET(i,&xfd) && NULL != sys->files[i][OOP_EXCEPTION].f)
					ret = sys->files[i][OOP_EXCEPTION].f(&sys->oop,i,OOP_EXCEPTION,
					                          sys->files[i][OOP_EXCEPTION].v);
			for (i = 0; i < sys->num_files && OOP_CONTINUE == ret; ++i)
				if (FD_ISSET(i,&wfd) && NULL != sys->files[i][OOP_WRITE].f)
					ret = sys->files[i][OOP_WRITE].f(&sys->oop,i,OOP_WRITE,
					                           sys->files[i][OOP_WRITE].v);
			for (i = 0; i < sys->num_files && OOP_CONTINUE == ret; ++i)
				if (FD_ISSET(i,&rfd) && NULL != sys->files[i][OOP_READ].f)
					ret = sys->files[i][OOP_READ].f(&sys->oop,i,OOP_READ,
					                          sys->files[i][OOP_READ].v);
			if (OOP_CONTINUE != ret) break;
		}

		/* Catch any leftover timeout events. */
		ret = sys_time_run(sys);
		if (OOP_CONTINUE != ret) break;

		if (NULL != sys->time_queue) {
			struct sys_time *p,**pp = &sys->time_queue;
			gettimeofday(&tv,NULL);
			while (NULL != *pp 
			   && (tv.tv_sec > (*pp)->tv.tv_sec
			   || (tv.tv_sec == (*pp)->tv.tv_sec
			   &&  tv.tv_usec >= (*pp)->tv.tv_usec)))
				pp = &(*pp)->next;
			p = *pp;
			*pp = NULL;
			sys->time_run = sys->time_queue;
			sys->time_queue = p;
		}

		ret = sys_time_run(sys);
	}

	sys->in_run = 0;
	return ret;
}

void oop_sys_delete(oop_source_sys *sys) {
	int i,j;

	assert(!sys->in_run && "cannot delete while in oop_sys_run");
	assert(NULL == sys->time_queue 
	&&     NULL == sys->time_run
	&&     "cannot delete with timeout");

	for (i = 0; i < OOP_NUM_SIGNALS; ++i)
		assert(NULL == sys->sig[i].list && "cannot delete with signal handler");

	for (i = 0; i < sys->num_files; ++i)
		for (j = 0; j < OOP_NUM_EVENTS; ++j)
			assert(NULL == sys->files[i][j].f && "cannot delete with file handler");

	assert(0 == sys->num_events);
	oop_free(sys);
}

oop_source *oop_sys_source(oop_source_sys *sys) {
	assert(&sys->oop == (oop_source *) sys);
	return &sys->oop;
}
