#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>

#include "fdlist.h"
#include "pidlist.h"

#define START	true
#define STOP	false

/* loglevel By default only notifies which processes are is starting and when
 * they close the output will print a dot. */
/* error are allways printed */
	
enum loglevels {FATAL, NORMAL, MISC, DEBUG};
int loglevel = NORMAL;
char *action = NULL;

int epollfd;

int pidnum=0;
pidlist_t pidlist;
bool sg_child_enable = false;
pidlist_t endedlist;

int fdnum=0;
fdlist_t fdlist;

int done[2];

void rcparlog(enum loglevels level, const char *format, ...) {
	va_list va_l;
	va_start(va_l, format);

	if (level == FATAL) 
		vfprintf(stderr, format, va_l);
	else if (loglevel >= level)
		vprintf(format, va_l);
	
	va_end(va_l);
}

void fdnode_cleanup (fdlist_node_t *fdnode)
{
	int n;
	struct epoll_event ev;

	rcparlog(DEBUG, "DEBUG: fdnode_cleanupfdnum fd: %d\n", fdnode->fd);

	memset(&ev,0,sizeof(struct epoll_event));
	ev.events=EPOLLIN;
	ev.data.fd = fdnode->fd;

	while ( ( n = read(fdnode->fd,
			fdnode->buffer + fdnode->n,
			fdnode->size - fdnode->n) ) > 0 ) {
		fdnode->n += n;
		if ( fdnode->n == fdnode->size ) {
			fdnode->buffer = realloc(fdnode->buffer, 2 * fdnode->size );
		}
	}

	rcparlog(DEBUG, "DEBUG: fdnode_cleanupfdnum fd: %d\n", fdnode->fd);

	epoll_ctl(epollfd, EPOLL_CTL_DEL, fdnode->fd, &ev);
	close(fdnode->fd);

	if ( fdnode->open ) {
		fdnode->open = false;
		fdnum--;
		rcparlog(DEBUG, "DEBUG: fdnum--: %d\n", fdnum);
	}
	rcparlog(DEBUG, "DEBUG: fdnode_cleanupfdnum fd: %d\n", fdnode->fd);
}

void pid_cleanup(pid_t pid, int status)
{
	pidlist_node_t *pidnode;
	
	rcparlog(DEBUG, "DEBUG: processing %d\n",pid);

	pidnode=pidlist_find(&pidlist, pid);
	if ( pidnode == NULL ) {
		rcparlog(FATAL, "\nERROR: trying to cleanup an unknown pid: %d\n", pid);
		return;
	}
	pidnode->status = status;

	/*
	 * process any remainning output and close the fds
	 */

	fdnode_cleanup( fdlist_find(&fdlist, pidnode->outfd) );
	fdnode_cleanup( fdlist_find(&fdlist, pidnode->errfd) );

	pidnum--;
	
	if ( pidnum == 0 ) {
		rcparlog(DEBUG, "DEBUG: Ok, I'm done ?\n");
		close(done[1]);
	}	
}

void sg_child(int signal, siginfo_t* info, void* data) {

	rcparlog(DEBUG, "DEBUG: signal caught for %d, enable %d ?\n",
			info->si_pid, sg_child_enable);

	if ( ! sg_child_enable ) {
		pidlist_node_t *pidnode;
		/* just add the pid and status to the endedlist to process it later */
		pidnode=pidlist_add(&endedlist,info->si_pid,"");
		pidnode->status = info->si_status;
		rcparlog(DEBUG, "DEBUG: Added %d to endedlist\n",info->si_pid);
		
		return;
	}
	
	pid_cleanup(info->si_pid, info->si_status);
}

void fd_setflag(int fd, int flag)
{
	int flags;
	flags = fcntl ( fd, F_GETFL );
	flags |= flag ;
	fcntl ( fd, F_SETFL, flags );
}

