/**
 *  Module dependencies.
 */
var fs   = require('fs')
  , path = require('path');
 

/**
 * Utility functions to synchronously test whether the giving path 
 * is a file or a directory or a symbolic link.
 */
var is = function(ret) {
  var shortcuts = {
      'file': 'File'
    , 'dir': 'Directory'
    , 'sym': 'SymbolicLink'
  };
  Object.keys(shortcuts).forEach(function(method) {
    ret[method] = function(fpath) { 
      var stat = fs[method === 'sym' ? 'lstatSync' :'statSync'];
      if (fs.existsSync(fpath)) {
        memo.push(fpath, method);
        return stat(fpath)['is' + shortcuts[method]]();  
      }
      return false;
    }
  });
  return ret;
}({});


/**
 *  Get sub-directories in a directory.
 */
var sub = function(parent, cb) {
  if (is.dir(parent)) {
    fs.readdir(parent, function(err, all) {
      all && all.forEach(function(f) {
        var sdir = path.join(parent, f) 
        if (is.dir(sdir)) {
          cb.call(null, sdir) 
        }
      });
    });
  }
};


/**
 *  Mixing object properties. 
 */
var mixin = function() {
  var mix = {}; 
  [].forEach.call(arguments, function(arg) { 
    for (var name in arg) {
      if (arg.hasOwnProperty(name)) {
        mix[name] = arg[name];
      }
    }
  });
  return mix;
};


/**
 * A container for memorizing names of files or directories.
 */
var memo = function(memo) {
  return {
    push: function(name, type) {
      memo[name] = type;
    }, 
    has: function(name) {
      return {}.hasOwnProperty.call(memo, name);
    },
    update: function(name) {
      if (!is.file(name) || !is.dir(name)) {
        delete memo[name];
      }
      return true;
    }
  };
}({});

  
/**
 *  A Container for storing unique and valid filenames.
 */
var fileNameCache = function(cache) {
  return {
    push: function(name) {
      cache[name] = 1;
      return this;
    },
    each: function() {
      var temp = Object.keys(cache).filter(function(name){ 
        return is.file(name) || memo.has(name) && memo.update(name);
      });
      temp.forEach.apply(temp, arguments);
      return this;
    },
    clear: function(){
      cache = {};
      return this;
    }
  };
}({});     


/**
 * Abstracting the way of avoiding duplicate function call.
 */
var worker = function() {
  var free = true;
  return {
    busydoing: function(cb) {
      if (free) {
        free = false;
        cb.call();
      }
    },
    free: function() {
      free = true;
    }
  }
}();


/**
 * Delay function call and ignore invalid filenames.
 */
var normalizeCall = function(fname, options, cb) {
  // Store each name of the modifying or temporary files generated by an editor.
  fileNameCache.push(fname);

  worker.busydoing(function() {
    // A heuristic delay of the write-to-file process.  
    setTimeout(function() {
      
      // When the write-to-file process is done, send all filtered filenames
      // to the callback function and call it.
      fileNameCache
        .each(function(f) { 
          // Watch new created directory.
          if (options.recursive && !memo.has(f) && is.dir(f)) {
            watch(f, options, cb);
          }
          cb.call(null, f); 
        }).clear();

      worker.free();

    }, 100);
  });
};


/**
 * Catch exception on Windows when deleting a directory.
 */
var catchException = function() {};


/**
 * Option handler for the `watch` function.
 */
var handleOptions = function(origin, defaultOptions) {
  return function() {
    var args = [].slice.call(arguments);
    if (Object.prototype.toString.call(args[1]) === '[object Function]') {
      args[2] = args[1];
    }
    if (!Array.isArray(args[0])) {
      args[0] = [args[0]];
    }
    //overwrite default options
    args[1] = mixin(defaultOptions, args[1]);           
    //handle multiple files.
    args[0].forEach(function(path) {
      origin.apply(null, [path].concat(args.slice(1))); 
    });  
  }
};


/**
 * Watch a file or a directory (recursively by default).
 *  
 * @param {String} fpath
 * @options {Object} options
 * @param {Function} cb
 * 
 * Options:
 *   `recursive`:      Watch it recursively or not (defaults to true).
 *   `followSymLinks`: Follow symbolic links or not (defaults to false).
 *   `maxSymLevel`:    The max number of following symbolic links (defaults to 1). 
 *
 * Example: 
 *
 *   watch('fpath', {recursive: true}, function(file) {
 *     console.log(file, ' changed');
 *   });
 */
function watch(fpath, options, cb) {
  if (is.sym(fpath) 
    && !(options.followSymLinks 
      && options.maxSymLevel--)) {
    return;
  }
  // Due to the unstalbe fs.watch(), if the `fpath` is a file then 
  // switch to watch its parent directory instead of watch it directly. 
  // Once the logged filename matches it then triggers the callback function.
  if (is.file(fpath)) {
    var parent = path.resolve(fpath, '..');
    fs.watch(parent, options, function(evt, fname) {
      if (path.basename(fpath) === fname) {
        normalizeCall(fpath, options, cb);
      }
    }).on('error', catchException);                         
  } else if (is.dir(fpath)) {
    fs.watch(fpath, options, function(evt, fname) {
      if (fname) {
        normalizeCall(path.join(fpath, fname), options, cb);
      }
    }).on('error', catchException);

    if (options.recursive) {
      // Recursively watch its sub-directories. 
      sub(fpath, function(dir) {
        watch(dir, options, cb);
      });    
    }
  }
}; 


/**
 * Set default options and expose.
 */ 
module.exports = handleOptions(watch, {
    recursive: true
  , followSymLinks: false
  , maxSymLevel: 1
});

