/*
 *  Copyright (C) 2006-2019, Thomas Maier-Komor
 *
 *  This is the source code of xjobs.
 *
 *  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 of the License, 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, see <http://www.gnu.org/licenses/>.
 */

#include <errno.h>
#include <limits.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "config.h"
#include "colortty.h"
#include "log.h"
#include "settings.h"
#include "support.h"
#include "version.h"

#ifndef SIGPOLL
#define SIGPOLL SIGIO
#endif

#ifdef __sun
#include <sys/stropts.h>
#endif

#ifdef __OpenBSD__
#include <sys/param.h>
#include <sys/sysctl.h>
#endif

/* SUSv3 does not have PATH_MAX */
#ifndef PATH_MAX
#define PATH_MAX _XOPEN_PATH_MAX
#endif

extern int yylex(void);

int (*gettoken)(void) = yylex;

flag_t ShowPID = flag_on, Echo = flag_on;
int Stdout = -1, Stderr = -1, Stdin = -1, Prompt = 0, ExitOnError = 0, RsrcUsage = 1
	, Input = STDIN_FILENO, InFlags = 0;
unsigned QLen = 0, Lines = 1;
long Limit = 0, Pagesize;
const char *Script = 0;
char *Path = 0;

RETSIGTYPE processSignal(int sig);


static flag_t parse_flag(const char *arg)
{
	if (0 == strcasecmp(arg,"true"))
		return flag_on;
	if (0 == strcasecmp(arg,"yes"))
		return flag_on;
	if (0 == strcasecmp(arg,"on"))
		return flag_on;
	if (0 == strcmp(arg,"1"))
		return flag_on;
	if (0 == strcasecmp(arg,"false"))
		return flag_off;
	if (0 == strcasecmp(arg,"no"))
		return flag_off;
	if (0 == strcasecmp(arg,"off"))
		return flag_off;
	if (0 == strcmp(arg,"0"))
		return flag_off;
	return flag_invalid;
}


static void set_color_mode(const char *m)
{
	if (m == 0)
		TtyMode = tty_auto;
	else if (!strcasecmp(m,"auto"))
		TtyMode = tty_auto;
	else if (!strcasecmp(m,"pipe"))
		TtyMode = tty_pipe;
	else if (!strcasecmp(m,"ansi"))
		TtyMode = tty_ansi;
	else if (!strcasecmp(m,"off"))
		TtyMode = tty_none;
	else if (!strcasecmp(m,"none"))
		TtyMode = tty_none;
	else {
		warn("invalid argument for color mode: '%s'; disabling color support\n",m);
		TtyMode = tty_none;
		return;
	}
	dbug("color mode set to '%s'\n",m);
}


void parse_option_j(const char *arg)
{
	double lf;
	char f;
	switch (sscanf(arg,"%lg%c",&lf,&f)) {
	case 2:
		if (f == 'x') {
#ifdef _SC_NPROCESSORS_ONLN
			lf *= sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(_SC_NPROCESSORS_CONF)
			lf *= sysconf(_SC_NPROCESSORS_CONF);
#elif defined(__OpenBSD__)
			int mib[2], nproc;
			size_t len = sizeof(nproc);
			mib[0] = CTL_HW;
			mib[1] = HW_NCPU;
			if (-1 == sysctl(mib,2,&nproc,&len,0,0)) {
				warn("unable to determine number of processors: %s\nassuming 1 processors\n",strerror(errno));
				nproc = 1;
			}
			lf *= nproc;
#else
			warn("unable to determine number of processors, assuming 1\n");
			lf *= 1;
#endif
		} else
			error("invalid suffix '%c' in argument for job limit setting\n",f);
		/*FALLTHROUGH*/
	case 1:
		Limit = (long)ceil(lf);
		if (Limit <= 0)
			error("invalid argument for option -j\n");
		dbug("maximum number of jobs set to %d\n",Limit);
		break;
	default:
		error("missing argument to option -j\n");
	}
}


