/*
 * 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.
 *
 *  $Id$
 */

#include <h/mh.h>

#define FALSE   0
#define TRUE    1

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

struct Folder {
    char *name;		/* name of folder */
    int priority;
    int private;	/* is the sequence, public or private   */
    int nMsgs;		/* number of messages in folder         */
    int nSeq;		/* number of messages in given sequence */
};

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

static char **foldersToDo;
static int nFoldersToDo;

static int toplevel   = 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 Total      = TRUE;	/* display info on number of messages in    *
				 * sequence found, and total num messages   */

static char *sequence;		/* name of sequence we are searching for    */
static char curfolder[BUFSIZ];	/* name of the current folder               */
static char *maildir;		/* 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 ScanMailDir(void);
void AddFolder(char *, int);
void BuildFolderList(char *);
void BuildFolderListRecurse(char *, struct stat *);
void PrintFolders(void);
static int num_digits (int);
void AllocFolders(struct Folder **, int *, int);
int AssignPriority(char *);
static void do_readonly_folders(void);



int
main(int argc, char **argv)
{
    char *cp, **ap;
    char **argp, **lastArg;
    char *arguments[MAXARGS];
    char buf[100];

#ifdef LOCALE
    setlocale(LC_ALL, "");
#endif
    invo_name = r1bindex(argv[0], '/');

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

    if ((cp = context_find(invo_name)) != NULL) {
	ap = brkstring(cp = getcpy(cp), " ", "\n");
	ap = copyip(ap, arguments);
    } else {
	ap = arguments;
    }
    lastArg = copyip(argv + 1, ap);
    argp = arguments;
    argc = lastArg - argp;
    foldersToDo = (char **) malloc(argc * sizeof(char *));
    nFoldersToDo = 0;

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

	    case HELPSW:
		sprintf(buf, "%s [+folder1 [+folder2 ...]][switches]", invo_name);
		print_help(buf, switches);
		done(1);
	    case VERSIONSW:
		print_version(invo_name);
		done (1);

	    case SEQSW:
		if (!(cp = *argp++) || *cp == '-')
		    adios (NULL, "missing argument to %s", argp[-2]);
		sequence = cp;
		break;

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

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

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

	    case TOTAL:
		Total = TRUE;
		break;
	    case NOTOTAL:
		Total = FALSE;
		break;

	    case RECURSE:
		recurse = TRUE;
		break;
	    case NORECURSE:
		recurse = FALSE;
		break;
	    }
	} else {
	    if (*cp == '+')
		++cp;
	    foldersToDo[nFoldersToDo++] = cp;
	}
    }

    if (!context_find ("path"))
        free (path ("./", TFOLDER));

    /*
     * If we didn't specify a sequence, we search
     * for the `unseen' sequence.
     */
    if (!sequence)
	sequence = context_find(usequence);

    strcpy(curfolder, getfolder(1));	/* get current folder     */
    maildir = m_maildir ("");		/* get nmh base directory */

    GetFolderOrder();
    ScanMailDir();
    qsort(folders, nFolders, sizeof(struct Folder), (qsort_comp) CompareFolders);
    PrintFolders();
}

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

void
GetFolderOrder(void)
{
    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 = (char *) malloc(p - s + 1);
	    strncpy(o->name, s, p - s);
	    o->name[p - s] = 0;
	} else
	    break;
    }
}

/*
 * Begin building the list of folders
 * to search by scanning
 *   1) The given list of folders (or)
 *   2) The top level of our Mail directory (or)
 *   3) The current folder.
 */

