/* $Cambridge: hermes/src/prayer/session/folderlist.c,v 1.7 2009/09/01 10:28:12 dpc22 Exp $ */

/* folderlist caches the mailboxlist used on the server */

#include "prayer_session.h"

/* ====================================================================== */

static struct folderitem *
folderitem_create(struct pool *pool, char *name)
{
    struct folderitem *result = pool_alloc(pool, sizeof(struct folderitem));

    result->next        = NIL;
    result->next        = NIL;
    result->child       = NIL;
    result->name        = pool_strdup(pool, name);
    result->noselect    = NIL;
    result->noinferiors = T;
    result->haschildren = T;   /* Assume true until server tells us otherwise */
    result->expanded    = NIL;
    result->size        = 0L;

    return (result);
}

/* ====================================================================== */

/* Set of routines for creating a folderlist using c-client callbacks */

/* Callback routine that c-client calls when generating a directory list */

static struct {
    struct pool *pool;
    BOOL toplevel;
    char hiersep;
    struct folderitem *inbox;
    struct folderitem *first;
    struct folderitem *last;
    unsigned long count;
    char *prefix;
    unsigned long prefix_length;
} callback;

static void
folderlist_callback(MAILSTREAM * stream,
                    int delimiter, char *name, long attributes)
{
    struct pool       *pool = callback.pool;
    struct folderitem *fi;
    struct folderitem *last, *current;
    size_t namelen;

    callback.count++;
    if (callback.prefix && callback.prefix[0]) {
        if (strncmp(name, callback.prefix, callback.prefix_length) != 0)
            return;             /* Didn't match prefix */

        name += callback.prefix_length;
    }

    /* UW and Dovecot return placeholder for directory when running
     *   . list "mail/" %
     *   * LIST (\NoSelect \HasChildren) "/" mail/
     *   * LIST (\NoInferiors \UnMarked) "/" mail/gg
     *
     * Cyrus does not.
     *
     * IMAP specification is ambiguous: '%' matches zero or more
     * characters, but example in RFC 3501 matches Cyrus result.
     *
     * Need to skip the placeholder line if present.
     */
    namelen = strlen(name);
    if ((namelen > 0) && name[namelen-1] == callback.hiersep)
	return;

    if (*name == '\0')
        return;                 /* Empty name */

    fi = folderitem_create(pool, name);
    fi->noselect    = (attributes & LATT_NOSELECT) ? T : NIL;
    fi->noinferiors = (attributes & LATT_NOINFERIORS) ? T : NIL;
    fi->haschildren =
        (attributes & (LATT_NOINFERIORS|LATT_HASNOCHILDREN)) ? NIL: T;

    /* Handle inbox at the end to make sorting easier */
    if (callback.toplevel && (!strcmp(name, "INBOX"))) {
        callback.inbox = fi;
        return;
    }

    /* Add to end of the list if definitely at the end */
    if (callback.last && strcmp(callback.last->name, name) < 0) {
        callback.last = callback.last->next = fi;
        return;
    }

    if (!callback.first) { 
        /* New list */
        fi->next       = NIL;
        callback.first = fi;
        callback.last  = fi;
        return;
    }

    if (strcmp(callback.first->name, name) > 0) { 
        /* Insert at head of list */
        fi->next       = callback.first;
        callback.first = fi;
        return;
    }

    last = callback.first;
    current = last->next;
    while (current && (strcmp(current->name, name) < 0)) {
        last = current;
        current = current->next;
    }

    if (current) {
        /* Insert in middle of list */
        fi->next   = current;
        last->next = fi;
    } else {
        /* Insert at end of list */
        fi->next = NIL;
        callback.last = last->next = fi;
    }
}

