var fs = require('fs');
var path = require('path');
var Chain = require('traverse-chain');


/**
 * Outline the APIs.
 */
var find = module.exports = {

  // file:      function([pat,] root, callback) {}
  // dir:       function([pat,] root, callback) {}

  // eachfile:  function([pat,] root, action) {}
  // eachdir:   function([pat,] root, action) {}

  // fileSync:  function([pat,] root) {}
  // dirSync:   function([pat,] root) {}
  // use::      function(options) {}

};


var fss = {};

/**
 *  Error handler wrapper.
 */
fss.errorHandler = function(err) {
  if (err) {
    if (find.__errorHandler) {
      find.__errorHandler(err);
    } else {
      throw err;
    }
  }
};


var error = {
  notExist: function(name) {
    return new Error(name + ' does not exist.');
  }
};


var is = (function() {
  function existed(name) {
    return fs.existsSync(name);
  }
  function fsType(type) {
    return function(name) {
      try {
        return fs.lstatSync(name)['is' + type]();
      } catch(err) {
        if (!/^(EPERM|EACCES)$/.test(err.code)) {
          fss.errorHandler(err);
        }
        else {
          console.warn('Warning: Cannot access %s', name);
        }
      }
    }
  }
  function objType(type) {
    return function(input) {
      if (type === 'Function') {
        return typeof input === 'function';
      }
      return ({}).toString.call(input) === '[object ' + type +  ']';
    }
  }
  return {
    existed:      existed,
    file:         fsType('File'),
    directory:    fsType('Directory'),
    symbolicLink: fsType('SymbolicLink'),

    string:       objType('String'),
    regexp:       objType('RegExp'),
    func:         objType('Function')
  };
}());


/**
 *  Method injection for handling errors.
 */
['readdir', 'lstat'].forEach(function(method) {
  fss[method] = function(path, callback) {
    var origin = fs[method];
    return origin.apply(fs, [path, function(err) {
      fss.errorHandler(err);
      return callback.apply(null, arguments);
    }]);
  }
});


/**
 * Enhancement for fs.readlink && fs.readlinkSync.
 */
fss.readlink = function(name, fn, depth) {
  if (depth == undefined) depth = 5;
  if (!is.existed(name) && (depth < 5)) {
    return fn(path.resolve(name));
  }
  var isSymbolicLink = is.symbolicLink(name);
  if (!isSymbolicLink) {
    fn(path.resolve(name));
  } else if (depth) {
    fs.realpath(name, function(err, origin) {
      if (err && /^(ENOENT|ELOOP|EPERM|EACCES)$/.test(err.code)) {
        fn(name);
      } else {
        if (err) {
          fss.errorHandler(err);
        } else {
          fss.readlink(origin, fn, --depth);
        }
      }
    });
  } else {
    fn(isSymbolicLink ? '' : path.resolve(name));
  }
}

fss.readlinkSync = function(name, depth) {
  if (depth == undefined) depth = 5;
  if (!is.existed(name) && depth < 5) {
    return path.resolve(name);
  }
  var isSymbolicLink = is.symbolicLink(name);
  if (!isSymbolicLink) {
    return path.resolve(name);
  } else if (depth) {
    var origin;
    try {
      origin = fs.realpathSync(name);
    } catch (err) {
      if (/^(ENOENT|ELOOP|EPERM|EACCES)$/.test(err.code)) {
        return name;
      } else {
        fss.errorHandler(err);
      }
    }
    return fss.readlinkSync(origin, --depth);
  } else {
    return isSymbolicLink ? '' : path.resolve(name);
  }
}


/**
 * Check pattern against the path
 */
var compare = function(pat, name) {
  var str = path.basename(name);
  return (
       is.regexp(pat) && pat.test(name)
    || is.string(pat) && pat === str
  );
};


/**
 * Traverse a directory recursively and asynchronously.
 *
 * @param {String} root
 * @param {String} type
 * @param {Function} action
 * @param {Function} callback
 * @param {Chain} c
 * @api private
 */
var traverseAsync = function(root, type, action, callback, c) {
  if (!is.existed(root)) {
    fss.errorHandler(error.notExist(root))
  }

  var originRoot = root;
  if (is.symbolicLink(root)) {
    root = fss.readlinkSync(root);
  }

  if (is.directory(root)) {
    fss.readdir(root, function(err, all) {
      var chain = Chain();
      all && all.forEach(function(dir) {
        dir = path.join(originRoot, dir);
        chain.add(function() {
          var handleFile = function() {
            if (type == 'file') action(dir);
            process.nextTick(function() { chain.next() });
          }
          var handleDir = function(skip) {
            if (type == 'dir') action(dir);
            if (skip) chain.next();
            else process.nextTick(function() { traverseAsync(dir, type, action, callback, chain)});
          }
          var isSymbolicLink = is.symbolicLink(dir);
          if (is.directory(dir)) {
            handleDir();
          } else if (isSymbolicLink) {
            fss.readlink(dir, function(origin) {
              if (origin) {
                if (is.existed(origin) && is.directory(origin)) {
                  handleDir(isSymbolicLink)
                } else {
                  handleFile()
                }
              } else {
                chain.next();
              }
            });
          } else {
            handleFile();
          }
        })
      });
      chain.traverse(function() {
        c ? c.next() : callback();
      });
    });
  }
}


