/*
** flist.c -- list nmh folders containing messages
**         -- in a given sequence
**
** originally by
** David Nichols, Xerox-PARC, November, 1992
**
** Copyright (c) 1994 Xerox Corporation.
** Use and copying of this software and preparation of derivative works based
** upon this software are permitted. Any distribution of this software or
** derivative works must comply with all applicable United States export
** control laws. This software is made available AS IS, and Xerox Corporation
** makes no warranty about the software, its performance or its conformity to
** any specification.
*/

#include <h/mh.h>
#include <h/utils.h>
#include <unistd.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>
#include <locale.h>
#include <sysexits.h>

/*
** We allocate space to record the names of folders
** (foldersToDo array), this number of elements at a time.
*/
#define MAXFOLDERS  100


static struct swit switches[] = {
#define SEQSW  0
	{ "sequence name", 0 },
#define ALLSW  1
	{ "all", 0 },
#define NOALLSW  2
	{ "noall", 2 },
#define RECURSE  3
	{ "recurse", 0 },
#define NORECURSE  4
	{ "norecurse", 2 },
#define SHOWZERO  5
	{ "showzero", 0 },
#define NOSHOWZERO  6
	{ "noshowzero", 2 },
#define ALPHASW  7
	{ "alpha", 0 },
#define NOALPHASW  8
	{ "noalpha", 2 },
#define FASTSW  9
	{ "fast", 0 },
#define NOFASTSW  10
	{ "nofast", 2 },
#define VERSIONSW  11
	{ "Version", 0 },
#define HELPSW  12
	{ "help", 0 },
	{ NULL, 0 }
};

char *version=VERSION;

struct Folder {
	char *name;  /* name of folder */
	int priority;
	int error;  /* error == 1 for unreadable folder */
	int nMsgs;  /* number of messages in folder */
	int nSeq[NUMATTRS];  /* number of messages in each sequence  */
	int private[NUMATTRS];  /* is given sequence, public or private */
};

static struct Folder *orders = NULL;
static int nOrders = 0;
static int nOrdersAlloced = 0;
static struct Folder *folders = NULL;
static unsigned int nFolders = 0;
static int nFoldersAlloced = 0;

/* info on folders to search */
static char **foldersToDo;
static int numfolders;
static int maxfolders;

/* info on sequences to search for */
static char *sequencesToDo[NUMATTRS];
static unsigned int numsequences;

static int all = FALSE;  /* scan all folders in top level? */
static int alphaOrder = FALSE;  /* want alphabetical order only */
static int recurse = FALSE;  /* show nested folders? */
static int showzero   = TRUE;  /* show folders even if no messages in seq? */
static int fastsw = FALSE;  /* display info on number of messages in
		* sequence found, and total num messages */

static char curfol[BUFSIZ];  /* name of the current folder */
static char *nmhdir;  /* base nmh mail directory */

/*
** Type for a compare function for qsort.  This keeps
** the compiler happy.
*/
typedef int (*qsort_comp) (const void *, const void *);

/*
** prototypes
*/
int CompareFolders(struct Folder *, struct Folder *);
void GetFolderOrder(void);
void ScanFolders(void);
int AddFolder(char *, int);
void BuildFolderList(char *, int);
void BuildFolderListRecurse(char *, struct stat *, int);
void PrintFolders(void);
void AllocFolders(struct Folder **, int *, int);
int AssignPriority(char *);
static void do_readonly_folders(void);



