/*
 * Directory editor mode for QEmacs.
 *
 * Copyright (c) 2001-2002 Fabrice Bellard.
 * Copyright (c) 2002-2026 Charlie Gordon.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "qe.h"
#include "variables.h"

#include <grp.h>
#include <pwd.h>

enum {
    DIRED_STYLE_NORMAL = QE_STYLE_DEFAULT,
    DIRED_STYLE_HEADER = QE_STYLE_STRING,
    DIRED_STYLE_DIRECTORY = QE_STYLE_COMMENT,
    DIRED_STYLE_FILENAME = QE_STYLE_FUNCTION,
};

enum {
    DIRED_SORT_FULLNAME = 0,
    DIRED_SORT_NAME = 1,
    DIRED_SORT_EXTENSION = 2,
    DIRED_SORT_SIZE = 4,
    DIRED_SORT_DATE = 8,
    DIRED_SORT_MASK = 1+2+4+8,
    DIRED_SORT_GROUP = 16,
    DIRED_SORT_DESCENDING = 32,
};

enum {
    DIRED_UPDATE_SORT = 1,
    DIRED_UPDATE_FILTER = 2,
    DIRED_UPDATE_COLUMNS = 4,
    DIRED_UPDATE_REBUILD = 8,
    DIRED_UPDATE_ALL = 15,
};

enum time_format {
    TF_COMPACT,
    TF_DOS,
    TF_DOS_LONG,
    TF_TOUCH,
    TF_TOUCH_LONG,
    TF_FULL,
    TF_SECONDS,
    TF_MAX = TF_SECONDS,
};

typedef struct DiredState DiredState;
typedef struct DiredItem DiredItem;

struct DiredState {
    QEModeData base;    /* derived from QEModeData */
    DiredItem **items;
    int nb_items;
    int nb_allocated;
    enum time_format time_format;
    int show_dot_files;
    int show_ds_store;
    int hflag, nflag, pf_flags;
    int details_flag, last_details_flag;
    int sort_mode;
    DiredItem *last_cur;
    long long total_bytes;
    int ndirs, nfiles, ndirs_hidden, nfiles_hidden;
    int blocksize;
    int last_width;
    int header_lines;
    int details_mask;
#define DIRED_SHOW_BLOCKS 0x01
#define DIRED_SHOW_MODE   0x02
#define DIRED_SHOW_LINKS  0x04
#define DIRED_SHOW_UID    0x08
#define DIRED_SHOW_GID    0x10
#define DIRED_SHOW_SIZE   0x20
#define DIRED_SHOW_DATE   0x40
#define DIRED_SHOW_ALL    0x7F
    int blockslen, modelen, linklen, uidlen, gidlen, sizelen, datelen, namelen;
    int fnamecol;
    char path[MAX_FILENAME_SIZE]; /* current path */
    char target[MAX_FILENAME_SIZE]; /* current target */
    char pattern[32]; /* filtering pattern */
};

/* opaque structure for sorting DiredState.items StringArray */
struct DiredItem {
    char    *name;
    mode_t  mode;   /* inode protection mode */
    nlink_t nlink;  /* number of hard links to the file */
    uid_t   uid;    /* user-id of owner */
    gid_t   gid;    /* group-id of owner */
    dev_t   rdev;   /* device type, for special file inode */
    time_t  mtime;
    off_t   size;
    int     offset;
    u8      flags;
#define DI_ISLNK   1 /* XXX: use bitfields */
#define DI_BROKEN  2
#define DI_ISDIR   4
    u8      level;
    char    hidden; /* XXX: use flag */
    char    mark;
    char    tick;
    char    fullname[1];
};

static ModeDef dired_mode;

static time_t dired_curtime;

static enum time_format dired_time_format;
static int dired_show_dot_files = 1;
static int dired_show_ds_store = 0;
static int dired_nflag = 0; /* 0=name, 1=numeric, 2=hidden */
static int dired_hflag = 0; /* 0=exact, 1=human-decimal, 2=human-binary */
#define DIRED_DETAILS_AUTO  0
#define DIRED_DETAILS_HIDE  1
#define DIRED_DETAILS_SHOW  2
static int dired_sort_mode = DIRED_SORT_GROUP | DIRED_SORT_FULLNAME;
static int dired_pf_flags;
// XXX: could use a regexp and make it extendable
static const char *dired_ignore_extensions = {
    "|bak"
    "|xls|xlsx|ppt|pptx"
    "|apk"
    "|bin|obj|dll|exe" /* DOS binaries */
    "|o|so|a" /* Unix binaries */
    "|dylib|dSYM" /* macOS */
    "|cma|cmi|cmo|cmt|cmti|cmx"
    "|class|jar" /* java */
    "|b"
};

static QVarType dired_sort_mode_set_value(EditState *s, VarDef *vp,
    void *ptr, const char *str, int sort_mode);
static QVarType dired_time_format_set_value(EditState *s, VarDef *vp,
    void *ptr, const char *str, int format);

static VarDef dired_variables[] = {
    G_VAR_F( "dired-sort-mode", dired_sort_mode, VAR_NUMBER, VAR_RW_SAVE,
            dired_sort_mode_set_value,
            "Sort order for dired display: any combination of `nefsdgur+-`" )
    G_VAR_F( "dired-time-format", dired_time_format, VAR_NUMBER, VAR_RW_SAVE,
            dired_time_format_set_value,
            "Format used for file times (default, compact, dos, dos-long, touch, touch-long, full, seconds)" )
    G_VAR( "dired-show-dot-files", dired_show_dot_files, VAR_NUMBER, VAR_RW_SAVE,
          "Set to show hidden files (starting with a `.`)" )
    G_VAR( "dired-show-ds-store", dired_show_ds_store, VAR_NUMBER, VAR_RW_SAVE,
          "Set to show infamous macOS .DS_Store system files" )
};

static inline DiredState *dired_get_state(EditBuffer *b, EditState *s)
{
    return qe_get_buffer_mode_data(b, &dired_mode, s);
}

static DiredItem *dired_get_cur_item(DiredState *ds, EditState *s) {
    int i, index;

    if (!s)
        return NULL;
    index = list_get_pos(s) - ds->header_lines;
    if (index >= 0) {
        for (i = 0; i < ds->nb_items; i++) {
            DiredItem *dip = ds->items[i];
            if (!dip->hidden && index-- == 0)
                return dip;
        }
    }
    return NULL;
}

static const char *dired_get_cur_filename(DiredState *ds, EditState *s,
                                          char *buf, size_t buf_size)
{
    DiredItem *dip = dired_get_cur_item(ds, s);
    if (dip) {
        pstrcpy(buf, buf_size, dip->fullname);
        return buf;
    }
    return NULL;
}

static void dired_free(DiredState *ds)
{
    if (ds) {
        int i;

        for (i = 0; i < ds->nb_items; i++) {
            qe_free(&ds->items[i]);
        }

        qe_free(&ds->items);
        ds->nb_items = 0;
        ds->nb_allocated = 0;
        ds->last_cur = NULL;
    }
}

static char *dired_get_filename(DiredState *ds, const DiredItem *dip,
                                char *buf, int buf_size)
{
    if (buf_size > 0)
        buf[0] = '\0';

    if (!dip)
        return NULL;

    pstrcpy(buf, buf_size, dip->fullname);
    return buf;
}

static DiredItem *dired_goto_target(DiredState *ds, EditState *s, const char *target, int force)
{
    int best_row = force ? ds->header_lines : -1;
    DiredItem *best_dip = NULL;

    if (target && *target) {
        int i, pos;
        int best_pos = 0;
        int row = ds->header_lines;
        for (i = 0; i < ds->nb_items; i++) {
            DiredItem *dip = ds->items[i];
            const char *fn = dip->fullname;

            if (dip->hidden)
                continue;

            for (pos = 0;; pos++) {
                if (fn[pos] == '\0') {
                    if ((target[pos] == '\0' || target[pos] == '/') && pos > best_pos) {
                        best_pos = pos;
                        best_row = row;
                        best_dip = dip;
                    }
                    break;
                }
                if (target[pos] != fn[pos])
                    break;
            }
            row++;
        }
    }
    if (best_row >= 0 && s) {
        s->offset = eb_goto_pos(s->b, best_row, 0);
    }
    return best_dip;
}

enum { SAME_NAME, DIR1_PARENT, DIR2_PARENT, DIR1_UNCLE, DIR2_UNCLE, SAME_DIR, DIFFERENT_DIR };

static int classify_fullnames(const char *p1, const char *p2) {
    if (*p1 == '/' && *p2 == '/' && p1[1] != p2[1]) {
        if (p1[1] == '\0')
            return DIR1_PARENT;
        if (p2[1] == '\0')
            return DIR2_PARENT;
    }
    for (; *p1 == *p2; p1++, p2++) {
        if (*p1 == '\0')
            return SAME_NAME;
    }
    if (*p1 == '\0' && *p2 == '/')
        return DIR1_PARENT;
    if (*p2 == '\0' && *p1 == '/')
        return DIR2_PARENT;
    p1 = strchr(p1, '/');
    p2 = strchr(p2, '/');
    if (p1 && p2)
        return DIFFERENT_DIR;
    if (p1)
        return DIR2_UNCLE;
    if (p2)
        return DIR1_UNCLE;
    return SAME_DIR;
}