void
ScanMailDir(void)
{
    int i;
    char *home, *path;
    DIR *dir;
    struct dirent *dp;

    /*
     * Did we specify which folders to check?
     */
    if (nFoldersToDo > 0) {
	for (i = 0; i < nFoldersToDo; ++i)
	    BuildFolderList(foldersToDo[i]);
    } else if (toplevel) {
	/*
	 * Do the readonly folders
	 */
	do_readonly_folders();

	/*
	 * Now scan all of top level folders of nmh directory
	 */
	if (!(home = getenv("HOME")) || (chdir(home) < 0))
	    adios(NULL, "Can't change to home directory.");
	if (!(path = context_find ("path")))
	    adios(NULL, "Can't find Mail directory.");
	if (!(dir = opendir(path)))
	    adios(NULL, "Can't open Path");

	while ((dp = readdir(dir))) {
	    if (dp->d_name[0] == '.')
		continue;
	    BuildFolderList(dp->d_name);
	}
    } else {
	/*
	 * Else scan current folder
	 */
	BuildFolderList(curfolder);
    }
}

/*
 * Initial building of folder list for top level folders
 */

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

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

    /* Add this folder to the list */
    AddFolder(dirName, showzero);

    /*
     * If recursing and directory has subfolders,
     * then build folder list for subfolders.
     */
    if (recurse && st.st_nlink > 2)
	BuildFolderListRecurse(dirName, &st);
}

/*
 * Recursive building of folder list for subfolders
 */

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

    /*
     * 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(m_mailpath(dirName))))
	adios(NULL, "Error while scanning Mail directory");

    while (nlinks && (dp = readdir(dir))) {
	if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) {
	    nlinks--;
	    continue;
	}
	if (dp->d_name[0] == '.')
	    continue;
	strcpy(name, dirName);
	if (*dirName)
	    strcat(name, "/");
	strcat(name, dp->d_name);
	if ((stat(m_mailpath(name), &st) != -1) && S_ISDIR(st.st_mode)) {
	    /* Add this folder to the list */
	    AddFolder(name, showzero);
	    nlinks--;
	    if (st.st_nlink > 2)
		BuildFolderListRecurse(name, &st);
	}
    }
    closedir(dir);
}

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

void
AddFolder(char *name, int force)
{
    int msgnum, seqnum, nSeq;
    struct Folder *f;
    struct msgs *mp;

    /* read folder and create message structure */
    if (!(mp = folder_read (name))) {
	advise(NULL, "Can't read folder %s.", name);
	return;
    }

    nSeq = 0;

    /* Count the number of messages in the sequence */
    if (sequence && ((seqnum = seq_getnum(mp, sequence)) != -1)) {
	for (msgnum = mp->lowmsg; msgnum <= mp->hghmsg; msgnum++) {
	    if (in_sequence(mp, seqnum, msgnum))
		nSeq++;
	}
    }

    /* Save folder/message information in Folder struct */
    if (nSeq > 0 || force) {
	AllocFolders(&folders, &nFoldersAlloced, nFolders + 1);
	f = &folders[nFolders++];
	f->name = getcpy(name);
	f->private = (seqnum != -1) ? is_seq_private(mp, seqnum) : 0;
	f->nMsgs = mp->nummsg;
	f->nSeq = nSeq;
	f->priority = AssignPriority(f->name);
    }

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

/*
 * Print the folder/sequence information
 */

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

    if (!Total) {
	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 > maxlen)
	    maxlen = len;

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

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

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

    for (i = 0; i < nFolders; ++i) {
	/* Add `+' to end of name of current folder */
	if (strcmp(curfolder, folders[i].name))
	    sprintf(tmpname, "%s", folders[i].name);
	else
	    sprintf(tmpname, "%s+", folders[i].name);

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

/*
 * Calculate the number of digits in positive integer
 */
static int
num_digits (int n)
{
    int ndigits = 0;

    while (n) {
	n /= 10;
	ndigits++;
    }

    return ndigits;
}

/*
 * 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 = (struct Folder *) malloc(*nfa * (sizeof(struct Folder)));
    } else {
	*nfa *= 2;
	*f = (struct Folder *) realloc(*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))
	    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))) {
	    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];
    register struct node *np;

    sprintf (atrcur, "atr-%s-", current);
    atrlen = strlen (atrcur);

    context_read ();
    for (np = m_defs; np; np = np->n_next)
        if (ssequal (atrcur, np->n_name)
                && !ssequal (maildir, np->n_name + atrlen))
            BuildFolderList (np->n_name + atrlen);
}
