/*
** whatnow.c -- the WhatNow shell
**
** This code is Copyright (c) 2002, by the authors of nmh.  See the
** COPYRIGHT file in the root directory of the nmh distribution for
** complete copyright information.
*/

#include <h/mh.h>
#include <fcntl.h>
#include <signal.h>
#include <h/mime.h>
#include <h/utils.h>
#include <unistd.h>
#include <sys/stat.h>
#include <locale.h>
#include <sysexits.h>

#ifdef HAVE_SYS_PARAM_H
# include <sys/param.h>
#endif

static struct swit switches[] = {
#define EDITRSW  0
	{ "editor editor", 0 },
#define PRMPTSW  1
	{ "prompt string", 0 },
#define VERSIONSW  2
	{ "Version", 0 },
#define HELPSW  3
	{ "help", 0 },
	{ NULL, 0 }
};

char *version=VERSION;

/*
** Options at the "whatnow" prompt
*/
static struct swit aleqs[] = {
#define EDITSW  0
	{ "edit [editor [switches]]", 0 },
#define LISTSW  1
	{ "list", 0 },
#define DISPSW  2
	{ "display", 0 },
#define WHOMSW  3
	{ "whom", 0 },
#define SENDSW  4
	{ "send", 0 },
#define REFILEOPT  5
	{ "refile +folder", 0 },
#define DELETESW  6
	{ "delete", 0 },
#define QUITSW  7
	{ "quit", 0 },
#define CDCMDSW  8
	{ "cd [directory]", 0 },
#define PWDCMDSW  9
	{ "pwd", 0 },
#define LSCMDSW  10
	{ "ls", 0 },
#define ALISTCMDSW  11
	{ "alist", 0 },
#define ATTACHCMDSW  12
	{ "attach files", 0 },
#define DETACHCMDSW  13
	{ "detach numbers", 0 },
	{ NULL, 0 }
};

static char *myprompt = "\nWhat now? ";

/*
** static prototypes
*/
static int editfile(char **, char **, char *);
static int sendfile(char **, char *);
static int refile(char **, char *);
static int removefile(char *);
static void writelscmd(char *, int, char **);
static void writesomecmd(char *, int, char *, char *, char **);
static FILE* popen_in_dir(const char *, const char *, const char *);
static int system_in_dir(const char *, const char *);