/* sort accoding to sort criteria */
static int dired_sort_func(void *opaque, const void *p1, const void *p2)
{
    DiredState *ds = opaque;
    const DiredItem * const *pp1 = (const DiredItem * const *)p1;
    const DiredItem * const *pp2 = (const DiredItem * const *)p2;
    const DiredItem *dip1 = *pp1;
    const DiredItem *dip2 = *pp2;
    int sort_mode = ds->sort_mode, res = 0;
    int is_dir1 = dip1->flags & DI_ISDIR;
    int is_dir2 = dip2->flags & DI_ISDIR;

    if (sort_mode & DIRED_SORT_GROUP) {
        /* when grouped, directories are always sorted in alpha order */
        switch (classify_fullnames(dip1->fullname, dip2->fullname)) {
        case SAME_NAME:     /* same full name: should not happen */
        case SAME_DIR:      /* dip1 and dip2 are in the same directory */
            /* sort directories before files in the same directory */
            if (is_dir1 != is_dir2)
                return is_dir2 - is_dir1;
            if (is_dir1)
                goto cmpnames;
            break;
        case DIR1_PARENT:   /* dip1 is the parent directory of dip2 */
            return -1;
        case DIR1_UNCLE:    /* dip1 is an entry in a parent directory of dip2 */
            if (!is_dir1)
                return 1;
            goto cmpnames;
        case DIR2_PARENT:   /* dip2 is the parent directory of dip1 */
            return 1;
        case DIR2_UNCLE:    /* dip2 is an entry in a parent directory of dip1 */
            if (!is_dir2)
                return -1;
            goto cmpnames;
        case DIFFERENT_DIR: /* different directories specified */
        cmpnames:
            return qe_strcollate(dip1->fullname, dip2->fullname);
        }
    }

    if ((sort_mode & DIRED_SORT_DATE) && dip1->mtime != dip2->mtime) {
        res = (dip1->mtime < dip2->mtime) ? -1 : 1;
    } else
    if ((sort_mode & DIRED_SORT_SIZE) && dip1->size != dip2->size) {
        res = (dip1->size < dip2->size) ? -1 : 1;
    } else {
        if (sort_mode & DIRED_SORT_EXTENSION) {
            res = qe_strcollate(get_extension(dip1->name),
                                get_extension(dip2->name));
        }
        if (!res && (sort_mode & DIRED_SORT_NAME)) {
            res = qe_strcollate(dip1->name, dip2->name);
        }
        if (!res) {
            res = qe_strcollate(dip1->fullname, dip2->fullname);
        }
    }
    return (sort_mode & DIRED_SORT_DESCENDING) ? -res : res;
}

int eb_put_filename(EditBuffer *b, const char *name, int flags) {
    // write a filename to a buffer, encoding special characters according
    // to flags
    // XXX: use local array to write to buffer as a single modification?
    int len = 0;
    char buf[8];
    u8 cc;
    while ((cc = *name++) != '\0') {
        if (cc > 0x7F && !(flags & PF_NO_UNICODE)) {
            const char *p = name - 1;
            char32_t c = utf8_decode(&p);
            if (c >= 128 && c <= 0x10FFFF) {
                int nc = utf8_encode(buf, c);
                if (nc == p - name + 1 && !memcmp(buf, name - 1, nc)) {
                    // valid UTF-8 encoded codepoint
                    if (flags & 1) {
                        if (c > 0xFFFF) {
                            len += eb_printf(b, "\\u{%X}", c);
                        } else {
                            len += eb_printf(b, "\\x%04X", c);
                        }
                    } else {
                        buf[nc] = '\0';
                        eb_puts(b, buf);
                        len += qe_wcwidth(c);
                    }
                    name = p;
                    continue;
                }
            }
        }
        if (cc < ' ' || cc >= 0x7F) {
            switch (flags & PF_ENCODING) {
            case PF_QUESTION:
                eb_putc(b, '?');
                len += 1;
                break;
            case PF_OCTAL:
                eb_putc(b, '\\');
                len += 2;
                switch (cc) {
                case '\n':  eb_putc(b, 'n'); break;
                case '\t':  eb_putc(b, 't'); break;
                case '\r':  eb_putc(b, 'r'); break;
                case '\b':  eb_putc(b, 'b'); break;
                case '\f':  eb_putc(b, 'f'); break;
                default:    eb_printf(b, "%03o", cc); len += 2; break;
                }
                break;
            case PF_HEX:
                eb_printf(b, "\\x%02X", cc);
                len += 4;
                break;
            case PF_CARET:
                if (cc < 32 || cc == 127) {
                    eb_printf(b, "\\^%c", (cc + '@') & 127);
                    len += 3;
                    break;
                }
                eb_printf(b, "\\x%02X", cc);
                len += 4;
                break;
            }
        } else {
            eb_putc(b, cc);
            len += 1;
        }
    }
    return len;
}

static int format_number(char *buf, int size, int human, off_t number)
{
    if (human == 0) {
        return snprintf(buf, size, "%lld", (long long)number);
    }
    if (human > 1) {
        const char *suffix = "BkMGTPEZY";

        /* metric version, powers of 1000 */
        while (suffix[1] && number >= 1000) {
            if (number < 10000) {
                buf[0] = '0' + (number / 1000);
                buf[1] = '.';
                buf[2] = '0' + ((number / 100) % 10);
                buf[3] = suffix[1];
                buf[4] = '\0';
                return 4;
            }
            number /= 1000;
            suffix++;
        }
        return snprintf(buf, size, "%d%c", (int)number, *suffix);
    } else {
        const char *suffix = "BKMGTPEZY";

        /* geek version, powers of 1024 */
        while (suffix[1] && number >= 1000) {
            if (number < 10200) {
                buf[0] = '0' + (number / 1020);
                buf[1] = '.';
                buf[2] = '0' + ((number / 102) % 10);
                buf[3] = suffix[1];
                buf[4] = '\0';
                return 4;
            }
            number >>= 10;
            suffix++;
        }
        return snprintf(buf, size, "%d%c", (int)number, *suffix);
    }
}

static int format_gid(char *buf, int size, int nflag, gid_t gid)
{
    // group_from_gid ?
    struct group *grp;

    if (!nflag && (grp = getgrgid(gid)) != NULL && grp->gr_name)
        return snprintf(buf, size, "%s", grp->gr_name);
    else
        return snprintf(buf, size, "%d", (int)gid);
}

static int format_uid(char *buf, int size, int nflag, uid_t uid)
{
    // user_from_uid ?
    struct passwd *pwp;

    if (!nflag && (pwp = getpwuid(uid)) != NULL && pwp->pw_name)
        return snprintf(buf, size, "%s", pwp->pw_name);
    else
        return snprintf(buf, size, "%d", (int)uid);
}

static int format_size(char *buf, int size, int human,
                       mode_t st_mode, dev_t st_rdev, off_t st_size)
{
    if (S_ISCHR(st_mode) || S_ISBLK(st_mode)) {
        int major = st_rdev >> ((sizeof(dev_t) == 2) ? 8 : 24);
        int minor = st_rdev & ((sizeof(dev_t) == 2) ? 0xff : 0xffffff);
        return snprintf(buf, size, "%3d, %3d", major, minor);
    } else {
        return format_number(buf, size, human, st_size);
    }
}

static int format_date(char *dest, int size,
                       const time_t systime,
                       enum time_format time_format)
{
    buf_t outbuf, *out;
    struct tm systm;
    int fmonth;
    static const char * const month[] = {
        "***",
        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
    };

    /* Should test for valid conversion */
    /* Should support extra precision on enabled systems */
    systm = *localtime(&systime);

    fmonth = systm.tm_mon + 1;
    if (fmonth <= 0 || fmonth > 12)
        fmonth = 0;

    out = buf_init(&outbuf, dest, size);

    switch (time_format) {
    case TF_TOUCH:
    case TF_TOUCH_LONG:
        buf_printf(out, "%02d%02d%02d%02d%02d",
                   systm.tm_year % 100,  /* year */
                   fmonth,               /* month */
                   systm.tm_mday,        /* day */
                   systm.tm_hour,        /* hours */
                   systm.tm_min);        /* minutes */
        if (time_format == TF_TOUCH_LONG) {
            buf_printf(out, ".%02d", systm.tm_sec); /* seconds */
        }
        break;
    case TF_DOS:
    case TF_DOS_LONG:
        buf_printf(out, "%s %2d %4d  %2d:%02d",
                   month[fmonth],        /* month */
                   systm.tm_mday,        /* day */
                   systm.tm_year + 1900, /* year */
                   systm.tm_hour,        /* hours */
                   systm.tm_min);        /* minutes */
        if (time_format == TF_DOS_LONG) {
            buf_printf(out, ":%02d", systm.tm_sec); /* seconds */
        }
        break;
    case TF_FULL:
        buf_printf(out, "%s %2d %02d:%02d:%02d %4d",
                   month[fmonth],        /* month */
                   systm.tm_mday,        /* day */
                   systm.tm_hour,        /* hours */
                   systm.tm_min,         /* minutes */
                   systm.tm_sec,         /* seconds */
                   systm.tm_year + 1900); /* year */
        break;
    case TF_SECONDS:
        buf_printf(out, "%10lu", (unsigned long)systime); /* seconds */
        break;
    default:
    case TF_COMPACT:
        if (systime > dired_curtime - 182 * 86400
        &&  systime < dired_curtime + 182 * 86400) {
            buf_printf(out,     "%s %2d %02d:%02d",
                       month[fmonth],        /* month */
                       systm.tm_mday,        /* day */
                       systm.tm_hour,        /* hours */
                       systm.tm_min);        /* minutes */
        } else {
            buf_printf(out,     "%s %2d  %4d",
                       month[fmonth],        /* month */
                       systm.tm_mday,        /* day */
                       systm.tm_year + 1900); /* year */
        }
        break;
    }

