/*
 * Copyright (c) 2000-2001 Silicon Graphics, Inc.
 * All Rights Reserved.
 *
 * 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.
 *
 * This program is distributed in the hope that it would 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 the Free Software Foundation,
 * Inc.,  51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <signal.h>
#include <assert.h>
#include <string.h>

#include "config.h"

#include "types.h"
#include "mlog.h"
#include "dlog.h"
#include "getopt.h"

static int dlog_ttyfd = -1;
static volatile bool_t dlog_allowed_flag = BOOL_FALSE;
static bool_t dlog_timeouts_flag = BOOL_FALSE;
static char *promptstr = " -> ";

static sigset_t dlog_registered_sigs;

static bool_t promptinput(char *buf,
			   size_t bufsz,
			   ix_t *exceptionixp,
			   time32_t timeout,
			   ix_t timeoutix,
			   ix_t sigintix,
			   ix_t sighupix,
			   ix_t sigquitix,
			   char *fmt, ...);
static void dlog_string_query_print(void *ctxp, char *fmt, ...);

bool_t
dlog_init(int argc, char *argv[])
{
	int c;

	/* can only call once
	 */
	assert(dlog_ttyfd == -1);

	/* initially allow dialog, use stdin fd
	 */
	dlog_ttyfd = 0; /* stdin */
	dlog_allowed_flag = BOOL_TRUE;
	dlog_timeouts_flag = BOOL_TRUE;

	sigemptyset(&dlog_registered_sigs);

	/* look for command line option claiming the operator knows
	 * what's up.
	 */
	optind = 1;
	opterr = 0;
	while ((c = getopt(argc, argv, GETOPT_CMDSTRING)) != EOF) {
		switch (c) {
		case GETOPT_FORCE:
			dlog_ttyfd = -1;
			dlog_allowed_flag = BOOL_FALSE;
			break;
		case GETOPT_NOTIMEOUTS:
			dlog_timeouts_flag = BOOL_FALSE;
			break;
		}
	}
#ifdef RESTORE
	/* look to see if restore source coming in on
	 * stdin. If so, try to open /dev/tty for dialogs.
	 */
	if (optind < argc && !strcmp(argv[optind ], "-")) {
		dlog_ttyfd = open("/dev/tty", O_RDWR);
		if (dlog_ttyfd < 0) {
			perror("/dev/tty");
			dlog_ttyfd = -1;
			dlog_allowed_flag = BOOL_FALSE;
		}
	}
#endif /* RESTORE */

#ifdef CHKSTDIN
	/* if stdin is not a tty, don't allow dialogs
	 */
	if (dlog_allowed_flag) {
		struct stat statbuf;
		int rval;

		assert(dlog_ttyfd >= 0);
		rval = fstat(dlog_ttyfd, &statbuf);
		if (rval) {
			mlog(MLOG_VERBOSE | MLOG_WARNING,
			      _("could not fstat stdin (fd %d): %s (%d)\n"),
			      dlog_ttyfd,
			      strerror(errno),
			      errno);
		} else {
			mlog(MLOG_DEBUG,
			      "stdin mode 0x%x\n",
			      statbuf.st_mode);
		}
	}
#endif /* CHKSTDIN */

	return BOOL_TRUE;
}

bool_t
dlog_allowed(void)
{
	return dlog_allowed_flag;
}

void
dlog_desist(void)
{
	dlog_allowed_flag = BOOL_FALSE;
}

int
dlog_fd(void)
{
	return dlog_ttyfd;
}

void
dlog_begin(char *preamblestr[], size_t preamblecnt)
{
	size_t ix;

	mlog_lock();
	for (ix = 0; ix < preamblecnt; ix++) {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      preamblestr[ix]);
	}
}

void
dlog_end(char *postamblestr[], size_t postamblecnt)
{
	size_t ix;

	for (ix = 0; ix < postamblecnt; ix++) {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      postamblestr[ix]);
	}
	mlog_unlock();
}

