text: 'use strict';
  
  const fs = require('fs');
  const sysPath = require('path');
  const { promisify } = require('util');
  
  let fsevents;
  try {
    fsevents = require('fsevents');
  } catch (error) {
    if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error);
  }
  
  if (fsevents) {
    // TODO: real check
    const mtch = process.version.match(/v(\d+)\.(\d+)/);
    if (mtch && mtch[1] && mtch[2]) {
      const maj = Number.parseInt(mtch[1], 10);
      const min = Number.parseInt(mtch[2], 10);
      if (maj === 8 && min < 16) {
        fsevents = undefined;
      }
    }
  }
  
  const {
    EV_ADD,
    EV_CHANGE,
    EV_ADD_DIR,
    EV_UNLINK,
    EV_ERROR,
    STR_DATA,
    STR_END,
    FSEVENT_CREATED,
    FSEVENT_MODIFIED,
    FSEVENT_DELETED,
    FSEVENT_MOVED,
    // FSEVENT_CLONED,
    FSEVENT_UNKNOWN,
    FSEVENT_TYPE_FILE,
    FSEVENT_TYPE_DIRECTORY,
    FSEVENT_TYPE_SYMLINK,
  
    ROOT_GLOBSTAR,
    DIR_SUFFIX,
    DOT_SLASH,
    FUNCTION_TYPE,
    EMPTY_FN,
    IDENTITY_FN
  } = require('./constants');
  
  const Depth = (value) => isNaN(value) ? {} : {depth: value};
  
  const stat = promisify(fs.stat);
  const lstat = promisify(fs.lstat);
  const realpath = promisify(fs.realpath);
  
  const statMethods = { stat, lstat };
  
  /**
   * @typedef {String} Path
   */
  
  /**
   * @typedef {Object} FsEventsWatchContainer
   * @property {Set<Function>} listeners
   * @property {Function} rawEmitter
   * @property {{stop: Function}} watcher
   */
  
  // fsevents instance helper functions
  /**
   * Object to hold per-process fsevents instances (may be shared across chokidar FSWatcher instances)
   * @type {Map<Path,FsEventsWatchContainer>}
   */
  const FSEventsWatchers = new Map();
  
  // Threshold of duplicate path prefixes at which to start
  // consolidating going forward
  const consolidateThreshhold = 10;
  
  const wrongEventFlags = new Set([
    69888, 70400, 71424, 72704, 73472, 131328, 131840, 262912
  ]);
  
  /**
   * Instantiates the fsevents interface
   * @param {Path} path path to be watched
   * @param {Function} callback called when fsevents is bound and ready
   * @returns {{stop: Function}} new fsevents instance
   */
  const createFSEventsInstance = (path, callback) => {
    const stop = fsevents.watch(path, callback);
    return {stop};
  };
  
  /**
   * Instantiates the fsevents interface or binds listeners to an existing one covering
   * the same file tree.
   * @param {Path} path           - to be watched
   * @param {Path} realPath       - real path for symlinks
   * @param {Function} listener   - called when fsevents emits events
   * @param {Function} rawEmitter - passes data to listeners of the 'raw' event
   * @returns {Function} closer
   */
  function setFSEventsListener(path, realPath, listener, rawEmitter) {
    let watchPath = sysPath.extname(path) ? sysPath.dirname(path) : path;
    const parentPath = sysPath.dirname(watchPath);
    let cont = FSEventsWatchers.get(watchPath);
  
    // If we've accumulated a substantial number of paths that
    // could have been consolidated by watching one directory
    // above the current one, create a watcher on the parent
    // path instead, so that we do consolidate going forward.
    if (couldConsolidate(parentPath)) {
      watchPath = parentPath;
    }
  
    const resolvedPath = sysPath.resolve(path);
    const hasSymlink = resolvedPath !== realPath;
  
    const filteredListener = (fullPath, flags, info) => {
      if (hasSymlink) fullPath = fullPath.replace(realPath, resolvedPath);
      if (
        fullPath === resolvedPath ||
        !fullPath.indexOf(resolvedPath + sysPath.sep)
      ) listener(fullPath, flags, info);
    };
  
    // check if there is already a watcher on a parent path
    // modifies `watchPath` to the parent path when it finds a match
    let watchedParent = false;
    for (const watchedPath of FSEventsWatchers.keys()) {
      if (realPath.indexOf(sysPath.resolve(watchedPath) + sysPath.sep) === 0) {
        watchPath = watchedPath;
        cont = FSEventsWatchers.get(watchPath);
        watchedParent = true;
        break;
      }
    }
  
    if (cont || watchedParent) {
      cont.listeners.add(filteredListener);
    } else {
      cont = {
        listeners: new Set([filteredListener]),
        rawEmitter,
        watcher: createFSEventsInstance(watchPath, (fullPath, flags) => {
          if (!cont.listeners.size) return;
          const info = fsevents.getInfo(fullPath, flags);
          cont.listeners.forEach(list => {
            list(fullPath, flags, info);
          });
  
          cont.rawEmitter(info.event, fullPath, info);
        })
      };
      FSEventsWatchers.set(watchPath, cont);
    }
  
    // removes this instance's listeners and closes the underlying fsevents
    // instance if there are no more listeners left
    return () => {
      const lst = cont.listeners;
  
      lst.delete(filteredListener);
      if (!lst.size) {
        FSEventsWatchers.delete(watchPath);
        if (cont.watcher) return cont.watcher.stop().then(() => {
          cont.rawEmitter = cont.watcher = undefined;
          Object.freeze(cont);
        });
      }
    };
  }
  
  // Decide whether or not we should start a new higher-level
  // parent watcher
  const couldConsolidate = (path) => {
    let count = 0;
    for (const watchPath of FSEventsWatchers.keys()) {
      if (watchPath.indexOf(path) === 0) {
        count++;
        if (count >= consolidateThreshhold) {
          return true;
        }
      }
    }
  
    return false;
  };
  
  // returns boolean indicating whether fsevents can be used
  const canUse = () => fsevents && FSEventsWatchers.size < 128;
  
  // determines subdirectory traversal levels from root to path
  const calcDepth = (path, root) => {
    let i = 0;
    while (!path.indexOf(root) && (path = sysPath.dirname(path)) !== root) i++;
    return i;
  };
  
  // returns boolean indicating whether the fsevents' event info has the same type
  // as the one returned by fs.stat
  const sameTypes = (info, stats) => (
    info.type === FSEVENT_TYPE_DIRECTORY && stats.isDirectory() ||
    info.type === FSEVENT_TYPE_SYMLINK && stats.isSymbolicLink() ||
    info.type === FSEVENT_TYPE_FILE && stats.isFile()
  )
  
  /**
   * @mixin
   */
  class FsEventsHandler {
  
  /**
   * @param {import('../index').FSWatcher} fsw
   */
  constructor(fsw) {
    this.fsw = fsw;
  }
  checkIgnored(path, stats) {
    const ipaths = this.fsw._ignoredPaths;
    if (this.fsw._isIgnored(path, stats)) {
      ipaths.add(path);
      if (stats && stats.isDirectory()) {
        ipaths.add(path + ROOT_GLOBSTAR);
      }
      return true;
    }
  
    ipaths.delete(path);
    ipaths.delete(path + ROOT_GLOBSTAR);
  }
  
  addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
    const event = watchedDir.has(item) ? EV_CHANGE : EV_ADD;
    this.handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts);
  }
  
  async checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts) {
    try {
      const stats = await stat(path)
      if (this.fsw.closed) return;
      if (sameTypes(info, stats)) {
        this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
      } else {
        this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
      }
    } catch (error) {
      if (error.code === 'EACCES') {
        this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
      } else {
        this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
      }
    }
  }
  
  handleEvent(event, path, fullPath, realPath, parent, watchedDir, item, info, opts) {
    if (this.fsw.closed || this.checkIgnored(path)) return;
  
    if (event === EV_UNLINK) {
      const isDirectory = info.type === FSEVENT_TYPE_DIRECTORY
      // suppress unlink events on never before seen files
      if (isDirectory || watchedDir.has(item)) {
        this.fsw._remove(parent, item, isDirectory);
      }
    } else {
      if (event === EV_ADD) {
        // track new directories
        if (info.type === FSEVENT_TYPE_DIRECTORY) this.fsw._getWatchedDir(path);
  
        if (info.type === FSEVENT_TYPE_SYMLINK && opts.followSymlinks) {
          // push symlinks back to the top of the stack to get handled
          const curDepth = opts.depth === undefined ?
            undefined : calcDepth(fullPath, realPath) + 1;
          return this._addToFsEvents(path, false, true, curDepth);
        }
  
        // track new paths
        // (other than symlinks being followed, which will be tracked soon)
        this.fsw._getWatchedDir(parent).add(item);
      }
      /**
       * @type {'add'|'addDir'|'unlink'|'unlinkDir'}
       */
      const eventName = info.type === FSEVENT_TYPE_DIRECTORY ? event + DIR_SUFFIX : event;
      this.fsw._emit(eventName, path);
      if (eventName === EV_ADD_DIR) this._addToFsEvents(path, false, true);
    }
  }
  
  /**
   * Handle symlinks encountered during directory scan
   * @param {String} watchPath  - file/dir path to be watched with fsevents
   * @param {String} realPath   - real path (in case of symlinks)
   * @param {Function} transform  - path transformer
   * @param {Function} globFilter - path filter in case a glob pattern was provided
   * @returns {Function} closer for the watcher instance
  */
  _watchWithFsEvents(watchPath, realPath, transform, globFilter) {
    if (this.fsw.closed || this.fsw._isIgnored(watchPath)) return;
    const opts = this.fsw.options;
    const watchCallback = async (fullPath, flags, info) => {
      if (this.fsw.closed) return;
      if (
        opts.depth !== undefined &&
        calcDepth(fullPath, realPath) > opts.depth
      ) return;
      const path = transform(sysPath.join(
        watchPath, sysPath.relative(watchPath, fullPath)
      ));
      if (globFilter && !globFilter(path)) return;
      // ensure directories are tracked
      const parent = sysPath.dirname(path);
      const item = sysPath.basename(path);
      const watchedDir = this.fsw._getWatchedDir(
        info.type === FSEVENT_TYPE_DIRECTORY ? path : parent
      );
  
      // correct for wrong events emitted
      if (wrongEventFlags.has(flags) || info.event === FSEVENT_UNKNOWN) {
        if (typeof opts.ignored === FUNCTION_TYPE) {
          let stats;
          try {
            stats = await stat(path);
          } catch (error) {}
          if (this.fsw.closed) return;
          if (this.checkIgnored(path, stats)) return;
          if (sameTypes(info, stats)) {
            this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
          } else {
            this.handleEvent(EV_UNLINK, path, fullPath, realPath, parent, watchedDir, item, info, opts);
          }
        } else {
          this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
        }
      } else {
        switch (info.event) {
        case FSEVENT_CREATED:
        case FSEVENT_MODIFIED:
          return this.addOrChange(path, fullPath, realPath, parent, watchedDir, item, info, opts);
        case FSEVENT_DELETED:
        case FSEVENT_MOVED:
          return this.checkExists(path, fullPath, realPath, parent, watchedDir, item, info, opts);
        }
      }
    };
  
    const closer = setFSEventsListener(
      watchPath,
      realPath,
      watchCallback,
      this.fsw._emitRaw
    );
  
    this.fsw._emitReady();
    return closer;
  }
  
  /**
   * Handle symlinks encountered during directory scan
   * @param {String} linkPath path to symlink
   * @param {String} fullPath absolute path to the symlink
   * @param {Function} transform pre-existing path transformer
   * @param {Number} curDepth level of subdirectories traversed to where symlink is
   * @returns {Promise<void>}
   */
  async _handleFsEventsSymlink(linkPath, fullPath, transform, curDepth) {
    // don't follow the same symlink more than once
    if (this.fsw.closed || this.fsw._symlinkPaths.has(fullPath)) return;
  
    this.fsw._symlinkPaths.set(fullPath, true);
    this.fsw._incrReadyCount();
  
    try {
      const linkTarget = await realpath(linkPath);
      if (this.fsw.closed) return;
      if (this.fsw._isIgnored(linkTarget)) {
        return this.fsw._emitReady();
      }
  
      this.fsw._incrReadyCount();
  
      // add the linkTarget for watching with a wrapper for transform
      // that causes emitted paths to incorporate the link's path
      this._addToFsEvents(linkTarget || linkPath, (path) => {
        let aliasedPath = linkPath;
        if (linkTarget && linkTarget !== DOT_SLASH) {
          aliasedPath = path.replace(linkTarget, linkPath);
        } else if (path !== DOT_SLASH) {
          aliasedPath = sysPath.join(linkPath, path);
        }
        return transform(aliasedPath);
      }, false, curDepth);
    } catch(error) {
      if (this.fsw._handleError(error)) {
        return this.fsw._emitReady();
      }
    }
  }
  
  /**
   *
   * @param {Path} newPath
   * @param {fs.Stats} stats
   */
  emitAdd(newPath, stats, processPath, opts, forceAdd) {
    const pp = processPath(newPath);
    const isDir = stats.isDirectory();
    const dirObj = this.fsw._getWatchedDir(sysPath.dirname(pp));
    const base = sysPath.basename(pp);
  
    // ensure empty dirs get tracked
    if (isDir) this.fsw._getWatchedDir(pp);
    if (dirObj.has(base)) return;
    dirObj.add(base);
  
    if (!opts.ignoreInitial || forceAdd === true) {
      this.fsw._emit(isDir ? EV_ADD_DIR : EV_ADD, pp, stats);
    }
  }
  
  initWatch(realPath, path, wh, processPath) {
    if (this.fsw.closed) return;
    const closer = this._watchWithFsEvents(
      wh.watchPath,
      sysPath.resolve(realPath || wh.watchPath),
      processPath,
      wh.globFilter
    );
    this.fsw._addPathCloser(path, closer);
  }
  
  /**
   * Handle added path with fsevents
   * @param {String} path file/dir path or glob pattern
   * @param {Function|Boolean=} transform converts working path to what the user expects
   * @param {Boolean=} forceAdd ensure add is emitted
   * @param {Number=} priorDepth Level of subdirectories already traversed.
   * @returns {Promise<void>}
   */
  async _addToFsEvents(path, transform, forceAdd, priorDepth) {
    if (this.fsw.closed) {
      return;
    }
    const opts = this.fsw.options;
    const processPath = typeof transform === FUNCTION_TYPE ? transform : IDENTITY_FN;
  
    const wh = this.fsw._getWatchHelpers(path);
  
    // evaluate what is at the path we're being asked to watch
    try {
      const stats = await statMethods[wh.statMethod](wh.watchPath);
      if (this.fsw.closed) return;
      if (this.fsw._isIgnored(wh.watchPath, stats)) {
        throw null;
      }
      if (stats.isDirectory()) {
        // emit addDir unless this is a glob parent
        if (!wh.globFilter) this.emitAdd(processPath(path), stats, processPath, opts, forceAdd);
  
        // don't recurse further if it would exceed depth setting
        if (priorDepth && priorDepth > opts.depth) return;
  
        // scan the contents of the dir
        this.fsw._readdirp(wh.watchPath, {
          fileFilter: entry => wh.filterPath(entry),
          directoryFilter: entry => wh.filterDir(entry),
          ...Depth(opts.depth - (priorDepth || 0))
        }).on(STR_DATA, (entry) => {
          // need to check filterPath on dirs b/c filterDir is less restrictive
          if (this.fsw.closed) {
            return;
          }
          if (entry.stats.isDirectory() && !wh.filterPath(entry)) return;
  
          const joinedPath = sysPath.join(wh.watchPath, entry.path);
          const {fullPath} = entry;
  
          if (wh.followSymlinks && entry.stats.isSymbolicLink()) {
            // preserve the current depth here since it can't be derived from
            // real paths past the symlink
            const curDepth = opts.depth === undefined ?
              undefined : calcDepth(joinedPath, sysPath.resolve(wh.watchPath)) + 1;
  
            this._handleFsEventsSymlink(joinedPath, fullPath, processPath, curDepth);
          } else {
            this.emitAdd(joinedPath, entry.stats, processPath, opts, forceAdd);
          }
        }).on(EV_ERROR, EMPTY_FN).on(STR_END, () => {
          this.fsw._emitReady();
        });
      } else {
        this.emitAdd(wh.watchPath, stats, processPath, opts, forceAdd);
        this.fsw._emitReady();
      }
    } catch (error) {
      if (!error || this.fsw._handleError(error)) {
        // TODO: Strange thing: "should not choke on an ignored watch path" will be failed without 2 ready calls -__-
        this.fsw._emitReady();
        this.fsw._emitReady();
      }
    }
  
    if (opts.persistent && forceAdd !== true) {
      if (typeof transform === FUNCTION_TYPE) {
        // realpath has already been resolved
        this.initWatch(undefined, path, wh, processPath);
      } else {
        let realPath;
        try {
          realPath = await realpath(wh.watchPath);
        } catch (e) {}
        this.initWatch(realPath, path, wh, processPath);
      }
    }
  }
  
  }
  
  module.exports = FsEventsHandler;
  module.exports.canUse = canUse;
  