    if (!fmonth) {
        memset(dest, ' ', out->len);
    }
    return out->pos;
}

static int get_trailchar(mode_t mode)
{
    int trailchar = 0;

    if (mode & S_IXUSR) {
        trailchar = '*';
    }
    if (S_ISDIR(mode)) {
        trailchar = '/';
    }
    if (S_ISLNK(mode)) {
        trailchar = '@';
    }
#ifdef S_ISSOCK
    if (S_ISSOCK(mode))
        trailchar = '=';
#endif
#ifdef S_ISWHT
    if (S_ISWHT(mode))
        trailchar = '%';
#endif
#ifdef S_ISFIFO
    if (S_ISFIFO(mode))
        trailchar = '|';
#endif
    return trailchar;
}

static char *getentryslink(char *path, int size, const char *filename)
{
    /* Warning: readlink does not append a null byte! */
    int len = readlink(filename, path, size - 1);
    if (len < 0)
        len = 0;
    path[len] = '\0';
    if (len)
        return path;
    else
        return NULL;
}

static char *compute_attr(char *atts, mode_t mode)
{
    strcpy(atts, "----------");

    /* File type */
    if (!S_ISREG(mode)) {
        if (S_ISDIR(mode))  /* directory */
            atts[0] = 'd';
#ifdef S_ISBLK
        if (S_ISBLK(mode))  /* block special */
            atts[0] = 'b';
#endif
#ifdef S_ISCHR
        if (S_ISCHR(mode))  /* char special */
            atts[0] = 'c';
#endif
#ifdef S_ISFIFO
        if (S_ISFIFO(mode))  /* fifo or socket */
            atts[0] = 'p';
#endif
#ifdef S_ISSOCK
        if (S_ISSOCK(mode)  /* socket */)
            atts[0] = 's';
#endif
        if (S_ISLNK(mode))  /* symbolic link */
            atts[0] = 'l';  /* overrides directory */
    }

    /* File mode */
    /* Read, write, execute/search by owner */
    if (mode & S_IRUSR)  /* [XSI] R for owner */
        atts[1] = 'r';
    if (mode & S_IWUSR)  /* [XSI] W for owner */
        atts[2] = 'w';
    if (mode & S_IXUSR)  /* [XSI] X for owner */
        atts[3] = 'x';
#ifdef S_ISUID
    if (mode & S_ISUID)  /* [XSI] set user id on execution */
        atts[3] = (mode & S_IXUSR) ? 's' : 'S';
#endif
            /* Read, write, execute/search by group */
    if (mode & S_IRGRP)  /* [XSI] R for group */
        atts[4] = 'r';
    if (mode & S_IWGRP)  /* [XSI] W for group */
        atts[5] = 'w';
    if (mode & S_IXGRP)  /* [XSI] X for group */
        atts[6] = 'x';
#ifdef S_ISGID
    if (mode & S_ISGID)  /* [XSI] set group id on execution */
        atts[6] = (mode & S_IXGRP) ? 's' : 'S';
#endif
            /* Read, write, execute/search by others */
    if (mode & S_IROTH)
        atts[7] = 'r';  /* [XSI] R for other */
    if (mode & S_IWOTH)
        atts[8] = 'w';  /* [XSI] W for other */
    if (mode & S_IXOTH)
        atts[9] = 'x';  /* [XSI] X for other */
#ifdef S_ISVTX
    if (mode & S_ISVTX)  /* [XSI] directory restrcted delete */
        atts[6] = (mode & S_IXOTH) ? 't' : 'T';
#endif
    return atts;
}

static void dired_filter_files(DiredState *ds)
{
    int i;

    ds->show_dot_files = dired_show_dot_files;
    ds->show_ds_store = dired_show_ds_store;
    ds->total_bytes = 0;
    ds->ndirs = ds->nfiles = 0;
    ds->ndirs_hidden = ds->nfiles_hidden = 0;

    for (i = 0; i < ds->nb_items; i++) {
        DiredItem *dip = ds->items[i];
        const char *p = dip->name;
        int hidden = 0;

        if (*p == '.') {
            if ((!dired_show_dot_files)
            ||  (!dired_show_ds_store && strequal(p, ".DS_Store")))
                hidden = 1;
        } else
        if (!dired_show_dot_files) {
            const char *ext = get_extension(dip->fullname);
            if (*ext && strfind(dired_ignore_extensions, ext + 1))
                hidden = 1;
        }
        /* XXX: should apply other filters? */
        // XXX: should hide full subtree if grouped?
        dip->hidden = hidden;
        if (hidden) {
            if (S_ISDIR(dip->mode)) {
                ds->ndirs_hidden++;
            } else {
                ds->nfiles_hidden++;
            }
        } else {
            if (S_ISDIR(dip->mode)) {
                ds->ndirs++;
            } else {
                ds->nfiles++;
                ds->total_bytes += dip->size;
            }
        }
    }
}

static void dired_compute_columns(DiredState *ds)
{
    char buf[32];
    int i, len;

    dired_curtime = time(NULL);
    ds->time_format = dired_time_format;
    ds->hflag = dired_hflag;
    ds->nflag = dired_nflag;
    ds->pf_flags = dired_pf_flags;
    ds->blockslen = ds->modelen = ds->linklen = 0;
    ds->uidlen = ds->gidlen = 0;
    ds->sizelen = ds->datelen = ds->namelen = 0;

    for (i = 0; i < ds->nb_items; i++) {
        DiredItem *dip = ds->items[i];

        len = strlen(dip->name);
        if (ds->namelen < len)
            ds->namelen = len;

        if (ds->details_flag == DIRED_DETAILS_HIDE)
            continue;

        len = snprintf(buf, sizeof(buf), "%ld",
                       (long)(((long long)dip->size + ds->blocksize - 1) /
                              ds->blocksize));
        if (ds->blockslen < len)
            ds->blockslen = len;

        ds->modelen = 10;

        len = snprintf(buf, sizeof(buf), "%d", (int)dip->nlink);
        if (ds->linklen < len)
            ds->linklen = len;

        len = format_uid(buf, sizeof(buf), ds->nflag, dip->uid);
        if (ds->uidlen < len)
            ds->uidlen = len;

        len = format_gid(buf, sizeof(buf), ds->nflag, dip->gid);
        if (ds->gidlen < len)
            ds->gidlen = len;

        len = format_size(buf, sizeof(buf), ds->hflag, dip->mode, dip->rdev, dip->size);
        if (ds->sizelen < len)
            ds->sizelen = len;

        len = format_date(buf, sizeof(buf), dip->mtime, dired_time_format);
        if (ds->datelen < len)
            ds->datelen = len;
    }
}

static int dired_format_details(DiredState *ds, DiredItem *dip,
                                int details_mask, char *dest, size_t size)
{
    char buf[32];
    buf_t bp[1];

    buf_init(bp, dest, size);

#if 0
    if (details_mask & DIRED_SHOW_BLOCKS) {
        buf_printf(bp, "%*ld ", ds->blockslen,
                   (long)(((long long)dip->size + ds->blocksize - 1) / ds->blocksize));
    }
#endif
    if (details_mask & DIRED_SHOW_MODE) {
        buf_printf(bp, "%s ", compute_attr(buf, dip->mode));
    }
    if (details_mask & DIRED_SHOW_LINKS) {
        buf_printf(bp, "%*d ", ds->linklen, (int)dip->nlink);
    }
    if (details_mask & DIRED_SHOW_UID) {
        format_uid(buf, sizeof(buf), ds->nflag, dip->uid);
        buf_printf(bp, "%-*s ", ds->uidlen, buf);
    }
    if (details_mask & DIRED_SHOW_GID) {
        format_gid(buf, sizeof(buf), ds->nflag, dip->gid);
        buf_printf(bp, "%-*s ", ds->gidlen, buf);
    }
    if (details_mask & DIRED_SHOW_SIZE) {
        format_size(buf, sizeof(buf), ds->hflag, dip->mode, dip->rdev, dip->size);
        buf_printf(bp, " %*s ", ds->sizelen, buf);
    }
    if (details_mask & DIRED_SHOW_DATE) {
        format_date(buf, sizeof(buf), dip->mtime, dired_time_format);
        buf_printf(bp, " %s ", buf);
    }
    return bp->len;
}