ix_t
dlog_multi_query(char *querystr[],
		  size_t querycnt,
		  char *choicestr[],
		  size_t choicecnt,
		  char *hilitestr,
		  size_t hiliteix,
		  char *defaultstr,
		  ix_t defaultix,
		  time32_t timeout,
		  ix_t timeoutix,
		  ix_t sigintix,
		  ix_t sighupix,
		  ix_t sigquitix)
{
	size_t ix;
	char buf[100];
	char *prepromptstr;

	/* sanity
	 */
	assert(dlog_allowed_flag);

	/* display query description strings
	 */
	for (ix = 0; ix < querycnt; ix++) {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      querystr[ix]);
	}

	/* display the choices: NOTE: display is 1-based, code intfs 0-based!
	 */
	for (ix = 0; ix < choicecnt; ix++) {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      "%u: %s",
		      ix + 1,
		      choicestr[ix]);
		if (ix == hiliteix) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      "%s",
			      hilitestr ?  hilitestr : " *");
		}
		if (ix == defaultix) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      "%s",
			      defaultstr ?  defaultstr : _(" (default)"));
		}
		if (dlog_timeouts_flag
		     &&
		     timeoutix != IXMAX
		     &&
		     ix == timeoutix) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      _(" (timeout in %u sec)"),
			      timeout);
		}
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      "\n");
	}

	/* read the tty until we get a proper answer or are interrupted
	 */
	prepromptstr = "";
	for (;;) {
		ix_t exceptionix;
		bool_t ok;

		/* prompt and accept input
		 */
		ok = promptinput(buf,
				  sizeof(buf),
				  &exceptionix,
				  timeout,
				  timeoutix,
				  sigintix,
				  sighupix,
				  sigquitix,
				  prepromptstr,
				  choicecnt);
		if (ok) {
			long int val;
			char *end = buf;

			if (!strlen(buf)) {
				return defaultix;
			}

			val = strtol(buf, &end, 10);
			if (*end != '\0' || val < 1 || val > choicecnt) {
				prepromptstr = _(
				      "please enter a value "
				      "between 1 and %d inclusive ");
				continue;
			}
			return val - 1; // return value is a 0-based index
		} else {
			return exceptionix;
		}
	}
	/* NOTREACHED */
}

void
dlog_multi_ack(char *ackstr[], size_t ackcnt)
{
	size_t ix;

	for (ix = 0; ix < ackcnt; ix++) {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      ackstr[ix]);
	}
}

ix_t
dlog_string_query(dlog_ucbp_t ucb, /* user's print func */
		   void *uctxp,	  /* user's context for above */
		   char *bufp,	  /* typed string returned in */
		   size_t bufsz,	  /* buffer size */
		   time32_t timeout,  /* secs b4 giving up */
		   ix_t timeoutix,
		   ix_t sigintix,
		   ix_t sighupix,
		   ix_t sigquitix,
		   ix_t okix)
{
	ix_t exceptionix;
	bool_t ok;

	/* sanity
	 */
	assert(dlog_allowed_flag);

	/* call the caller's callback with his context, print context, and
	 * print operator
	 */
	(*ucb)(uctxp, dlog_string_query_print, 0);

	/* if called for, print the timeout and a newline.
	 * if not, print just a newline
	 */
	if (dlog_timeouts_flag && timeoutix != IXMAX) {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      _(" (timeout in %u sec)\n"),
		      timeout);
	} else {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      "\n");
	}

	/* prompt and accept input
	 */
	ok = promptinput(bufp,
			  bufsz,
			  &exceptionix,
			  timeout,
			  timeoutix,
			  sigintix,
			  sighupix,
			  sigquitix,
			  "");
	if (ok) {
		return okix;
	} else {
		return exceptionix;
	}
}

void
dlog_string_ack(char *ackstr[], size_t ackcnt)
{
	dlog_multi_ack(ackstr, ackcnt);
}

/* ok that this is a static, since used under mutual exclusion lock
 */
static volatile int dlog_signo_received;

bool_t
dlog_sighandler(int signo)
{
	if (sigismember(&dlog_registered_sigs, signo) < 1)
		return BOOL_FALSE;
	// only process the first signal
	sigemptyset(&dlog_registered_sigs);
	dlog_signo_received = signo;
	return BOOL_TRUE;
}

/* ARGSUSED */
static void
dlog_string_query_print(void *ctxp, char *fmt, ...)
{
	va_list args;

	assert(!ctxp);

	va_start(args, fmt);
	mlog_va(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE, fmt, args);
	va_end(args);
}