void read_config(const char *cf)
{
	struct stat st;
	dbug("looking for config file %s\n",cf);
	int fd = open(cf,O_RDONLY);
	if (fd == -1) {
		if (errno == ENOENT)
			dbug("no config file %s\n",cf);
		else
			warn("unable to open config file %s: %s\n",cf,strerror(errno));
		return;
	}
	if (-1 == fstat(fd,&st)) {
		close(fd);
		warn("unable to stat config file %s: %s\n",cf,strerror(errno));
		return;
	}
	if (st.st_uid && (getuid() != st.st_uid)) {
		close(fd);
		warn("ignoring config file %s from different user\n",cf);
		return;
	}
	if (st.st_size == 0) {
		close(fd);
		dbug("ignoring empty config file %s\n",cf);
		return;
	}
	char *buf = Malloc(st.st_size);
	if (-1 == read(fd,buf,st.st_size)) {
		close(fd);
		free(buf);
		warn("unable to read config file %s: %s\n",cf,strerror(errno));
		return;
	}
	close(fd);
	dbug("parsing config file %s\n",cf);
	char *line = buf;
	while (line && (line-buf < st.st_size)) {
		char key[64],value[256];
		char *nl = strchr(line,'\n');
		if (nl) {
			*nl = 0;
			++nl;
		}
		char *pound = strchr(line,'#');
		if (pound)
			*pound = 0;
		while ((*line == ' ') || (*line == '\t'))
			++line;
		if (2 != sscanf(line,"%63[A-Za-z_.]%*[ \t=:]%255[0-9A-Za-z _/:$()-]",key,value)) {
			line = nl;
			continue;
		}
		dbug("parsing key/value pair %s=%s\n",key,value);
		char *valuestr = value;
		if (strchr(value,'$')) {
			dbug("resolving %s\n",value);
			valuestr = resolve_env(value);
		}

		if (strcasecmp(key,"showpid") == 0) {
			flag_t f = parse_flag(valuestr);
			if (f != flag_invalid)
				ShowPID = f;
			else
				warn("ignoring invalid argument for key %s: %s\n",key,valuestr);
		} else if (strcasecmp(key,"path") == 0) {
			Path = valuestr;
			dbug("PATH set to %s\n",Path);
			line = nl;
			continue;	// needed so that valuestr will not be free()'ed
		} else if (strcasecmp(key,"echo") == 0) {
			flag_t f = parse_flag(valuestr);
			if (f != flag_invalid)
				Echo = f;
			else
				warn("ignoring invalid argument for key %s: %s\n",key,valuestr);
		} else if (strcasecmp(key,"color.mode") == 0) {
			set_color_mode(valuestr);
		} else if (strcasecmp(key,"color.fail") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorFail = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"color.done") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorDone = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"color.debug") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorDebug = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"color.info") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorInfo = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"color.warn") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorWarn = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"color.out") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorOut = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"color.error") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorError = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"color.start") == 0) {
			color_t c = str2color(valuestr);
			if (c != invalid_color)
				ColorStart = c;
			else
				warn("ignoring invalid color %s\n",valuestr);
		} else if (strcasecmp(key,"jobs") == 0) {
			parse_option_j(valuestr);
		} else if (strcasecmp(key,"verbose") == 0) {
			if ((valuestr[1] == 0) && (valuestr[0] >= '0') && (valuestr[0] <= '5'))
				Verbose = (verbose_t)(valuestr[0] - '0');
			else if (strcasecmp(valuestr,"silent") == 0)
				Verbose = Silent;
			else if (strcasecmp(valuestr,"error") == 0)
				Verbose = Error;
			else if (strcasecmp(valuestr,"warning") == 0)
				Verbose = Warning;
			else if (strcasecmp(valuestr,"status") == 0)
				Verbose = Status;
			else if (strcasecmp(valuestr,"info") == 0)
				Verbose = Info;
			else if (strcasecmp(valuestr,"debug") == 0)
				Verbose = Debug;
			else
				warn("invalid argument '%s' to setting 'verbose'\n",valuestr);
		} else {
			warn("unknown key %s\n",key);
		}
		if (valuestr != value)
			free(valuestr);
		line = nl;
	}
}