void launch_processes(int argc, char** argv,
		pidlist_t* pidlist, fdlist_t* fdlist, int epollfd)
{
	int infd;
	int outfd[2];
	int errfd[2];
	int i;
	pid_t pid;
	char *fnc, *filename;

	struct stat init_stat;
	struct epoll_event ev;
	struct sigaction ignore;
	
	ignore.sa_handler = SIG_IGN;
	sigemptyset(&ignore.sa_mask);
	ignore.sa_flags = 0;
	
	memset(&ev,0,sizeof(struct epoll_event));
	
	rcparlog(NORMAL, "Executing: ");
	if ( ( infd = open("/dev/null",	O_RDONLY) ) < 0 ) {
		rcparlog(FATAL,	"\nERROR: open failed, errno: %d, %s\n",
			errno, strerror(errno));
	}

	for ( i=0; i < argc ; i++) {
		if ( ! ( ( stat(argv[i], &init_stat) == 0 ) &&
			S_ISREG (init_stat.st_mode) &&
			( access(argv[i],X_OK) == 0  ) ) ) {
			rcparlog(FATAL, "\nERROR: %s FAILED, can't be executed\n",
				argv[i]);
			continue;
		}

		if ( ( lstat(argv[i], &init_stat) == 0 ) &&
				S_ISLNK(init_stat.st_mode) ) {
			fnc = malloc( (init_stat.st_size + 1) * sizeof(char) );
			readlink ( argv[i], fnc, init_stat.st_size );
			fnc[init_stat.st_size] = '\0';
		} else {
			fnc = strdup( argv[i] );
		}
		filename = basename(fnc);
		
		if ( ( pipe(outfd) != 0 ) || ( pipe(errfd) != 0 ) ) {
			rcparlog(FATAL,	"\nERROR: pipe failed, errno: %d, %s\n",
				errno, strerror(errno));
			continue;
		}
		fd_setflag(outfd[0],O_NONBLOCK);
		fd_setflag(errfd[0],O_NONBLOCK);

		pid=fork();
		if( pid < 0 ) {
			rcparlog(FATAL, "\nERROR: fork failed, errno: %d, %s\n",
				errno, strerror(errno));
		} else if ( pid==0 ) {
			/* child
			 */
			close(outfd[0]);
			close(errfd[0]);
			close(done[0]);
			close(done[1]);
			dup2(infd,0);
			dup2(outfd[1],1);
			dup2(errfd[1],2);
			sigaction(SIGCHLD,&ignore,NULL);
			
			if ( action!= NULL ) {
				execl(argv[i],filename, action, (char*) NULL);
			} else {
				execl(argv[i],filename, (char*) NULL);
			}
			rcparlog(FATAL,	"\nERROR: exec failed, errno: %d, %s\n",
				errno, strerror(errno) );
			exit(127);

		} else {
			/* parent
			 */
			pidlist_node_t* pidnode;
			fdlist_node_t *fdnode;

			ev.events=EPOLLIN;

			rcparlog(DEBUG, "DEBUG: adding pid: %d\n", pid);
			
			close(outfd[1]);
			close(errfd[1]);
			pidnode=pidlist_add(pidlist,pid,filename);
			pidnode->outfd=outfd[0];
			pidnode->errfd=errfd[0];
			pidnum++;

			fdnode=fdlist_add(fdlist, outfd[0], pid);
			fdnode->n=0;
			fdnode->size=1024;
			fdnode->buffer=malloc(fdnode->size);

			ev.data.fd=fdnode->fd;

			fdnum++;
			rcparlog(DEBUG, "DEBUG: fdnum++: %d\n", fdnum);

			rcparlog(DEBUG, "DEBUG: adding fd:%d\n", fdnode->fd);

			epoll_ctl(epollfd, EPOLL_CTL_ADD,
				fdnode->fd, &ev);

			fdnode=fdlist_add(fdlist, errfd[0], pid);
			fdnode->n=0;
			fdnode->size=1024;
			fdnode->buffer=malloc(fdnode->size);

			ev.data.fd=fdnode->fd;

			fdnode = NULL;

			fdnum++;
			rcparlog(DEBUG, "DEBUG: fdnum++: %d\n", fdnum);

			rcparlog(DEBUG, "DEBUG: adding fd:%d\n", ev.data.fd);

			epoll_ctl(epollfd, EPOLL_CTL_ADD,
				ev.data.fd, &ev);

			rcparlog(NORMAL, "%s (%d)",filename,pid);
			if ( i < argc-1 ) {
				rcparlog(NORMAL, ", ");
			}

		}
		free(fnc);
	}	
	fflush(stdout);
}