/**
 * Traverse a directory recursively.
 *
 * @param {String} root
 * @param {String} type
 * @param {Function} action
 * @return {Array} the result
 * @api private
 */
var traverseSync = function(root, type, action) {
  if (!is.existed(root)) throw error.notExist(root);
  var originRoot = root;
  if (is.symbolicLink(root)) {
    root = fss.readlinkSync(root);
  }
  if (is.directory(root)) {
    fs.readdirSync(root).forEach(function(dir) {
      dir = path.join(originRoot, dir);
      var handleDir = function(skip) {
        if (type == 'dir') action(dir);
        if (skip) return;
        traverseSync(dir, type, action);
      }
      var handleFile = function() {
        if (type == 'file') action(dir);
      }
      var isSymbolicLink = is.symbolicLink(dir);
      if (is.directory(dir)) {
        handleDir();
      } else if (isSymbolicLink) {
        var origin = fss.readlinkSync(dir);
        if (origin) {
          if (is.existed(origin) && is.directory(origin)) {
            handleDir(isSymbolicLink);
          } else {
            handleFile();
          }
        }
      } else {
        handleFile();
      }
    });
  }
};


['file', 'dir'].forEach(function(type) {

  /**
   * `find.file` and `find.dir`
   *
   * Find files or sub-directories in a given directory and
   * passes the result in an array as a whole. This follows
   * the default callback style of nodejs, think about `fs.readdir`,
   *
   * @param {RegExp|String} pat
   * @param {String} root
   * @param {Function} fn
   * @api public
   */
  find[type] = function(pat, root, fn) {
    var buffer = [];
    if (arguments.length == 2) {
      fn = root;
      root = pat;
      pat = '';
    }
    process.nextTick(function() {
      traverseAsync(
        root
      , type
      , function(n) { buffer.push(n);}
      , function() {
          if (is.func(fn) && pat) {
            fn(buffer.filter(function(n) {
              return compare(pat, n);
            }));
          } else {
            fn(buffer);
          }
        }
      );
    });
    return {
      error: function(handler) {
        if (is.func(handler)) {
          find.__errorHandler = handler;
        }
      }
    }
  }

  /**
   * `find.eachfile` and `find.eachdir`
   *
   * Find files or sub-directories in a given directory and
   * apply with a given action to each result immediately
   * rather than pass them back as an array.
   *
   * @param {RegExp|String} pat
   * @param {String} root
   * @param {Function} action
   * @return {Object} for chain methods
   * @api public
   *
   */
  find['each' + type] = function(pat, root, action) {
    var callback = function() {}
    if (arguments.length == 2) {
      action = root;
      root = pat;
      pat = '';
    }
    process.nextTick(function() {
      traverseAsync(
          root
        , type
        , function(n) {
            if (!is.func(action)) return;
            if (!pat || compare(pat, n)) {
              action(n);
            }
          }
        , callback
      );
    });
    return {
      end: function(fn) {
        if (is.func(fn)) {
          callback = fn;
        }
        return this;
      },
      error: function(handler) {
        if (is.func(handler)) {
          find.__errorHandler = handler;
        }
        return this;
      }
    };
  }

  /**
   * `find.fileSync` and `find.dirSync`
   *
   * Find files or sub-directories in a given directory synchronously
   * and returns the result as an array. This follows the default 'Sync'
   * methods of nodejs, think about `fs.readdirSync`,
   *
   * @param {RegExp|String} pat
   * @param {String} root
   * @return {Array} the result
   * @api public
   */
  find[type + 'Sync'] = function(pat, root) {
    var buffer = [];
    if (arguments.length == 1) {
      root = pat;
      pat = '';
    }
    traverseSync(root, type, function(n) {
      buffer.push(n);
    });
    return pat && buffer.filter(function(n) {
      return compare(pat, n);
    }) || buffer;
  }

});


var fsMethods = [
  'existsSync',
  'lstatSync',
  'realpath',
  'realpathSync',
  'readdir',
  'readdirSync'
];


/**
 * Configuations for internal usage
 *
 * @param {Object} options
 * @api public
 */
find.use = function(options) {
  if (options && options.fs) {
    if (fsMethods.every(n => !!options.fs[n])) {
      fs = options.fs;
    } else {
      throw new Error('The provided fs object is not compatiable with native fs.');
    }
  }
  return find;
}