#define inflect(n, singular, plural)  ((n) == 1 ? (singular) : (plural))

/* `b` is valid, `ds` and `s` may be NULL */
static void dired_update_buffer(DiredState *ds, EditBuffer *b, EditState *s,
                                int flags)
{
    QEmacsState *qs = b->qs;
    char buf[MAX_FILENAME_SIZE];
    DiredItem *dip, *cur_item;
    int i, col, width, window_width, top_line, indent;
    int header_lines;
    const char *fname;

    if (!ds)
        return;

    header_lines = ds->header_lines;
    /* Try and preserve scroll position */
    if (s) {
        window_width = s->width;
        width = window_width / s->char_width;

        eb_get_pos(s->b, &top_line, &col, s->offset_top);
        /* XXX: should use dip->offset and delay to rebuild phase */
        cur_item = dired_get_cur_item(ds, s);
        header_lines = 2;
        if (s->width <= qs->width / 3)
            header_lines = 1;
    } else {
        width = window_width = 80;
        col = top_line = 0;
        cur_item = NULL;
    }

    if (ds->header_lines != header_lines) {
        ds->header_lines = header_lines;
        flags |= DIRED_UPDATE_REBUILD;
    }

    if (ds->sort_mode != dired_sort_mode)
        flags |= DIRED_UPDATE_SORT;

    if (flags & DIRED_UPDATE_SORT) {
        flags |= DIRED_UPDATE_REBUILD;
        ds->sort_mode = dired_sort_mode;
        qe_qsort_r(ds->items, ds->nb_items, sizeof(DiredItem *),
                   ds, dired_sort_func);
    }

    if (ds->show_dot_files != dired_show_dot_files
    ||  ds->show_ds_store != dired_show_ds_store) {
        flags |= DIRED_UPDATE_FILTER;
    }

    if (flags & DIRED_UPDATE_FILTER) {
        flags |= DIRED_UPDATE_REBUILD;
        dired_filter_files(ds);
    }

    if (ds->time_format != dired_time_format
    ||  ds->nflag != dired_nflag
    ||  ds->hflag != dired_hflag
    ||  ds->pf_flags != dired_pf_flags
    ||  ds->details_flag != ds->last_details_flag) {
        flags |= DIRED_UPDATE_COLUMNS;
    }

    if (flags & DIRED_UPDATE_COLUMNS) {
        flags |= DIRED_UPDATE_REBUILD;
        dired_compute_columns(ds);
    }

    if (!(flags & DIRED_UPDATE_REBUILD))
        return;

    ds->last_details_flag = ds->details_flag;
    ds->last_width = window_width;
    ds->last_cur = NULL;
    width -= clamp_int(ds->namelen, 16, 40);
    ds->details_mask = DIRED_SHOW_ALL;
    if (ds->header_lines == 1 || ds->details_flag == DIRED_DETAILS_HIDE) {
        ds->details_mask = 0;
    } else
    if (ds->details_flag == DIRED_DETAILS_AUTO) {
        if ((width -= ds->sizelen + 2) < 0)
            ds->details_mask ^= DIRED_SHOW_SIZE;
        if ((width -= ds->datelen + 2) < 0)
            ds->details_mask ^= DIRED_SHOW_DATE;
        if ((width -= ds->modelen + 1) < 0)
            ds->details_mask ^= DIRED_SHOW_MODE;
        if ((ds->nflag == 2) || ((width -= ds->uidlen + 1) < 0))
            ds->details_mask ^= DIRED_SHOW_UID;
        if ((ds->nflag == 2) || ((width -= ds->gidlen + 1) < 0))
            ds->details_mask ^= DIRED_SHOW_GID;
        if ((width -= ds->linklen + 1) < 0)
            ds->details_mask ^= DIRED_SHOW_LINKS;
        // disable blocks display to avoid confusing output
        ds->details_mask ^= DIRED_SHOW_BLOCKS;
    }

    /* construct list buffer */
    /* deleting buffer contents resets s->offset and s->offset_top */
    eb_clear(b);

    if (ds->header_lines == 1) {
        b->cur_style = DIRED_STYLE_HEADER;
        eb_puts(b, "  Explorer \n");
    } else {
        int seq = ' ';
        b->cur_style = DIRED_STYLE_HEADER;
        eb_puts(b, "  Directory of ");
        b->cur_style = DIRED_STYLE_DIRECTORY;
        eb_put_filename(b, ds->path, ds->pf_flags);
        b->cur_style = DIRED_STYLE_HEADER;
        eb_puts(b, "\n  ");
        if (ds->ndirs) {
            eb_printf(b, "%c %d %s", seq, ds->ndirs,
                      inflect(ds->ndirs, "directory", "directories"));
            seq = ',';
        }
        if (ds->ndirs_hidden) {
            eb_printf(b, "%c %d %s", seq, ds->ndirs_hidden,
                      inflect(ds->ndirs_hidden, "hidden directory", "hidden directories"));
            seq = ',';
        }
        if (ds->nfiles) {
            eb_printf(b, "%c %d %s", seq, ds->nfiles,
                      inflect(ds->nfiles, "file", "files"));
            seq = ',';
        }
        if (ds->nfiles_hidden) {
            eb_printf(b, "%c %d %s", seq, ds->nfiles_hidden,
                      inflect(ds->nfiles_hidden, "hidden file", "hidden files"));
            seq = ',';
        }
        if (ds->total_bytes) {
            format_number(buf, sizeof(buf), ds->hflag, ds->total_bytes);
            eb_printf(b, "%c %s %s", seq, buf,
                      inflect(ds->total_bytes, "byte", "bytes"));
            seq = ',';
        }
        if (ds->ndirs + ds->ndirs_hidden + ds->nfiles + ds->nfiles_hidden == 0) {
            eb_printf(b, "%c empty", seq);
        }
        eb_putc(b, '\n');
    }
    b->cur_style = DIRED_STYLE_NORMAL;

    for (i = 0; i < ds->nb_items; i++) {
        dip = ds->items[i];
        dip->offset = b->offset;
        if (dip == cur_item) {
            ds->last_cur = dip;
            if (s)
                s->offset = b->offset;
        }
        if (dip->hidden)
            continue;

        dired_format_details(ds, dip, ds->details_mask, buf, sizeof buf);
        col = eb_printf(b, "%c %s", dip->mark, buf);
        ds->fnamecol = col;
        if (ds->sort_mode & DIRED_SORT_GROUP) {
            fname = dip->name;
            indent = dip->level;
        } else {
            if (*dip->name == '~' || *dip->name == '/') {
                fname = dip->name;
            } else {
                fname = get_relativename(dip->fullname, ds->path);
                fname += (*fname == '/' && fname[1] != '\0');
            }
            indent = 0;
        }
        col += eb_printf(b, "%*s%c ", indent, "", dip->tick);

        if (dip->flags & DI_ISDIR)
            b->cur_style = DIRED_STYLE_DIRECTORY;
        else
            b->cur_style = DIRED_STYLE_FILENAME;

        eb_put_filename(b, fname, ds->pf_flags);

        if (*fname != '/' || fname[1]) {
            int trailchar = get_trailchar(dip->mode);
            if (trailchar) {
                eb_putc(b, trailchar);
            }
        }
        if (S_ISLNK(dip->mode)
        &&  getentryslink(buf, sizeof(buf), dip->fullname)) {
            eb_puts(b, " -> ");
            eb_put_filename(b, buf, ds->pf_flags);
        }
        b->cur_style = DIRED_STYLE_NORMAL;
        eb_putc(b, '\n');
    }
    b->modified = 0;
    b->flags |= BF_READONLY;
    if (s) {
        s->offset_top = eb_goto_pos(b, top_line, 0);
    }
}

/* dired-mode commands */

static void dired_up_down(EditState *s, int dir)
{
    DiredState *ds;
    int line;

    if (!(ds = dired_get_state(s->b, s)))
        return;

    if (dir)
        text_move_up_down(s, dir);

    if (s->offset && s->offset == s->b->total_size)
        text_move_up_down(s, -1);

    line = list_get_pos(s);
    if (line >= ds->header_lines)
        s->offset = eb_goto_pos(s->b, line, 0);
}

static void dired_mark(EditState *s, int mark)
{
    DiredState *ds;
    DiredItem *dip;
    int dir = 1, flags;
    char32_t ch;

    if (!(ds = dired_get_state(s->b, s)))
        return;

    if (mark < 0) {
        dir = -1;
        mark = ' ';
    }
    if (dir < 0)
        dired_up_down(s, -1);

    dip = dired_get_cur_item(ds, s);
    if (dip) {
        ch = dip->mark = mark;
        do_bol(s);
        flags = s->b->flags & BF_READONLY;
        s->b->flags ^= flags;
        eb_replace_char32(s->b, s->offset, ch);
        s->b->flags ^= flags;
    }
    if (dir > 0)
        dired_up_down(s, 1);
}

