/*
** new.c -- as new,    list all folders with unseen messages
**       -- as fnext,  move to next folder with unseen messages
**       -- as fprev,  move to previous folder with unseen messages
**       -- as unseen, scan all unseen messages
** This code is Copyright (c) 2008, by the authors of nmh.  See the
** COPYRIGHT file in the root directory of the nmh distribution for
** complete copyright information.
**
** Inspired by Luke Mewburn's new: http://www.mewburn.net/luke/src/new
*/

#include <h/mh.h>
#include <h/crawl_folders.h>
#include <h/utils.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <locale.h>
#include <sysexits.h>

static struct swit switches[] = {
#define MODESW 0
	{ "mode", 0 },
#define FOLDERSSW 1
	{ "folders", 0 },
#define VERSIONSW 2
	{ "Version", 0 },
#define HELPSW 3
	{ "help", 0 },
	{ NULL, 0 }
};

char *version=VERSION;

static enum { NEW, FNEXT, FPREV, UNSEEN } run_mode = NEW;

/*
** check_folders uses this to maintain state with both .folders list of
** folders and with crawl_folders.
*/
struct list_state {
	struct node **first, **cur_node;
	size_t *maxlen;
	char *cur;
	char **sequences;
	struct node *node;
};

/* Return the number of messages in a string list of message numbers. */
static int
count_messages(char *field)
{
	int total = 0;
	int j, k;
	char *cp, **ap;

	field = mh_xstrdup(field);

	/* copied from seq_read.c:seq_init */
	for (ap = brkstring(field, " ", "\n"); *ap; ap++) {
		if ((cp = strchr(*ap, '-')))
			*cp++ = '\0';
		if ((j = m_atoi(*ap)) > 0) {
			k = cp ? m_atoi(cp) : j;

			total += k - j + 1;
		}
	}

	mh_free0(&field);

	return total;
}

/* Return TRUE if the sequence 'name' is in 'sequences'. */
static boolean
seq_in_list(char *name, char *sequences[])
{
	int i;

	for (i = 0; sequences[i] != NULL; i++) {
		if (strcmp(name, sequences[i]) == 0) {
			return TRUE;
		}
	}

	return FALSE;
}

/*
** Return the string list of message numbers from the sequences file,
** or NULL if none.
*/
static char *
get_msgnums(char *folder, char *sequences[])
{
	enum state state;
	struct field f = {{0}};
	char *seqfile = concat(toabsdir(folder), "/", mh_seq, (void *)NULL);
	FILE *fp = fopen(seqfile, "r");
	char *msgnums = NULL, *this_msgnums, *old_msgnums;

	/* no sequences file -> no messages */
	if (fp == NULL) {
		return NULL;
	}

	for (state = FLD2;;) {
		switch (state = m_getfld2(state, &f, fp)) {
		case LENERR2:
			state = FLD2;
			/* FALL */

		case FLD2:
			/*
			** if it's in a sequence we want,
			** save the list of messages.
			*/
			if (seq_in_list(f.name, sequences)) {
				this_msgnums = trimcpy(f.value);
				if (strlen(this_msgnums) == 0) {
					free(this_msgnums);
					continue;
				} else if (msgnums == NULL) {
					msgnums = this_msgnums;
				} else {
					old_msgnums = msgnums;
					msgnums = concat(old_msgnums, " ",
							this_msgnums,
							NULL);
					mh_free0(&old_msgnums);
					mh_free0(&this_msgnums);
				}
			}
			continue;

		case BODY2:
			adios(EX_DATAERR, NULL, "no blank lines are permitted in %s", seqfile);
			/* FALL */

		case FILEEOF2:
			break;

		default:
			adios(EX_SOFTWARE, NULL, "%s is poorly formatted", seqfile);
		}
		break;
	}

	fclose(fp);

	return msgnums;
}

/*
** Check `folder' (of length `len') for interesting messages,
** filling in the list in `b'.
*/
static void
check_folder(char *folder, size_t len, struct list_state *b)
{
	char *msgnums = get_msgnums(folder, b->sequences);
	int is_cur = strcmp(folder, b->cur) == 0;

	if (is_cur || msgnums != NULL) {
		if (*b->first == NULL) {
			*b->first = b->node = mh_xcalloc(1, sizeof(*b->node));
		} else {
			b->node->n_next = mh_xcalloc(1, sizeof(*b->node));
			b->node = b->node->n_next;
		}
		b->node->n_name = folder;
		b->node->n_field = msgnums;

		if (*b->maxlen < len) {
			*b->maxlen = len;
		}
	}

	/* Save the node for the current folder, so we can fall back to it. */
	if (is_cur) {
		*b->cur_node = b->node;
	}
}

