/*
** anno.c -- annotate messages
**
** 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.
**
** Three new options have been added: delete, list, and number. Adding
** features to generalize the anno command seemed to be a better approach
** than the creation of a new command whose features would overlap with
** those of the anno command.
*/

#include <h/mh.h>
#include <h/utils.h>
#include <h/tws.h>
#include <fcntl.h>
#include <errno.h>
#include <utime.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/stat.h>
#include <locale.h>
#include <sysexits.h>

static enum { MODE_ADD, MODE_DEL, MODE_LIST } mode = MODE_ADD;

static struct swit switches[] = {
#define COMPSW 0
	{ "component field", 0 },
#define DATESW 1
	{ "date", 0 },
#define NDATESW 2
	{ "nodate", 2 },
#define TEXTSW 3
	{ "text body", 0 },
#define VERSIONSW 4
	{ "Version", 0 },
#define HELPSW 5
	{ "help", 0 },
#define LISTSW 6
	{ "list", 0 },
#define DELETESW 7
	{ "delete", 0 },
#define NUMBERSW 8
	{ "number", 0 },
#define APPENDSW 9
	{ "append", 0 },
#define PRESERVESW 10
	{ "preserve", 0 },
#define NOPRESERVESW 11
	{ "nopreserve", 2 },
	{ NULL, 0 }
};

char *version=VERSION;

/*
** static prototypes
*/
static void make_comp(unsigned char **);
static int annotate(char *, unsigned char *, char *, int, int, int, int);
static void annolist(char *, unsigned char *, int);
static void dodel(int, unsigned char *, char *, FILE *, int);
static void doadd(int, unsigned char *, char *, FILE *, int, int);


int
main(int argc, char **argv)
{
	int datesw = 1;
	int preserve = 0;
	int msgnum;
	char *cp, *maildir;
	unsigned char *comp = NULL;
	char *text = NULL, *folder = NULL, buf[BUFSIZ];
	char *file = NULL;
	char **argp, **arguments;
	struct msgs_array msgs = { 0, 0, NULL };
	struct msgs *mp;
	int append = 0;  /* append annotations instead of default prepend */
	int number = 0; /* delete specific number of like elements if set */

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

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

	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 [+folder] [msgs] [switches]",
					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 DELETESW:  /* delete annotations */
				mode = MODE_DEL;
				continue;

			case LISTSW:  /* produce a listing */
				mode = MODE_LIST;
				continue;

			case COMPSW:
				if (comp)
					adios(EX_USAGE, NULL, "only one component at a time!");
				if (!(comp = *argp++) || *comp == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				continue;

			case TEXTSW:
				if (text)
					adios(EX_USAGE, NULL, "only one body at a time!");
				if (!(text = *argp++) || *text == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				continue;

			case NUMBERSW: /* number listing or delete by number */
				if (mode == MODE_ADD) {
					adios(EX_USAGE, NULL, "-number switch must appear after -list or -delete, only.");
				}
				if (mode == MODE_LIST) {
					number = 1;
					continue;
				}
				/* MODE_DEL */
				if (number) {
					adios(EX_USAGE, NULL, "only one number at a time!");
				}
				if (*argp && strcmp(*argp, "all")==0) {
					number = -1;
					argp++;
					continue;
				}
				if (!*argp || !(number = atoi(*argp))) {
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-1]);
				}
				if (number < 0) {
					adios(EX_USAGE, NULL, "invalid number (%d).",
							number);
				}
				argp++;
				continue;

			case DATESW:
				datesw++;
				continue;
			case NDATESW:
				datesw = 0;
				continue;

			case APPENDSW:
				append = 1;
				continue;

			case PRESERVESW:
				preserve = 1;
				continue;

			case NOPRESERVESW:
				preserve = 0;
				continue;
			}
		}
		if (*cp == '+' || *cp == '@') {
			if (folder)
				adios(EX_USAGE, NULL, "only one folder at a time!");
			else
				folder = mh_xstrdup(expandfol(cp));
		} else if (*cp == '/' || *cp == '.') {
			if (file)
				adios(EX_USAGE, NULL, "only one file at a time!");
			file = cp;
		} else {
			app_msgarg(&msgs, cp);
		}
	}

	if (file && (folder || msgs.size)) {
		adios(EX_USAGE, NULL, "Don't intermix files and messages.");
	}
	if (!datesw && !text) {
		adios(EX_USAGE, NULL, "-nodate without -text is a no-op.");
	}
	if (number && text) {
		adios(EX_USAGE, NULL, "Don't combine -number with -text.");
	}

	if (file) {
		if (mode == MODE_LIST)
			annolist(file, comp, number);
		else
			annotate(file, comp, text, datesw, number,
					append, preserve);
		exit(EX_OK);
	}

	if (!msgs.size)
		app_msgarg(&msgs, seq_cur);
	if (!folder)
		folder = getcurfol();
	maildir = toabsdir(folder);

	if (chdir(maildir) == NOTOK)
		adios(EX_OSERR, maildir, "unable to change directory to");

	/* read folder and create message structure */
	if (!(mp = folder_read(folder)))
		adios(EX_IOERR, NULL, "unable to read folder %s", folder);

	/* check for empty folder */
	if (mp->nummsg == 0)
		adios(EX_DATAERR, NULL, "no messages in %s", folder);

	/* parse all the message ranges/sequences and set SELECTED */
	for (msgnum = 0; msgnum < msgs.size; msgnum++) {
		if (!m_convert(mp, msgs.msgs[msgnum])) {
			exit(EX_SOFTWARE);
		}
	}

	/* annotate all the SELECTED messages */
	for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
		if (is_selected(mp, msgnum)) {
			if (mode == MODE_LIST)
				annolist(m_name(msgnum), comp, number);
			else
				annotate(m_name(msgnum), comp, text, datesw,
						number, append, preserve);
		}
	}

	context_replace(curfolder, folder);
	seq_setcur(mp, mp->lowsel);
	seq_save(mp);
	folder_free(mp);
	context_save();
	return 0;
}