static void sortkey_complete(CompleteState *cp, CompleteFunc enumerate) {
    char current[MAX_FILENAME_SIZE];
    const char *p;
    size_t len;

    pstrcpy(current, sizeof current, cp->current);
    len = strlen(current);
    if (len + 1 < sizeof(current)) {
        current[len + 1] = '\0';
        for (p = "fnesdug+-r"; *p; p++) {
            current[len] = *p;
            (*enumerate)(cp, current, CT_GLOB);
        }
    }
}

static int sortkey_print_entry(CompleteState *cp, EditState *s, const char *name) {
    if (name && *name) {
        char c = name[strlen(name) - 1];
        const char *msg = "";
        switch (c) {
        case 'n':       /* name */
            msg = "sort entries by filename";
            break;
        case 'f':       /* fullname */
            msg = "sort entries by full pathname";
            break;
        case 'e':       /* extension */
            msg = "sort entries by file name extension";
            break;
        case 's':       /* size */
            msg = "sort entries by file size";
            break;
        case 'd':       /* date */
            msg = "sort entries by file modification time";
            break;
        case 'g':       /* group */
            msg = "group directories";
            break;
        case 'u':       /* ungroup */
            msg = "ungroup directories";
            break;
        case 'r':       /* reverse */
            msg = "reverse sorting order";
            break;
        case '+':       /* ascending */
            msg = "sort by ascending order";
            break;
        case '-':       /* descending */
            msg = "sort by descending";
            break;
        }
        return eb_printf(s->b, "%c\t%s", c, msg);
    }
    return 0;
}

static CompletionDef dired_sort_completion = {
    .name = "sortkey",
    .enumerate = sortkey_complete,
    .print_entry = sortkey_print_entry,
};

static QVarType dired_sort_mode_set_value(EditState *s, VarDef *vp,
    void *ptr, const char *str, int sort_mode)
{
    const char *p;

    for (p = str; p && *p; p++) {
        switch (qe_tolower((unsigned char)*p)) {
        case 'n':       /* name */
            sort_mode &= ~DIRED_SORT_MASK;
            sort_mode |= DIRED_SORT_NAME;
            break;
        case 'f':       /* fullname */
            sort_mode &= ~DIRED_SORT_MASK;
            sort_mode |= DIRED_SORT_FULLNAME;
            break;
        case 'e':       /* extension */
            sort_mode &= ~DIRED_SORT_MASK;
            sort_mode |= DIRED_SORT_EXTENSION;
            break;
        case 's':       /* size */
            sort_mode &= ~DIRED_SORT_MASK;
            sort_mode |= DIRED_SORT_SIZE;
            break;
        case 'd':       /* date */
            sort_mode &= ~DIRED_SORT_MASK;
            sort_mode |= DIRED_SORT_DATE;
            break;
        case 'g':       /* group */
            sort_mode |= DIRED_SORT_GROUP;
            break;
        case 'u':       /* ungroup */
            sort_mode &= ~DIRED_SORT_GROUP;
            break;
        case 'r':       /* reverse */
            sort_mode ^= DIRED_SORT_DESCENDING;
            break;
        case '+':       /* ascending */
            sort_mode &= ~DIRED_SORT_DESCENDING;
            break;
        case '-':       /* descending */
            sort_mode |= DIRED_SORT_DESCENDING;
            break;
        }
    }
    if (dired_sort_mode != sort_mode) {
        /* XXX: should broadcast modification for side
         * effect on all windows.
         */
        dired_sort_mode = sort_mode;
        vp->modified = 1;
    }
    return VAR_NUMBER;
}

static void dired_sort(EditState *s, const char *sort_order)
{
    DiredState *ds = dired_get_state(s->b, NULL);

    dired_sort_mode_set_value(s, &dired_variables[0], &dired_sort_mode,
                              sort_order, dired_sort_mode);

    // FIXME: should update all dired buffers
    if (ds)
        dired_update_buffer(ds, s->b, s, 0);
}

static QVarType dired_time_format_set_value(EditState *s, VarDef *vp,
                                            void *ptr, const char *str, int format)
{
    if (str) {
        if (!strxcmp(str, "default"))    format = TF_COMPACT;    else
        if (!strxcmp(str, "compact"))    format = TF_COMPACT;    else
        if (!strxcmp(str, "dos"))        format = TF_DOS;        else
        if (!strxcmp(str, "dos-long"))   format = TF_DOS_LONG;   else
        if (!strxcmp(str, "touch"))      format = TF_TOUCH;      else
        if (!strxcmp(str, "touch-long")) format = TF_TOUCH_LONG; else
        if (!strxcmp(str, "full"))       format = TF_FULL;       else
        if (!strxcmp(str, "seconds"))    format = TF_SECONDS;    else
            return VAR_UNKNOWN;
    }
    if (format < 0 || format > TF_MAX)
       return VAR_UNKNOWN;

    if (dired_time_format != (enum time_format)format) {
        dired_time_format = format;
        vp->modified = 1;
    }
    return VAR_NUMBER;
}

static void dired_set_time_format(EditState *s, int format)
{
    dired_time_format_set_value(s, &dired_variables[1], &dired_time_format,
                                NULL, format);
}

static DiredItem *dired_add_item(DiredState *ds, const char *name,
                                 const char *fullname, int level)
{
    struct stat st;
    DiredItem *dip;
    size_t fullname_size = strlen(fullname) + 1;
    size_t name_size = strlen(name) + 1;
    size_t name_offset = fullname_size;

    if (lstat(fullname, &st) < 0)
        memset(&st, 0, sizeof st);

    if (fullname_size >= name_size
    &&  !memcmp(fullname + fullname_size - name_size, name, name_size)) {
        /* share space for name and fullname */
        name_offset = fullname_size - name_size;
        name_size = 0;
    }

    dip = qe_malloc_hack(DiredItem, fullname_size + name_size);
    if (!dip)
        return NULL;
    dip->flags = 0;
    if (S_ISLNK(st.st_mode)) {
        struct stat st1;
        dip->flags |= DI_ISLNK;
        if (!stat(fullname, &st1)) {
            if (S_ISDIR(st1.st_mode))
                dip->flags |= DI_ISDIR;
        } else {
            /* broken symbolic link */
            dip->flags |= DI_BROKEN;
        }
    } else
    if (S_ISDIR(st.st_mode)) {
        dip->flags |= DI_ISDIR;
    }
    dip->mode = st.st_mode;
    dip->nlink = st.st_nlink;
    dip->uid = st.st_uid;
    dip->gid = st.st_gid;
    dip->rdev = st.st_rdev;
    dip->mtime = st.st_mtime;
    dip->size = st.st_size;
    dip->hidden = 0;
    dip->mark = ' ';
    dip->tick = ' ';
    dip->level = level;
    if (dip->flags & DI_ISDIR)
        dip->tick = '>';
    memcpy(dip->fullname, fullname, fullname_size);
    dip->name = dip->fullname + name_offset;
    memcpy(dip->name, name, name_size);

    if (ds->nb_items >= ds->nb_allocated) {
        int n = ds->nb_allocated + (ds->nb_allocated / 2) + 32;
        if (!qe_realloc_array(&ds->items, n)) {
            qe_free(&dip);
            return NULL;
        }
        ds->nb_allocated = n;
    }
    return ds->items[ds->nb_items++] = dip;
}

/* `ds` and `dir` are valid, `dip` and `pattern` may be NULL */
static int dired_expand_dir(DiredState *ds, DiredItem *dip,
                            const char *dir, const char *pattern)
{
    FindFileState *ffst;
    char filename[MAX_FILENAME_SIZE];
    int count = 0;
    int level = dip ? dip->level + 1 : 0;

    /* XXX: should scan directory for subdirectories and filter with
     * pattern only for regular files.
     * XXX: should handle generalized file patterns.
     * XXX: should use a separate thread to make the scan asynchronous.
     * XXX: should compute recursive size data.
     * XXX: should track file creation, deletion and modifications.
     */
    if (!pattern)
        pattern = "*";
    ffst = find_file_open(dir, pattern, FF_NOXXDIR);
    while (!find_file_next(ffst, filename, sizeof(filename))) {
        if (dired_add_item(ds, get_basename(filename), filename, level))
            count++;
    }
    find_file_close(&ffst);
    if (dip)
        dip->tick = count ? 'v' : '-';
    return count;
}

static int dired_collapse_dir(DiredState *ds, DiredItem *dip0)
{
    int i, j, count = 0;
    size_t len = strlen(dip0->fullname);

    if (dip0->flags & DI_ISDIR)
        dip0->tick = '>';
    // XXX: should hide the whole subtree?
    for (i = j = 0; i < ds->nb_items; i++) {
        DiredItem *dip = ds->items[i];
        if (dip != dip0
        &&  !strncmp(dip0->fullname, dip->fullname, len)
        &&  dip->fullname[len] == '/')
        {
            qe_free(&ds->items[i]);
            count++;
        } else {
            ds->items[j++] = ds->items[i];
        }
    }
    ds->nb_items = j;
    return count;
}