void
folderlist_expand(struct session *session, struct folderitem *fi)
{
    struct folderlist *fl = session->folderlist;
    MAILSTREAM *stream = session->stream;
    char *prefix = NIL;
    char *s;

    if (fi->child)
        return;

    prefix = session_dir(session, fl->pool, fi->name);
    s = strchr(prefix, '}');

    callback.prefix        = prefix;
    callback.prefix_length = (s) ? (s+1-prefix) : 0; /* Skip {machine} */

    callback.pool  = fl->pool;
    callback.first = NIL;
    callback.last  = NIL;
    callback.count = 0;
    callback.toplevel = NIL;
    callback.hiersep = fl->hiersep;
    callback.inbox    = NIL;

    mm_register_list_callback(folderlist_callback);

    if (!ml_list(session, stream, callback.prefix, "%"))
        return;

    fi->child = callback.first;

    fl->need_update = NIL;
}

static void
folderlist_toplevel(struct session *session)
{
    MAILSTREAM *stream = session->stream;
    struct prefs *prefs = session->options->prefs;
    struct folderlist *fl = session->folderlist;
    char *prefix;
    struct folderitem *fi;
    char *s;
    BOOL auto_expand_inbox = T;

    if (prefs->maildir && prefs->maildir[0]) {
        prefix = session_dir(session, fl->pool, prefs->maildir);
        auto_expand_inbox = NIL;
    } else
        prefix = session_mailbox(session, fl->pool, "");

    s = strchr(prefix, '}');

    callback.prefix        = prefix;
    callback.prefix_length = (s) ? (s+1-prefix) : 0; /* Skip {machine} */

    callback.pool  = fl->pool;
    callback.first = NIL;
    callback.last  = NIL;
    callback.count = 0;
    callback.toplevel = T;
    callback.hiersep = fl->hiersep;
    callback.inbox    = NIL;

    mm_register_list_callback(folderlist_callback);

    if (!ml_list(session, stream, callback.prefix, "%"))
        return;

    fl->need_update = NIL;
    fl->tree = callback.first;

    if (auto_expand_inbox && callback.inbox) {
        /* Special handling for dual use inbox */
        fi = callback.inbox;
        if (!fi->noinferiors) {
            fi->expanded = T;
            folderlist_expand(session, fi);
        }
    } else {
        /* Add inbox to start of tree */
        fi = folderitem_create(fl->pool, "INBOX");
        fi->noselect = NIL;
        fi->haschildren = NIL;
    }
    fi->next = fl->tree;
    fl->tree = fi;
}

/* ====================================================================== */

/* Public Interface */

struct folderlist *
folderlist_create(char *hiersep)
{
    struct pool       *pool   = pool_create(FOLDERLIST_PREFERRED_POOL_SIZE);
    struct folderlist *result = pool_alloc(pool, sizeof(struct folderlist));

    result->pool  = pool;
    result->tree  = NIL;
    result->need_update = T;
    result->hiersep = (hiersep && hiersep[0]) ? hiersep[0] : '/';

    return (result);
}

void
folderlist_free(struct folderlist *fl)
{
    pool_free(fl->pool);
}

static void
folderlist_copy_expand(struct session *session,
                       struct folderitem *new, struct folderitem *old)
{

    while (old && new) {
        while (new && (strcmp(new->name, old->name) < 0))
            new = new->next;

        if (new && !strcmp(old->name, new->name)) {
            new->expanded = NIL;
            if (old->expanded && new->haschildren) {
                new->expanded = T;
                folderlist_expand(session, new);
                if (old->child && new->child)
                    folderlist_copy_expand(session, new->child, old->child);
            }
            new = new->next;
        }
        old = old->next;
    }
}

struct folderlist *folderlist_fetch(struct session *session)
{
    struct folderlist *old = session->folderlist;
    struct folderlist *new;

    if (old && !old->need_update)
        return(old);

    session->folderlist = new = folderlist_create(session->hiersep);

    folderlist_toplevel(session);

    if (old) {
        folderlist_copy_expand(session, new->tree, old->tree);
        folderlist_free(old);
    }

    return(new);
}

static BOOL
is_inferior(char *parent, char *name, char hiersep)
{
    int len = strlen(parent);

    if (!strncmp(parent, name, len) &&
        ((name[len] == hiersep) || (name[len] == '\0')))
        return(T);

    return(NIL);
}

