"use strict";
/* IMPORT */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
const aborter_1 = __importDefault(require("./aborter/controller"));
const events_1 = require("events");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const string_indexes_1 = __importDefault(require("./string-indexes"));
const constants_1 = require("./constants");
const watcher_handler_1 = __importDefault(require("./watcher_handler"));
const watcher_locker_1 = __importDefault(require("./watcher_locker"));
const watcher_poller_1 = __importDefault(require("./watcher_poller"));
const utils_1 = __importDefault(require("./utils"));
/* WATCHER */
class Watcher extends events_1.EventEmitter {
    /* CONSTRUCTOR */
    constructor(target, options, handler) {
        super();
        this._closed = false;
        this._ready = false;
        this._closeAborter = new aborter_1.default();
        this._closeSignal = this._closeAborter.signal;
        this.on("close" /* CLOSE */, () => this._closeAborter.abort());
        this._closeWait = new Promise(resolve => this.on("close" /* CLOSE */, resolve));
        this._readyWait = new Promise(resolve => this.on("ready" /* READY */, resolve));
        this._locker = new watcher_locker_1.default(this);
        this._roots = new Set();
        this._poller = new watcher_poller_1.default();
        this._pollers = new Set();
        this._subwatchers = new Set();
        this._watchers = {};
        this._watchersLock = Promise.resolve();
        this._watchersRestorable = {};
        this.watch(target, options, handler);
    }
    /* API */
    isClosed() {
        return this._closed;
    }
    isIgnored(targetPath, ignore) {
        return !!ignore && !!ignore(targetPath);
    }
    isReady() {
        return this._ready;
    }
    close() {
        this._locker.reset();
        this._poller.reset();
        this._roots.clear();
        this.watchersClose();
        if (this.isClosed())
            return false;
        this._closed = true;
        return this.emit("close" /* CLOSE */);
    }
    error(exception) {
        if (this.isClosed())
            return false;
        const error = utils_1.default.lang.castError(exception);
        return this.emit("error" /* ERROR */, error);
    }
    event(event, targetPath, targetPathNext) {
        if (this.isClosed())
            return false;
        this.emit("all" /* ALL */, event, targetPath, targetPathNext);
        return this.emit(event, targetPath, targetPathNext);
    }
    ready() {
        if (this.isClosed() || this.isReady())
            return false;
        this._ready = true;
        return this.emit("ready" /* READY */);
    }
    pollerExists(targetPath, options) {
        for (const poller of this._pollers) {
            if (poller.targetPath !== targetPath)
                continue;
            if (!utils_1.default.lang.areShallowEqual(poller.options, options))
                continue;
            return true;
        }
        return false;
    }
    subwatcherExists(targetPath, options) {
        for (const subwatcher of this._subwatchers) {
            if (subwatcher.targetPath !== targetPath)
                continue;
            if (!utils_1.default.lang.areShallowEqual(subwatcher.options, options))
                continue;
            return true;
        }
        return false;
    }
    watchersClose(folderPath, filePath, recursive = true) {
        if (!folderPath) {
            for (const folderPath in this._watchers) {
                this.watchersClose(folderPath, filePath, false);
            }
        }
        else {
            const configs = this._watchers[folderPath];
            if (configs) {
                for (const config of configs) {
                    if (filePath && config.filePath !== filePath)
                        continue;
                    this.watcherClose(config);
                }
            }
            if (recursive) {
                for (const folderPathOther in this._watchers) {
                    if (!utils_1.default.fs.isSubPath(folderPath, folderPathOther))
                        continue;
                    this.watchersClose(folderPathOther, filePath, false);
                }
            }
        }
    }
    watchersLock(callback) {
        return this._watchersLock.then(() => {
            return this._watchersLock = new Promise(async (resolve) => {
                await callback();
                resolve();
            });
        });
    }
    watchersRestore() {
        delete this._watchersRestoreTimeout;
        const watchers = Object.entries(this._watchersRestorable);
        this._watchersRestorable = {};
        for (const [targetPath, config] of watchers) {
            this.watchPath(targetPath, config.options, config.handler);
        }
    }
    async watcherAdd(config, baseWatcherHandler) {
        const { folderPath } = config;
        const configs = this._watchers[folderPath] = (this._watchers[folderPath] || []);
        configs.push(config);
        const watcherHandler = new watcher_handler_1.default(this, config, baseWatcherHandler);
        await watcherHandler.init();
        return watcherHandler;
    }
    watcherClose(config) {
        config.watcher.close();
        const configs = this._watchers[config.folderPath];
        if (configs) {
            const index = configs.indexOf(config);
            configs.splice(index, 1);
            if (!configs.length) {
                delete this._watchers[config.folderPath];
            }
        }
        const rootPath = config.filePath || config.folderPath, isRoot = this._roots.has(rootPath);
        if (isRoot) {
            this._watchersRestorable[rootPath] = config;
            if (!this._watchersRestoreTimeout) {
                this._watchersRestoreTimeout = utils_1.default.lang.defer(() => this.watchersRestore());
            }
        }
    }
    watcherExists(folderPath, options, handler, filePath) {
        const configsSibling = this._watchers[folderPath];
        if (!!(configsSibling === null || configsSibling === void 0 ? void 0 : configsSibling.find(config => config.handler === handler && (!config.filePath || config.filePath === filePath) && config.options.ignore === options.ignore && !!config.options.native === !!options.native && (!options.recursive || config.options.recursive))))
            return true;
        let folderAncestorPath = path_1.default.dirname(folderPath);
        for (let depth = 1; depth < Infinity; depth++) {
            const configsAncestor = this._watchers[folderAncestorPath];
            if (!!(configsAncestor === null || configsAncestor === void 0 ? void 0 : configsAncestor.find(config => { var _a; return (depth === 1 || (config.options.recursive && depth <= ((_a = config.options.depth) !== null && _a !== void 0 ? _a : constants_1.DEPTH))) && config.handler === handler && (!config.filePath || config.filePath === filePath) && config.options.ignore === options.ignore && !!config.options.native === !!options.native && (!options.recursive || (config.options.recursive && (constants_1.HAS_NATIVE_RECURSION && config.options.native !== false))); })))
                return true;
            if (!constants_1.HAS_NATIVE_RECURSION)
                break; // No other ancestor will possibly be found
            const folderAncestorPathNext = path_1.default.dirname(folderPath);
            if (folderAncestorPath === folderAncestorPathNext)
                break;
            folderAncestorPath = folderAncestorPathNext;
        }
        return false;
    }
    async watchDirectories(foldersPaths, options, handler, filePath, baseWatcherHandler) {
        if (this.isClosed())
            return;
        foldersPaths = utils_1.default.lang.uniq(foldersPaths).sort();
        let watcherHandlerLast;
        for (const folderPath of foldersPaths) {
            if (this.isIgnored(folderPath, options.ignore))
                continue;
            if (this.watcherExists(folderPath, options, handler, filePath))
                continue;
            try {
                const watcherOptions = (!options.recursive || (constants_1.HAS_NATIVE_RECURSION && options.native !== false)) ? options : { ...options, recursive: false }, // Ensuring recursion is explicitly disabled if not available
                    watcher = fs_1.default.watch(folderPath, watcherOptions), watcherConfig = { watcher, handler, options, folderPath, filePath }, watcherHandler = watcherHandlerLast = await this.watcherAdd(watcherConfig, baseWatcherHandler);
                const isRoot = this._roots.has(filePath || folderPath);
                if (isRoot) {
                    const parentOptions = { ...options, ignoreInitial: true, recursive: false }, // Ensuring only the parent folder is being watched
                        parentFolderPath = path_1.default.dirname(folderPath), parentFilePath = folderPath;
                    await this.watchDirectories([parentFolderPath], parentOptions, handler, parentFilePath, watcherHandler);
                    //TODO: Watch parents recursively with the following code, which requires other things to be changed too though
                    // while ( true ) {
                    //   await this.watchDirectories ( [parentFolderPath], parentOptions, handler, parentFilePath, watcherHandler );
                    //   const parentFolderPathNext = path.dirname ( parentFolderPath );
                    //   if ( parentFolderPath === parentFolderPathNext ) break;
                    //   parentFilePath = parentFolderPath;
                    //   parentFolderPath = parentFolderPathNext;
                    // }
                }
            }
            catch (error) {
                this.error(error);
            }
        }
        return watcherHandlerLast;
    }
    async watchDirectory(folderPath, options, handler, filePath, baseWatcherHandler) {
        var _a;
        if (this.isClosed())
            return;
        if (this.isIgnored(folderPath, options.ignore))
            return;
        if (!options.recursive || (constants_1.HAS_NATIVE_RECURSION && options.native !== false)) {
            return this.watchersLock(() => {
                return this.watchDirectories([folderPath], options, handler, filePath, baseWatcherHandler);
            });
        }
        else {
            options = { ...options, recursive: true }; // Ensuring recursion is explicitly enabled
            const depth = (_a = options.depth) !== null && _a !== void 0 ? _a : constants_1.DEPTH, [folderSubPaths] = await utils_1.default.fs.readdir(folderPath, options.ignore, depth, this._closeSignal, options.readdirMap);
            return this.watchersLock(async () => {
                const watcherHandler = await this.watchDirectories([folderPath], options, handler, filePath, baseWatcherHandler);
                if (folderSubPaths.length) {
                    const folderPathDepth = string_indexes_1.default(folderPath, path_1.default.sep).length;
                    for (const folderSubPath of folderSubPaths) {
                        const folderSubPathDepth = string_indexes_1.default(folderSubPath, path_1.default.sep).length, subDepth = Math.max(0, depth - (folderSubPathDepth - folderPathDepth)), subOptions = { ...options, depth: subDepth }; // Updating the maximum depth to account for depth of the sub path
                        await this.watchDirectories([folderSubPath], subOptions, handler, filePath, baseWatcherHandler || watcherHandler);
                    }
                }
            });
        }
    }
    async watchFileOnce(filePath, options, callback) {
        if (this.isClosed())
            return;
        options = { ...options, ignoreInitial: false }; // Ensuring initial events are detected too
        if (this.subwatcherExists(filePath, options))
            return;
        const config = { targetPath: filePath, options };
        const handler = (event, targetPath) => {
            if (targetPath !== filePath)
                return;
            stop();
            callback();
        };
        const watcher = new Watcher(handler);
        const start = () => {
            this._subwatchers.add(config);
            this.on("close" /* CLOSE */, stop); // Ensuring the subwatcher is stopped on close
            watcher.watchFile(filePath, options, handler);
        };
        const stop = () => {
            this._subwatchers.delete(config);
            this.removeListener("close" /* CLOSE */, stop); // Ensuring there are no leftover listeners
            watcher.close();
        };
        return start();
    }
    async watchFile(filePath, options, handler) {
        if (this.isClosed())
            return;
        if (this.isIgnored(filePath, options.ignore))
            return;
        options = { ...options, recursive: false }; // Ensuring recursion is explicitly disabled
        const folderPath = path_1.default.dirname(filePath);
        return this.watchDirectory(folderPath, options, handler, filePath);
    }
    async watchPollingOnce(targetPath, options, callback) {
        if (this.isClosed())
            return;
        let isDone = false;
        const poller = new watcher_poller_1.default();
        const disposer = await this.watchPolling(targetPath, options, async () => {
            if (isDone)
                return;
            const events = await poller.update(targetPath, options.pollingTimeout);
            if (!events.length)
                return; // Nothing actually changed, skipping
            if (isDone)
                return; // Another async callback has done the work already, skipping
            isDone = true;
            disposer();
            callback();
        });
    }
    async watchPolling(targetPath, options, callback) {
        var _a;
        if (this.isClosed())
            return utils_1.default.lang.noop;
        if (this.pollerExists(targetPath, options))
            return utils_1.default.lang.noop;
        const watcherOptions = { ...options, interval: (_a = options.pollingInterval) !== null && _a !== void 0 ? _a : constants_1.POLLING_INTERVAL }; // Ensuring a default interval is set
        const config = { targetPath, options };
        const start = () => {
            this._pollers.add(config);
            this.on("close" /* CLOSE */, stop); // Ensuring polling is stopped on close
            fs_1.default.watchFile(targetPath, watcherOptions, callback);
        };
        const stop = () => {
            this._pollers.delete(config);
            this.removeListener("close" /* CLOSE */, stop); // Ensuring there are no leftover listeners
            fs_1.default.unwatchFile(targetPath, callback);
        };
        utils_1.default.lang.attempt(start);
        return () => utils_1.default.lang.attempt(stop);
    }
    async watchUnknownChild(targetPath, options, handler) {
        if (this.isClosed())
            return;
        const watch = () => this.watchPath(targetPath, options, handler);
        return this.watchFileOnce(targetPath, options, watch);
    }
    async watchUnknownTarget(targetPath, options, handler) {
        if (this.isClosed())
            return;
        const watch = () => this.watchPath(targetPath, options, handler);
        return this.watchPollingOnce(targetPath, options, watch);
    }
    async watchPaths(targetPaths, options, handler) {
        if (this.isClosed())
            return;
        targetPaths = utils_1.default.lang.uniq(targetPaths).sort();
        const isParallelizable = targetPaths.every((targetPath, index) => targetPaths.every((t, i) => i === index || !utils_1.default.fs.isSubPath(targetPath, t))); // All paths are about separate subtrees, so we can start watching in parallel safely //TODO: Find parallelizable chunks rather than using an all or nothing approach
        if (isParallelizable) { // Watching in parallel
            await Promise.all(targetPaths.map(targetPath => {
                return this.watchPath(targetPath, options, handler);
            }));
        }
        else { // Watching serially
            for (const targetPath of targetPaths) {
                await this.watchPath(targetPath, options, handler);
            }
        }
    }
    async watchPath(targetPath, options, handler) {
        if (this.isClosed())
            return;
        targetPath = path_1.default.normalize(targetPath);
        if (this.isIgnored(targetPath, options.ignore))
            return;
        const stats = await utils_1.default.fs.poll(targetPath, options.pollingTimeout);
        if (!stats) {
            const parentPath = path_1.default.dirname(targetPath), parentStats = await utils_1.default.fs.poll(parentPath, options.pollingTimeout);
            if (parentStats === null || parentStats === void 0 ? void 0 : parentStats.isDirectory()) {
                return this.watchUnknownChild(targetPath, options, handler);
            }
            else {
                return this.watchUnknownTarget(targetPath, options, handler);
            }
        }
        else if (stats.isFile()) {
            return this.watchFile(targetPath, options, handler);
        }
        else if (stats.isDirectory()) {
            return this.watchDirectory(targetPath, options, handler);
        }
        else {
            this.error(`"${targetPath}" is not supported`);
        }
    }
    async watch(target, options, handler = utils_1.default.lang.noop) {
        if (utils_1.default.lang.isFunction(target))
            return this.watch([], {}, target);
        if (utils_1.default.lang.isUndefined(target))
            return this.watch([], options, handler);
        if (utils_1.default.lang.isFunction(options))
            return this.watch(target, {}, options);
        if (utils_1.default.lang.isUndefined(options))
            return this.watch(target, {}, handler);
        if (this.isClosed())
            return;
        if (this.isReady())
            options.readdirMap = undefined; // Only usable before initialization
        const targetPaths = utils_1.default.lang.castArray(target);
        targetPaths.forEach(targetPath => this._roots.add(targetPath));
        await this.watchPaths(targetPaths, options, handler);
        if (this.isClosed())
            return;
        if (handler !== utils_1.default.lang.noop) {
            this.on("all" /* ALL */, handler);
        }
        options.readdirMap = undefined; // Only usable before initialization
        this.ready();
    }
}
/* EXPORT */
module.exports = Watcher;
module.exports.default = Watcher;
Object.defineProperty(module.exports, "__esModule", { value: true });
//# sourceMappingURL=data:application/json;base64,[encoded data, 20924 characters]