/*
 * A simple and quick! "find . -type f" replacement program for Windows.
 *
 * Features:
 *  1. Only prints file names - it does *not* print directory names.
 *  2. Prints all a directory's files, before processing its subdirectories.
 *  3. Does *not* sort the output. But FindNextFile() does that on NTFS.
 *  4. Prints filenames in UNIX "/" separated format.
 *  5. Does not prefix outputted filenames with redundant "./".
 *
 * Copyright (c) 2007-2008 Alex Davies. All rights reserved. This program
 * is free software; you can redistribute it and/or modify it under the
 * same terms as Perl itself.
 */

#include <windows.h>
#include <stdio.h>

#define QFIND_VERSION "1.05"

/* MAX_PATH = 260, but have seen filenames with a few more characters in. */
#define MAXPATH 270

#define DIR_STACK_SIZE       (512 * 1024)
#define DIR_STACK_BUF_SIZE   (DIR_STACK_SIZE * 10)

/* Use a stack of directories to allow us to print all the files
 *  in a directory before processing its subdirectories. */
static char* dir_stack[DIR_STACK_SIZE];

/* The string pointers in dir_stack[i] point inside: */
static char dir_stack_buf[DIR_STACK_BUF_SIZE];

/* The next free slot in dir_stack: */
static int dir_stack_top;

static char *dir_stack_buf_end = dir_stack_buf + DIR_STACK_BUF_SIZE;

/* Functions */
static void    scan_dir(char *dirname);
static void    print_files(char *dirname);
static void    warn(char *dirname, DWORD err, char *msg);
static char *  get_error_msg(DWORD err);

/**********************************************************************/

int __cdecl
main(int argc, char *argv[])
{
    char *s, rootDir[MAXPATH+1];

    /* Initialise the dir_stack system. */
    dir_stack[0] = &dir_stack_buf[0];
    dir_stack_top = 0;

    if (argc <= 1) {
	scan_dir("");
    } else if (argc == 2 && *argv[1]) {
	strncpy(rootDir, argv[1], MAXPATH);
	/* Replace \'s with /'s and ensure it ends in a /. */
	for (s = rootDir; *s; ++s) {
	    if (*s == '\\')
		*s = '/';
	}
	if (*(s-1) != '/') {
	    *s = '/';
	    *(s+1) = 0;
	}
	scan_dir(rootDir);
    } else {
	fprintf(stderr, "Usage: qfind [DIR]\nVersion %s\n", QFIND_VERSION);
	exit(1);
    }

    return 0;
}

/**********************************************************************/

static void
scan_dir(char *dirname) /* dirname is either "" or terminated with a '/' */
{
    char subDir[MAXPATH+1];
    int i, start, end;

    start = dir_stack_top;

    print_files(dirname);

    end = dir_stack_top;

    /* And now process sub directories. */
    for (i = start; i < end; ++i) {
	sprintf(subDir, "%s%s/", dirname, dir_stack[i]);
	scan_dir(subDir);
    }

    dir_stack_top = start;
}

/**********************************************************************/

static void
print_files(char *dirname)
{
    WIN32_FIND_DATA  fData;
    HANDLE           hList;
    char            *next_slot;
    char             dirBuf[MAXPATH+1];
    char             err;

    /* NB. dirname is either "" or terminated with a '/'. */
    sprintf(dirBuf, "%s*", dirname); /* the glob pattern */

    /* Get the *first* file. */
    hList = FindFirstFile(dirBuf, &fData);

    if (hList == INVALID_HANDLE_VALUE) {
	warn(dirname, GetLastError(), "");
	return;
    }

    for (;;) {
	if (fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
	    /* Ignore "." and ".." */
	    if (!(fData.cFileName[0] == '.' &&
		    (!fData.cFileName[1] ||
		    (fData.cFileName[1] == '.' && !fData.cFileName[2]))))
	    {
		next_slot = dir_stack[dir_stack_top] + strlen(fData.cFileName) + 1;
		if (dir_stack_top >= DIR_STACK_SIZE) {
		    err = 1;
		    goto process_dir_now;
		} else if (next_slot >= dir_stack_buf_end) {
		    err = 2;
process_dir_now:
		    /* No space to store the directory in the dir_stack,
		     * so process it now. */
		    sprintf(dirBuf, "%s%s/", dirname, (char *) fData.cFileName);
		    warn(dirBuf, 0, ((err == 1)
			? "exceeded DIR_STACK_SIZE: "
			: "exceeded DIR_STACK_BUF_SIZE: "));
		    scan_dir(dirBuf);
		} else {
		    /* Add the directory to the dir_stack. */
  		    strcpy(dir_stack[dir_stack_top++], (char *) fData.cFileName);
		    dir_stack[dir_stack_top] = next_slot;
		}
	    }
	} else {
	    printf("%s%s\n", dirname, fData.cFileName);
	}
	/* Get the *next* file. */
	if (!FindNextFile(hList, &fData)) {
	    DWORD err = GetLastError();
	    if (err != ERROR_NO_MORE_FILES) {
		warn(dirname, err, "FindNextFile error: ");
	    }
	    break;
	}
    }

    if (FindClose(hList) == 0) {
	warn(dirname, GetLastError(), "FindClose error: ");
    }
}

/**********************************************************************/

static void
warn(char *dirname, DWORD err, char *msg)
{
    char *s, *slash = NULL;

    /* Remove any trailing '/' from the dirname in the error message
     *  except for the case of the root directory. */
    for (s = dirname; *s; ++s) {
	if (!*(s+1) && *s == '/' && s != dirname) {
	    slash = s;
	    *s = 0;
	    break;
	}
    }
    if (err) {
	fprintf(stderr, "qfind: %s%s: %s\n", msg, dirname, get_error_msg(err));
    } else {
	fprintf(stderr, "qfind: %s%s\n", msg, dirname);
    }
    if (slash != NULL)
	*slash = '/';
}

/**********************************************************************/

static char *
get_error_msg(DWORD err)
{
    static char msgBuf[256];
    char *s;

    if (FormatMessage( /* ...returns 0 on error */
	    FORMAT_MESSAGE_FROM_SYSTEM,
	    NULL,
	    err,
	    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), /* Default language */
	    msgBuf,
	    255,
	    NULL
    )) {
	/* Chomp any trailing newline from the error message. Needed! */
	for (s = msgBuf; *s; ++s) {
	    if (*s == '\n') {
		*s = 0;
		break;
	    }
	}
    } else {
	sprintf(msgBuf, "error code %d (FormatMessage error: %s)",
	    err, GetLastError());
    }
    return (char *) msgBuf;
}