/* `ds` and `b` are valid, `s` may be NULL */
static void dired_build_list(DiredState *ds, const char *path)
{
    char dirname[MAX_FILENAME_SIZE];
    char name[MAX_FILENAME_SIZE];
    DiredItem *dip;

    /* free previous list, if any */
    dired_free(ds);

    ds->last_cur = NULL;
    ds->blocksize = 1024;   /* XXX: should get from the filesystem */
    ds->last_width = 0;

    canonicalize_path(ds->path, sizeof(ds->path), path);

    if (is_directory(ds->path)) {
        pstrcpy(dirname, sizeof dirname, ds->path);
        strcpy(ds->pattern, "*");
    } else {
        get_dirname(dirname, sizeof dirname, ds->path);
        pstrcpy(ds->pattern, sizeof(ds->pattern), get_basename(ds->path));
        if (!is_filepattern(ds->pattern))
            strcpy(ds->pattern, "*");
    }

    if (ds->header_lines == 1) {
        make_user_path(name, sizeof name, dirname);
        dip = dired_add_item(ds, name, dirname, 0);
        dired_expand_dir(ds, dip, dirname, ds->pattern);
    } else {
        dired_expand_dir(ds, NULL, dirname, ds->pattern);
    }
}

/* select current item */
static void dired_select(EditState *s, int mode)
{
    char filename[MAX_FILENAME_SIZE];
    DiredState *ds;
    struct stat st;
    EditState *e;
    DiredItem *dip;

    if (!(ds = dired_get_state(s->b, s)))
        return;

    dip = dired_get_cur_item(ds, s);
    if (!dip) {
        dired_goto_target(ds, s, ds->target, TRUE);
        return;
    }

    if (dip->flags & DI_ISDIR) {
        if (dip->tick == '>' || dip->tick == '-') {
            dired_expand_dir(ds, dip, dip->fullname, NULL);
            dired_update_buffer(ds, s->b, s, DIRED_UPDATE_ALL);
            if (classify_fullnames(dip->fullname, ds->target) == DIR1_PARENT)
                dired_goto_target(ds, s, ds->target, TRUE);
        } else
        if (dip->tick == 'v') {
            if (mode == 2) {
                dired_collapse_dir(ds, dip);
                dired_update_buffer(ds, s->b, s, DIRED_UPDATE_ALL);
            } else {
                if (classify_fullnames(dip->fullname, ds->target) != DIR1_PARENT
                ||  !dired_goto_target(ds, s, ds->target, TRUE)) {
                    dired_up_down(s, 1);
                }
            }
        }
        return;
    }

    if (!dired_get_filename(ds, dip, filename, sizeof(filename)))
        return;

    /* Check if path leads somewhere */
    if (stat(filename, &st) < 0)
        return;

    if (S_ISDIR(st.st_mode)) {
        /* DO descend into directories pointed to by symlinks */
        /* XXX: should expand directory below current position
         * or merge generated items with existing items in ds->items */
        dired_build_list(ds, filename);
        dired_update_buffer(ds, s->b, s, DIRED_UPDATE_ALL);
    } else
    if (S_ISREG(st.st_mode)) {
        /* do explore files pointed to by symlinks */
        e = find_window(s, KEY_RIGHT, NULL);
        if (e) {
#if 1
            s->qs->active_window = e;
            if (mode == 1) {
                /* XXX: should keep BF_PREVIEW flag and set pager-mode */
                e->b->flags &= ~BF_PREVIEW;
            }
#else
            /* delete dired window */
            do_delete_window(s, 1);
            /* XXX: should keep BF_PREVIEW flag and set pager-mode */
            e->b->flags &= ~BF_PREVIEW;
#endif
        } else {
            do_find_file(s, filename, 0);
        }
    }
}

static EditState *dired_view_file(EditState *s, const char *filename)
{
    EditBuffer *b;
    EditState *e;
    int rc;

    e = find_window(s, KEY_RIGHT, NULL);
    if (!e)
        return NULL;

    /* close previous temporary buffers, if any */
    b = e->b;
    if (b && (b->flags & BF_PREVIEW) && !b->modified) {
        /* will free the buffer if no longer viewed */
        b->flags |= BF_TRANSIENT;
    }

    /* Load file and attach to window. If file not loaded already, mark
     * new buffer as BF_PREVIEW, to trigger paging mode and so that it
     * will get freed if closed.
     */
    rc = qe_load_file(e, filename, LF_NOWILDCARD, BF_PREVIEW);
    if (rc >= 0) {
        return e;
    } else {
        /* if file failed to load, show a scratch buffer */
        b = qe_new_buffer(s->qs, "*scratch*", BF_SAVELOG | BF_UTF8 | BF_PREVIEW);
        if (b) {
            // XXX: should show error cause
            eb_printf(b, "Cannot load file %s", filename);
            switch_to_buffer(e, b);
        }
        return NULL;
    }
}

static void dired_execute(EditState *s)
{
    /* Actually delete, copy, or move the marked items */
    put_error(s, "Not yet implemented");
}

static void dired_parent(EditState *s, int collapse)
{
    char path[MAX_FILENAME_SIZE];
    char dir[MAX_FILENAME_SIZE];
    DiredItem *dip;
    DiredState *ds;

    if (!(ds = dired_get_state(s->b, s)))
        return;

    dip = dired_get_cur_item(ds, s);
    if (dip) {
        if (strcmp(dip->fullname, ds->path)) {
            if (dip->tick == 'v' && collapse) {
                dired_collapse_dir(ds, dip);
                dired_update_buffer(ds, s->b, s, DIRED_UPDATE_ALL);
                return;
            }
            get_dirname(dir, sizeof dir, dip->fullname);
            if (dired_goto_target(ds, s, dir, FALSE))
                return;
        }
    }
    if (s->b->flags & BF_PREVIEW) {
        EditState *e = find_window(s, KEY_LEFT, NULL);
        if (e && (e->flags & WF_FILELIST)) {
            s->qs->active_window = e;
            return;
        }
    }
    /* FIXME: should just prepend parent directory */
    pstrcpy(path, sizeof path, ds->path);
    get_dirname(dir, sizeof dir, path);
    dired_build_list(ds, dir);
    dired_update_buffer(ds, s->b, s, DIRED_UPDATE_ALL);
    dip = dired_goto_target(ds, s, path, TRUE);
    if (dip) {
        dired_expand_dir(ds, dip, dip->fullname, NULL);
        dired_update_buffer(ds, s->b, s, DIRED_UPDATE_ALL);
    }
}

static void dired_toggle_human(EditState *s)
{
    dired_hflag = (dired_hflag + 1) % 3;
}

static void dired_toggle_nflag(EditState *s)
{
    dired_nflag = (dired_nflag + 1) % 3;
}

static void dired_cycle_encoding(EditState *s)
{
    int encoding = dired_pf_flags & PF_ENCODING;
    dired_pf_flags &= ~PF_ENCODING;
    dired_pf_flags |= (encoding + 1) & PF_ENCODING;
}

static void dired_toggle_unicode(EditState *s)
{
    dired_pf_flags ^= PF_NO_UNICODE;
}

static void dired_hide_details_mode(EditState *s)
{
    DiredState *ds;
    if (!(ds = dired_get_state(s->b, s)))
        return;

    ds->details_flag = (ds->details_flag + 1) % 3;
}

static void dired_refresh(EditState *s)
{
    DiredState *ds;
    char target[MAX_FILENAME_SIZE];

    if (!(ds = dired_get_state(s->b, s)))
        return;

    *target = '\0';
    dired_get_cur_filename(ds, s, target, sizeof(target));
    dired_build_list(ds, ds->path);
    dired_update_buffer(ds, s->b, s, DIRED_UPDATE_ALL);
    dired_goto_target(ds, s, target, TRUE);
}

static void dired_toggle_dot_files(EditState *s, int val)
{
    if (val == -1)
        val = !dired_show_dot_files;

    if (dired_show_dot_files != val) {
        DiredState *ds = dired_get_state(s->b, NULL);
        dired_show_dot_files = val;
        if (ds)
            dired_update_buffer(ds, s->b, s, DIRED_UPDATE_FILTER);
        put_status(s, "Dot files are %s", val ? "visible" : "hidden");
    }
}

static void dired_display_hook(EditState *s)
{
    char filename[MAX_FILENAME_SIZE];
    DiredState *ds;
    DiredItem *dip;
    int flags = 0;

    if (!(ds = dired_get_state(s->b, NULL)))
        return;

    /* Prevent point from going beyond list */
    if (s->offset && s->offset == s->b->total_size)
        dired_up_down(s, -1);

    if (s->x1 == 0 && s->y1 == 0 && ds->last_width != s->width) {
        /* rebuild buffer contents according to new window width */
        /* XXX: this may cause problems if buffer is displayed in
         * multiple windows, hence the test on s->y1.
         * Should test for current window */
        flags |= DIRED_UPDATE_REBUILD;
    }

    dired_update_buffer(ds, s->b, s, flags);

    if (s->x1 == 0) {
        /* open file so that user can see it before it is selected */
        /* XXX: find a better solution (callback) */
        dip = dired_get_cur_item(ds, s);
        if (dip && dip != ds->last_cur) {
            ds->last_cur = dip;
            if (dired_get_filename(ds, dip, filename, sizeof(filename))) {
                dired_view_file(s, filename);
                if (!(dip->flags & DI_ISDIR)) {
                    char tmp[MAX_FILENAME_SIZE];
                    char buf[64];
                    pstrcpy(ds->target, sizeof ds->target, filename);
                    dired_format_details(ds, dip, DIRED_SHOW_ALL, buf, sizeof buf);
                    put_status(s, "-> %s %s", buf, make_user_path(tmp, sizeof tmp, filename));
                }
            }
        }
    }
}