static boolean
crawl_callback(char *folder, void *baton)
{
	check_folder(folder, strlen(folder), baton);
	return TRUE;
}

/*
** Scan folders, returning:
** first        -- list of nodes for all folders which have desired messages;
**                 if the current folder is listed in .folders, it is also in
**                 the list regardless of whether it has any desired messages
** last         -- last node in list
** cur_node     -- node of current folder, if listed in .folders
** maxlen       -- length of longest folder name
**
** `cur' points to the name of the current folder, `folders' points to the
** name of a .folder (if NULL, crawl all folders), and `sequences' points to
** the array of sequences for which to look.
**
** An empty list is returned as first=last=NULL.
*/
static void
check_folders(struct node **first, struct node **last,
	struct node **cur_node, size_t *maxlen,
	char *cur, char *folders, char *sequences[])
{
	struct list_state b;
	FILE *fp;
	char *line;
	size_t len;

	*first = *last = *cur_node = NULL;
	*maxlen = 0;

	b.first = first;
	b.cur_node = cur_node;
	b.maxlen = maxlen;
	b.cur = cur;
	b.sequences = sequences;

	if (folders == NULL) {
		chdir(toabsdir("+"));
		crawl_folders(".", crawl_callback, &b);
	} else {
		fp = fopen(folders, "r");
		if (fp  == NULL) {
			adios(EX_IOERR, NULL, "failed to read %s", folders);
		}
		while (vfgets(fp, &line) == OK) {
			len = strlen(line) - 1;
			line[len] = '\0';
			check_folder(mh_xstrdup(line), len, &b);
		}
		fclose(fp);
	}

	if (*first != NULL) {
		b.node->n_next = NULL;
		*last = b.node;
	}
}

/* Return a single string of the `sequences' joined by a space (' '). */
static char *
join_sequences(char *sequences[])
{
	int i;
	size_t len = 0;
	char *result, *cp;

	for (i = 0; sequences[i] != NULL; i++) {
		len += strlen(sequences[i]) + 1;
	}
	result = mh_xcalloc(len + 1, sizeof(char));

	for (i = 0, cp = result; sequences[i] != NULL; i++, cp += len + 1) {
		len = strlen(sequences[i]);
		memcpy(cp, sequences[i], len);
		cp[len] = ' ';
	}
	/* -1 to overwrite the last delimiter */
	*--cp = '\0';

	return result;
}

/*
** Return a struct node for the folder to change to.  This is the next
** (previous, if FPREV mode) folder with desired messages, or the current
** folder if no folders have desired.  If NEW or UNSEEN mode, print the
** output but don't change folders.
**
** n_name is the folder to change to, and n_field is the string list of
** desired message numbers.
*/
static struct node *
doit(char *cur, char *folders, char *sequences[])
{
	struct node *first, *cur_node, *node, *last = NULL, *prev;
	size_t folder_len;
	int count, total = 0;
	char *sequences_s = NULL;
	int argc = 0;
	char *argv[MAXARGS];
	char **seqp;
	char buf[BUFSIZ];

	if (cur == NULL || cur[0] == '\0') {
		cur = "inbox";
	}

	check_folders(&first, &last, &cur_node, &folder_len, cur,
			folders, sequences);

	if (run_mode == FNEXT || run_mode == FPREV) {
		if (first == NULL) {
			/* No folders at all... */
			return NULL;
		} else if (first->n_next == NULL) {
			/*
			** We have only one node; any desired messages in it?
			*/
			if (first->n_field == NULL) {
				return NULL;
			} else {
				return first;
			}
		} else if (cur_node == NULL) {
			/*
			** Current folder is not listed in .folders,
			** return first.
			*/
			return first;
		}
	} else if (run_mode == UNSEEN) {
		sequences_s = join_sequences(sequences);
	}

	for (node = first, prev = NULL;
		 node != NULL;
		 prev = node, node = node->n_next) {
		if (run_mode == FNEXT) {
			/*
			** If we have a previous node and it is the current
			** folder, return this node.
			*/
			if (prev != NULL && strcmp(prev->n_name, cur) == 0) {
				return node;
			}
		} else if (run_mode == FPREV) {
			if (strcmp(node->n_name, cur) == 0) {
				/*
				** Found current folder in fprev mode;
				** if we have a previous node in the list,
				** return it; else return the last node.
				*/
				if (prev == NULL) {
					return last;
				}
				return prev;
			}
		} else if (run_mode == UNSEEN) {
			if (node->n_field == NULL) {
				continue;
			}

			printf("\n%d %s messages in %s",
				   count_messages(node->n_field),
				   sequences_s,
				   node->n_name);
			if (strcmp(node->n_name, cur) == 0) {
				puts(" (*: current folder)");
			} else {
				puts("");
			}
			fflush(stdout);

			argc = 0;
			argv[argc++] = "scan";
			snprintf(buf, sizeof buf, "+%s", node->n_name);
			argv[argc++] = buf;
			for (seqp=sequences; *seqp; seqp++) {
				argv[argc++] = *seqp;
			}
			argv[argc] = (char *)NULL;
			execprog(*argv, argv);
		} else {
			if (node->n_field == NULL) {
				continue;
			}

			count = count_messages(node->n_field);
			total += count;

			printf("%-*s %6d.%c %s\n", (int) folder_len,
				node->n_name, count,
				(strcmp(node->n_name, cur) == 0 ? '*' : ' '),
				node->n_field);
		}
	}

	/*
	** If we're fnext, we haven't checked the last node yet.  If it's the
	** current folder, return the first node.
	*/
	if (run_mode == FNEXT && strcmp(last->n_name, cur) == 0) {
		return first;
	}

	if (run_mode == NEW) {
		printf("%-*s %6d.\n", (int) folder_len, " total", total);
	}

	return cur_node;
}