static struct folderitem *
folderlist_lookup_work(struct folderitem *fi, char *name, char hiersep)
{
    struct folderitem *result;

    while (fi) {
        /* Only recurse if match possible */
        if (fi->child && is_inferior(fi->name, name, hiersep) &&
            (result=folderlist_lookup_work(fi->child, name, hiersep)))
            return(result);

        if (!strcmp(fi->name, name))
            return(fi);
        fi = fi->next;
    }
    return(NIL);

}

struct folderitem *folderlist_lookup(struct folderlist *fl, char *name)
{
    return(folderlist_lookup_work(fl->tree, name, fl->hiersep));
}

static void
folderlist_expand_work(struct folderitem *fi, char *name, char hiersep)
{
    while (fi) {
        /* Only recurse if match possible */
        if (is_inferior(fi->name, name, hiersep)) {
            fi->expanded = T;
            if (fi->child)
                folderlist_expand_work(fi->child, name, hiersep);
        }
        fi = fi->next;
    }
}

static void
folderlist_expand_parents(struct folderlist *fl, char *name)
{
    folderlist_expand_work(fl->tree, name, fl->hiersep);
}

/* ====================================================================== */

static BOOL
folderlist_update_sizes_single(struct folderitem *fi,
                               MAILSTREAM *stream,
                               struct session *session)
{
    unsigned long msgno;
    MESSAGECACHE *elt;

    if (!ml_fetch_fast(session, stream, "1:*", 0))
        return(NIL);

    fi->size = 0L;
    for (msgno = 1; msgno <= stream->nmsgs; msgno++) {
        if (!(elt = ml_elt(session, stream, msgno)))
            return (NIL);
        fi->size += elt->rfc822_size;
    }
    return(T);
}

static BOOL
folderlist_update_sizes_work(struct folderitem *fi,
                             MAILSTREAM **streamp,
                             struct session *session)
{
    struct pool *pool = session->request->pool;

    while (fi) {
        if (!fi->noselect) {
            *streamp = ml_open(session, *streamp,
                               session_mailbox(session, pool, fi->name),
                               OP_READONLY);

            if (!(*streamp))
                return(NIL);
            folderlist_update_sizes_single(fi, *streamp, session);
        }
        if (fi->child && fi->expanded) {
            folderlist_update_sizes_work(fi->child, streamp, session);
        }
        fi = fi->next;
    }
    return(T);
}

BOOL
folderlist_update_sizes(struct folderlist *fl,
                        struct session *session)
{
    BOOL rc;
    MAILSTREAM *tmp = ml_open(session, NIL,
                              session_mailbox_prefs(session, NIL),
                              OP_HALFOPEN);
    if (!tmp)
        return(NIL);

    rc = folderlist_update_sizes_work(fl->tree, &tmp, session);

    if (tmp)
        ml_close(session, tmp);

    return(rc);
}

/* ====================================================================== */

void folderlist_invalidate(struct folderlist *fl)
{
    fl->need_update = T;
}

/* folderlist_invalidate(fl) is always a safe failback when adding or
 * removing entries to the folderlist: just forces refetch from the server
 */

void
folderlist_add(struct folderlist *fl, char *name,
               BOOL noselect, BOOL noinferiors)
{
    struct folderitem *fi;
    struct folderitem *last, *current;

    /* Don't try to optimise difficult cases */
    if (strchr(name, fl->hiersep)) {
        folderlist_expand_parents(fl, name);
        folderlist_invalidate(fl);
        return;
    }
    if (!strcmp(name, "INBOX") || !fl->tree ||
        (strcmp(fl->tree->name, name) > 0)) {
        folderlist_expand_parents(fl, name);
        folderlist_invalidate(fl);
        return;
    }
    
    /* Adding new fi into middle or at end of list */
    fi = folderitem_create(fl->pool, name);
    fi->noselect    = noselect;
    fi->noinferiors = noinferiors;
    fi->haschildren = NIL;

    last    = fl->tree;
    current = last->next;
    while (current && (strcmp(current->name, name) < 0)) {
        last    = current;
        current = current->next;
    }
    if (current && !strcmp(current->name, name)) {
        folderlist_invalidate(fl);  /* Mailbox already has children */
    } else {
        last->next = fi;            /* Add to middle or end of list */
        fi->next = current;
    }
}

