// TODO
//  * add types by listener dynamically
//  * unroll loops for better readability?
//  * should emitted errors wait for `next()`?
(function (undefined) {
  var fs = require('fs'),
    upath = require('path'),
    util = require('util'),
    Futures = require('futures'),
    events = require('events'),
    noop = function () {},
    // "FIFO" isn't easy to convert to camelCame and back reliably
    isFnodeTypes = [
      "isFile", "isDirectory",  "isBlockDevice",  "isCharacterDevice",  "isSymbolicLink", "isFIFO", "isSocket"
    ],
    fnodeTypes = [
      "file",   "directory",    "blockDevice",    "characterDevice",    "symbolicLink",   "FIFO",   "socket"
    ],
    fnodeTypesPlural = [
      "files",  "directories",  "blockDevices",   "characterDevices",   "symbolicLinks",  "FIFOs",  "sockets"
    ];

  function newVersion() {
    throw new Error("New Version. Please see API on github.com/coolaj86/node-walk");
  }

  // Create a new walk instance
  function create(path, options, cb) {
    if (cb) {
      newVersion();
    }
    
    var emitter = new events.EventEmitter(),
      fstat = (options||{}).followLinks ? fs.stat : fs.lstat;


    // Get the current number of listeners (which may change)
    // Emit events to each listener
    // Wait for all listeners to `next()` before continueing
    // (in theory this may avoid disk thrashing)
    function emitSingleEvents(path, stats, next) {
      var num = 1 + emitter.listeners(stats.type).length + emitter.listeners("node").length;

      function nextWhenReady() {
        num -= 1;
        if (0 === num) { next(); }
      }

      emitter.emit(stats.type, path, stats, nextWhenReady);
      emitter.emit("node", path, stats, nextWhenReady);
      nextWhenReady();
    }


    // Since the risk for disk thrashing among anything
    // other than files is relatively low, all types are
    // emitted at once, but all must complete before advancing
    function emitPluralEvents(path, nodes, next) {
      var num = 1;

      function nextWhenReady() {
        num -= 1;
        if (0 === num) { next(); }
      }

      fnodeTypesPlural.concat(["nodes", "errors"]).forEach(function (fnodeType) {
        if (0 === nodes[fnodeType].length) { return; }
        num += emitter.listeners(fnodeType).length;
        emitter.emit(fnodeType, path, nodes[fnodeType], nextWhenReady);
      });
      nextWhenReady();
    }


    // Determine each file node's type
    // 
    function sortFnodesByType(path, stats, fnodes, nextFile) {
      isFnodeTypes.forEach(function (isType, i) {
        if (stats[isType]()) {
          if (stats.type) { throw new Error("is_" + type + " and " + isType); }
          stats.type = fnodeTypes[i];
          fnodes[fnodeTypesPlural[i]].push(stats);
          // TODO throw to break;
        }
      });
      if (!stats.type) { throw new Error(upath.join(path, stats.name) + ' isAnUndefinedType'); }
      emitSingleEvents(path, stats, nextFile);
    }


    // Asynchronously get the stats 
    //
    function getStats(path, files, walkDirs) {
      var nodeGroups = {};

      fnodeTypesPlural.concat("nodes", "errors").forEach(function (fnodeTypePlural) {
        nodeGroups[fnodeTypePlural] = [];
      });

      function nextFile() {
        var file = files.pop(), dirs = [], fnames = [];

        if (undefined === file) {
          emitPluralEvents(path, nodeGroups, function () {
            nodeGroups.directories.forEach(function (dir) {
              dirs.push(dir.name);
            });
            walkDirs(dirs);
          });
          return;
        }

        fstat(upath.join(path, file), function (err, stats) {
          stats = stats || {};
          stats.name = file;
          nodeGroups.nodes.push(stats);
          if (err) {
            stats.error = err;
            stats.type = 'error';
            nodeGroups.errors.push(stats);
            //emitter.emit('fileError', path, stats, noop);
            return nextFile();
          }
          sortFnodesByType(path, stats, nodeGroups, nextFile);
        });
      }
      nextFile();
    }

    function walk(path, next) {
      fs.readdir(path, function (err, nodes) {
        if (err) { 
          emitter.emit('directoryError', path, { error: err, name: path }, noop);
          return next(); /*TODO*/ throw err;
        }
        getStats(path, nodes, function (dirs) {
          walkDirs(path, dirs, next);
        });
      });
    }

    function walkDirs(path, dirs, cb) {
      function nextDir() {
        var dir = dirs.pop();
        if (undefined === dir) {
          delete dirs;
          return cb();
        }
        walk(upath.join(path, dir), nextDir);
      }
      nextDir();
    }

    walk(upath.normalize(path), function () {
      emitter.emit('end');
    });
    emitter.walk = newVersion;
    emitter.whenever = newVersion;
    return emitter;
  }
  module.exports = create;
  module.exports.isFnodeTypes = isFnodeTypes;
  module.exports.fnodeTypes = fnodeTypes;
  module.exports.fnodeTypesPlural = fnodeTypesPlural;
}());