int
main(int argc, char **argv)
{
	char **ap, *cp, **argp, **arguments;
	char help[BUFSIZ];
	char *folders = NULL;
	char *sequences[NUMATTRS + 1];
	int i = 0;
	char *unseen;
	struct node *folder;

	sequences[0] = NULL;
	sequences[1] = NULL;

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

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

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

	/*
	** Parse 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(help, sizeof(help),
						"%s [switches] [sequences]",
						invo_name);
				print_help(help, switches, 1);
				exit(argc == 2 ? EX_OK : EX_USAGE);
			case VERSIONSW:
				print_version(invo_name);
				exit(argc == 2 ? EX_OK : EX_USAGE);

			case FOLDERSSW:
				if (!(folders = *argp++) || *folders == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				continue;
			case MODESW:
				if (!(invo_name = *argp++) || *invo_name == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				invo_name = mhbasename(invo_name);
				continue;
			}
		}
		/* have a sequence argument */
		if (!seq_in_list(cp, sequences)) {
			sequences[i++] = cp;
			sequences[i] = NULL;
		}
	}

	if (strcmp(invo_name, "fnext") == 0) {
		run_mode = FNEXT;
	} else if (strcmp(invo_name, "fprev") == 0) {
		run_mode = FPREV;
	} else if (strcmp(invo_name, "unseen") == 0) {
		run_mode = UNSEEN;
	}

	if (folders == NULL) {
		/* will flists */
	} else {
		if (folders[0] != '/') {
			folders = toabsdir(folders);
		}
	}

	if (i == 0) {
		char *dp;
		/* no sequence arguments; use unseen */
		if ((unseen = context_find(usequence))) {
			if (!*unseen) {
				adios(EX_CONFIG, NULL, "profile entry %s set, but empty, and no sequences given", usequence);
			}
		} else {
			unseen = seq_unseen;  /* use default */
		}
		dp = mh_xstrdup(unseen);
		for (ap = brkstring(dp, " ", "\n"); *ap; ap++) {
			sequences[i++] = *ap;
		}
	}
	sequences[i] = NULL;

	folder = doit(context_find(curfolder), folders, sequences);
	if (folder == NULL) {
		exit(EX_OK);
		return 1;
	}

	if (run_mode == UNSEEN) {
		/*
		** All the scan(1)s it runs change the current folder, so we
		** need to put it back.  Unfortunately, context_replace lamely
		** ignores the new value you give it if it is the same one it
		** has in memory.  So, we'll be lame, too.  I'm not sure if i
		** should just change context_replace...
		*/
		context_replace(curfolder, "defeat_context_replace_optimization");
	}

	/* update current folder */
	context_replace(curfolder, folder->n_name);

	if (run_mode == FNEXT || run_mode == FPREV) {
		printf("%s  %s\n", folder->n_name, folder->n_field);
	}

	context_save();

	return 0;
}