static void
make_comp(unsigned char **ap)
{
	unsigned char *cp;
	char buffer[BUFSIZ];

	if (!*ap) {
		printf("Enter component name: ");
		fflush(stdout);

		if (!fgets(buffer, sizeof buffer, stdin)) {
			exit(EX_IOERR);
		}
		*ap = trimcpy(buffer);
	}

	if ((cp = *ap + strlen(*ap) - 1) > *ap && *cp == ':')
		*cp = '\0';
	if (strlen(*ap) == 0)
		adios(EX_SOFTWARE, NULL, "null component name");
	if (**ap == '-')
		adios(EX_CONFIG, NULL, "invalid component name %s", *ap);
	if (strlen(*ap) >= NAMESZ)
		adios(EX_SOFTWARE, NULL, "too large component name %s", *ap);

	for (cp = *ap; *cp; cp++)
		if (!isalnum(*cp) && *cp != '-')
			adios(EX_CONFIG, NULL, "invalid component name %s", *ap);
}


/*
**  Produce a listing of all header fields (annotations) whose field
**  name matches comp.  Number the listing if number is set.
*/
static void
annolist(char *file, unsigned char *comp, int number)
{
	int c;
	int count = 1;  /* header field (annotation) counter */
	char *cp;
	char *field;
	int field_size;
	FILE *fp;
	int length;
	int n;  /* number of bytes written */

	if ((fp = fopen(file, "r")) == NULL) {
		adios(EX_IOERR, file, "unable to open");
	}

	/* We'll grow this buffer as needed. */
	field = mh_xcalloc(field_size = 256, sizeof(char));

	make_comp(&comp);
	length = strlen(comp); /* Convenience copy. */

	do {
		/*
		** Get a line from the input file, growing the field buffer
		** as needed.  We do this so that we can fit an entire line
		** in the buffer making it easy to do a string comparison
		** on both the field name and the field body which might be
		** a long path name.
		*/
		for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) {
			if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') {
				ungetc(c, fp);
				c = '\n';
				break;
			}
			if (++n >= field_size - 1) {
				field = mh_xrealloc(field, field_size += 256);
				cp = field + n - 1;
			}
		}
		*cp = '\0';

		if (strncasecmp(field, comp, length)==0 &&
				field[length] == ':') {
			cp = trim(field + length + 1);
			if (number) {
				printf("%d\t", count++);
			}
			printf("%s\n", cp);
		}

	} while (*field && *field != '-');

	mh_free0(&field);
	fclose(fp);

	return;
}