int
main(int argc, char **argv)
{
	char *cp, **argp;
	char **arguments;
	char buf[BUFSIZ];

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

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

	/*
	** If program was invoked with name ending
	** in `s', then add switch `-all'.
	*/
	if (argv[0][strlen(argv[0]) - 1] == 's')
		all = TRUE;

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

	/* allocate the initial space to record the folder names */
	numfolders = 0;
	maxfolders = MAXFOLDERS;
	foldersToDo = mh_xcalloc(maxfolders, sizeof(*foldersToDo));

	/* no sequences yet */
	numsequences = 0;

	/* 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(buf, sizeof(buf), "%s [+folder1 [+folder2 ...]][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 SEQSW:
				if (!(cp = *argp++) || *cp == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);

				/* check if too many sequences specified */
				if (numsequences >= NUMATTRS)
					adios(EX_USAGE, NULL, "too many sequences (more than %d) specified", NUMATTRS);
				sequencesToDo[numsequences++] = cp;
				break;

			case ALLSW:
				all = TRUE;
				break;
			case NOALLSW:
				all = FALSE;
				break;

			case SHOWZERO:
				showzero = TRUE;
				break;
			case NOSHOWZERO:
				showzero = FALSE;
				break;

			case ALPHASW:
				alphaOrder = TRUE;
				break;
			case NOALPHASW:
				alphaOrder = FALSE;
				break;

			case FASTSW:
				fastsw = TRUE;
				break;
			case NOFASTSW:
				fastsw = FALSE;
				break;

			case RECURSE:
				recurse = TRUE;
				break;
			case NORECURSE:
				recurse = FALSE;
				break;
			}
		} else {
			/*
			** Check if we need to allocate more space
			** for folder names.
			*/
			if (numfolders >= maxfolders) {
				maxfolders += MAXFOLDERS;
				foldersToDo = mh_xrealloc(foldersToDo, maxfolders * sizeof(*foldersToDo));
			}
			if (*cp == '+' || *cp == '@') {
				foldersToDo[numfolders++] = mh_xstrdup(expandfol(cp));
			} else
				foldersToDo[numfolders++] = cp;
		}
	}

	/* get current folder */
	strncpy(curfol, getcurfol(), sizeof(curfol));

	/* get nmh base directory */
	nmhdir = toabsdir("+");

	/*
	** If no sequences specified, we use the `unseen' sequence(s)
	** We check to make sure that the Unseen-Sequence entry doesn't
	** contain too many sequences.
	*/
	if (numsequences == 0) {
		char **ap, *dp;

		if ((cp = context_find(usequence))) {
			if (!*cp) {
				adios(EX_CONFIG, NULL, "profile entry %s set, but empty, and no sequence given", usequence);
			}
		} else {
			cp = seq_unseen;  /* use default */
		}
		dp = mh_xstrdup(cp);
		ap = brkstring(dp, " ", "\n");
		for (; ap && *ap; ap++) {
			if (numsequences >= NUMATTRS) {
				adios(EX_USAGE, NULL, "too many sequences (more than %d) in %s profile entry", NUMATTRS, usequence);
			} else {
				sequencesToDo[numsequences++] = *ap;
			}
		}
	}

	GetFolderOrder();
	ScanFolders();
	qsort(folders, nFolders, sizeof(struct Folder),
			(qsort_comp) CompareFolders);
	PrintFolders();
	return EX_OK;
}

/*
** Read the Flist-Order profile entry to determine
** how to sort folders for output.
*/

void
GetFolderOrder(void)
{
	unsigned char *p, *s;
	int priority = 1;
	struct Folder *o;

	if (!(p = context_find("Flist-Order")))
		return;
	for (;;) {
		while (isspace(*p))
			++p;
		s = p;
		while (*p && !isspace(*p))
			++p;
		if (p != s) {
			/* Found one. */
			AllocFolders(&orders, &nOrdersAlloced, nOrders + 1);
			o = &orders[nOrders++];
			o->priority = priority++;
			o->name = mh_xcalloc(p - s + 1, sizeof(char));
			strncpy(o->name, s, p - s);
			o->name[p - s] = 0;
		} else
			break;
	}
}

/*
** Scan all the necessary folders
*/

void
ScanFolders(void)
{
	int i;

	/*
	 * change directory to base of nmh directory
	 */
	if (chdir(nmhdir) == NOTOK)
		adios(EX_OSERR, nmhdir, "unable to change directory to");

	if (numfolders > 0) {
		/* Update context */
		strncpy(curfol, foldersToDo[numfolders - 1], sizeof(curfol));
		context_replace(curfolder, curfol); /* update current folder */
		context_save();  /* save the context file */

		/*
		** Scan each given folder.  If -all is given,
		** then also scan the 1st level subfolders under
		** each given folder.
		*/
		for (i = 0; i < numfolders; ++i)
			BuildFolderList(foldersToDo[i], all ? 1 : 0);
	} else {
		if (all) {
			/* Do the readonly folders */
			do_readonly_folders();

			/* Now scan the entire nmh directory for folders */
			BuildFolderList(".", 0);
		} else {
			/* Else scan current folder */
			BuildFolderList(curfol, 0);
		}
	}
}

/*
** Initial building of folder list for
** the top of our search tree.
*/

void
BuildFolderList(char *dirName, int searchdepth)
{
	struct stat st;

	/* Make sure we have a directory */
	if ((stat(dirName, &st) == -1) || !S_ISDIR(st.st_mode))
		return;

	/*
	** If base directory, don't add it to the
	** folder list. We just recurse into it.
	*/
	if (strcmp(dirName, ".")==0) {
		BuildFolderListRecurse(".", &st, 0);
		return;
	}

	/*
	** Add this folder to the list.
	** If recursing and directory has subfolders,
	** then build folder list for subfolders.
	*/
	if (AddFolder(dirName, showzero) && (recurse || searchdepth) &&
			st.st_nlink > 2)
		BuildFolderListRecurse(dirName, &st, searchdepth - 1);
}

