/* IMPORT */
import { once } from 'node:events';
import { createWriteStream } from 'node:fs';
import path from 'node:path';
import { Readable } from 'node:stream';
import fs from 'stubborn-fs';
import { DEFAULT_ENCODING, DEFAULT_FILE_MODE, DEFAULT_FOLDER_MODE, DEFAULT_READ_OPTIONS, DEFAULT_WRITE_OPTIONS, DEFAULT_USER_UID, DEFAULT_USER_GID, DEFAULT_INTERVAL_ASYNC, DEFAULT_TIMEOUT_ASYNC, DEFAULT_TIMEOUT_SYNC, IS_POSIX } from './constants.js';
import { isException, isFunction, isString, isUndefined } from './utils/lang.js';
import Scheduler from './utils/scheduler.js';
import Temp from './utils/temp.js';
function readFile(filePath, options = DEFAULT_READ_OPTIONS) {
    if (isString(options))
        return readFile(filePath, { encoding: options });
    const timeout = options.timeout ?? DEFAULT_TIMEOUT_ASYNC;
    const retryOptions = { timeout, interval: DEFAULT_INTERVAL_ASYNC };
    return fs.retry.readFile(retryOptions)(filePath, options);
}
function readFileSync(filePath, options = DEFAULT_READ_OPTIONS) {
    if (isString(options))
        return readFileSync(filePath, { encoding: options });
    const timeout = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
    const retryOptions = { timeout };
    return fs.retry.readFileSync(retryOptions)(filePath, options);
}
function writeFile(filePath, data, options, callback) {
    if (isFunction(options))
        return writeFile(filePath, data, DEFAULT_WRITE_OPTIONS, options);
    const promise = writeFileAsync(filePath, data, options);
    if (callback)
        promise.then(callback, callback);
    return promise;
}
async function writeFileAsync(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
    if (isString(options))
        return writeFileAsync(filePath, data, { encoding: options });
    const timeout = options.timeout ?? DEFAULT_TIMEOUT_ASYNC;
    const retryOptions = { timeout, interval: DEFAULT_INTERVAL_ASYNC };
    let schedulerCustomDisposer = null;
    let schedulerDisposer = null;
    let tempDisposer = null;
    let tempPath = null;
    let fd = null;
    try {
        if (options.schedule)
            schedulerCustomDisposer = await options.schedule(filePath);
        schedulerDisposer = await Scheduler.schedule(filePath);
        const filePathReal = await fs.attempt.realpath(filePath);
        const filePathExists = !!filePathReal;
        filePath = filePathReal || filePath;
        [tempPath, tempDisposer] = Temp.get(filePath, options.tmpCreate || Temp.create, !(options.tmpPurge === false));
        const useStatChown = IS_POSIX && isUndefined(options.chown);
        const useStatMode = isUndefined(options.mode);
        if (filePathExists && (useStatChown || useStatMode)) {
            const stats = await fs.attempt.stat(filePath);
            if (stats) {
                options = { ...options };
                if (useStatChown) {
                    options.chown = { uid: stats.uid, gid: stats.gid };
                }
                if (useStatMode) {
                    options.mode = stats.mode;
                }
            }
        }
        if (!filePathExists) {
            const parentPath = path.dirname(filePath);
            await fs.attempt.mkdir(parentPath, {
                mode: DEFAULT_FOLDER_MODE,
                recursive: true
            });
        }
        fd = await fs.retry.open(retryOptions)(tempPath, 'w', options.mode || DEFAULT_FILE_MODE);
        if (options.tmpCreated) {
            options.tmpCreated(tempPath);
        }
        if (isString(data)) {
            await fs.retry.write(retryOptions)(fd, data, 0, options.encoding || DEFAULT_ENCODING);
        }
        else if (data instanceof Readable) {
            const writeStream = createWriteStream(tempPath, { fd, autoClose: false });
            const finishPromise = once(writeStream, 'finish');
            data.pipe(writeStream);
            await finishPromise;
        }
        else if (!isUndefined(data)) {
            await fs.retry.write(retryOptions)(fd, data, 0, data.length, 0);
        }
        if (options.fsync !== false) {
            if (options.fsyncWait !== false) {
                await fs.retry.fsync(retryOptions)(fd);
            }
            else {
                fs.attempt.fsync(fd);
            }
        }
        await fs.retry.close(retryOptions)(fd);
        fd = null;
        if (options.chown && (options.chown.uid !== DEFAULT_USER_UID || options.chown.gid !== DEFAULT_USER_GID)) {
            await fs.attempt.chown(tempPath, options.chown.uid, options.chown.gid);
        }
        if (options.mode && options.mode !== DEFAULT_FILE_MODE) {
            await fs.attempt.chmod(tempPath, options.mode);
        }
        try {
            await fs.retry.rename(retryOptions)(tempPath, filePath);
        }
        catch (error) {
            if (!isException(error))
                throw error;
            if (error.code !== 'ENAMETOOLONG')
                throw error;
            await fs.retry.rename(retryOptions)(tempPath, Temp.truncate(filePath));
        }
        tempDisposer();
        tempPath = null;
    }
    finally {
        if (fd)
            await fs.attempt.close(fd);
        if (tempPath)
            Temp.purge(tempPath);
        if (schedulerCustomDisposer)
            schedulerCustomDisposer();
        if (schedulerDisposer)
            schedulerDisposer();
    }
}
function writeFileSync(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
    if (isString(options))
        return writeFileSync(filePath, data, { encoding: options });
    const timeout = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
    const retryOptions = { timeout };
    let tempDisposer = null;
    let tempPath = null;
    let fd = null;
    try {
        const filePathReal = fs.attempt.realpathSync(filePath);
        const filePathExists = !!filePathReal;
        filePath = filePathReal || filePath;
        [tempPath, tempDisposer] = Temp.get(filePath, options.tmpCreate || Temp.create, !(options.tmpPurge === false));
        const useStatChown = IS_POSIX && isUndefined(options.chown);
        const useStatMode = isUndefined(options.mode);
        if (filePathExists && (useStatChown || useStatMode)) {
            const stats = fs.attempt.statSync(filePath);
            if (stats) {
                options = { ...options };
                if (useStatChown) {
                    options.chown = { uid: stats.uid, gid: stats.gid };
                }
                if (useStatMode) {
                    options.mode = stats.mode;
                }
            }
        }
        if (!filePathExists) {
            const parentPath = path.dirname(filePath);
            fs.attempt.mkdirSync(parentPath, {
                mode: DEFAULT_FOLDER_MODE,
                recursive: true
            });
        }
        fd = fs.retry.openSync(retryOptions)(tempPath, 'w', options.mode || DEFAULT_FILE_MODE);
        if (options.tmpCreated) {
            options.tmpCreated(tempPath);
        }
        if (isString(data)) {
            fs.retry.writeSync(retryOptions)(fd, data, 0, options.encoding || DEFAULT_ENCODING);
        }
        else if (!isUndefined(data)) {
            fs.retry.writeSync(retryOptions)(fd, data, 0, data.length, 0);
        }
        if (options.fsync !== false) {
            if (options.fsyncWait !== false) {
                fs.retry.fsyncSync(retryOptions)(fd);
            }
            else {
                fs.attempt.fsync(fd);
            }
        }
        fs.retry.closeSync(retryOptions)(fd);
        fd = null;
        if (options.chown && (options.chown.uid !== DEFAULT_USER_UID || options.chown.gid !== DEFAULT_USER_GID)) {
            fs.attempt.chownSync(tempPath, options.chown.uid, options.chown.gid);
        }
        if (options.mode && options.mode !== DEFAULT_FILE_MODE) {
            fs.attempt.chmodSync(tempPath, options.mode);
        }
        try {
            fs.retry.renameSync(retryOptions)(tempPath, filePath);
        }
        catch (error) {
            if (!isException(error))
                throw error;
            if (error.code !== 'ENAMETOOLONG')
                throw error;
            fs.retry.renameSync(retryOptions)(tempPath, Temp.truncate(filePath));
        }
        tempDisposer();
        tempPath = null;
    }
    finally {
        if (fd)
            fs.attempt.closeSync(fd);
        if (tempPath)
            Temp.purge(tempPath);
    }
}
/* EXPORT */
export { readFile, readFileSync, writeFile, writeFileSync };
