#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>  /* opendir  */
#include <dirent.h>     /* opendir, readdir */
#include <errno.h>

typedef int (*fltfn) (void *, void*, void *);       /* typedef for filter function  */

struct dirent **rddirflt (char *fn, fltfn filter,   /* read dir & filter contents   */
                           void *ptr, void *eptr);
int filter (void *de, void *m, void *e);            /* filter by both type and ext  */
int cmpdep (const void *p1, const void *p2);        /* qsort compare for dirent**   */
struct dirent **realloc_de (struct dirent **dep,    /* realloc array of  dirent**   */
                            size_t *n);
void free_dep (struct dirent **p);                  /* free array of  dirent**      */
char *fn_ext (char *fn);                            /* return ext from filename     */

int main (int argc, char **argv) {

    struct dirent **dlist = NULL;                   /* directory content array      */
    char *dname = (argc > 1) ? argv[1] : ".";       /* dirname supplied as argument */
    size_t idx = 0;                                 /* content array index          */
    unsigned char fltmode = argc > 2 ?              /* file mode for filter         */
        (unsigned char)strtoul (argv[2], NULL, 10)
        : 14;
    char *eptr = argc > 3 ? argv[3] : NULL;         /* ptr to extension for filter  */

    /* read array of directory entries into dlist */
    if (!(dlist = rddirflt (dname, filter, &fltmode, eptr)))
    {
        fprintf (stderr, "error: rddir failed for '%s'\n", dname);
        return 1;
    }

    /* output search criteria used, sorted forward and reverse listings */
    printf ("\n opendir/readdir: rddirflt (%s, filter, &%hhu, %s)\n\n",
            dname, fltmode, eptr);
    while (dlist[idx])
    {
        printf ("  dlist[%2zu]->d_name %-22s  ext: %s\n",
                idx, dlist[idx]->d_name, fn_ext(dlist[idx]->d_name));
        idx++;
    }

    printf ("\n reverse:\n\n");
    while (idx--)
        printf ("  dlist[%2zu]->d_name %s\n", idx, dlist[idx]->d_name);

    free_dep (dlist);   /* free dir list memory */
    // dlist = NULL;       /* reset pointer NULL   */
    // idx = 0;            /* reset index counter  */

    printf ("\n");

    return 0;
}

/* opendir/readdir selection based on generic filter function.
 * memory for the returned listing is allocated dynamically and
 * reallocated as necessary, removing any arbitrary constraint
 * concerning listing size. the free_dep function can be use to
 * free all listing data when no longer needed.
 *
 * NOTE (LATEST)
 * filter readdir results based on file type and file extension.
 * a single function pointer passes the filter function and critera
 * for the function is passed as void types through ptr and eptr.
 * (simply short for pointer and extension-pointer)
 */
struct dirent **rddirflt (char *fn, fltfn filter, void *ptr, void *eptr)
{
    DIR *dp = opendir (fn);

    if (!dp) {
        fprintf (stderr, "error: opendir failed on '%s'\n", fn);
        return NULL;
    }

    struct dirent **dlist = NULL;   /* ptr to array of ptrs to de       */
    size_t idx = 0;                 /* dir listing array index          */
    size_t imax = 128;              /* initial listing allocation size  */
    struct dirent *de = NULL;       /* dirent pointer for readdir       */

    /* allocate imax char pointers  */
    if (!(dlist = calloc (imax, sizeof *dlist))) {
        fprintf (stderr, "Error: dlist memory allocation failed\n");
        return NULL;
    }

    while ((de = readdir (dp)))
    {
        /* skip dot files */
        if (!strcmp (de->d_name, ".") || !strcmp (de->d_name, ".."))
            continue;

        /* skip files not matching filter criteria */
        if (filter)
            if (!filter (de, ptr, eptr))
                continue;

        if (!(dlist[idx] = calloc (1, sizeof **dlist))) {
            fprintf (stderr, "Error: dlist memory allocation failed\n");
            return NULL;
        }
        memcpy (dlist[idx], de, sizeof *de);

        idx++;

        /* check to realloc dir content array if idx >= imax  */
        if (idx == imax)
            dlist = realloc_de (dlist, &imax);
    }
    closedir (dp);

    /* sort readdir results */
    qsort (dlist, idx, sizeof *dlist, cmpdep);

    return dlist;
}

/*
 * filter dirent entries based on type and extension
 */
int filter (void *de, void *m, void *e)
{
    if (!de) return 0;

    unsigned char mode = *(unsigned char *) m;
    struct dirent *dp = (struct dirent *) de;

    /* select only matching file types based on mode.
     * (dirname, regular files & symlinks)
     */
    if ((mode >> 1) & 1 && dp-> d_type == DT_LNK)
        goto typeok;
    if ((mode >> 2) & 1 && dp-> d_type == DT_DIR)
        goto typeok;
    if ((mode >> 3) & 1 && dp-> d_type == DT_REG)
        goto typeok;

    return 0;

  typeok:;

    /* select only matching files based on extension. */
    if (!e) return 1;
    char *sp = dp->d_name;
    char *ep = NULL;

    if (!(ep = strrchr (sp, '.'))) return 0;
    if (ep == sp) return 0;
    ep++;

    if (strcmp (ep, (char *)e) == 0)
        return 1;
    else
        return 0;
}

/* comparison function for qsort (struct dirent **) */
int cmpdep (const void *p1, const void *p2)
{
    /* The actual arguments to this function are "pointers to
     *  pointers to struct dirent", but strcmp(3) arguments are
     *  "pointers to char", hence the following cast plus dereference
     */

    return strcmp ((*(struct dirent * const *)p1)->d_name,
                   (*(struct dirent * const *)p2)->d_name);
}

/* free all memory allocated to ptr to array of char pointers */
struct dirent **realloc_de (struct dirent **dep, size_t *n)
{
    struct dirent **tmp = realloc (dep, 2 * *n * sizeof (*dep));
    if (!tmp) {
        fprintf (stderr, "Error: struct reallocation failure.\n");
        exit (EXIT_FAILURE);
    }
    dep = tmp;
    memset (dep + *n, 0, *n * sizeof *dep); /* memset new ptrs 0 */
    *n *= 2;

    return dep;
}

/* free array of dirent pointers */
void free_dep (struct dirent **p)
{
    if (!p) return;
    size_t i = 0;
    while (p[i])
        free (p[i++]);
    free (p);
}

/* debug output - return extension from filename */
char *fn_ext (char *fn)
{
    char *sp = NULL;                /* start pointer */
    char *ext;

    if (!fn) return NULL;
    if ((sp = strrchr (fn, '/')))   /* test for '/' to eliminate '.' in path */
        sp++;
    else
        sp = fn;

    if ((ext = strrchr (sp, '.')))
    {
        if (ext == fn)              /* dot file case */
            return NULL;
        ext++;
    }
    else
        ext = NULL;

    return ext;
}