"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CustomStatWatcher = exports.assertStatus = exports.Status = exports.Event = void 0;
const tslib_1 = require("tslib");
const events_1 = require("events");
const statUtils = tslib_1.__importStar(require("../../statUtils"));
var Event;
(function (Event) {
    Event["Change"] = "change";
    Event["Stop"] = "stop";
})(Event || (exports.Event = Event = {}));
var Status;
(function (Status) {
    Status["Ready"] = "ready";
    Status["Running"] = "running";
    Status["Stopped"] = "stopped";
})(Status || (exports.Status = Status = {}));
function assertStatus(current, expected) {
    if (current !== expected) {
        throw new Error(`Invalid StatWatcher status: expected '${expected}', got '${current}'`);
    }
}
exports.assertStatus = assertStatus;
class CustomStatWatcher extends events_1.EventEmitter {
    static create(fakeFs, path, opts) {
        const statWatcher = new CustomStatWatcher(fakeFs, path, opts);
        statWatcher.start();
        return statWatcher;
    }
    constructor(fakeFs, path, { bigint = false } = {}) {
        super();
        this.status = Status.Ready;
        this.changeListeners = new Map();
        this.startTimeout = null;
        this.fakeFs = fakeFs;
        this.path = path;
        this.bigint = bigint;
        this.lastStats = this.stat();
    }
    start() {
        assertStatus(this.status, Status.Ready);
        this.status = Status.Running;
        // Node allows other listeners to be registered up to 3 milliseconds
        // after the watcher has been started, so that's what we're doing too
        this.startTimeout = setTimeout(() => {
            this.startTimeout = null;
            // Per the Node FS docs:
            // "When an fs.watchFile operation results in an ENOENT error,
            // it will invoke the listener once, with all the fields zeroed
            // (or, for dates, the Unix Epoch)."
            if (!this.fakeFs.existsSync(this.path)) {
                this.emit(Event.Change, this.lastStats, this.lastStats);
            }
        }, 3);
    }
    stop() {
        assertStatus(this.status, Status.Running);
        this.status = Status.Stopped;
        if (this.startTimeout !== null) {
            clearTimeout(this.startTimeout);
            this.startTimeout = null;
        }
        this.emit(Event.Stop);
    }
    stat() {
        try {
            return this.fakeFs.statSync(this.path, { bigint: this.bigint });
        }
        catch (error) {
            // From observation, all errors seem to be mostly ignored by Node.
            // Checked with ENOENT, ENOTDIR, EPERM
            const statInstance = this.bigint
                ? new statUtils.BigIntStatsEntry()
                : new statUtils.StatEntry();
            return statUtils.clearStats(statInstance);
        }
    }
    /**
     * Creates an interval whose callback compares the current stats with the previous stats and notifies all listeners in case of changes.
     *
     * @param opts.persistent Decides whether the interval should be immediately unref-ed.
     */
    makeInterval(opts) {
        const interval = setInterval(() => {
            const currentStats = this.stat();
            const previousStats = this.lastStats;
            if (statUtils.areStatsEqual(currentStats, previousStats))
                return;
            this.lastStats = currentStats;
            this.emit(Event.Change, currentStats, previousStats);
        }, opts.interval);
        return opts.persistent ? interval : interval.unref();
    }
    /**
     * Registers a listener and assigns it an interval.
     */
    registerChangeListener(listener, opts) {
        this.addListener(Event.Change, listener);
        this.changeListeners.set(listener, this.makeInterval(opts));
    }
    /**
     * Unregisters the listener and clears the assigned interval.
     */
    unregisterChangeListener(listener) {
        this.removeListener(Event.Change, listener);
        const interval = this.changeListeners.get(listener);
        if (typeof interval !== `undefined`)
            clearInterval(interval);
        this.changeListeners.delete(listener);
    }
    /**
     * Unregisters all listeners and clears all assigned intervals.
     */
    unregisterAllChangeListeners() {
        for (const listener of this.changeListeners.keys()) {
            this.unregisterChangeListener(listener);
        }
    }
    hasChangeListeners() {
        return this.changeListeners.size > 0;
    }
    /**
     * Refs all stored intervals.
     */
    ref() {
        for (const interval of this.changeListeners.values())
            interval.ref();
        return this;
    }
    /**
     * Unrefs all stored intervals.
     */
    unref() {
        for (const interval of this.changeListeners.values())
            interval.unref();
        return this;
    }
}
exports.CustomStatWatcher = CustomStatWatcher;