void get_outputs(pidlist_t* pidlist, fdlist_t* fdlist, int epollfd)
{
	int num, nfds;
	int i;
	fdlist_node_t* fdnode;
	pidlist_node_t* pidnode;

	struct epoll_event *events =
		malloc ( ( fdnum + 1)  * sizeof(struct epoll_event));

	while ( ( fdnum > 0 ) &&
		( nfds=epoll_wait(epollfd,events,fdnum + 1,-1) ) > 0 ) {
		
		for ( i=0; i < nfds; i++) {
			fdnode = fdlist_find(fdlist,events[i].data.fd);
			if ( fdnode == NULL ) {
				/* TODO:
				 * fix this.
				 */
				/* Instead of decreasing fdnum, I might need to do something
				 * safer, like, call wait(), and forget about outputs
				 */ 
				if ( events[i].data.fd != done[0] ) {
					rcparlog(FATAL,	
						"\nERROR: unknown fd %d\n", events[i].data.fd);
					fdnum--;
				} 
				/* Fail gratefully */
				epoll_ctl(epollfd, EPOLL_CTL_DEL, 
						events[i].data.fd, &events[i]);
				close(events[i].data.fd);
				continue;
			}

#ifdef DEBUG
			pidnode = pidlist_find(pidlist,fdnode->pid);
			if ( pidnode != NULL ) {
				rcparlog(FATAL,
					"DEBUG: fd %d, events %d, process %s\n",
					events[i].data.fd, events[i].events, pidnode->name);
			} else {
				rcparlog(FATAL,
					"DEBUG: unknown pid, fd %d, events %d\n",
					events[i].data.fd, events[i].events);
			}
#endif
				
			if ( events[i].events & EPOLLERR ) {
				pidnode = pidlist_find(pidlist,fdnode->pid);
				if ( pidnode != NULL ) {
					rcparlog(FATAL,
						"\nERROR: error on fd: %d of %s\n",
						events[i].data.fd, pidnode->name );
				} else {
					rcparlog(FATAL,
						"\nERROR: error on fd: %d, unknown process\n",
						events[i].data.fd );
				}
			}


			if ( events[i].events & EPOLLIN ) {
				if ( ( num = read( events[i].data.fd,
						fdnode->buffer + fdnode->n,
						fdnode->size - fdnode->n ) ) > 0 ) {

					fdnode->n += num;
					
					if ( fdnode->n == fdnode->size ) {
						fdnode->buffer = 
							realloc(fdnode->buffer, 2 * fdnode->size );
					}
				}
			}
			if ( events[i].events & EPOLLHUP ) {
				fdnode_cleanup(fdnode);
			}
		}
	}
}

void usage(char* name)
{
	printf("%s: starts/stops initscript in parallel\n", name);
	printf("\nUsage: %s ([-a action] [-q|-v] paths | [-h] )\n", name);
	printf("	-a action: arg to scripts\n");
	printf("	-k: stop\n");
	printf("	-q: quiet\n");
	printf("	-v: verbose\n");
	printf("	paths is the full path to the scripts start/stop\n");
	printf("	-h: this help\n");
}


/* executes several programs in parallel
 * stores programs stdout and stderr separetedly
 * when all childs have closed the fds, prints the outputs
 */

/*
 * uses two data structures
 * a sorted list of pids with the name, the fds of each child
 * a sorted list of fds, with the pid, and a buffer* with the data that has
 * been received from that fd
 */