void
folderlist_delete(struct folderlist *fl, char *name)
{
    struct folderitem *last, *current;

    /* Don't try to optimise difficult cases */
    if (strchr(name, fl->hiersep)) {
        folderlist_invalidate(fl);
        return;
    }
    if (!fl->tree)
        return;

    /* Remove entry from start of list */
    if (!strcmp(fl->tree->name, name)) {
        if (fl->tree->haschildren)
            folderlist_invalidate(fl);
        else
            fl->tree = fl->tree->next;
        return;
    }

    /* Remove entry from middle of list */
    last    = fl->tree;
    current = last->next;
    while (current && (strcmp(name, current->name) != 0)) {
        last = current;
        current = current->next;
    }
    if (current) {
        if (current->haschildren)
            folderlist_invalidate(fl);
        last->next = current->next;
    }
}

void
folderlist_rename(struct folderlist *fl, char *oldname, char *newname)
{
    struct folderitem *fi = folderlist_lookup(fl, oldname);

    if (fi) {
        folderlist_add(fl, newname, fi->noselect, fi->noinferiors);
        folderlist_delete(fl, oldname);

        if (fi->child)
            folderlist_invalidate(fl);
    }
}

/* ====================================================================== */

static int
folderlist_count_visible_work(struct folderitem *fi, BOOL suppress_dotfiles)
{
    int result = 0;

    while (fi) {
        if (!fi->noselect && (!suppress_dotfiles || fi->name[0] != '.'))
            result++;
        if (fi->child && fi->expanded)
            result += folderlist_count_visible_work(fi->child,
                                                    suppress_dotfiles);
        fi = fi->next;
    }
    return(result);
}

int
folderlist_count_visible_folders(struct folderlist *fl, BOOL suppress_dotfiles)
{
    return(folderlist_count_visible_work(fl->tree, suppress_dotfiles));
}

/* ====================================================================== */

/* XXX Following will all be obsolete when template work is finished XXX */

static void
folderlist_showdirs_select_work(struct folderitem *fi, 
                                BOOL suppress_dotfiles,
                                struct pool *pool,
                                struct buffer *b, 
                                char *selected)
                         
{
    while (fi) {
        if (!fi->noinferiors && (!suppress_dotfiles || fi->name[0] != '.')) {
            bprintf(b, "<option value=\"%s\"%s>", 
                    string_canon_encode(pool, fi->name),
                    (selected && !strcmp(selected, fi->name)
                     ? " selected" : ""));
            html_quote_string(b, utf8_from_imaputf7(pool, fi->name));
            bputs(b, "</option>" CRLF);
        }
        if (fi->child && fi->expanded) {
            folderlist_showdirs_select_work(fi->child, suppress_dotfiles,
                                            pool, b, selected);
        }
        fi = fi->next;
    }
}

void
folderlist_showdirs_select(struct folderlist *fl, BOOL suppress_dotfiles,
                           struct pool *pool, struct buffer *b, char *parent)
{
    folderlist_showdirs_select_work(fl->tree, suppress_dotfiles,
                                    pool, b, parent);
}

static void
folderlist_showfolders_select_work(struct folderitem *fi, 
                                   BOOL suppress_dotfiles,
                                   struct pool *pool,
                                   struct buffer *b, 
                                   char *selected)
{
    while (fi) {
        if (!fi->noselect && (!suppress_dotfiles || fi->name[0] != '.')) {
            bprintf(b, "<option value=\"%s\"%s>", 
                    string_canon_encode(pool, fi->name),
                    (selected && !strcmp(selected, fi->name)
                     ? " selected" : ""));
            html_quote_string(b, utf8_from_imaputf7(pool, fi->name));
            bputs(b, "</option>" CRLF);
        }
        if (fi->child && fi->expanded) {
            folderlist_showfolders_select_work(fi->child,
                                               suppress_dotfiles,
                                              pool, b, selected);
        }
        fi = fi->next;
    }
}

