"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.patchFs = patchFs;
exports.extendFs = extendFs;
const util_1 = require("util");
const NodePathFS_1 = require("../NodePathFS");
const FileHandle_1 = require("./FileHandle");
const SYNC_IMPLEMENTATIONS = new Set([
    `accessSync`,
    `appendFileSync`,
    `createReadStream`,
    `createWriteStream`,
    `chmodSync`,
    `fchmodSync`,
    `chownSync`,
    `fchownSync`,
    `closeSync`,
    `copyFileSync`,
    `linkSync`,
    `lstatSync`,
    `fstatSync`,
    `lutimesSync`,
    `mkdirSync`,
    `openSync`,
    `opendirSync`,
    `readlinkSync`,
    `readFileSync`,
    `readdirSync`,
    `readlinkSync`,
    `realpathSync`,
    `renameSync`,
    `rmdirSync`,
    `rmSync`,
    `statSync`,
    `symlinkSync`,
    `truncateSync`,
    `ftruncateSync`,
    `unlinkSync`,
    `unwatchFile`,
    `utimesSync`,
    `watch`,
    `watchFile`,
    `writeFileSync`,
    `writeSync`,
]);
const ASYNC_IMPLEMENTATIONS = new Set([
    `accessPromise`,
    `appendFilePromise`,
    `fchmodPromise`,
    `chmodPromise`,
    `fchownPromise`,
    `chownPromise`,
    `closePromise`,
    `copyFilePromise`,
    `linkPromise`,
    `fstatPromise`,
    `lstatPromise`,
    `lutimesPromise`,
    `mkdirPromise`,
    `openPromise`,
    `opendirPromise`,
    `readdirPromise`,
    `realpathPromise`,
    `readFilePromise`,
    `readdirPromise`,
    `readlinkPromise`,
    `renamePromise`,
    `rmdirPromise`,
    `rmPromise`,
    `statPromise`,
    `symlinkPromise`,
    `truncatePromise`,
    `ftruncatePromise`,
    `unlinkPromise`,
    `utimesPromise`,
    `writeFilePromise`,
    `writeSync`,
]);
//#endregion
function patchFs(patchedFs, fakeFs) {
    // We wrap the `fakeFs` with a `NodePathFS` to add support for all path types supported by Node
    fakeFs = new NodePathFS_1.NodePathFS(fakeFs);
    const setupFn = (target, name, replacement) => {
        const orig = target[name];
        target[name] = replacement;
        // Preserve any util.promisify implementations
        if (typeof orig?.[util_1.promisify.custom] !== `undefined`) {
            replacement[util_1.promisify.custom] = orig[util_1.promisify.custom];
        }
    };
    /** Callback implementations */
    {
        setupFn(patchedFs, `exists`, (p, ...args) => {
            const hasCallback = typeof args[args.length - 1] === `function`;
            const callback = hasCallback ? args.pop() : () => { };
            process.nextTick(() => {
                fakeFs.existsPromise(p).then(exists => {
                    callback(exists);
                }, () => {
                    callback(false);
                });
            });
        });
        // Adapted from https://github.com/nodejs/node/blob/e5c1fd7a2a1801fd75bdde23b260488e85453eb2/lib/fs.js#L603-L667
        setupFn(patchedFs, `read`, (...args) => {
            let [fd, buffer, offset, length, position, callback] = args;
            if (args.length <= 3) {
                // Assume fs.read(fd, options, callback)
                let options = {};
                if (args.length < 3) {
                    // This is fs.read(fd, callback)
                    callback = args[1];
                }
                else {
                    // This is fs.read(fd, {}, callback)
                    options = args[1];
                    callback = args[2];
                }
                ({
                    buffer = Buffer.alloc(16384),
                    offset = 0,
                    length = buffer.byteLength,
                    position,
                } = options);
            }
            if (offset == null)
                offset = 0;
            length |= 0;
            if (length === 0) {
                process.nextTick(() => {
                    callback(null, 0, buffer);
                });
                return;
            }
            if (position == null)
                position = -1;
            process.nextTick(() => {
                fakeFs.readPromise(fd, buffer, offset, length, position).then(bytesRead => {
                    callback(null, bytesRead, buffer);
                }, error => {
                    // https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/lib/fs.js#L655-L658
                    // Known issue: bytesRead could theoretically be > than 0, but we currently always return 0
                    callback(error, 0, buffer);
                });
            });
        });
        for (const fnName of ASYNC_IMPLEMENTATIONS) {
            const origName = fnName.replace(/Promise$/, ``);
            if (typeof patchedFs[origName] === `undefined`)
                continue;
            const fakeImpl = fakeFs[fnName];
            if (typeof fakeImpl === `undefined`)
                continue;
            const wrapper = (...args) => {
                const hasCallback = typeof args[args.length - 1] === `function`;
                const callback = hasCallback ? args.pop() : () => { };
                process.nextTick(() => {
                    fakeImpl.apply(fakeFs, args).then((result) => {
                        callback(null, result);
                    }, (error) => {
                        callback(error);
                    });
                });
            };
            setupFn(patchedFs, origName, wrapper);
        }
        patchedFs.realpath.native = patchedFs.realpath;
    }
    /** Sync implementations */
    {
        setupFn(patchedFs, `existsSync`, (p) => {
            try {
                return fakeFs.existsSync(p);
            }
            catch {
                return false;
            }
        });
        // Adapted from https://github.com/nodejs/node/blob/e5c1fd7a2a1801fd75bdde23b260488e85453eb2/lib/fs.js#L684-L725
        setupFn(patchedFs, `readSync`, (...args) => {
            let [fd, buffer, offset, length, position] = args;
            if (args.length <= 3) {
                // Assume fs.read(fd, buffer, options)
                const options = args[2] || {};
                ({ offset = 0, length = buffer.byteLength, position } = options);
            }
            if (offset == null)
                offset = 0;
            length |= 0;
            if (length === 0)
                return 0;
            if (position == null)
                position = -1;
            return fakeFs.readSync(fd, buffer, offset, length, position);
        });
        for (const fnName of SYNC_IMPLEMENTATIONS) {
            const origName = fnName;
            if (typeof patchedFs[origName] === `undefined`)
                continue;
            const fakeImpl = fakeFs[fnName];
            if (typeof fakeImpl === `undefined`)
                continue;
            setupFn(patchedFs, origName, fakeImpl.bind(fakeFs));
        }
        patchedFs.realpathSync.native = patchedFs.realpathSync;
    }
    /** Promise implementations */
    {
        // `fs.promises` is a getter that returns a reference to require(`fs/promises`),
        // so we can just patch `fs.promises` and both will be updated
        const patchedFsPromises = patchedFs.promises;
        // `fs.promises.exists` doesn't exist
        for (const fnName of ASYNC_IMPLEMENTATIONS) {
            const origName = fnName.replace(/Promise$/, ``);
            if (typeof patchedFsPromises[origName] === `undefined`)
                continue;
            const fakeImpl = fakeFs[fnName];
            if (typeof fakeImpl === `undefined`)
                continue;
            // Open is a bit particular with fs.promises: it returns a file handle
            // instance instead of the traditional file descriptor number
            if (fnName === `open`)
                continue;
            setupFn(patchedFsPromises, origName, (pathLike, ...args) => {
                if (pathLike instanceof FileHandle_1.FileHandle) {
                    return pathLike[origName].apply(pathLike, args);
                }
                else {
                    return fakeImpl.call(fakeFs, pathLike, ...args);
                }
            });
        }
        setupFn(patchedFsPromises, `open`, async (...args) => {
            // @ts-expect-error
            const fd = await fakeFs.openPromise(...args);
            return new FileHandle_1.FileHandle(fd, fakeFs);
        });
        // `fs.promises.realpath` doesn't have a `native` property
    }
    /** util.promisify implementations */
    {
        // TODO add promisified `fs.readv` and `fs.writev`, once they are implemented
        // Override the promisified versions of `fs.read` and `fs.write` to return an object as per
        // https://github.com/nodejs/node/blob/dc79f3f37caf6f25b8efee4623bec31e2c20f595/lib/fs.js#L559-L560
        // and
        // https://github.com/nodejs/node/blob/dc79f3f37caf6f25b8efee4623bec31e2c20f595/lib/fs.js#L690-L691
        // and
        // https://github.com/nodejs/node/blob/ba684805b6c0eded76e5cd89ee00328ac7a59365/lib/internal/util.js#L293
        // @ts-expect-error
        patchedFs.read[util_1.promisify.custom] = async (fd, buffer, ...args) => {
            const res = fakeFs.readPromise(fd, buffer, ...args);
            return { bytesRead: await res, buffer };
        };
        // @ts-expect-error
        patchedFs.write[util_1.promisify.custom] = async (fd, buffer, ...args) => {
            const res = fakeFs.writePromise(fd, buffer, ...args);
            return { bytesWritten: await res, buffer };
        };
    }
}
function extendFs(realFs, fakeFs) {
    const patchedFs = Object.create(realFs);
    patchFs(patchedFs, fakeFs);
    return patchedFs;
}