static bool_t
promptinput(char *buf,
	     size_t bufsz,
	     ix_t *exceptionixp,
	     time32_t timeout,
	     ix_t timeoutix,
	     ix_t sigintix,
	     ix_t sighupix,
	     ix_t sigquitix,
	     char *fmt,
	     ...)
{
	va_list args;
	time32_t now = time(NULL);
	int nread = -1;
	sigset_t orig_set;
	char *bufp = buf;

	/* display the pre-prompt
	 */
	va_start(args, fmt);
	mlog_va(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE, fmt, args);
	va_end(args);

	/* display standard prompt
	 */
#ifdef NOTYET
	if (dlog_timeouts_flag && timeoutix != IXMAX) {
		mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
		      _("(timeout in %d sec)"),
		      timeout);
	}
#endif /* NOTYET */
	mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE, promptstr);

	/* set up timeout
	 */
	if (dlog_timeouts_flag && timeoutix != IXMAX) {
		timeout += now;
	} else {
		timeout = TIMEMAX;
	}

	/* set up signal handling
	 * the mlog lock is held for the life of the dialog (see dlog_begin)
	 * and it's possible the main thread, which normally does the signal
	 * handling, is now waiting on the mlog lock trying to log a message.
	 * so unblock the relevant signals for this thread. note this means
	 * the current thread or the main thread might handle one of these
	 * signals.
	 */
	dlog_signo_received = -1;
	sigemptyset(&dlog_registered_sigs);
	if (sigintix != IXMAX) {
		sigaddset(&dlog_registered_sigs, SIGINT);
	}
	if (sighupix != IXMAX) {
		sigaddset(&dlog_registered_sigs, SIGHUP);
		sigaddset(&dlog_registered_sigs, SIGTERM);
	}
	if (sigquitix != IXMAX) {
		sigaddset(&dlog_registered_sigs, SIGQUIT);
	}

	pthread_sigmask(SIG_UNBLOCK, &dlog_registered_sigs, &orig_set);

	/* wait for input, timeout, or interrupt.
	 * note we come out of the select() frequently in order to
	 * check for a signal. the signal may have been handled by the
	 * the main thread, so we can't rely on the signal waking us
	 * up from the select().
	 */
	while (now < timeout && dlog_signo_received == -1 && dlog_allowed_flag) {
		int rc;
		fd_set rfds;
		struct timeval tv = { 0, 100000 }; // 100 ms

		FD_ZERO(&rfds);
		FD_SET(dlog_ttyfd, &rfds);

		rc = select(dlog_ttyfd + 1, &rfds, NULL, NULL, &tv);
		if (rc > 0 && FD_ISSET(dlog_ttyfd, &rfds)) {
			nread = read(dlog_ttyfd, bufp, bufsz);
			if (nread < 0) {
				break; // error handled below
			} else if (nread == 0 && buf == bufp) {
				mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE, "\n");
				*bufp = 0;
				break; // no input, return an empty string
			} else if (nread > 0 && bufp[nread-1] == '\n') {
				// received a full line, chomp the newline
				bufp[nread-1] = 0;
				break;
			} else if (nread == bufsz) {
				// no more room, truncate and return
				bufp[nread-1] = 0;
				break;
			}

			// keep waiting for a full line of input
			bufp += nread;
			bufsz -= nread;
			nread = -1;
		}
		now = time(NULL);
	}

	/* restore signal handling
	 */
	pthread_sigmask(SIG_SETMASK, &orig_set, NULL);
	sigemptyset(&dlog_registered_sigs);

	/* check for timeout or interrupt
	 */
	if (nread < 0) {
		if (now >= timeout) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      _("timeout\n"));
			*exceptionixp = timeoutix;
		} else if (dlog_signo_received == SIGINT) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      _("keyboard interrupt\n"));
			mlog_exit_hint(RV_KBD_INTR);
			*exceptionixp = sigintix;
		} else if (dlog_signo_received == SIGHUP) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      _("hangup\n"));
			*exceptionixp = sighupix;
		} else if (dlog_signo_received == SIGTERM) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      _("terminate\n"));
			*exceptionixp = sighupix;
		} else if (dlog_signo_received == SIGQUIT) {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      _("keyboard quit\n"));
			mlog_exit_hint(RV_KBD_INTR);
			*exceptionixp = sigquitix;
		} else {
			mlog(MLOG_NORMAL | MLOG_NOLOCK | MLOG_BARE,
			      _("abnormal dialog termination\n"));
			*exceptionixp = sigquitix;
		}
		return BOOL_FALSE;
	} else {
		assert(dlog_signo_received == -1);
		*exceptionixp = 0;
		return BOOL_TRUE;
	}
}
