/***************************************************************************

  gbx_signal.c

  (c) 2000-2017 Benoît Minisini <g4mba5@gmail.com>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2, or (at your option)
  any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA.

***************************************************************************/

#define __GBX_SIGNAL_C

#include "gb_alloc.h"
#include "gb_error.h"
#include "gb_array.h"
#include "gbx_api.h"
#include "gbx_signal.h"

//#define DEBUG_ME 1

static SIGNAL_HANDLER *_handlers = NULL;
static int _pipe[2] = { -1, -1 };
static volatile int _count = 0;
static int _raising_callback = 0;

void SIGNAL_install(SIGNAL_HANDLER *handler, int signum, void (*callback)(int, siginfo_t *, void *))
{
	struct sigaction action;
	
	#if DEBUG_ME
	fprintf(stderr, "SIGNAL_install: %d %p\n", signum, callback);
	#endif
	
	handler->signum = signum;
	
	action.sa_flags = SA_SIGINFO;
	sigemptyset(&action.sa_mask);
	action.sa_sigaction = callback;

	if (sigaction(signum, NULL, &handler->old_action) != 0 || sigaction(signum, &action, NULL) != 0)
		ERROR_panic("Cannot install signal handler: %s", strerror(errno));
}

void SIGNAL_uninstall(SIGNAL_HANDLER *handler, int signum)
{
	#if DEBUG_ME
	fprintf(stderr, "SIGNAL_uninstall: %d\n", signum);
	#endif
	
	if (sigaction(signum, &handler->old_action, NULL) != 0)
		ERROR_panic("Cannot uninstall signal handler");

	while (handler->callbacks)
		SIGNAL_unregister(handler->signum, handler->callbacks);
}

void SIGNAL_previous(SIGNAL_HANDLER *handler, int signum, siginfo_t *info, void *context)
{
	if (handler->old_action.sa_handler != SIG_DFL && handler->old_action.sa_handler != SIG_IGN)
	{
		if (handler->old_action.sa_flags & SA_SIGINFO)
		{
			//fprintf(stderr, "Calling old action %p\n", _old_SIGCHLD_action.sa_sigaction);
			(*handler->old_action.sa_sigaction)(signum, info, context);
		}
		else
		{
			//fprintf(stderr, "Calling old handler %p\n", _old_SIGCHLD_action.sa_handler);
			(*handler->old_action.sa_handler)(signum);
		}
	}
}

static SIGNAL_HANDLER *find_handler(int signum)
{
	int i;
	
	for (i = 0; i < ARRAY_count(_handlers); i++)
	{
		if (_handlers[i].signum == signum)
			return &_handlers[i];
	}
	
	return NULL;
}

static SIGNAL_HANDLER *add_handler(void)
{
	if (!_handlers)
		ARRAY_create_inc(&_handlers, 1);
	
	return ARRAY_add_void(&_handlers);
}

static void handle_signal(int signum, siginfo_t *info, void *context)
{
	char buffer;
	int save_errno;

	save_errno = errno;
	
	if (_count)
	{
		buffer = signum;
		for(;;)
		{
			if (write(_pipe[1], &buffer, 1) == 1)
				break;
			
			if (errno != EINTR)
			{
				ERROR_warning("cannot write signal #%d into signal pipe: %s", signum, strerror(errno));
				break;
			}
		}
	}
	
	SIGNAL_previous(find_handler(signum), signum, info, context);
	
	errno = save_errno;
}

static bool _must_purge_callbacks = FALSE;
static int _purge_signum;
static SIGNAL_HANDLER *_purge_handler;

static void purge_callbacks(void)
{
	SIGNAL_CALLBACK *cb, *next_cb;

	_raising_callback--;
	if (_raising_callback)
		return;
	
	#if DEBUG_ME
	fprintf(stderr, ">> purge_callbacks\n");
	#endif

	while (_must_purge_callbacks)
	{
		_must_purge_callbacks = FALSE;

		cb = _purge_handler->callbacks;
		while (cb)
		{
			#if DEBUG_ME
			fprintf(stderr, "purge_callbacks: cb = %p\n", cb);
			#endif
			next_cb = cb->next;

			if (!cb->callback)
				SIGNAL_unregister(_purge_signum, cb);
			
			cb = next_cb;
		}
	}

	#if DEBUG_ME
	fprintf(stderr, "<< purge_callbacks\n");
	#endif
}

void SIGNAL_raise_callbacks(int fd, int type, void *data)
{
	SIGNAL_HANDLER *handler;
	SIGNAL_CALLBACK *cb;
	char signum;
	int ret;

	/*old = signal(SIGCHLD, signal_child);*/
	#if DEBUG_ME
	fprintf(stderr, "SIGNAL_raise_callbacks: fd = %d blocking = %d\n", fd, (fcntl(fd, F_GETFL) & O_NONBLOCK) == 0);
	#endif

	for(;;)
	{
		ret = read(fd, &signum, 1);
		if (ret != 1)
		{
			#if DEBUG_ME
			fprintf(stderr, "SIGNAL_raise_callbacks: read -> %d / errno = %d\n", ret, errno);
			#endif
			return;
		}
		
		#if DEBUG_ME
		fprintf(stderr, "SIGNAL_raise_callbacks: signum = %d\n", signum);
		#endif

		handler = find_handler(signum);
		if (!handler)
		{
			#if DEBUG_ME
			fprintf(stderr, "SIGNAL_raise_callbacks: no handler\n");
			#endif
			return;
		}
		
		#if DEBUG_ME
		fprintf(stderr, ">> SIGNAL_raise_callbacks (%d)\n", _raising_callback);
		#endif

		_raising_callback++;
		_purge_signum = signum;
		_purge_handler = handler;
		
		ON_ERROR(purge_callbacks)
		{
			cb = handler->callbacks;
			while (cb)
			{
				#if DEBUG_ME
				fprintf(stderr, "SIGNAL_raise_callbacks: cb = %p cb->callback = %p\n", cb, cb->callback);
				#endif
				if (cb->callback)
					(*cb->callback)((int)signum, cb->data);
				
				cb = cb->next;
			}
		}
		END_ERROR
		
		#if DEBUG_ME
		fprintf(stderr, "SIGNAL_raise_callbacks: purge_callbacks\n");
		#endif
		purge_callbacks();

		#if DEBUG_ME
		fprintf(stderr, "<< SIGNAL_raise_callbacks (%d)\n", _raising_callback);
		#endif
	}
}