int
main(int argc, char **argv)
{
	int use = NOUSE;
	char *cp;
	char *ed = NULL, *drft = NULL;
	char buf[BUFSIZ], prompt[BUFSIZ];
	char **argp, **arguments;
	struct stat st;
	char cwd[MAXPATHLEN + 1];  /* current working directory */
	char file[MAXPATHLEN + 1];  /* file name buffer */
	char shell[MAXPATHLEN + 1];  /* shell response buffer */
	FILE *f;  /* read pointer for bgnd proc */

	setlocale(LC_ALL, "");
	invo_name = mhbasename(argv[0]);

	/* read user profile/context */
	context_read();

	advise(NULL, "whatnow is deprecated. Consider switching to whatnow2.");

	arguments = getarguments(invo_name, argc, argv, 1);
	argp = arguments;

	/*
	** Get the initial current working directory.
	*/

	if (!getcwd(cwd, sizeof (cwd))) {
		adios(EX_USAGE, "getcwd", "could not get working directory");
	}

	while ((cp = *argp++)) {
		if (*cp == '-') {
			switch (smatch(++cp, switches)) {
			case AMBIGSW:
				ambigsw(cp, switches);
				exit(EX_USAGE);
			case UNKWNSW:
				adios(EX_USAGE, NULL, "-%s unknown", cp);

			case HELPSW:
				snprintf(buf, sizeof(buf),
						"%s [switches] [file]",
						invo_name);
				print_help(buf, switches, 1);
				exit(argc == 2 ? EX_OK : EX_USAGE);
			case VERSIONSW:
				print_version(invo_name);
				exit(argc == 2 ? EX_OK : EX_USAGE);

			case EDITRSW:
				if (!(ed = *argp++) || *ed == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				continue;

			case PRMPTSW:
				if (!(myprompt = *argp++) || *myprompt == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				continue;

			}
		}
		if (drft)
			adios(EX_USAGE, NULL, "only one draft at a time!");
		else
			drft = cp;
	}

	if ((!drft && !(drft = getenv("mhdraft"))) || !*drft)
		drft = mh_xstrdup(m_draft(seq_cur));

	if ((cp = getenv("mhuse")) && *cp)
		use = atoi(cp);

	if (!ed && !(ed = getenv("mheditor"))) {
		ed = "";  /* Don't initially edit the draft */
	}

	/* start editing the draft, unless editor is the empty string */
	if (*ed) {
		if (editfile(&ed, NULL, drft) <0) {
			if (!use) {
				unlink(drft);
			}
			adios(EX_SOFTWARE, NULL, "Try again.");
		}
	}

	snprintf(prompt, sizeof(prompt), myprompt, invo_name);
	while ((argp = getans(prompt, aleqs))) {
		switch (smatch(*argp, aleqs)) {
		case DISPSW:
			/* display the msg being replied to or distributed */
			if ((cp = getenv("mhaltmsg")) && *cp) {
				execprogl(listproc, listproc, "-file", cp,
						(char *)NULL);
			} else {
				advise(NULL, "no alternate message to display");
			}
			break;

		case EDITSW:
			/* Call an editor on the draft file */
			if (*++argp)
				ed = *argp++;
			editfile(&ed, argp, drft);
			break;

		case LISTSW:
			/* display the draft file */
			execprogl(listproc, listproc, "-file", drft,
					(char *)NULL);
			break;

		case QUITSW:
			/* quit */
			if (stat(drft, &st) != NOTOK) {
				advise(NULL, "draft left on %s", drft);
			}
			exit(EX_OK);

		case DELETESW:
			/* Delete draft and exit */
			removefile(drft);
			exit(EX_OK);

		case SENDSW:
			/* Send draft */
			sendfile(++argp, drft);
			break;

		case REFILEOPT:
			/* Refile the draft */
			if (refile(++argp, drft) == 0) {
				exit(EX_OK);
			}
			break;

		case CDCMDSW:
			/*
			** Change the working directory for attachments
			**
			** Run the directory through the user's shell
			** so that we can take advantage of any syntax
			** that the user is accustomed to.  Read back
			** the absolute path.
			*/

			if (*(argp+1) == NULL) {
				sprintf(buf, "$SHELL -c \"cd;pwd\"");
			} else {
				writesomecmd(buf, BUFSIZ, "cd", "pwd", argp);
			}
			if ((f = popen_in_dir(cwd, buf, "r"))) {
				fgets(cwd, sizeof (cwd), f);

				if (strchr(cwd, '\n'))
					*strchr(cwd, '\n') = '\0';

				pclose(f);
			} else {
				advise("popen", "could not get directory");
			}

			break;

		case PWDCMDSW:
			/* Print the working directory for attachments */
			printf("%s\n", cwd);
			break;

		case LSCMDSW:
			/*
			** List files in the current attachment working
			** directory
			**
			** Use the user's shell so that we can take
			** advantage of any syntax that the user is
			** accustomed to.
			*/
			writelscmd(buf, sizeof(buf), argp);
			system_in_dir(cwd, buf);
			break;

		case ALISTCMDSW:
			/*
			** List attachments on current draft.
			*/
			if (execprogl("anno", "anno", "-list", "-comp",
					attach_hdr, "-number", drft,
					(char *)NULL) != 0) {
				advise(NULL, "Could not list attachment headers.");
			}
			break;

		case ATTACHCMDSW:
			/*
			** Attach files to current draft.
			*/

			if (*(argp+1) == NULL) {
				advise(NULL, "attach command requires file argument(s).");
				break;
			}

			/*
			** Build a command line that causes the user's
			** shell to list the file name arguments.
			** This handles and wildcard expansion, tilde
			** expansion, etc.
			*/
			writelscmd(buf, sizeof(buf), argp);

			/*
			** Read back the response from the shell,
			** which contains a number of lines with one
			** file name per line.  Remove off the newline.
			** Determine whether we have an absolute or
			** relative path name.  Prepend the current
			** working directory to relative path names.
			** Add the attachment annotation to the draft.
			*/
			if (!(f = popen_in_dir(cwd, buf, "r"))) {
				advise("popen", "could not get file from shell");
				break;
			}

			while (fgets(shell, sizeof(shell), f)) {
				*(strchr(shell, '\n')) = '\0';

				if (*shell == '/')
					sprintf(file, "%s", shell);
				else {
					sprintf(file, "%s/%s", cwd, shell);
				}
				if (execprogl("anno", "anno",
						"-nodate", "-append",
						"-comp", attach_hdr,
						"-text", file,
						drft, (char *)NULL) != 0) {
					advise(NULL, "Could not add attachment header.");
				}
			}
			pclose(f);
			break;

		case DETACHCMDSW:
			/*
			** Detach files from current draft.
			**
			** Interpret the arguments as
			** attachment numbers.  Decrement any remaining
			** argument number that is greater than the one
			** just processed after processing each one so
			** that the numbering stays correct.
			*/
			for (arguments=argp+1; *arguments; arguments++) {
				int n;

				if (**arguments == '\0') {
					continue;
				}

				if (execprogl("anno", "anno", "-delete",
						"-comp", attach_hdr,
						"-number", *arguments, drft,
						(char *)NULL) != 0) {
					advise(NULL, "Could not delete attachment header.");
				}

				n = atoi(*arguments);
				for (argp=arguments+1; *argp; argp++) {
					if (atoi(*argp) > n) {
						if (atoi(*argp) == 1) {
							*argp = "";
						} else {
							sprintf(*argp, "%d", atoi(*argp) - 1);
						}
					}
				}
			}
			break;

		case WHOMSW:
			/* list recipients */
			execprogl("whom", "whom", drft, (char *)NULL);
			break;

		default:
			/* Unknown command */
			advise(NULL, "say what?");
			break;
		}
	}

	exit(EX_IOERR);
}



/*
** Build a command line of the form $SHELL -c "cd 'cwd'; cmd argp ... ;
** trailcmd".
*/
static void
writesomecmd(char *buf, int bufsz, char *cmd, char *trailcmd, char **argp)
{
	char *cp;
	/*
	** Note that we do not quote -- the argp from the user
	** is assumed to be quoted as they desire. (We can't treat
	** it as pure literal as that would prevent them using ~,
	** wildcards, etc.) The buffer produced by this function
	** should be given to popen_in_dir() or system_in_dir() so
	** that the current working directory is set correctly.
	*/
	int ln = snprintf(buf, bufsz, "$SHELL -c \"%s", cmd);
	/*
	** NB that some snprintf() return -1 on overflow rather than the
	** new C99 mandated 'number of chars that would have been written'
	*/
	/*
	** length checks here and inside the loop allow for the
	** trailing ';', trailcmd, '"' and NUL
	*/
	int trailln = strlen(trailcmd) + 3;
	if (ln < 0 || ln + trailln > bufsz)
		adios(EX_USAGE, NULL, "arguments too long");

	cp = buf + ln;

	while (*++argp) {
		ln = strlen(*argp);
		/* +1 for leading space */
		if (ln + trailln + 1 > bufsz - (cp-buf))
			adios(EX_USAGE, NULL, "arguments too long");
		*cp++ = ' ';
		memcpy(cp, *argp, ln+1);
		cp += ln;
	}
	if (*trailcmd) {
		*cp++ = ';';
		strcpy(cp, trailcmd);
		cp += trailln - 3;
	}
	*cp++ = '"';
	*cp = 0;
}

/*
** Build a command line that causes the user's shell to list the file name
** arguments.  This handles and wildcard expansion, tilde expansion, etc.
*/
static void
writelscmd(char *buf, int bufsz, char **argp)
{
	writesomecmd(buf, bufsz, "ls", "", argp);
}

/*
** Like system(), but run the command in directory dir.
** This assumes the program is single-threaded!
*/
static int
system_in_dir(const char *dir, const char *cmd)
{
	char olddir[BUFSIZ];
	int r;
	if (getcwd(olddir, sizeof(olddir)) == 0)
		adios(EX_OSERR, "getcwd", "could not get working directory");
	if (chdir(dir) != 0)
		adios(EX_OSERR, "chdir", "could not change working directory");
	r = system(cmd);
	if (chdir(olddir) != 0)
		adios(EX_OSERR, "chdir", "could not change working directory");
	return r;
}

/* ditto for popen() */
static FILE*
popen_in_dir(const char *dir, const char *cmd, const char *type)
{
	char olddir[BUFSIZ];
	FILE *f;
	if (getcwd(olddir, sizeof(olddir)) == 0)
		adios(EX_OSERR, "getcwd", "could not get working directory");
	if (chdir(dir) != 0)
		adios(EX_OSERR, "chdir", "could not change working directory");
	f = popen(cmd, type);
	if (chdir(olddir) != 0)
		adios(EX_OSERR, "chdir", "could not change working directory");
	return f;
}


/*
** EDIT
*/

static char *edsave = NULL;  /* the editor we used previously */


static int
editfile(char **ed, char **arg, char *file)
{
	int pid, status, vecp;
	char *cp, *vec[MAXARGS];

	if (!*ed || !**ed) {
		/* We have no explicit editor. */
		if (edsave) {
			/* Use the previous editor ... */
			*ed = edsave;
			if (!(cp = mhbasename(*ed)))
				cp = *ed;

			/* but prefer one specified via "editor-next" */
			cp = concat(cp, "-next", NULL);
			if ((cp = context_find(cp)))
				*ed = cp;
		} else {
			/* set initial editor */
			*ed = defaulteditor;
		}
	}

	context_save();
	fflush(stdout);

	switch (pid = fork()) {
	case NOTOK:
		advise("fork", "unable to");
		status = EX_OSERR;
		break;

	case OK:
		vecp = 0;
		vec[vecp++] = mhbasename(*ed);
		while (arg && *arg) {
			vec[vecp++] = *arg++;
		}
		vec[vecp++] = file;
		vec[vecp] = NULL;

		execvp(*ed, vec);
		fprintf(stderr, "%s: unable to exec ", invo_name);
		perror(*ed);
		_exit(EX_OSERR);

	default:
		if ((status = pidwait(pid, NOTOK))) {
			if ((status & 0xff00) == 0xff00) {
				/* cmd not found or pidwait() failed */
				status = EX_SOFTWARE;
				break;
			}
			if (status & 0x00ff) {
				/* terminated by signal */
				advise(NULL, "%s terminated by signal %d",
						*ed, status & 0x7f);
			} else {
				/* failure exit */
				advise(NULL, "%s exited with return code %d",
						*ed, (status & 0xff00) >> 8);
			}
			status = -1;
			break;
		}
	}

	/* remember which editor we used */
	edsave = mh_xstrdup(*ed);

	*ed = NULL;

	return status;
}


/*
** SEND
*/

static int
sendfile(char **arg, char *file)
{
	int vecp = 0;
	char *vec[MAXARGS];

	context_save();
	fflush(stdout);

	vec[vecp++] = "send";
	while (arg && *arg) {
		vec[vecp++] = *arg++;
	}
	vec[vecp++] = file;
	vec[vecp] = NULL;
	execvp(*vec, vec);
	fprintf(stderr, "%s: unable to exec ", invo_name);
	perror("send");
	_exit(EX_OSERR);
}


/*
** refile msg into another folder
*/
static int
refile(char **arg, char *file)
{
	int vecp = 0;
	char *vec[MAXARGS];

	vec[vecp++] = "refile";
	vec[vecp++] = "-nolink";  /* override bad .mh_profile defaults */
	vec[vecp++] = "-file";
	vec[vecp++] = file;

	while (arg && *arg) {
		vec[vecp++] = *arg++;
	}
	vec[vecp] = NULL;

	context_save();
	fflush(stdout);

	return execprog(*vec, vec);
}


/*
** Remove the draft file
*/

static int
removefile(char *drft)
{
	if (unlink(drft) == NOTOK)
		adios(EX_IOERR, drft, "unable to unlink");

	return OK;
}