/*
** Recursive building of folder list
*/

void
BuildFolderListRecurse(char *dirName, struct stat *s, int searchdepth)
{
	char *base, name[PATH_MAX];
	unsigned char *n;
	int nlinks;
	DIR *dir;
	struct dirent *dp;
	struct stat st;

	/*
	** Keep track of number of directories we've seen so we can
	** stop stat'ing entries in this directory once we've seen
	** them all.  This optimization will fail if you have extra
	** directories beginning with ".", since we don't bother to
	** stat them.  But that shouldn't generally be a problem.
	*/
	nlinks = s->st_nlink;

	if (!(dir = opendir(dirName)))
		adios(EX_IOERR, dirName, "can't open directory");

	/*
	** A hack so that we don't see a
	** leading "./" in folder names.
	*/
	base = (strcmp(dirName, ".")==0) ? dirName + 1 : dirName;

	while (nlinks && (dp = readdir(dir))) {
		if (strcmp(dp->d_name, ".")==0 ||
				strcmp(dp->d_name, "..")==0) {
			nlinks--;
			continue;
		}
		if (dp->d_name[0] == '.')
			continue;
		/* Check to see if the name of the file is a number
		** if it is, we assume it's a mail file and skip it
		*/
		for (n = dp->d_name; *n && isdigit(*n); n++);
		if (!*n)
			continue;
		strncpy(name, base, sizeof(name) - 2);
		if (*base)
			strcat(name, "/");
		strncat(name, dp->d_name, sizeof(name) - strlen(name) - 1);
		if ((stat(name, &st) != -1) && S_ISDIR(st.st_mode)) {
			/*
			** Check if this was really a symbolic link pointing
			** to a directory.  If not, then decrement link count.
			*/
			if (lstat(name, &st) == -1)
				nlinks--;
			/* Add this folder to the list */
			if (AddFolder(name, showzero) &&
					(recurse || searchdepth) &&
					st.st_nlink > 2)
				BuildFolderListRecurse(name, &st,
						searchdepth - 1);
		}
	}
	closedir(dir);
}

/*
** Add this folder to our list, counting the total number of
** messages and the number of messages in each sequence.
*/

int
AddFolder(char *name, int force)
{
	unsigned int i;
	int msgnum, nonzero;
	int seqnum[NUMATTRS], nSeq[NUMATTRS];
	struct Folder *f;
	struct msgs *mp;

	/* Read folder and create message structure */
	if (!(mp = folder_read(name))) {
		/* Oops, error occurred.  Record it and continue. */
		AllocFolders(&folders, &nFoldersAlloced, nFolders + 1);
		f = &folders[nFolders++];
		f->name = mh_xstrdup(name);
		f->error = 1;
		f->priority = AssignPriority(f->name);
		return 0;
	}

	for (i = 0; i < numsequences; i++) {
		/* Convert sequences to their sequence numbers */
		if (sequencesToDo[i])
			seqnum[i] = seq_getnum(mp, sequencesToDo[i]);
		else
			seqnum[i] = -1;

		/* Now count messages in this sequence */
		nSeq[i] = 0;
		if (mp->nummsg > 0 && seqnum[i] != -1) {
			for (msgnum = mp->lowmsg; msgnum <= mp->hghmsg;
					msgnum++) {
				if (in_sequence(mp, seqnum[i], msgnum))
					nSeq[i]++;
			}
		}
	}

	/* Check if any of the sequence checks were nonzero */
	nonzero = 0;
	for (i = 0; i < numsequences; i++) {
		if (nSeq[i] > 0) {
			nonzero = 1;
			break;
		}
	}

	if (nonzero || force) {
		/* save general folder information */
		AllocFolders(&folders, &nFoldersAlloced, nFolders + 1);
		f = &folders[nFolders++];
		f->name = mh_xstrdup(name);
		f->nMsgs = mp->nummsg;
		f->error = 0;
		f->priority = AssignPriority(f->name);

		/* record the sequence information */
		for (i = 0; i < numsequences; i++) {
			f->nSeq[i] = nSeq[i];
			f->private[i] = (seqnum[i] != -1) ?
					is_seq_private(mp, seqnum[i]) : 0;
		}
	}

	folder_free(mp);  /* free folder/message structure */
	return 1;
}

/*
** Print the folder/sequence information
*/