static int
annotate(char *file, unsigned char *comp, char *text, int datesw,
		int number, int append, int preserve)
{
	int fd;
	struct utimbuf b;
	int perms, tmpfd;
	char tmpfil[BUFSIZ];
	struct stat st;
	FILE *tmp;

	/* open and lock the file to be annotated */
	if ((fd = lkopen(file, O_RDWR, 0)) == NOTOK) {
		switch (errno) {
		case ENOENT:
			break;
		default:
			admonish(file, "unable to lock and open");
			break;
		}
		return 1;
	}

	if (stat(file, &st) == -1) {
		advise("can't get access and modification times for %s", file);
		preserve = 0;
	}
	b.actime = st.st_atime;
	b.modtime = st.st_mtime;

	perms = fstat(fd, &st) != NOTOK ?
			(int)(st.st_mode & 0777) : m_gmprot();

	strncpy(tmpfil, m_mktemp2(file, "annotate", NULL, &tmp),
			sizeof(tmpfil));
	chmod(tmpfil, perms);

	make_comp(&comp);

	if (mode == MODE_DEL) {
		dodel(fd, comp, text, tmp, number);
	}
	if (mode == MODE_ADD) {
		doadd(fd, comp, text, tmp, datesw, append);
	}

	cpydata(fd, fileno(tmp), file, tmpfil);
	fclose(tmp);

	if ((tmpfd = open(tmpfil, O_RDONLY)) == NOTOK) {
		adios(EX_IOERR, tmpfil, "unable to open for re-reading");
	}
	lseek(fd, (off_t) 0, SEEK_SET);

	/*
	**  We're making the file shorter if we're deleting a header field
	**  so the file has to be truncated or it will contain garbage.
	*/
	if (mode == MODE_DEL && ftruncate(fd, 0) == -1) {
		adios(EX_IOERR, tmpfil, "unable to truncate.");
	}
	cpydata(tmpfd, fd, tmpfil, file);
	close(tmpfd);
	unlink(tmpfil);

	if (preserve && utime(file, &b) == -1) {
		advise("can't set access and modification times for %s", file);
	}
	lkclose(fd, file);
	return 0;
}