void
folderlist_showfolders_select(struct folderlist *fl, BOOL suppress_dotfiles,
                              struct pool *pool, struct buffer *b,
                              char *selected)
{
    folderlist_showfolders_select_work(fl->tree, suppress_dotfiles,
                                       pool, b, selected);
}

/* ====================================================================== */

static char *
mbytes(unsigned long bytes)
{
    static char buf[64];
    unsigned long kbytes   = bytes / 1024;
    unsigned long whole    = kbytes / (1024);
    unsigned long fraction = ((kbytes % 1024) * 100) / 1024;

    if (fraction > 9)
        sprintf(buf, "%lu.%lu", whole, fraction);
    else
        sprintf(buf, "%lu.0%lu", whole, fraction);

    return(buf);
}

static void
folderlist_tvals_tree_work(struct folderitem *fi, 
                           BOOL suppress_dotfiles, char hiersep,
                           struct template_vals *tvals,
                           int *count, int indent, char *array)
{
    char *short_name, *s;
    struct pool *pool = tvals->pool;
    char *size_str;

    while (fi) {
        if ((s=strrchr(fi->name, hiersep)))
            short_name = s+1;
        else
            short_name = fi->name;

        if ((!suppress_dotfiles || fi->name[0] != '.')) {
            template_vals_foreach_init(tvals, array, *count);
            template_vals_foreach_string(tvals, array, *count,
                                         "name", fi->name);
            template_vals_foreach_string(tvals, array, *count,
                                         "short_name", short_name);
            template_vals_foreach_ulong(tvals, array, *count,
                                        "indent", indent);
            if (fi->noselect)
                template_vals_foreach_ulong(tvals, array, *count,
                                            "noselect", 1);
            if (fi->noinferiors)
                template_vals_foreach_ulong(tvals, array, *count,
                                            "noinferiors", 1);
            if (fi->haschildren)
                template_vals_foreach_ulong(tvals, array, *count,
                                            "haschildren", 1);
            if (fi->expanded)
                template_vals_foreach_ulong(tvals, array, *count,
                                            "expanded", 1);
            if (*count % 2 == 0)
                template_vals_foreach_ulong(tvals, array, *count,
                                            "even_row", 1);
            if (fi->size > 0)
                size_str = pool_printf(pool, "%s MBytes", mbytes(fi->size));
            else
                size_str = "0.00 MBytes";

            template_vals_foreach_string(tvals, array, *count,
                                         "size", size_str);

            (*count)++;
        }
        if (fi->child && fi->expanded) {
            folderlist_tvals_tree_work(fi->child, 0, hiersep,
                                       tvals, count, indent+1, array);
        }
        fi = fi->next;
    }
}

void
folderlist_template_vals_tree(struct folderlist *fl, BOOL suppress_dotfiles,
                              struct template_vals *tvals, char *array)
{
    int count = 0;

    folderlist_tvals_tree_work(fl->tree, suppress_dotfiles, fl->hiersep,
                               tvals, &count, 0, array);
}

static void
folderlist_tvals_list_work(struct folderitem *fi, 
                           BOOL suppress_dotfiles,
                           struct template_vals *tvals,
                           int *count, BOOL showdirs, char *array)
{
    BOOL showfi;

    while (fi) {
        if (suppress_dotfiles && (fi->name[0] == '.')) {
            fi = fi->next;
            continue;
        }

        if (showdirs)
            showfi = (fi->noinferiors) ? NIL : T;
        else
            showfi = (fi->noselect) ? NIL : T;

        if (showfi) {
            template_vals_foreach_init(tvals, array, *count);
            template_vals_foreach_string(tvals, array, *count,
                                         "name", fi->name);
            (*count)++;
        }
        if (fi->child && fi->expanded) {
            folderlist_tvals_list_work(fi->child, 0, tvals,
                                       count, showdirs, array);
        }
        fi = fi->next;
    }
}

void
folderlist_template_vals_list(struct folderlist *fl, BOOL suppress_dotfiles,
                              struct template_vals *tvals,
                              BOOL showdirs, char *array)
{
    int count = 0;

    folderlist_tvals_list_work(fl->tree, suppress_dotfiles, tvals,
                               &count, showdirs, array);
}