void init_limit(void)
{
#ifdef _SC_NPROCESSORS_ONLN
	Limit = sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(_SC_NPROCESSORS_CONF)
	Limit = sysconf(_SC_NPROCESSORS_CONF);
#elif defined(__OpenBSD__)
	int mib[2], nproc;
	size_t len = sizeof(nproc);
	mib[0] = CTL_HW;
	mib[1] = HW_NCPU;
	if (-1 == sysctl(mib,2,&nproc,&len,0,0)) {
		warn("unable to determine number of processors: %s\nassuming 1 processor\n",strerror(errno));
		nproc = 1;
	}
	Limit = nproc;
#else
	warn("unable to determine number of available processors - assuming 1 processor\n");
	Limit = 1;
#endif
	dbug("%d processors currently online (default job limit)\n",Limit);
}


void init_defaults(const char *exe)
{
	long ps = sysconf(_SC_PAGESIZE);
	if (ps == -1) {
		warn("unable to determine page size: %s\n",strerror(errno));
		Pagesize = 4096;
	} else
		Pagesize = ps;
	char cfname[PATH_MAX+1];
	if (0 != getcwd(cfname,sizeof(cfname))) {
		size_t cl = strlen(cfname);
		assert(cl < sizeof(cfname)-1);
		if (cfname[cl-1] != '/') {
			cfname[cl++] = '/';
			cfname[cl] = 0;
		}
		if ((exe[0] == '.') && (exe[1] == '/'))
			exe += 2;
		size_t el = strlen(exe);
		assert(cl+el < sizeof(cfname));
		memcpy(cfname+cl,exe,el+1);
		char *sl = strrchr(cfname,'/');
		assert((sl-cfname)+17<sizeof(cfname));
		memcpy(sl+1,"../etc/xjobs.rc",16);
		read_config(cfname);
	}
	const char *home = getenv("HOME");
	if (home) {
		size_t hl = strlen(home);
		assert(hl+11 <= sizeof(cfname));
		memcpy(cfname,home,hl);
		memcpy(cfname+hl,"/.xjobs.rc",11);
		read_config(cfname);
	}
}


static void open_script(const char *a)
{
	int ret;
	struct stat st;

	Input = open(a,O_RDONLY);
	if (Input == -1)
		error("could not open input file %s: %s\n",optarg,strerror(errno));
	dbug("opening input script %s\n",optarg);
	ret = fstat(Input, &st);
	assert(ret != -1);
	if (S_ISFIFO(st.st_mode)) {
		struct sigaction sig;
		dbug("input script is a named pipe\n");
		sig.sa_handler = processSignal;
		sigemptyset(&sig.sa_mask);
		sigaddset(&sig.sa_mask,SIGPOLL);
		sig.sa_flags = SA_RESTART;
		ret = sigaction(SIGPOLL,&sig,0);
		assert(ret == 0);
#ifndef __CYGWIN__
		(void) open(optarg,O_WRONLY);
#endif
#ifndef __sun
		ret = fcntl(Input,F_SETOWN,getpid());
		if (ret != 0)
			warn("unable to set owning process for SIGPIPE of named pipe %s: %s\n",optarg,strerror(errno));
#endif
		ret = fcntl(Input,F_SETFL,O_RDONLY|O_NONBLOCK
#ifdef FASYNC
				| FASYNC
#endif
			   );
		assert(ret == 0);
#ifndef FASYNC
		ret = ioctl(Input,I_SETSIG,S_RDNORM);
		if (ret == -1)
			error("unable to setup SIGPOLL: %s\n",strerror(errno));
#endif
		InFlags = fcntl(Input,F_GETFL);
		Script = optarg;
	}
	close_onexec(Input);
	dbug("input file set to %s\n",optarg);
}


