'use strict';

const fs = require('fs');
const path = require('path');
const common = require('./common');
const EventEmitter = require('events').EventEmitter;
let fsevents;

try {
  fsevents = require('fsevents');
} catch (e) {
  // Ignore.
}

/**
 * Constants
 */

const CHANGE_EVENT = common.CHANGE_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const ADD_EVENT = common.ADD_EVENT;
const ALL_EVENT = common.ALL_EVENT;

/**
 * Export `FSEventsWatcher` class.
 * Watches `dir`.
 *
 * @class FSEventsWatcher
 * @param String dir
 * @param {Object} opts
 * @public
 */

module.exports = class FSEventsWatcher extends EventEmitter {
  constructor(dir, opts) {
    if (!fsevents) {
      throw new Error(
        '`fsevents` unavailable (this watcher can only be used on Darwin)'
      );
    }

    super();

    common.assignOptions(this, opts);

    this.root = path.resolve(dir);
    this.watcher = fsevents(this.root);

    this.watcher.start().on('change', this.handleEvent.bind(this));
    this._tracked = Object.create(null);
    common.recReaddir(
      this.root,
      filepath => (this._tracked[filepath] = true),
      filepath => (this._tracked[filepath] = true),
      this.emit.bind(this, 'ready'),
      this.emit.bind(this, 'error'),
      this.ignored
    );
  }

  handleEvent(filepath) {
    const relativePath = path.relative(this.root, filepath);
    if (
      !common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
    ) {
      return;
    }

    fs.lstat(
      filepath,
      function(error, stat) {
        if (error && error.code !== 'ENOENT') {
          this.emit('error', error);
          return;
        }

        if (error) {
          // Ignore files that aren't tracked and don't exist.
          if (!this._tracked[filepath]) {
            return;
          }

          this._emit(DELETE_EVENT, relativePath);
          delete this._tracked[filepath];
          return;
        }

        if (this._tracked[filepath]) {
          this._emit(CHANGE_EVENT, relativePath, stat);
        } else {
          this._tracked[filepath] = true;
          this._emit(ADD_EVENT, relativePath, stat);
        }
      }.bind(this)
    );
  }

  /**
   * End watching.
   *
   * @public
   */

  close(callback) {
    this.watcher.stop();
    this.removeAllListeners();
    if (typeof callback === 'function') {
      process.nextTick(callback.bind(null, null, true));
    }
  }

  /**
   * Emit events.
   *
   * @private
   */

  _emit(type, file, stat) {
    this.emit(type, file, this.root, stat);
    this.emit(ALL_EVENT, type, file, this.root, stat);
  }
};