"use strict";

const fs = require("fs");
const path = require("path");
const { EventEmitter } = require("events");

// This may have to be tweaked based on experience.
const DEBOUNCE_MS = 100;
const CLOSING = Symbol("CLOSING");
const ERROR = Symbol("ERROR");

/**
 * Relatively feature-free file watcher that deals with some of the
 * idiosyncrasies of fs.watch.  On some OS's, change notifications are doubled
 * up.  Watch the owning directory instead of the file, so that when the file
 * doesn't exist then gets created we get a change notification instead of an
 * error.  When the file is moved in or out of the directory, don't track the
 * inode of the original file.  No notification is given on file deletion,
 * just when the file is ready to be read.
 */
class Watcher extends EventEmitter {
  /**
   * Creates an instance of Watcher.  This only works for files in a small
   * number of directories.
   *
   * @param {string[]} filenames The files to watch.  Should be one or more
   *   strings, each of which is the name of a plain file, not a directory,
   *   pipe, etc.
   */
  constructor(...filenames) {
    super();

    const resolved = new Set(filenames.map(fn => path.resolve(fn)));
    const dirs = new Set([...resolved].map(fn => path.dirname(fn)));

    this.timeout = null;
    this.watchers = [];

    for (const dir of dirs) {
      const changed = (/** @type {string} */_typ, /** @type {string} */fn) => {
        if (typeof this.timeout === "symbol") {
          return;
        }
        const filename = path.join(dir, fn);
        // Might be a different file changing in one of the target dirs
        if (resolved.has(filename)) {
          if (!this.timeout) {
            fs.stat(filename, (er, stats) => {
              if (!er && stats.isFile()) {
                this.emit("change", filename, stats);
              }
            });
          } else {
            clearTimeout(this.timeout);
          }

          // De-bounce, across all files
          this.timeout = setTimeout(() => {
            this.timeout = null;
          }, Watcher.interval);
        }
      };

      const w = fs.watch(dir);
      w.on("error", er => {
        const t = this.timeout;
        this.timeout = ERROR;
        if (t && (typeof t !== "symbol")) {
          clearTimeout(t);
        }
        this.emit("error", er);
        this.close();
      });
      w.on("change", changed);
      this.watchers.push(w);
    }

    // Fire initial time if file exists.
    setImmediate(() => {
      if (this.watchers.length > 0) {
        // First watcher will correspond to the directory of the first filename.
        const w = this.watchers[0];
        w.emit("change", "initial", path.basename([...resolved][0]));
      }
    });
  }

  /**
   * Close the watcher.  Safe to call multiple times.
   *
   * @returns {Promise<void>} Always resolves.
   */
  close() {
    // Stop any more events from firing, immediately
    const t = this.timeout;

    if (t) {
      if (typeof t !== "symbol") {
        this.timeout = CLOSING;
        clearTimeout(t);
      }
    }

    const p = [];
    for (const w of this.watchers) {
      p.push(new Promise(resolve => {
        w.once("close", resolve);
      }));
      w.close();
    }
    return Promise.all(p).then(() => {
      this.watchers = [];
      if (t !== ERROR) {
        this.emit("close");
      }
    });
  }
}

Watcher.interval = DEBOUNCE_MS;
module.exports = Watcher;
