/*
 * main.c: logapp main file
 *
 *
 * Copyright (C) 2007-2011 Michael Brunner <mibru@gmx.de>
 *
 * 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
 * General Public License for more details.
 */

#include "logapp.h"

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>

#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <signal.h>

#if defined(__FreeBSD__)
#include <libutil.h>
#include <sys/ioctl.h>
#include <termios.h>
#elif defined(__OpenBSD__) || defined(__NetBSD__)
#include <util.h>
#include <termios.h>
#elif defined(__APPLE__)
#include <util.h>
#else
#include <pty.h>
#endif

#include "configuration.h"
#include "logfile.h"
#include "capture.h"

const char string_outofmemory[] = EXECUTABLE " error: out of memory\n";

void error_outofmemory(void)
{
	int ret;

	ret = write(STDERR_FILENO, string_outofmemory,
		    sizeof(string_outofmemory));
	if (ret)
		exit(EXIT_FAILURE);
}

#define max(a, b) ((a) > (b) ? (a) : (b))
void print(t_printtype type, const char *format,...)
{
	va_list argptr;
	int format_len;
	int fmt_len;
	int exec_len;
	int warn_len;
	int err_len;
	char *fmtstring;

	format_len = strlen(format);
	exec_len = strlen(EXECUTABLE);
	warn_len = strlen(STRING_WARNING);
	err_len = strlen(STRING_ERROR);
	fmt_len = format_len + exec_len + max(warn_len, err_len) + 1;

	fmtstring = malloc(fmt_len);
	if (!fmtstring) {
		error_outofmemory();
		exit(EXIT_FAILURE);
	}

	va_start(argptr, format);

	if (type!=MESSAGE) {
		memcpy(fmtstring, EXECUTABLE, exec_len);
		fmt_len = exec_len;
	} else {
		fmt_len = 0;
		*fmtstring = '\0';
	}

	switch (type) {
		case WARNING:
			memcpy(fmtstring + exec_len, STRING_WARNING, warn_len);
			fmt_len+=warn_len;
			break;
		case ERROR:
			memcpy(fmtstring + exec_len, STRING_ERROR, err_len);
			fmt_len+=err_len;
			break;
		default: break;
	}

	memcpy(fmtstring + fmt_len, format, format_len);
	*(fmtstring + fmt_len + format_len) = '\0';

	if (vfprintf(stderr, fmtstring, argptr) < 0) {
		exit(EXIT_FAILURE);
	}

	va_end(argptr);

	free(fmtstring);
}

int execcmd(char* cmd)
{
	return system(cmd);
}

#define LINELENGTH 80
#define DESCOFFSET 33
void usage(void)
{
	int i;
	unsigned int j;
	int offset;
	char linebuf[LINELENGTH + 1];

	message("%s %s\n\n", EXECUTABLE, VERSION);
	message("Usage: %s [OPTION]... APPLICATION...\n", EXECUTABLE);

	/* omit rest of the usage instructions if it has not been requested
	 * with the --help parameter */
	if (!show_usage) {
		message("\nTry \"%s --help\" for more options.\n", EXECUTABLE);
		return;
	}

	for (i=0; i<arglistsize; i++) {
		unsigned int descoffset = 0;
		unsigned int desclen;

		if ((!arglist[i].shrt) && (!arglist[i].lng)
		    && (arglist[i].desc)) {
			message("\n%s\n", arglist[i].desc);
			continue;
		}

		snprintf(linebuf, 3, "  ");
		if (arglist[i].shrt) {
			snprintf(&linebuf[2], 4, "-%c ", arglist[i].shrt);
		} else
			snprintf(&linebuf[2], 4, "    ");

		offset = 5;
		if (arglist[i].lng)
			offset += snprintf(&linebuf[5], LINELENGTH-5,
					  "--%s", arglist[i].lng);
		else
			offset += snprintf(&linebuf[5], 5, "    ");

		if (arglist[i].parm)
			offset += snprintf(&linebuf[offset], LINELENGTH-offset,
					  "=%s ", arglist[i].parm);

		/* Not checking return values of snprintf above, but be sure
		 * that offset didn't get smaller before continuing */
		if (offset < 5) {
			error("unable to generate usage information\n");
			exit(EXIT_FAILURE);
		}

		for (j=offset; j<DESCOFFSET; j++) {
			linebuf[j] = ' ';
			offset++;
		}
		linebuf[j] = '\0';
		if (!arglist[i].desc) {
			message("%s\n", linebuf);
			continue;
		}

		for (j=0; j<((desclen = strlen(arglist[i].desc)) + 2); j++) {
			if (arglist[i].desc[j] == ' ' ||
			    (arglist[i].desc[j] == '\n') ||
			    (j > desclen)) {
				if ((offset + (j - descoffset)) >= LINELENGTH) {
					linebuf[offset] = '\n';
					linebuf[offset+1] = '\0';
					/* Indent further description lines */
					offset = DESCOFFSET + 1;
					message("%s", linebuf);
					memset(linebuf, ' ', offset);
				}
				memcpy(&linebuf[offset],
				       &arglist[i].desc[descoffset],
				       j - descoffset);
				offset += j - descoffset;
				descoffset = j;
				linebuf[offset] = '\0';
			}
		}

		message("%s\n", linebuf);
	}

	message("\nPlease send bug reports and feature requests to %s\n",
		AUTHOR);
}

