// Generated by CoffeeScript 1.4.0
/*
Watchr is used to be nofitied when a change happens to, or within a directory.
You will not be notified what file was changed, or how it was changed.
It will track new files and their changes too, and remove listeners for deleted files appropriatly.

The source code here is written as an experiment of literate programming
Which means you would be able to understand it, without knowing code
*/

var EventEmitter, Watcher, balUtil, createWatcher, debug, fsUtil, pathUtil, watch, watchers,
  __hasProp = {}.hasOwnProperty,
  __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
  __slice = [].slice,
  __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };

pathUtil = require('path');

fsUtil = require('fs');

balUtil = require('bal-util');

EventEmitter = require('events').EventEmitter;

debug = false;

watchers = {};

Watcher = (function(_super) {

  __extends(_Class, _super);

  _Class.prototype.path = null;

  _Class.prototype.isDirectory = null;

  _Class.prototype.stat = null;

  _Class.prototype.fswatcher = null;

  _Class.prototype.children = null;

  _Class.prototype.state = 'pending';

  _Class.prototype.method = null;

  _Class.prototype.config = null;

  function _Class(config, next) {
    var applyStat, listener, watcher, _base, _base1, _base2, _base3, _i, _len, _ref, _ref1, _ref2, _ref3, _ref4, _ref5,
      _this = this;
    _ref = balUtil.extractOptsAndCallback(config, next), config = _ref[0], next = _ref[1];
    watcher = this;
    this.children = {};
    applyStat = function(stat) {
      _this.stat = stat;
      _this.isDirectory = stat.isDirectory();
      return _this.watch(function(err) {
        return typeof next === "function" ? next(err, watcher) : void 0;
      });
    };
    this.path = config.path;
    this.config = config;
    if ((_ref1 = (_base = this.config).ignoreHiddenFiles) == null) {
      _base.ignoreHiddenFiles = false;
    }
    if ((_ref2 = (_base1 = this.config).ignorePatterns) == null) {
      _base1.ignorePatterns = false;
    }
    if ((_ref3 = (_base2 = this.config).interval) == null) {
      _base2.interval = 100;
    }
    if ((_ref4 = (_base3 = this.config).persistent) == null) {
      _base3.persistent = true;
    }
    if (config.listener) {
      this.listen(config.listener);
    }
    if (config.listeners) {
      _ref5 = config.listeners;
      for (_i = 0, _len = _ref5.length; _i < _len; _i++) {
        listener = _ref5[_i];
        this.listen(listener);
      }
    }
    if (config.stat) {
      applyStat(config.stat);
    } else {
      balUtil.stat(config.path, function(err, stat) {
        if (watcher.state !== 'pending') {
          return;
        }
        if (err) {
          throw err;
        }
        return applyStat(stat);
      });
    }
  }

  _Class.prototype.listen = function(listener) {
    this.removeListener('changed', listener);
    this.on('changed', listener);
    if (debug) {
      console.log("added a listener: on " + this.path);
    }
    return this;
  };

  _Class.prototype.bubble = function() {
    var args, currentStat, eventName, filename, previousStat;
    args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
    eventName = args[0], filename = args[1], currentStat = args[2], previousStat = args[3];
    if (debug) {
      console.log("bubble: " + eventName + ": " + filename + " on " + this.path);
    }
    this.emit('changed', eventName, filename, currentStat, previousStat);
    return this;
  };

  _Class.prototype.changed = function() {
    var args, currentStat, determineTheChange, fileExists, fileFullPath, isTheSame, me, previousStat,
      _this = this;
    args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
    me = this;
    fileFullPath = this.path;
    currentStat = null;
    previousStat = this.stat;
    fileExists = null;
    if (debug) {
      console.log("watch event triggered on " + this.path + "\n", args);
    }
    isTheSame = function() {
      if ((currentStat != null) && (previousStat != null)) {
        if (currentStat.size === previousStat.size && currentStat.mtime.toString() === previousStat.mtime.toString()) {
          return true;
        }
      }
      return false;
    };
    determineTheChange = function() {
      if (!fileExists) {
        if (debug) {
          console.log('determined unlink:', fileFullPath);
        }
        return _this.close('unlink');
      } else {
        if (isTheSame()) {
          if (debug) {
            return console.log("determined same:", fileFullPath);
          }
        } else {
          if (_this.isDirectory) {
            if (isTheSame() === false) {
              return balUtil.readdir(fileFullPath, function(err, newFileRelativePaths) {
                if (err) {
                  throw err;
                }
                balUtil.each(newFileRelativePaths, function(newFileRelativePath) {
                  var newFileFullPath;
                  if (_this.children[newFileRelativePath] != null) {

                  } else {
                    newFileFullPath = pathUtil.join(fileFullPath, newFileRelativePath);
                    return balUtil.stat(newFileFullPath, function(err, newFileStat) {
                      if (err) {
                        throw err;
                      }
                      if (debug) {
                        console.log('determined new:', newFileFullPath);
                      }
                      _this.emit('changed', 'new', newFileFullPath, newFileStat, null);
                      return _this.watchChild(newFileFullPath, newFileRelativePath, newFileStat);
                    });
                  }
                });
                return balUtil.each(_this.children, function(childFileWatcher, childFileRelativePath) {
                  var childFileFullPath;
                  if (__indexOf.call(newFileRelativePaths, childFileRelativePath) >= 0) {

                  } else {
                    childFileFullPath = childFileWatcher.path;
                    if (debug) {
                      console.log('determined unlink:', childFileRelativePath);
                    }
                    return _this.closeChild(childFileRelativePath, 'unlink');
                  }
                });
              });
            }
          } else {
            if (debug) {
              console.log('determined change:', fileFullPath);
            }
            return _this.emit('changed', 'change', fileFullPath, currentStat, previousStat);
          }
        }
      }
    };
    balUtil.exists(fileFullPath, function(exists) {
      fileExists = exists;
      if (fileExists) {
        return balUtil.stat(fileFullPath, function(err, stat) {
          if (err) {
            throw err;
          }
          currentStat = stat;
          me.stat = currentStat;
          return determineTheChange();
        });
      } else {
        return determineTheChange();
      }
    });
    return this;
  };

  _Class.prototype.close = function(type) {
    var childRelativePath, watchr, _ref;
    if (this.state !== 'active') {
      return this;
    }
    if (debug) {
      console.log("close: " + this.path + " ", new Error('trace').stack);
    }
    _ref = this.children;
    for (childRelativePath in _ref) {
      if (!__hasProp.call(_ref, childRelativePath)) continue;
      watchr = _ref[childRelativePath];
      this.closeChild(childRelativePath, type);
    }
    if (this.method === 'watchFile') {
      fsUtil.unwatchFile(this.path);
    } else if (this.method === 'watch' && this.fswatcher) {
      this.fswatcher.close();
      this.fswatcher = null;
    }
    if (type === 'unlink') {
      this.emit('changed', 'unlink', this.path, null, this.stat);
      this.state = 'unlink';
    } else {
      this.state = 'closed';
    }
    if (watchers[this.path] != null) {
      delete watchers[this.path];
    }
    return this;
  };

  _Class.prototype.closeChild = function(fileRelativePath, type) {
    var watcher;
    watcher = this.children[fileRelativePath];
    if (watcher) {
      watcher.close(type);
      delete this.children[fileRelativePath];
    }
    return this;
  };

  _Class.prototype.watchChild = function(fileFullPath, fileRelativePath, fileStat, next) {
    var config, me;
    me = this;
    config = this.config;
    return watch({
      path: fileFullPath,
      listener: function() {
        var args;
        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
        if (args.length > 3 && args[0] === 'changed' && args[1] === 'unlink' && args[2] === fileFullPath) {
          this.closeChild(fileRelativePath, 'unlink');
        }
        return me.bubble.apply(me, args);
      },
      stat: fileStat,
      ignoreHiddenFiles: config.ignoreHiddenFiles,
      ignorePatterns: config.ignorePatterns,
      next: function(err, watcher) {
        if (err) {
          return typeof next === "function" ? next(err) : void 0;
        }
        me.children[fileRelativePath] = watcher;
        return typeof next === "function" ? next() : void 0;
      }
    });
  };

  _Class.prototype.watch = function(next) {
    var config, me, startWatching,
      _this = this;
    me = this;
    config = this.config;
    this.close();
    if (debug) {
      console.log("watch: " + this.path, new Error('trace').stack);
    }
    startWatching = function() {
      var tasks, watchFileOpts;
      tasks = new balUtil.Group(function(err) {
        return typeof next === "function" ? next(err) : void 0;
      });
      tasks.total = 2;
      if (_this.isDirectory) {
        balUtil.scandir({
          path: _this.path,
          ignoreHiddenFiles: config.ignoreHiddenFiles,
          ignorePatterns: config.ignorePatterns,
          recurse: false,
          next: function(err) {
            return tasks.complete(err);
          },
          action: function(fileFullPath, fileRelativePath, nextFile, fileStat) {
            return me.watchChild(fileFullPath, fileRelativePath, fileStat, function(err) {
              return nextFile(err);
            });
          }
        });
      } else {
        tasks.complete();
      }
      try {
        watchFileOpts = {
          persistent: config.persistent,
          interval: config.interval
        };
        fsUtil.watchFile(_this.path, watchFileOpts, function() {
          var args;
          args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
          return me.changed.apply(me, args);
        });
        _this.method = 'watchFile';
      } catch (err) {
        _this.fswatcher = fsUtil.watch(_this.path, function() {
          var args;
          args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
          return me.changed.apply(me, args);
        });
        _this.method = 'watch';
      }
      _this.state = 'active';
      return tasks.complete();
    };
    balUtil.exists(this.path, function(exists) {
      if (!exists) {
        next();
        return this;
      }
      return startWatching();
    });
    return this;
  };

  return _Class;

})(EventEmitter);