/*
** We're trying to delete a header field (annotation).
**
** - If number is greater than zero,
**   we're deleting the nth header field that matches
**   the field (component) name.
** - If number is zero and text is NULL,
**   we're deleting the first field in which the field name
**   matches the component name.
** - If number is zero and text is set,
**   we're deleting the first field in which both the field name
**   matches the component name and the field body matches the text.
** - If number is -1,
**   we delete all matching fields.
*/
static void
dodel(int fd, unsigned char *comp, char *text, FILE *tmp, int number)
{
	int length = strlen(comp);  /* convenience copy */
	int count = 1;  /* Number of matching header line. */
	int c, n;
	char *cp;
	char *field = NULL;
	int field_size = 256;
	FILE *fp;

	/*
	** We're going to need to copy some of the message file to the
	** temporary file while examining the contents.  Convert the
	** message file descriptor to a file pointer since it's a lot
	** easier and more efficient to use stdio for this.  Also allocate
	** a buffer to hold the header components as they're read in.
	** This buffer is grown as needed later.
	*/
	if ((fp = fdopen(fd, "r")) == NULL) {
		adios(EX_IOERR, NULL, "unable to fdopen file.");
	}
	field = mh_xcalloc(field_size, sizeof(char));

	/*
	**  Copy lines from the input file to the temporary file
	**  until we either find the one that we're looking
	**  for (which we don't copy) or we reach the end of
	**  the headers.  Both a blank line and a line beginning
	**  with a - terminate the headers so that we can handle
	**  both drafts and RFC-2822 format messages.
	*/
	do {
		/*
		** Get a line from the input file, growing the
		** field buffer as needed.  We do this so that
		** we can fit an entire line in the buffer making
		** it easy to do a string comparison on both the
		** field name and the field body which might be
		** a long path name.
		*/
		for (n=0, cp=field; (c=getc(fp)) != EOF; *cp++ = c) {
			if (c == '\n') {
				ungetc((c = getc(fp)), fp);
				if (c != ' ' && c != '\t') {
					c = '\n';
					break;
				}
				c = '\n';
			}

			if (++n >= field_size - 1) {
				field = mh_xrealloc(field, field_size *= 2);
				cp = field + n - 1;
			}
		}
		*cp = '\0';

		if (strncasecmp(field, comp, length)==0 &&
				field[length] == ':') {
			/*
			** This component matches and thus is a candidate.
			** We delete the line by not copying it to the
			** temporary file. Thus:
			** - Break if we've found the one to delete.
			** - Continue if this is one to delete, but
			**   there'll be further ones.
			*/

			if (!number && !text) {
				/* this first one is it */
				break;
			}

			if (number == -1) {
				/* delete all of them */
				continue;
			} else if (number == count++) {
				/* delete this specific one */
				break;
			}

			if (text) {
				/* delete the first matching one */
				cp = field+length+1;
				while (*cp==' ' || *cp=='\t') {
					cp++;  /* eat leading whitespace */
				}
				if (*text == '/' && strcmp(text, cp)==0) {
					break;  /* full path matches */
				} else if (strcmp(text, mhbasename(cp))==0) {
					break;  /* basename matches */
				}
			}
			/*
			** Although the compoment name mached, it
			** wasn't the right one.
			*/
		}

		/* Copy it. */
		if ((n = fputs(field, tmp)) == EOF ||
				(c=='\n' && fputc('\n', tmp)==EOF)) {
			adios(EX_CANTCREAT, NULL, "unable to write temporary file.");
		}

	} while (*field && *field != '-');

	mh_free0(&field);

	fflush(tmp);
	fflush(fp); /* The underlying fd will be closed by lkclose() */

	/*
	** We've been messing with the input file position.  Move the
	** input file descriptor to the current place in the file
	** because the stock data copying routine uses the descriptor,
	** not the pointer.
	*/
	if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) {
		adios(EX_IOERR, NULL, "can't seek.");
	}
}


static void
doadd(int fd, unsigned char *comp, char *text, FILE *tmp, int datesw,
		int append)
{
	char *cp, *sp;
	int c;
	FILE *fp = NULL;

	if (append) {
		/*
		** We're going to need to copy some of the message
		** file to the temporary file while examining the
		** contents.  Convert the message file descriptor to
		** a file pointer since it's a lot easier and more
		** efficient to use stdio for this.  Also allocate
		** a buffer to hold the header components as they're
		** read in.  This buffer is grown as needed later.
		*/
		if ((fp = fdopen(fd, "r")) == NULL) {
			adios(EX_IOERR, NULL, "unable to fdopen file.");
		}
		/* Find the end of the headers. */
		if ((c = getc(fp)) == '\n') {
			/* Special check for no headers is needed. */
			rewind(fp);
		} else {
			/*
			** Copy lines from the input file to the
			** temporary file until we reach the end
			** of the headers.
			*/
			putc(c, tmp);
			while ((c = getc(fp)) != EOF) {
				putc(c, tmp);
				if (c == '\n') {
					ungetc(c = getc(fp), fp);
					if (c == '\n' || c == '-') {
						break;
					}
				}
			}
		}
	}

	if (datesw) {
		fprintf(tmp, "%s: %s\n", comp, dtimenow());
	}
	if ((cp = text)) {
		/* Add body text header */
		do {
			while (*cp == ' ' || *cp == '\t') {
				cp++;
			}
			sp = cp;
			while (*cp && *cp++ != '\n') {
				continue;
			}
			if (cp - sp) {
				fprintf(tmp, "%s: %*.*s", comp,
					(int)(cp - sp),
					(int)(cp - sp), sp);
			}
		} while (*cp);
		if (cp[-1] != '\n' && cp != text) {
			putc('\n', tmp);
		}
	}
	fflush(tmp);

	/*
	** We've been messing with the input file position.  Move the
	** input file descriptor to the current place in the file
	** because the stock data copying routine uses the descriptor,
	** not the pointer.
	*/
	if (append) {
		if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) {
			adios(EX_IOERR, NULL, "can't seek.");
		}
	}
}