void
PrintFolders(void)
{
	char tmpname[BUFSIZ];
	unsigned int i, j, len, has_private = 0;
	unsigned int maxfolderlen = 0, maxseqlen = 0;
	int maxnum = 0, maxseq = 0;

	if (fastsw) {
		for (i = 0; i < nFolders; i++)
			printf("%s\n", folders[i].name);
		return;
	}

	/*
	** Find the width we need for various fields
	*/
	for (i = 0; i < nFolders; ++i) {
		/* find the length of longest folder name */
		len = strlen(folders[i].name);
		if (len > maxfolderlen)
			maxfolderlen = len;

		/* If folder had error, skip the rest */
		if (folders[i].error)
			continue;

		/* find the maximum total messages */
		if (folders[i].nMsgs > maxnum)
			maxnum = folders[i].nMsgs;

		for (j = 0; j < numsequences; j++) {
			/* find maximum width of sequence name */
			len = strlen(sequencesToDo[j]);
			if ((folders[i].nSeq[j] > 0 || showzero) &&
					(len > maxseqlen))
				maxseqlen = len;

			/* find the maximum number of messages in sequence */
			if (folders[i].nSeq[j] > maxseq)
				maxseq = folders[i].nSeq[j];

			/*
			** check if this sequence is private in any of
			** the folders
			*/
			if (folders[i].private[j])
				has_private = 1;
		}
	}

	/* Now print all the folder/sequence information */
	for (i = 0; i < nFolders; i++) {
		for (j = 0; j < numsequences; j++) {
			if (folders[i].nSeq[j] > 0 || showzero) {
				/* Add `+' to end of name of current folder */
				if (strcmp(curfol, folders[i].name)!=0)
					snprintf(tmpname, sizeof(tmpname),
							"%s", folders[i].name);
				else
					snprintf(tmpname, sizeof(tmpname),
							"%s+",
							folders[i].name);

				if (folders[i].error) {
					printf("%-*s is unreadable\n",
							maxfolderlen+1,
							tmpname);
					continue;
				}

				printf("%-*s has %*d in sequence %-*s%s; out of %*d\n", maxfolderlen+1, tmpname, num_digits(maxseq), folders[i].nSeq[j], maxseqlen, sequencesToDo[j], !has_private ? "" : folders[i].private[j] ? " (private)" : "          ", num_digits(maxnum), folders[i].nMsgs);
			}
		}
	}
}

/*
** Put them in priority order.
*/

int
CompareFolders(struct Folder *f1, struct Folder *f2)
{
	if (!alphaOrder && f1->priority != f2->priority)
		return f1->priority - f2->priority;
	else
		return strcmp(f1->name, f2->name);
}

/*
** Make sure we have at least n folders allocated.
*/

void
AllocFolders(struct Folder **f, int *nfa, int n)
{
	if (n <= *nfa)
		return;
	if (*f == NULL) {
		*nfa = 10;
		*f = mh_xcalloc(*nfa, sizeof(struct Folder));
	} else {
		*nfa *= 2;
		*f = mh_xrealloc(*f, *nfa * sizeof(struct Folder));
	}
}

/*
** Return the priority for a name.  The highest comes from an exact match.
** After that, the longest match (then first) assigns the priority.
*/
int
AssignPriority(char *name)
{
	int i, ol, nl;
	int best = nOrders;
	int bestLen = 0;
	struct Folder *o;

	nl = strlen(name);
	for (i = 0; i < nOrders; ++i) {
		o = &orders[i];
		if (strcmp(name, o->name)==0)
			return o->priority;
		ol = strlen(o->name);
		if (nl < ol - 1)
			continue;
		if (ol < bestLen)
			continue;
		if (o->name[0] == '*' &&
				strcmp(o->name + 1, name + (nl - ol + 1))==0) {
			best = o->priority;
			bestLen = ol;
		} else if (o->name[ol - 1] == '*' &&
				strncmp(o->name, name, ol - 1) == 0) {
			best = o->priority;
			bestLen = ol;
		}
	}
	return best;
}

/*
** Do the read only folders
*/

static void
do_readonly_folders(void)
{
	int atrlen;
	char atrcur[BUFSIZ];
	struct node *np;

	snprintf(atrcur, sizeof(atrcur), "atr-%s-", seq_cur);
	atrlen = strlen(atrcur);

	for (np = m_defs; np; np = np->n_next)
		if (strncmp(np->n_name, atrcur, atrlen)==0
				&& strncmp(np->n_name+atrlen, nmhdir, strlen(nmhdir))!=0)
			/* Why do we exclude absolute path names? --meillo */
			BuildFolderList(np->n_name + atrlen, 0);
}