static char *dired_get_default_path(EditBuffer *b, int offset,
                                    char *buf, int buf_size)
{
    DiredState *ds;

    ds = dired_get_state(b, NULL);
    if (ds) {
        EditState *s = eb_find_window(b, NULL);
        DiredItem *dip = dired_get_cur_item(ds, s);
        if (dip && strcmp(dip->fullname, ds->path)) {
            get_dirname(buf, buf_size, dip->fullname);
        } else {
            pstrcpy(buf, buf_size, ds->path);
            if (!is_directory(buf))
                get_dirname(buf, buf_size, buf);
        }
        append_slash(buf, buf_size);
        return buf;
    }
    if (b->filename[0]) {
        if (is_directory(b->filename)) {
            pstrcpy(buf, buf_size, b->filename);
        } else {
            get_dirname(buf, buf_size, b->filename);
        }
        append_slash(buf, buf_size);
        return buf;
    } else {
        return getcwd(buf, buf_size) ? buf : NULL;
    }
}

static int dired_mode_init(EditState *s, EditBuffer *b, int flags)
{
    DiredState *ds = qe_get_buffer_mode_data(b, &dired_mode, NULL);

    if (!ds)
        return -1;

    list_mode.mode_init(s, b, flags);

    if (flags & MODEF_NEWINSTANCE) {
        b->flags |= BF_DIRED;
        ds->header_lines = 2;
        if (s->width <= s->qs->width / 3)
            ds->header_lines = 1;
        eb_create_style_buffer(b, BF_STYLE1);
        /* XXX: File system charset should be detected automatically */
        /* XXX: If file system charset is not utf8, eb_printf will fail */
        eb_set_charset(b, &charset_utf8, b->eol_type);
        /* XXX: should be built by buffer_load API */
        if (*b->filename) {
            dired_build_list(ds, b->filename);
            dired_update_buffer(ds, b, s, DIRED_UPDATE_ALL);
            s->offset = eb_goto_pos(b, ds->header_lines, 0);
        }
    }
    return 0;
}

static void dired_mode_free(EditBuffer *b, void *state)
{
    DiredState *ds = state;

    dired_free(ds);
}

/* can only apply dired mode on directories and file patterns */
static int dired_mode_probe(ModeDef *mode, ModeProbeData *p)
{
    if (qe_get_buffer_mode_data(p->b, &dired_mode, NULL))
        return 100;

    if (S_ISDIR(p->st_mode))
        return 95;

    if (p->st_errno == ENOENT && is_filepattern(p->real_filename))
        return 90;

    return 0;
}

void do_dired_path(EditState *s, const char *filename)
{
    QEmacsState *qs = s->qs;
    EditBuffer *b;
    EditState *e;
    DiredState *ds;

    if ((s->flags & WF_POPLEFT) && (s->b->flags & BF_DIRED)
    &&  (ds = dired_get_state(s->b, NULL)) != NULL) {
        /* rebuild from current entry */
        dired_get_cur_filename(ds, s, ds->target, sizeof ds->target);
        e = s;
        b = s->b;
        goto has_buffer;
    }
    for (e = qs->first_window; e != NULL; e = e->next_window) {
        if ((e->flags & WF_POPLEFT) && (e->b->flags & BF_DIRED)
        &&  (ds = dired_get_state(e->b, NULL)) != NULL) {
            /* reuse existing dired pane */
            b = e->b;
            goto new_window;
        }
    }
    b = qe_new_buffer(qs, "*dired*", BC_CLEAR | BF_READONLY | BF_UTF8);
    if (!b)
        return;

    e = insert_window_left(b, qs->width / 5, WF_MODELINE | WF_FILELIST);
    if (!e)
        return;
    /* set dired mode: dired_mode_init() will load buffer content */
    edit_set_mode(e, &dired_mode);
    ds = dired_get_state(b, NULL);
    if (!ds)
        return;

new_window:
    /* modify active window */
    qs->active_window = e;
    /* Set target as specified filename (or directory) */
    pstrcpy(ds->target, sizeof ds->target, filename);

has_buffer:
    dired_build_list(ds, filename);
    dired_update_buffer(ds, b, e, DIRED_UPDATE_ALL);
    dired_goto_target(ds, e, ds->target, TRUE);
}

/* open dired window on the left. The directory of the current file is used */
void do_dired(EditState *s, int argval)
{
    char filename[MAX_FILENAME_SIZE];

    if (argval != NO_ARG) {
        do_filelist(s, argval);
        return;
    }

    pstrcpy(filename, sizeof filename, s->b->filename);
    if (!*filename) {
        get_default_path(s->b, s->offset, filename, sizeof filename);
    }
    do_dired_path(s, filename);
}

/* specific dired commands */
static const CmdDef dired_commands[] = {
    /* Emacs bindings:
       e .. f      dired-find-file
       !           dired-do-shell-command
       $           dired-hide-subdir
       +           dired-create-directory
       -           negative-argument
       0 .. 9      digit-argument
       <           dired-prev-dirline
       =           dired-diff
       >           dired-next-dirline
       ?           dired-summary
       A           dired-do-search
       B           dired-do-byte-compile
       C           dired-do-copy
       D           dired-do-delete
       G           dired-do-chgrp
       H           dired-do-hardlink
       L           dired-do-load
       M           dired-do-chmod
       O           dired-do-chown
       P           dired-do-print
       Q           dired-do-query-replace-regexp
       R           dired-do-rename
                     rename a file or move selection to another directory
       S           dired-do-symlink
       T           dired-do-touch
       U           dired-unmark-all-marks
       X           dired-do-shell-command
       Z           dired-do-compress
       ^           dired-up-directory
       a           dired-find-alternate-file
       h           describe-mode
       i, +        dired-maybe-insert-subdir
       j           dired-goto-file
       g           revert-buffer
                     read all currently expanded directories aGain.
       k           dired-do-kill-lines
       l           dired-do-redisplay
                     relist single directory or marked files?
       o           dired-find-file-other-window
       q           quit-window
       s           dired-sort-toggle-or-edit
                     toggle sorting by name and by date
                     with prefix: set the ls command line options
       t           dired-toggle-marks
       v           dired-view-file
       w           dired-copy-filename-as-kill
       x           dired-do-flagged-delete
       y           dired-show-file-type
       ~           dired-flag-backup-files
       M-C-d       dired-tree-down
       M-C-n       dired-next-subdir
       M-C-p       dired-prev-subdir
       M-C-u       dired-tree-up
       M-$         dired-hide-all
       M-{         dired-prev-marked-file
       M-}         dired-next-marked-file
       M-DEL       dired-unmark-all-files
     * C-n         dired-next-marked-file
     * C-p         dired-prev-marked-file
     * !           dired-unmark-all-marks
     * %           dired-mark-files-regexp
     * *           dired-mark-executables
     * /           dired-mark-directories
     * ?           dired-unmark-all-files
     * @           dired-mark-symlinks
     * c           dired-change-marks
     * m           dired-mark
     * s           dired-mark-subdir-files
     * t           dired-toggle-marks
     * u           dired-unmark
     * need commands for splitting, unsplitting, zooming,
       marking files globally.
     */
    CMD1( "dired-enter", "RET, LF",
          "Select the current entry",
          dired_select, 1)
    CMD1( "dired-expand", "SPC",
          "Expand / collapse directory",
          dired_select, 2)
    CMD1( "dired-right", "right",
          "Select the current entry in preview mode",
          dired_select, 0)
    CMD3( "dired-tab", "TAB",
          "Move focus to the current file",
          do_other_window, ESi, "#" "v", 1)
    /* dired-abort should restore previous buffer in right-window */
    CMD1( "dired-abort", "C-g, q",
          "Quit the dired mode",
          do_delete_window, 0)
    CMD1( "dired-unmark-backward", "DEL",
          "Move to the previous entry and unmark it",
          dired_mark, -1)
    CMD2( "dired-sort", "s",
          "Sort entries using option string",
          dired_sort, ESs,
          "s{Sort order [fnesdug+-r]: }[sortkey]|sortkey|")
    CMD2( "dired-set-time-format", "t",
          "Select the format for file times",
          dired_set_time_format, ESi,
          "n{Time format: }[timeformat]")
    CMD1( "dired-delete", "d",
          "Mark the entry for deletion",
          dired_mark, 'D')
    CMD1( "dired-copy", "c",
          "Mark the entry for copying",
          dired_mark, 'C')
    CMD1( "dired-mark", "m",
          "Mark the entry for something",
          dired_mark, '*')
    CMD1( "dired-unmark", "u",
          "Unmark the current entry",
          dired_mark, ' ')
    CMD0( "dired-execute", "x",
          "Execute the pending operations on marked entries (not implemented yet)",
          dired_execute)
    CMD1( "dired-next-line", "n, C-n, down",
          "Move to the next entry",
          dired_up_down, 1)
    CMD1( "dired-previous-line", "p, C-p, up",
          "Move to the previous entry",
          dired_up_down, -1)
    CMD0( "dired-refresh", "r",
          "Refresh directory contents",
          dired_refresh)
    CMD1( "dired-toggle-dot-files", ".",
          "Display or hide entries starting with .",
          dired_toggle_dot_files, -1)
    CMD1( "dired-parent", "^",
          "Select the parent directory",
          dired_parent, 0)
    CMD1( "dired-collapse-or-parent", "left",
          "Collapse the directory or select the parent directory",
          dired_parent, 1)
    CMD0( "dired-toggle-human", "H",
          "Change the format for file sizes (human readable vs: actual byte count)",
          dired_toggle_human)
    CMD0( "dired-toggle-nflag", "N",
          "Change the format for uid and gid (name vs: number)",
          dired_toggle_nflag)
    CMD0( "dired-hide-details-mode", "(",
          "Toggle visibility of detailed information in current Dired buffer)",
          dired_hide_details_mode)
    CMD0( "dired-cycle-encoding", "\\",
          "Cycle display of non printing characters in filenames",
          dired_cycle_encoding)
    CMD0( "dired-toggle-unicode", "U",
          "Toggle display of Unicode characters in filenames",
          dired_toggle_unicode)
    CMD2( "dired-summary", "?",
          "Display a summary of dired commands",
          do_apropos, ESs, "@{dired}")
};