createWatcher = function(opts, next) {
  var listener, listeners, path, watcher, watchr, _i, _len, _listener, _ref;
  _ref = balUtil.extractOptsAndCallback(opts, next), opts = _ref[0], next = _ref[1];
  path = opts.path, listener = opts.listener, listeners = opts.listeners;
  watchr = null;
  if (!balUtil.existsSync(path)) {
    if (typeof next === "function") {
      next(null, watcher);
    }
    return;
  }
  if (watchers[path] != null) {
    watcher = watchers[path];
    if (listener) {
      watcher.listen(listener);
    }
    if (listeners) {
      for (_i = 0, _len = listeners.length; _i < _len; _i++) {
        _listener = listeners[_i];
        watcher.listen(_listener);
      }
    }
    if (typeof next === "function") {
      next(null, watcher);
    }
  } else {
    watcher = new Watcher(opts);
    watchers[path] = watcher;
  }
  return watcher;
};

watch = function(opts, next) {
  var path, paths, result, tasks, _i, _len, _ref;
  _ref = balUtil.extractOptsAndCallback(opts, next), opts = _ref[0], next = _ref[1];
  paths = opts.paths;
  result = null;
  delete opts.paths;
  if (paths instanceof Array) {
    result = [];
    tasks = new balUtil.Group(function(err) {
      return typeof next === "function" ? next(err, result) : void 0;
    });
    for (_i = 0, _len = paths.length; _i < _len; _i++) {
      path = paths[_i];
      tasks.push({
        path: path
      }, function(complete) {
        var localOpts, watchr;
        localOpts = balUtil.extend({}, opts);
        localOpts.path = this.path;
        localOpts.next = complete;
        watchr = createWatcher(localOpts);
        if (watchr) {
          return result.push(watchr);
        }
      });
    }
    tasks.async();
  } else {
    result = createWatcher(opts, next);
  }
  return result;
};

module.exports = {
  watch: watch,
  Watcher: Watcher
};