int main ( int argc, char* argv[] ) {
	int i, c, args;
	int status = 0;
	
	struct sigaction sa_child;

	struct epoll_event ev;
	
	/* TODO: manage arguments
	 */
	while ( ( c = getopt(argc, argv,"a:eqv?h") ) != EOF  ) {
		switch (c) {
			case 'a': /* action */
				action=optarg;
				break;
			case 'q': /* quiet */
				loglevel--;
				break;
			case 'v': /* verbose */
				loglevel++;
				break;
			case '?':
			case 'h': /* help */
				usage(argv[0]);
				return 0;
				break;
		}
	}

	if ( optind >= argc ) {
		usage(argv[0]);
		return 0;
	}
	
	args = argc - optind;
	
	pidlist_new(&pidlist, args );
	pidlist_new(&endedlist, args );
	fdlist_new(&fdlist, 2 * args );
	epollfd = epoll_create( ( 2 *  args ) + 1 );
	
	if ( pipe(done) != 0 ) {
		rcparlog(FATAL,	"\nERROR: pipe failed, errno: %d, %s\n",
			errno, strerror(errno));
		return 126;
	}
	memset(&ev,0,sizeof(struct epoll_event));
	ev.events=EPOLLIN;
	ev.data.fd=done[0];
	epoll_ctl(epollfd, EPOLL_CTL_ADD, done[0], &ev);

	memset (&sa_child, 0, sizeof(struct sigaction));
	sa_child.sa_flags = SA_RESTART | SA_NOCLDWAIT | SA_NOCLDSTOP | SA_SIGINFO;
	
	/*
	 * sa_child.sa_flags = SA_NOCLDWAIT | SA_NOCLDSTOP | SA_SIGINFO;
	 */
	sa_child.sa_sigaction = &sg_child;

	if ( sigaction(SIGCHLD, &sa_child, NULL)  < 0 ) {
		rcparlog(FATAL,"\nERROR: sigaction failed\n");
	}

	rcparlog(DEBUG, "DEBUG: sg_child_enable: %d\n", sg_child_enable);

	launch_processes(args, argv + optind, &pidlist, &fdlist, epollfd);

	sg_child_enable = true;

	rcparlog(DEBUG, "DEBUG: sg_child_enable: %d\n", sg_child_enable);

	for (i = 0; i < endedlist.num; i++) {
		rcparlog(DEBUG, "DEBUG: process endedlist %d, pid %d\n", i,
				(endedlist.list + i)->pid);
		pid_cleanup( (endedlist.list + i)->pid, (endedlist.list + i)->status );
	}

	get_outputs(&pidlist, &fdlist, epollfd);

	rcparlog(NORMAL, ".\n");

	for ( i=0; i < pidlist.num ; i++) {
		fdlist_node_t *fdnode;
		pidlist_node_t *pidnode = pidlist.list + i;

		if ( pidnode->status != 0 ) {
			rcparlog(MISC, "%s (%d): FAILED\n", pidnode->name, pidnode->pid);
			status++;
		}

		fdnode = fdlist_find(&fdlist,pidnode->outfd);
		if ( ( ( loglevel > NORMAL ) || ( pidnode->status != 0 ) ) &&
				fdnode->n > 0 ) {
			rcparlog(MISC, "%s (%d) output:\n", pidnode->name,pidnode->pid);
			write(1,fdnode->buffer,fdnode->n);
		}
		fdnode = fdlist_find(&fdlist,pidnode->errfd);
		if ( ( loglevel || ( pidnode->status != 0 ) ) && 
				fdnode->n > 0 ) {
			rcparlog(NORMAL, "%s (%d) error:\n", pidnode->name,pidnode->pid);
			write(2,fdnode->buffer,fdnode->n);
		}
	}

	/* the kernel takes care of this
	pidlist_destroy(&pidlist);
	fdlist_destroy(&fdlist);
	*/
	return status;
}