static const CmdDef dired_global_commands[] = {
    CMD2( "dired", "C-x C-d",
          "Display the directory window and start dired mode",
          do_dired, ESi, "#" "P")
};

#if 0
static int dired_buffer_load(EditBuffer *b, FILE *f)
{
    /* XXX: launch subprocess to list dired contents */
    return -1;
}

static int dired_buffer_save(EditBuffer *b, int start, int end,
                               const char *filename)
{
    /* XXX: prevent saving parsed contents to dired file */
    return -1;
}

static void dired_buffer_close(EditBuffer *b)
{
    /* XXX: free dired structures? */
}

static EditBufferDataType dired_data_type = {
    "dired",
    dired_buffer_load,
    dired_buffer_save,
    dired_buffer_close,
    NULL, /* next */
};
#endif

static int filelist_init(QEmacsState *qs);

static int dired_init(QEmacsState *qs)
{
    /* inherit from list mode */
    /* CG: assuming list_mode already initialized ? */
    // XXX: remove this mess
    memcpy(&dired_mode, &list_mode, offsetof(ModeDef, first_key));
    dired_mode.name = "dired";
    dired_mode.mode_probe = dired_mode_probe;
    dired_mode.buffer_instance_size = sizeof(DiredState);
    dired_mode.mode_init = dired_mode_init;
    dired_mode.mode_free = dired_mode_free;
    /* CG: not a good idea, display hook has side effect on layout */
    dired_mode.display_hook = dired_display_hook;
    dired_mode.get_default_path = dired_get_default_path;

    //qe_register_data_type(qs, &dired_data_type);
    qe_register_mode(qs, &dired_mode, /* MODEF_DATATYPE | */ MODEF_MAJOR | MODEF_VIEW);
    qe_register_variables(qs, dired_variables, countof(dired_variables));
    qe_register_commands(qs, &dired_mode, dired_commands, countof(dired_commands));
    qe_register_commands(qs, NULL, dired_global_commands, countof(dired_global_commands));
    qe_register_completion(qs, &dired_sort_completion);

    filelist_init(qs);

    return 0;
}

int file_print_entry(CompleteState *cp, EditState *s, const char *name) {
    struct stat st;
    EditBuffer *b = s->b;
    char buf[20];
    int len, sizelen = 10, linklen = 2, uidlen = 8, gidlen = 8;

    if (!stat(name, &st)) {
        b->cur_style = S_ISDIR(st.st_mode) ? DIRED_STYLE_DIRECTORY : DIRED_STYLE_FILENAME;
        len = eb_put_filename(b, name, dired_pf_flags);
        b->tab_width = max3_int(16, len + 2, b->tab_width);
        b->cur_style = DIRED_STYLE_NORMAL;
        format_size(buf, sizeof(buf), dired_hflag, st.st_mode, st.st_dev, st.st_size);
        len += eb_printf(b, "\t%*s", sizelen, buf);
        format_date(buf, sizeof(buf), st.st_mtime, dired_time_format);
        len += eb_printf(b, "  %s", buf);
        len += eb_printf(b, "  %s", compute_attr(buf, st.st_mode));
        format_uid(buf, sizeof(buf), dired_nflag, st.st_uid);
        len += eb_printf(b, "  %-*s", uidlen, buf);
        format_gid(buf, sizeof(buf), dired_nflag, st.st_gid);
        len += eb_printf(b, "  %-*s", gidlen, buf);
        len += eb_printf(b, "  %*d", linklen, (int)st.st_nlink);
    } else {
        len = eb_put_filename(b, name, dired_pf_flags);
    }
    return len;
}

/*---------------- filelist mode ----------------*/

static char filelist_last_buf[MAX_FILENAME_SIZE];

static ModeDef filelist_mode;

static void filelist_display_hook(EditState *s)
{
    char buf[MAX_FILENAME_SIZE];
    char dir[MAX_FILENAME_SIZE];
    char filename[MAX_FILENAME_SIZE];
    QEmacsState *qs = s->qs;
    EditState *e;
    int i, len, offset, target_line;

    offset = eb_goto_bol(s->b, s->offset);
    len = eb_fgets(s->b, buf, sizeof(buf), offset, &offset);
    buf[len] = '\0';   /* strip the trailing newline if any */

    if (s->x1 == 0 && s->y1 == 0 && s->width != qs->width
    &&  *buf && !strequal(buf, filelist_last_buf)) {
        /* open file so that user can see it before it is selected */
        /* XXX: find a better solution (callback) */
        pstrcpy(filelist_last_buf, sizeof(filelist_last_buf), buf);
        get_default_path(s->b, offset, dir, sizeof(dir));
        makepath(filename, sizeof(filename), dir, buf);
        target_line = 0;
        if (access(filename, R_OK)) {
            /* try parsing an error message: `:` or `(` a linenumber */
            i = strcspn(buf, ":(");
            if (i < len) {
                char c = buf[i];
                buf[i] = '\0';
                makepath(filename, sizeof(filename), dir, buf);
                buf[i] = c;
                target_line = strtol(buf + i + 1, NULL, 10);
            }
            i = 0;
            while (access(filename, R_OK)) {
                /* try skipping initial words */
                i += strcspn(buf + i, " ");
                i += strspn(buf + i, " ");
                if (i == len)
                    break;
                makepath(filename, sizeof(filename), dir, buf + i);
            }
        }
        if (!access(filename, R_OK)) {
            e = dired_view_file(s, filename);
            if (e) {
                if (target_line > 0)
                    do_goto_line(e, target_line, 0);
            }
            put_status(e, "Previewing %s", filename);
        } else {
            put_error(s, "No access to %s", filename);
        }
    }
}

void do_filelist(EditState *s, int argval)
{
    QEmacsState *qs = s->qs;
    EditState *e;

    e = insert_window_left(s->b, qs->width / 5, WF_MODELINE | WF_FILELIST);
    if (e != NULL) {
        edit_set_mode(e, &filelist_mode);
        /* XXX: should come from mode.default_wrap */
        e->wrap = WRAP_TRUNCATE;
        filelist_last_buf[0] = '\0';
        qs->active_window = e;
    }
}

static int filelist_mode_init(EditState *s, EditBuffer *b, int flags)
{
    if (s) {
        /* XXX: should come from mode.default_wrap */
        s->wrap = WRAP_TRUNCATE;
    }
    return 0;
}

static const CmdDef filelist_commands[] = {
    CMD3( "filelist-select", "RET, LF, right",
          "Select the current entry",
          do_other_window, ESi, "#" "p", 1)
    CMD3( "filelist-tab", "TAB",
          "Move focus to the current file",
          do_other_window, ESi, "#" "v", 1)
    /* filelist-abort should restore previous buffer in right-window
     * or at least exit preview mode */
    CMD1( "filelist-abort", "C-g",
          "Quit the filelist mode",
          do_delete_window, 0)
};

static const CmdDef filelist_global_commands[] = {
    CMD2( "filelist", "",
          "Run the filelist-mode on the current region",
          do_filelist, ESi, "#" "p")
};

static int filelist_init(QEmacsState *qs)
{
    // XXX: remove this mess
    memcpy(&filelist_mode, &text_mode, offsetof(ModeDef, first_key));
    filelist_mode.name = "filelist";
    filelist_mode.mode_probe = NULL;
    filelist_mode.mode_init = filelist_mode_init;
    filelist_mode.display_hook = filelist_display_hook;

    qe_register_mode(qs, &filelist_mode, MODEF_VIEW);
    qe_register_commands(qs, &filelist_mode, filelist_commands, countof(filelist_commands));
    qe_register_commands(qs, NULL, filelist_global_commands, countof(filelist_global_commands));
    return 0;
}

qe_module_init(dired_init);
