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

#define EMAX  64        /* general perror string character limit */
#define FMAX 128        /* initial number of file per-directory allocation */

typedef int (*fltfn) (void *, void*, void *);       /* typedef for filter function  */
int filter (void *de, void *m, void *e);            /* filter by both type and ext  */

/* return allocated list of filenames within dname, using filter fnfilter, ptr
 * passes the mode (DT_LNK, DT_DIR, DT_REG encoded as unsigned char), eptr a
 * pointer to extension to filter, nfiles updated with number of files.
 */
char **rddirfltc (char *dname, fltfn fnfilter, void *ptr,
                  void *eptr, size_t *nfiles);
int cmpdstr (const void *p1, const void *p2);       /* qsort compare for char**     */

void *xcalloc (size_t nmemb, size_t sz);            /* calloc w/error check         */
void *xrealloc (void *ptr, size_t psz, size_t *nelem);      /* realloc w/err check  */
char *fn_ext (char *fn);                            /* return ext within filename   */

/* arguments 1 - dirname, [2 - mode, 3 - extension] */
int main (int argc, char **argv) {

    char **dlist = NULL,                            /* directory content array      */
        *dname = (argc > 1) ? argv[1] : ".";        /* dirname supplied as argument */
    size_t idx = 0,                                 /* content array index          */
           nfiles = 0;                              /* no. of files in directory    */
    unsigned char fltmode = argc > 2 ?              /* file mode for filter         */
        (unsigned char)strtoul (argv[2], NULL, 0) : 14;   /* LNK=2 | DIR=4 | REG=8  */
    char *eptr = argc > 3 ? argv[3] : NULL;         /* ptr to extension for filter  */

    /* read array of directory entries into dlist */
    if (!(dlist = rddirfltc (dname, filter, &fltmode, eptr, &nfiles))) {
        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, &nfiles)\n\n",
            dname, fltmode, eptr);
    for (idx = 0; idx < nfiles; idx++)
        printf ("  dlist[%2zu] %-22s  ext: %s\n", idx, dlist[idx],
                fn_ext(dlist[idx]));

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

    for (idx = 0; idx < nfiles; idx++)
        free (dlist[idx]);              /* free filenames */
    free (dlist);                       /* free dir list memory */

    return 0;
}

/** filter dirent entries based on type and extension.
 *  the m (mode) value encodes DT_LNK, DT_DIR, DT_REG as:
 *  (symlink bit-1, dirname bit-2, regular bit-3, bit-0 reserved)
 */
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. */
    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;
}

/** 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.
 *
 *  NOTE:
 *  filter readdir results are 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.
 *  (short for pointer and extension-pointer)
 */
char **rddirfltc (char *dname, fltfn fnfilter, void *ptr,
                  void *eptr, size_t *nfiles)
{
    DIR *dp = opendir (dname);
    char **dlist = NULL;                /* ptr to array of ptrs to de       */
    size_t imax = FMAX;                 /* initial listing allocation size  */
    struct dirent *de = NULL;           /* dirent pointer for readdir       */

    if (!dp) {  /* validate directory open for reading */
        char errbuf[PATH_MAX] = "";
        sprintf (errbuf, "opendir failed on '%s'", dname);
        perror (errbuf);
        return NULL;
    }

    /* allocate/validate imax char pointers  */
    if (!(dlist = xcalloc (imax, sizeof *dlist)))
        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 (fnfilter)
            if (!fnfilter (de, ptr, eptr))
                continue;

        /* allocate/copy de->d_name to dlist */
        if (!(dlist[*nfiles] = strdup (de->d_name))) {
            char errbuf[EMAX] = "";
            sprintf (errbuf, "strdup memory allocation failed. "
                                "dlist[%2zu]", *nfiles);
            perror (errbuf);
            return NULL;
        }
        (*nfiles)++;

        /* check to realloc dir content array if idx >= imax  */
        if (*nfiles == imax) {
            void *tmp;
            if (!(tmp = xrealloc (dlist, sizeof *dlist, &imax)))
                break;      /* return existing dlist */
            dlist = tmp;    /* assign reallocated block to dlist */
        }
    }
    closedir (dp);

    /* sort readdir results */
    qsort (dlist, *nfiles, sizeof *dlist, cmpdstr);

    return dlist;
}

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

/** xcalloc allocates memory using calloc and validates the return.
 *  xcalloc allocates memory and reports an error if the value is
 *  null, returning a memory address only if the value is nonzero
 *  freeing the caller of validating within the body of code.
 */
void *xcalloc (size_t nmemb, size_t sz)
{
    register void *memptr = calloc (nmemb, sz);
    if (memptr == 0) {
        perror ("xcalloc() virtual memory exhausted.");
        return NULL;
    }

    return memptr;
}

/** realloc 'ptr' of 'nelem' of 'psz' to 'nelem * 2' of 'psz'.
 *  returns pointer to reallocated block of memory with new
 *  memory initialized to 0/NULL. return must be assigned to
 *  original pointer in caller.
 */
void *xrealloc (void *ptr, size_t psz, size_t *nelem)
{
    void *memptr = realloc ((char *)ptr, *nelem * 2 * psz);
    if (!memptr) {  /* validate reallocation */
        perror ("realloc(): virtual memory exhausted.");
        return NULL;
    }   /* zero new memory (optional) */
    memset ((char *)memptr + *nelem * psz, 0, *nelem * psz);
    *nelem *= 2;
    return memptr;
}

/** return pointer to extension within filename */
char *fn_ext (char *fn)
{
    char *sp = NULL,                /* start pointer */
         *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;
}