"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DirentFromStats = void 0;
exports.walkDir = walkDir;
const makeQ_1 = require("./makeQ");
const promises_1 = require("fs/promises");
const const_1 = require("./const");
const path_1 = require("path");
const cross_1 = require("./cross");
const node_fs_1 = require("node:fs");
const events_1 = __importDefault(require("./events"));
const lodash_1 = __importDefault(require("lodash"));
const fswin_1 = __importDefault(require("fswin"));
const util_files_1 = require("./util-files");
const dirQ = (0, makeQ_1.makeQ)(3);
// cb returns void = just go on, null = stop, false = go on but don't recur (in case of depth)
function walkDir(path, { depth = 0, hidden = true, parallelizeRecursion = false, ctx }, cb) {
    let stopped = false;
    const closingQ = [];
    return new Promise(async (resolve, reject) => {
        if (!await (0, util_files_1.isDirectory)(path))
            return reject(Error('ENOTDIR'));
        dirQ.add(() => readDir('', depth)
            .then(res => {
            Promise.resolve(res?.branchDone).then(resolve);
        }, reject));
    });
    async function readDir(relativePath, depth) {
        if (stopped)
            return;
        const base = (0, path_1.join)(path, relativePath);
        const subDirsDone = [];
        let n = 0;
        let last;
        const res = (await events_1.default.emitAsync('listDiskFolder', { path: base, ctx, hidden }))?.[0]; // consider only first result
        const pluginReceiver = lodash_1.default.isFunction(res) && res || null;
        const pluginIterator = lodash_1.default.isFunction(res?.[Symbol.asyncIterator] || res?.[Symbol.iterator]) && res;
        if (const_1.IS_WINDOWS && !pluginIterator) { // use native apis to read the 'hidden' attribute
            const direntMethods = {
                isDir: false,
                isFile() { return !this.isDir; },
                isDirectory() { return this.isDir; },
                isBlockDevice() { return false; },
                isCharacterDevice() { return false; },
            };
            await new Promise(res => fswin_1.default.find(base + '\\*', (event, f) => {
                if (event !== 'FOUND')
                    return res();
                if (stopped)
                    return true; // stop signal
                if (!hidden && f.IS_HIDDEN)
                    return;
                work(Object.assign(Object.create(direntMethods), {
                    isDir: f.IS_DIRECTORY,
                    name: f.LONG_NAME,
                    stats: {
                        size: f.SIZE,
                        birthtime: f.CREATION_TIME, birthtimeMs: f.CREATION_TIME.getTime(),
                        mtime: f.LAST_WRITE_TIME, mtimeMs: f.LAST_WRITE_TIME.getTime(),
                        isFile: () => !f.IS_DIRECTORY,
                        isDirectory: () => f.IS_DIRECTORY,
                    }
                }));
            }, true));
        }
        else
            for await (let entry of (pluginIterator || await (0, promises_1.opendir)(base))) {
                if (stopped)
                    break;
                if (!hidden && !const_1.IS_WINDOWS && entry.name[0] === '.')
                    continue;
                const stats = entry.isSymbolicLink?.() && await (0, util_files_1.statWithTimeout)((0, path_1.join)(base, entry.name)).catch(() => null);
                if (stats === null)
                    continue;
                if (stats)
                    entry = new DirentFromStats(entry.name, stats);
                const expanded = entry;
                if (stats)
                    expanded.stats = stats;
                await work(expanded);
            }
        pluginReceiver?.(!stopped);
        const branchDone = Promise.allSettled(subDirsDone);
        if (last) // using streams, we don't know when the entries are received, so we need to notify on last item
            last.closingBranch = branchDone.then(() => relativePath);
        else
            closingQ.push(relativePath); // ok, we'll ask next one to carry this info
        // don't return the promise directly, as this job ends here, but communicate to caller the promise for the whole branch
        return { branchDone, n };
        async function work(entry) {
            entry.path = (relativePath && relativePath + '/') + entry.name;
            pluginReceiver?.(entry);
            if (last && closingQ.length) // pending entries
                last.closingBranch = Promise.resolve(closingQ.shift());
            last = entry;
            const res = await cb(entry);
            if (res === null)
                return stopped = true;
            if (res === false)
                return;
            n++;
            if (!depth || !entry.isDirectory())
                return;
            const branchDone = (0, cross_1.pendingPromise)(); // per-job
            subDirsDone.push(branchDone);
            const job = () => readDir(entry.path, depth - 1) // recur
                .then(x => x, () => { }) // mute errors
                .then(res => {
                if (!res?.n)
                    closingQ.push(entry.path); // no children to tell i'm done
                Promise.resolve(res?.branchDone).then(() => branchDone.resolve());
            });
            if (parallelizeRecursion)
                dirQ.add(job);
            else
                await job();
        }
    }
}
const kStats = Symbol('stats');
// Adapting an internal class in Node.js to mimic the behavior of `Dirent` when creating it manually from `Stats`.
// https://github.com/nodejs/node/blob/a4cf6b204f0b160480153dc293ae748bf15225f9/lib/internal/fs/utils.js#L199C1-L213
class DirentFromStats extends node_fs_1.Dirent {
    [kStats];
    constructor(name, stats) {
        // @ts-expect-error The constructor has parameters, but they are not represented in types.
        // https://github.com/nodejs/node/blob/a4cf6b204f0b160480153dc293ae748bf15225f9/lib/internal/fs/utils.js#L164
        super(name, null);
        this[kStats] = stats;
    }
}
exports.DirentFromStats = DirentFromStats;
for (const key of Reflect.ownKeys(node_fs_1.Dirent.prototype)) {
    const name = key;
    if (name === 'constructor' || typeof name === 'symbol')
        continue;
    DirentFromStats.prototype[name] = function () {
        return this[kStats][name]();
    };
}