void version(void)
{
	message("%s %s\n", EXECUTABLE, VERSION);
	if (DEBUG_BUILD) {
		message("DEBUG VERSION!\n", __DATE__, __TIME__);
		message("Build date: %s %s\n", __DATE__, __TIME__);
	}
	if (strlen(SVN_REVISION))
		message("SVN revision: %s\n", SVN_REVISION);

	message("\nCompiletime configuration:\n");
	message("  PTY support %s\n", CONFIG_SUPPORT_PTY?"enabled":"disabled");
	message("  Thread usage %s\n\n",
		CONFIG_USE_THREADS?"enabled":"disabled");
}

static void sig_handler(int signr)
{
	switch (signr) {
		case SIGWINCH:	adjust_clipping();
				break;
		case SIGCONT:	app.active = 1;
				break;
		case SIGCHLD: 
		case SIGTERM:
		case SIGQUIT:
		case SIGINT:
		default:	app.active = 0;
				app.doexit = 1;
				break;
	}
}

int fork_child()
{
	int pipe_stdout[2];
	int pipe_stderr[2];

	if (!config.disable) {
		get_display_parameters();

		if (app.ptytermios != NULL) {
			memcpy(app.ptytermios_bak, app.ptytermios,
		       sizeof(struct termios));
		}

		if (CONFIG_SUPPORT_PTY && config.usepty) {
			if (openpty(&pipe_stdout[0], &pipe_stdout[1], NULL,
				app.ptytermios, app.ptysize) == -1) {
				error("opening PTY for %s failed with error "
				      "%d\n", app.pstdout->name, errno);
				/* no need to remove CRs if PTY cannot be
				 * opened */
				app.pstdout->ptyremovecr = 0;
			}
		} else {
			if (pipe(pipe_stdout) < 0) {
				error("creating stdout pipe handler\n");
				return -1;
			}
		}

		if (pipe(pipe_stderr) < 0) {
			error("creating stderr pipe handler\n");
			return -1;
		}
	}

	signal(SIGCONT, sig_handler);

	/* flush all streams before forking */
	fflush(NULL);

	if ((app.pid = fork()) < 0) {
		error("fork error\n");
		return -1;
	};
	
	if (app.pid> 0) {
		if (!config.disable) {
			close(pipe_stdout[1]);
			close(pipe_stderr[1]);
			app.pstdout->fh = pipe_stdout[0];
			app.pstderr->fh = pipe_stderr[0];
		}

		/* synchronize with child */
		while (!app.active && !app.doexit) /* wait for child */
			usleep(10000);
		kill(app.pid, SIGCONT); /* tell the child we are ready */

		/* Store current time */
		time(&app.starttime);
		gettimeofday(&app.toffset, NULL);
	} else if (app.pid==0) {
		if (!config.disable) {
			close(pipe_stdout[0]);
			close(pipe_stderr[0]);

			if (pipe_stdout[1] != STDOUT_FILENO) {
				if (dup2(pipe_stdout[1], STDOUT_FILENO)
				    != STDOUT_FILENO)
					error("redirecting stdout failed");
				close(pipe_stdout[1]);
			}

			if (pipe_stderr[1] != STDERR_FILENO) {
				if (dup2(pipe_stderr[1], STDERR_FILENO)
				    != STDERR_FILENO)
					error("redirecting stderr failed");
				close(pipe_stderr[1]);
			}

			if (app.ptytermios != NULL) {
				tcsetattr(STDIN_FILENO, TCSADRAIN,
					  app.ptytermios);
			}
		}
		
		/* synchronize with parent */
		kill(getppid(), SIGCONT); /* tell the parent we are ready */
		while (!app.active && !app.doexit) /* wait for parent */
			usleep(10000);

		if (execvp(app.exe, app.argv) < 0) {
			error("executing application %s failed\n", app.exe);
			exit(EXIT_FAILURE);
		}
	} else {
		error("forking process failed\n");
		return -1;
	};

	return 0;
}