static void create_pipe(void)
{
	if (pipe(_pipe) != 0)
		ERROR_panic("Cannot create signal handler pipes: %s", strerror(errno));

	if (_pipe[0] == 0)
		BREAKPOINT();
	
	fcntl(_pipe[0], F_SETFD, FD_CLOEXEC);
	fcntl(_pipe[1], F_SETFD, FD_CLOEXEC);
	// Allows to read the signal pipe without blocking
	fcntl(_pipe[0], F_SETFL, fcntl(_pipe[0], F_GETFL) | O_NONBLOCK);

	GB_Watch(_pipe[0], GB_WATCH_READ, (void *)SIGNAL_raise_callbacks, 0);
	
	#if DEBUG_ME
	fprintf(stderr, "create_pipe: fd = %d\n", _pipe[0]);
	#endif
}

static void delete_pipe(void)
{
	#if DEBUG_ME
	fprintf(stderr, "delete_pipe: fd = %d\n", _pipe[0]);
	#endif
	GB_Watch(_pipe[0], GB_WATCH_NONE, NULL, 0);
	close(_pipe[0]);
	close(_pipe[1]);
	_pipe[0] = -1;
	_pipe[1] = -1;
}

SIGNAL_CALLBACK *SIGNAL_register(int signum, void (*callback)(int, intptr_t), intptr_t data)
{
	SIGNAL_HANDLER *handler;
	SIGNAL_CALLBACK *cb;
	
	if (!_count)
		create_pipe();
	
	_count++;
	
	handler = find_handler(signum);
	if (!handler)
	{
		handler = add_handler();
		SIGNAL_install(handler, signum, handle_signal);
	}
	
	ALLOC(&cb, sizeof(SIGNAL_CALLBACK));
	
	cb->prev = NULL;
	cb->next = handler->callbacks;
	cb->callback = callback;
	cb->data = data;
	
	if (cb->next)
		cb->next->prev = cb;
	handler->callbacks = cb;

	#if DEBUG_ME
	fprintf(stderr, "SIGNAL_register: %d -> %p (%p)\n", signum, cb, cb->callback);
	#endif
	
	#if DEBUG_ME
	fprintf(stderr, "handler->callbacks %p:", handler);
	SIGNAL_CALLBACK *save = cb;
	cb = handler->callbacks;
	while (cb)
	{
		fprintf(stderr, " -> %p (%p)", cb, cb->callback);
		cb = cb->next;
	}
	fprintf(stderr, "\n");
	cb = save;
	#endif

	return cb;
}

void SIGNAL_unregister(int signum, SIGNAL_CALLBACK *cb)
{
	SIGNAL_HANDLER *handler = find_handler(signum);
	
	if (!handler)
		return;
	
	if (_raising_callback)
	{
		#if DEBUG_ME
		fprintf(stderr, "SIGNAL_unregister: disable %d %p (%p)\n", signum, cb, cb->callback);
		#endif
		cb->callback = NULL;
		_must_purge_callbacks = TRUE;
		return;
	}
	
	#if DEBUG_ME
	fprintf(stderr, "SIGNAL_unregister: remove %d %p (%p)\n", signum, cb, cb->callback);
	#endif
	
	if (cb->prev)
		cb->prev->next = cb->next;
	
	if (cb->next)
		cb->next->prev = cb->prev;
	
	if (cb == handler->callbacks)
		handler->callbacks = cb->next;
	
	IFREE(cb);
	
	_count--;
	
	if (_count == 0)
		delete_pipe();
	
	#if DEBUG_ME
	fprintf(stderr, "handler->callbacks %p:", handler);
	cb = handler->callbacks;
	while (cb)
	{
		fprintf(stderr, " -> %p (%p)", cb, cb->callback);
		cb = cb->next;
	}
	fprintf(stderr, "\n");
	#endif
}

void SIGNAL_exit(void)
{
	int i;
	SIGNAL_HANDLER *handler;
	
	if (_handlers)
	{
		_raising_callback = 0;
		for (i = 0; i < ARRAY_count(_handlers); i++)
		{
			handler = &_handlers[i];
			SIGNAL_uninstall(handler, handler->signum);
		}
		
		ARRAY_delete(&_handlers);
	}
}

int SIGNAL_get_fd(void)
{
	return _pipe[0];
}

void SIGNAL_has_forked(void)
{
	if (!_count)
		return;
	
	GB_Watch(_pipe[0], GB_WATCH_NONE, NULL, 0);
	close(_pipe[0]);
	close(_pipe[1]);
	create_pipe();
}