void parse_options(int argc, char **argv)
{
	int i;
	while ((i = getopt(argc,argv,"01c:dehj:L:l:nNpq:rs:tv::V")) != -1) {
		switch (i) {
		default:
			abort();
			break;
		case '0':
			if (gettoken != yylex)
				error("conflicting options -0 and -1");
			dbug("set scanning mode to 1 argument ending on \\0\n");
			gettoken = read_to_0;
			break;
		case '1':
			if (gettoken != yylex)
				error("conflicting options -0 and -1");
			dbug("set scanning mode to 1 argument ending on \\n\n");
			gettoken = read_to_nl;
			break;
		case 'c':
			set_color_mode(optarg);
			break;
		case 'd':
			dbug("stdout and stderr will be unbuffered\n");
			Stdout = dup(STDOUT_FILENO);
			Stderr = dup(STDERR_FILENO);
			break;
		case 'e':
			dbug("user requests to exit on error\n");
			ExitOnError = 1;
			break;
		case 'h':
			usage();
			break;
		case 'j': 
			parse_option_j(optarg);
			break;
		case 'L':
			set_log(optarg);
			break;
		case 'l':
			if (1 == sscanf(optarg,"%u",&Lines) && (Lines > 0))
				dbug("combining %u lines to a single command\n",Lines);
			else
				error("error in argument to option -l\n");
			break;
		case 'N':
			Stdout = open("/dev/null",O_WRONLY);
			if (Stdout == -1)
				error("could not open /dev/null: %s\n",strerror(errno));
			else
				dbug("stdout and stderr redirected to /dev/null\n");
			Stderr = Stdout;
			break;
		case 'n':
			Stdout = open("/dev/null",O_WRONLY);
			if (Stdout == -1)
				error("could not open /dev/null: %s\n",strerror(errno));
			else
				dbug("stdout redirected to /dev/null\n");
			break;
		case 'p':
			{
				Prompt = 1;
				dbug("enabling prompt mode\n");
			} break;
		case 'q':
			{
				unsigned tmp;
				if (1 == sscanf(optarg,"%u",&tmp) && (tmp > 0)) {
					QLen = tmp;
					dbug("limiting queue length to %lu elements\n",QLen);
				} else
					error("error in argument to option -q\n");
			}
			break;
		case 'r':
			RsrcUsage = 0;
			dbug("disabling display of resource usage\n");
			break;
		case 's':
			open_script(optarg);
			break;
		case 't':
			/* ignored for backward compatibility */
			dbug("ignoring option -t for backward compatibility\n");
			break;
		case 'v':
			/* must be done again after config file, so that
			 * the command line overrides the config file
			 */
			if ((optarg != 0) && (optarg[0] >= '0') && (optarg[0] <= '5') && (optarg[1] == 0))
				Verbose = optarg[0] - '0';
			else
				error("missing or invalid argument for option -v\n");
			break;
		case 'V':
			version();
			exit(0);
			break;
		case '?':
			error("unknown option -%c\n",optopt);
			break;
		case ':':
			error("option -%c requires an operand\n",optopt);
			break;
		}
	}
}


void version(void)
{
	(void) printf(
	"xjobs version " VERSION "\n"
	"Copyright 2006-2019, Thomas Maier-Komor\n"
	"License: GPLv2\n"
	"\n"
	);
}


void usage(void)
{
	version();
	(void) printf(
	"synopsis:\n"
	"xjobs [options] [command [option|argument ...]]\n"
	"xjobs [options] -- [command [option|argument ...]]\n"
	"\n"
	"valid options are:\n"
	"-h           : print this usage information and exit\n"
	"-L <log>     : set log file to <log> (default: stderr)\n"
	"-j <num>     : maximum number of jobs to execute in parallel (default %ld)\n"
	"-j <times>x  : set maximum number based on available processors (e.g. 0.5x)\n"
	"-s <file>    : script to execute (default: read from stdin)\n"
	"-l <num>     : combine <num> lines to a single job\n"
	"-n           : redirect stdout of childs to /dev/null\n"
	"-N           : redirect stdout and stderr of childs to /dev/null\n"
	"-d           : direct unbuffered output of stdout and stderr\n"
	"-v <level>   : set verbosity to level\n"
	"               (0=silent,1=error,2=warning,3=info,4=status,5=debug)\n"
	"-c <color>   : set color mode (none/auto/pipe/ansi)\n"
	"-p           : prompt user, whether job should be started\n"
	"-q <num>     : limit queue to <num> entries\n"
	"-t           : print total time before exiting\n"
	"-e           : exit if a job terminates with an error\n"
	"-0           : one argument per job terminated by a null-character\n"
	"-1           : one argument per job terminated by a new-line\n"
#ifdef HAVE_WAIT4
	"-r           : omit display of resource usage\n"
#endif
	"--           : last option to xjobs, following options are passed to jobs\n"
	"-V           : print version and exit\n"
	, Limit
	);
	exit(0);
}