int main(int argc, char *argv[])
{
	char*	argv0;
	int	ret;
	char*	env;

	/* check if we are executed from a dumb terminal */
	env = getenv("TERM");
	if (env) {
		if (!strcmp(env, "dumb")) {
			config.dumbterm = 1;
		}
	} else {
		/* Terminal environment variable not set, so assuming a dumb
		 * terminal */
		config.dumbterm = 1;
	}

	/* check if the called filename defines an application to run */
	argv0 = strrchr(argv[0], '/');
	if (argv0)
		argv0++;
	else
		argv0 = argv[0];
	if (strcmp(argv0, EXECUTABLE)) {
		app.exe = strstr(argv0, config.strip_prefix);
		if (app.exe == argv0) {
			if (strlen(app.exe)>strlen(config.strip_prefix))
				app.exe += strlen(config.strip_prefix);
		} else {
			app.exe = argv0;
		}
	}

	if ((ret = parse_args(argc, argv))) {
		error("error parsing " EXECUTABLE " argument %d\n", ret);
		exit(EXIT_FAILURE);
	}

	if (show_usage) {
		usage();
		exit(EXIT_SUCCESS);
	}

	if (show_version) {
		version();
		exit(EXIT_SUCCESS);
	}

	/* also show usage if no application has been provided */
	if (!app.exe && !show_config) {
		message("%s: No application parameter has been provided.\n",
			EXECUTABLE);
		usage();
		exit(EXIT_SUCCESS);
	}

	/* get default configuration from file */
	if (get_config()) {
		error("problem while loading default config from file\n");
		exit(EXIT_FAILURE);
	}

	if (fixup_config()) {
		error("problem processing configuration\n");
		exit(EXIT_FAILURE);
	}

	if (check_for_disable_keywords()) {
		error("failed to check for disable keywords\n");
		exit(EXIT_FAILURE);
	}

	if (show_config) {
		show_configuration();
		exit(EXIT_SUCCESS);
	}

	/* Get window size changes */
	if ((app.pstderr->eclip == 2) || (app.pstdout->eclip == 2)) {
		if (signal(SIGWINCH, sig_handler) == SIG_ERR) {
			error("creating signal handler\n");
			return -1;
		}
		adjust_clipping();
	}

	if (signal(SIGINT, sig_handler) == SIG_ERR) {
		error("creating signal handler\n");
		exit(EXIT_FAILURE);
	}

	if (signal(SIGQUIT, sig_handler) == SIG_ERR) {
		error("creating signal handler\n");
		exit(EXIT_FAILURE);
	}

	if (signal(SIGTERM, sig_handler) == SIG_ERR) {
		error("creating signal handler\n");
		exit(EXIT_FAILURE);
	}

	if (signal(SIGCHLD, sig_handler) == SIG_ERR) {
		error("creating signal handler\n");
		exit(EXIT_FAILURE);
	}

	/* Start the application */
	if (fork_child()) {
		exit(EXIT_FAILURE);
	}

	/* Open logfile */
	if (logfile_open(config.logname, app.logfile)) {
		error("unable to open logfile %s\n", config.logname);
		exit(EXIT_FAILURE);
	}

	/* execute preexec command */
	if (config.preexeccmd) {
		int ret = execcmd(config.preexeccmd);
		if (ret) {
			print(config.exitonexecfail?ERROR:WARNING,
			      "preexec command \"%s\" returned 0x%x\n",
			      config.preexeccmd, ret);
			if (config.exitonexecfail) {
				exit(EXIT_FAILURE);
			}
		}
	}

	/* Start the capture threads for stdin and stderr */
	if (!config.disable) {
		if (capture_start()) {
			error("unable to capture streams\n");
			exit(EXIT_FAILURE);
		}
	}

	/* Wait for application to exit get its exit status */
	waitpid(app.pid, &app.exit_state, 0);
	
	/* Wait for capture threads to exit */
	if (!config.disable)
		capture_end();

	if (logfile_close(app.logfile)) {
		error("problem closing logfile");
	}

	/* execute postexec command */
	if (config.postexeccmd) {
		int ret = execcmd(config.postexeccmd);
		if (ret) {
			print(config.exitonexecfail?ERROR:WARNING,
			      "postexec command \"%s\" returned 0x%x\n",
			      config.postexeccmd, ret);
		}
	}

	/* Be sure the console is not messed up when we exit */
	if (!config.disable)
		reset_console();

	/* flush all streams before we write the summary */
	fflush(NULL);

	/* Print short summary */
	if (config.printsummary)
		print_summary();

	/* free data allocated for configuration */
	cleanup_config();

	/* We are returning the exit status of the application if possible */
	if (WIFEXITED(app.exit_state))
		exit(WEXITSTATUS(app.exit_state));
	else
		exit(EXIT_FAILURE);
}
