import { promises as fsp, default as fs } from 'node:fs';
import { isArrayBuffer, isUint8Array } from 'util/types';
import { t } from "./i18n/instance.js";
import { noop, to_json } from "./prototype.js";
import { pack, parse } from "./io.common.js";
import { path } from "./path.js";
import { check, Lock, url_width, throttle, decode, strcmp } from "./utils.js";
import { noprint } from "./process.js";
export { fsp };
export const encodings = ['utf-8', 'gb18030', 'shift-jis', 'utf-16le'];
export const print_files = { info: true, files: true };
export const print_info = { info: true, files: false };
export const print_info_options = { print: print_info };
export const binary_encoding = { encoding: 'binary' };
export const ramdisk = fexists('T:/TEMP/', { print: false });
/** fp 所指向的 文件/ 文件夹 是否存在
    - print?: `true` */
export function fexists(fp, { print = true } = {}) {
    const exists = fs.existsSync(fp);
    if (print)
        console.log(exists ? t('已存在') : t('不存在'), fp);
    return exists;
}
/** 打开或创建文件，返回 FileHandle, 如果文件夹不存在，会自动创建文件夹
    Some characters (`< > : " / \ | ? *`) are reserved under Windows as documented
    by [Naming Files, Paths, and Namespaces](https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file). Under NTFS, if the filename contains
    a colon, Node.js will open a file system stream, as described by [this MSDN page](https://docs.microsoft.com/en-us/windows/desktop/FileIO/using-streams).
    
    - flags: `'r'`  https://nodejs.org/docs/latest/api/fs.html#file-system-flags
    - options?:
        - mode?: `'0o666'` 设置新建文件时的 mode (permission and sticky bits)
        - print?: `false` */
export async function fopen(fp, flags = 'r', { mode, print } = {}) {
    if (print)
        console.log(t('打开文件'), fp);
    await fmkdir(fp.fdir, noprint);
    return Object.assign(await fsp.open(fp, flags, mode), { fp, flags, mode });
}
export async function fread(fp, { encoding = 'utf-8', print = true } = {}) {
    const is_handle = typeof fp === 'object' && fp && 'fd' in fp;
    if (is_handle) {
        if (print && fp.fp)
            console.log('读取', fp);
    }
    else {
        check(!fp.isdir, 'fp 必须是文件，不能以 / 结尾');
        check(path.isAbsolute(fp), `fp 必须是绝对路径: ${fp}`);
        if (print)
            console.log('读取', fp);
    }
    switch (encoding) {
        case 'utf-8':
            return fsp.readFile(fp, { encoding: 'utf-8' });
        case 'binary':
            return fsp.readFile(fp);
        default:
            return new TextDecoder(encoding)
                .decode(await fsp.readFile(fp));
    }
}
/** 读取文件，划分为行，并去掉最后一个 \n 之后的 '' */
export async function fread_lines(fp, options = {}) {
    return (await fread(fp, options))
        .split_lines();
}
export async function fread_json(fp, options = {}) {
    return JSON.parse(await fread(fp, options));
}
export async function fparse(fp, options) {
    return parse(await fread(fp, { ...options, encoding: 'binary' }));
}
/** 比较两个文件是否一致，遇到任何文件系统错误都返回 false */
export async function fequals(fp_left, fp_right, { print = true } = {}) {
    if (print)
        console.log(`比较 ${fp_left} 和 ${fp_right}`);
    try {
        const fps = [fp_left, fp_right];
        const [{ size: size_left }, { size: size_right }] = await Promise.all(fps.map(async (fp) => fstat(fp)));
        if (size_left === size_right) {
            const [data_left, data_right] = await Promise.all(fps.map(async (fp) => fread(fp, { encoding: 'binary', print: false })));
            return data_left.equals(data_right);
        }
        else
            return false;
    }
    catch {
        return false;
    }
}
export async function fwrite(fp, data, { print = true, mtime, flush, mode } = {}) {
    const is_handle = typeof fp === 'object' && fp && 'fd' in fp;
    if (is_handle) {
        if (print)
            console.log('写入', fp.fp);
    }
    else {
        check(path.isAbsolute(fp), `fp 必须是绝对路径，当前为: ${fp}`);
        if (print)
            console.log('写入', fp);
    }
    if (!isUint8Array(data) && typeof data !== 'string')
        data = to_json(data);
    // 需要设置修改时间，先打开文件再写入
    if (mtime !== undefined && !is_handle) {
        let handle = await fopen(fp, 'w', { mode, print: false });
        try {
            await handle.write(data, 0);
            const mseconds = mtime / 1000;
            await handle.utimes(mseconds, mseconds);
        }
        finally {
            await handle.close();
        }
        return fp;
    }
    const write_options = { flush, mode };
    try {
        await fsp.writeFile(fp, data, write_options);
    }
    catch (error) {
        if (error.code === 'ENOENT' && !is_handle) {
            await fmkdir(fp.fdir, noprint);
            await fsp.writeFile(fp, data, write_options);
        }
        else
            throw error;
    }
    if (mtime !== undefined) {
        const mseconds = mtime / 1000;
        await fp.utimes(mseconds, mseconds);
    }
    return fp;
}
export async function fpack(fp, data, options) {
    return fwrite(fp, pack(data), options);
}
/** 追加内容到文件末尾，如果文件不存在会自动创建 */
export async function fappend(fp, data, { print = true } = {}) {
    check(path.isAbsolute(fp), `${t('fp 必须是绝对路径，当前为:')} ${fp}`);
    if (print)
        console.log(t('追加'), fp);
    check(isUint8Array(data) || typeof data === 'string');
    try {
        await fsp.appendFile(fp, data);
    }
    catch (error) {
        if (error.code === 'ENOENT') {
            await fmkdir(fp.fdir, noprint);
            await fsp.appendFile(fp, data);
        }
        else
            throw error;
    }
}
const fake_time = new Date(0);
const fake_stats = {
    fp: '',
    size: 0n,
    atimeNs: 0n,
    mtimeNs: 0n,
    ctimeNs: 0n,
    birthtimeNs: 0n,
    atimeMs: 0n,
    mtimeMs: 0n,
    ctimeMs: 0n,
    birthtimeMs: 0n,
    mtime: fake_time,
    atime: fake_time,
    ctime: fake_time,
    birthtime: fake_time,
};
export async function flist(fpd, options = {}) {
    const { filter, deep = false, absolute = false, print = true, stats = false, best_effort = false } = options;
    check(path.isAbsolute(fpd), t("flist: 参数 fpd: '{{fpd}}' 必须是绝对路径", { fpd }));
    check(fpd.isdir, t("flist: 参数 fpd: '{{fpd}}' 必须以 / 结尾", { fpd }));
    if (!path.isAbsolute(fpd))
        throw new Error(t('参数 fpd: ') + fpd + t(' 必须是绝对路径'));
    if (!fpd.isdir)
        throw new Error(t('参数 fpd: ') + fpd + t(' 必须以 / 结尾'));
    // readdir withFileTypes 参数在底层有什么区别，速度上有什么差异
    // 都调用了 uv_fs_scandir, 且调用参数相同，仅仅是 Node.js 侧的回调不同 AfterScanDir / AfterScanDirWithTypes
    // 回调中通过 uv_fs_scandir_next 获取到每个条目的信息，而 uv_fs_scandir_next 中都会读取 type
    // 速度上：都在 0.2 ms 左右就可以完成
    const files = await fsp.readdir(fpd, { withFileTypes: true, encoding: 'utf-8' });
    const filter_regexp = filter instanceof RegExp;
    const filter_fn = Boolean(filter && !filter_regexp);
    let fps = [];
    for (const file of files) {
        const fp = (absolute ? fpd : '') +
            file.name +
            (file.isDirectory() ? '/' : '');
        if (filter_regexp && !filter.test(fp))
            continue;
        if (filter_fn && !filter(fp))
            continue;
        if (print)
            console.log(fp);
        fps.push(fp);
    }
    async function _fstat(fp) {
        let _stats;
        try {
            _stats = await fstat(absolute ? fp : fpd + fp);
        }
        catch (error) {
            if (best_effort)
                _stats = { ...fake_stats };
            else
                throw error;
        }
        if (!absolute)
            _stats.fp = fp;
        return _stats;
    }
    if (deep)
        return (await Promise.all(
        // 顶层文件/文件夹
        fps.map(async (fp) => {
            /** 顶层文件夹的大小 */
            let fpd_stats;
            return fp.isdir ?
                [
                    stats ? (fpd_stats = await _fstat(fp)) : fp,
                    ...(await flist(absolute ? fp : fpd + fp, options)).map((fp_or_stats) => {
                        if (stats) {
                            if (!absolute)
                                fp_or_stats.fp = fp + fp_or_stats.fp;
                            // 所有顶层文件夹中的文件（不包括文件夹）大小总和加起来作为文件夹大小
                            if (!fp_or_stats.fp.isdir)
                                fpd_stats.size += fp_or_stats.size;
                            return fp_or_stats;
                        }
                        else
                            return absolute ? fp_or_stats : fp + fp_or_stats;
                    })
                ]
                :
                    stats ? _fstat(fp) : fp;
        }))).flat();
    else if (stats)
        return Promise.all(fps.map(_fstat));
    else
        return fps;
}
export async function fstat(fp) {
    let stat;
    if (typeof fp === 'object' && fp && 'fd' in fp) {
        stat = await fp.stat({ bigint: true });
        if (fp.fp)
            stat.fp = fp.fp;
    }
    else {
        check(path.isAbsolute(fp), `fstat: 参数 fp: '${fp}' 必须是绝对路径`);
        stat = await fsp.stat(fp, { bigint: true });
        stat.fp = fp;
    }
    return stat;
}
export async function flstat(fp) {
    check(path.isAbsolute(fp), t("flstat: 参数 fp: '{{fp}}' 必须是绝对路径", { fp }));
    let stat = await fsp.lstat(fp, { bigint: true });
    stat.fp = fp;
    return stat;
}
export async function ffstat(handle) {
    return new Promise((resolve, reject) => {
        fs.fstat(handle.fd, { bigint: true }, (error, stats) => {
            if (error)
                reject(error);
            else
                resolve(stats);
        });
    });
}
/** 删除文件或文件夹，返回是否实际进行了删除操作
    - fp: 文件或文件夹的完整路径
    - options?:
        - print?: `true` */
export async function fdelete(fp, { print = true } = {}) {
    check(fp.length >= 6, `fp: ${fp} ${t('不能太短，防止误删文件')}`);
    check(path.isAbsolute(fp), t('fp 必须是绝对路径'));
    const { isdir } = fp;
    try {
        await fsp.rm(fp, { recursive: true });
        if (print)
            console.log(`${isdir ? t('删除了文件夹') : t('删除了文件')} ${fp}`);
        return true;
    }
    catch (error) {
        if (error.code === 'ENOENT') {
            if (print)
                console.log(`${isdir ? t(`已不存在文件`) : t('已不存在文件夹')} ${fp}`);
            return false;
        }
        throw error;
    }
}
/** 清空文件夹中的内容，实际为删除该文件夹后新建 */
export async function fdclear(fpd, { print = true, permission = false } = {}) {
    const need_permission = permission && process.platform !== 'win32';
    let stat;
    if (need_permission)
        stat = await fstat(fpd);
    await fdelete(fpd, noprint);
    await fmkdir(fpd, noprint);
    if (need_permission) {
        await fsp.chown(fpd, Number(stat.uid), Number(stat.gid));
        await fsp.chmod(fpd, Number(stat.mode));
    }
    if (print)
        console.log(`清空了文件夹 ${fpd}`);
    return fpd;
}
/** 复制文件或文件夹
    会在因不存在父文件夹导致复制失败时，自动创建父文件夹，并再次尝试复制
    最好预先创建父文件夹，减少文件系统操作，提升性能
    - fp_src: 源 文件/文件夹 完整路径
    - fp_dst: 目标 文件/文件夹 完整路径
    - options?:
        - print?: `true`
        - overwrite?: `true`
        - filter?: 当 fp_src 为文件夹时选择性复制里面的部分内容，
            和 flist 的 filter 选项不同，只支持函数，且函数返回值决定是否要继续进行过滤
            - true: 复制文件夹的所有文件，交给 fsp.cp
            - false 或不返回任何值: 不复制
            - 'partial': 进一步进行过滤复制（先 flist with filter 再 fcopy），只有文件夹能返回这个值
        
    @example
    fcopy('D:/temp/camera/', 'D:/camera/') */
export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, filter, } = {}) {
    const { isdir } = fp_src;
    check(isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
    check(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
    if (!isdir && filter)
        throw new Error(t('filter 选项只适用于 fp_src 为文件夹'));
    if (print)
        log_action('复制', fp_src, fp_dst);
    if (isdir)
        if (filter) {
            await fmkdir(fp_dst, { print });
            await Promise.all((await flist(fp_src, { filter, print: false }))
                .map(async (fname) => fcopy(`${fp_src}${fname}`, `${fp_dst}${fname}`, {
                print,
                overwrite,
                ...fname.isdir && filter(fname) === 'filter' ? {
                    filter: fp => filter(`${fname}${fp}`),
                } : {}
            })));
        }
        else
            await fsp.cp(fp_src, fp_dst, {
                recursive: true,
                force: overwrite,
                errorOnExist: !overwrite,
                mode: overwrite ? 0 : fs.constants.COPYFILE_EXCL,
            });
    else
        try {
            await fsp.copyFile(fp_src, fp_dst, overwrite ? 0 : fs.constants.COPYFILE_EXCL);
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                await fmkdir(fp_dst.fdir, { print });
                await fsp.copyFile(fp_src, fp_dst, overwrite ? 0 : fs.constants.COPYFILE_EXCL);
            }
            else
                throw error;
        }
    return fp_dst;
}
/** 移动文件或文件夹
    相同分区 / 文件系统下使用 rename, 否则 fallback 到复制后删除源文件
    - src: 源 文件/文件夹 完整路径
    - dst: 目标 文件/文件夹 完整路径
    - options?:
        - print?: `true`
        - overwrite?: `false`
    
    @example
    fmove('D:/temp/camera/', 'D:/camera/') */
export async function fmove(fp_src, fp_dst, { overwrite = false, print = true } = {}) {
    check(fp_src.isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
    check(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
    if (print)
        log_action('移动', fp_src, fp_dst);
    if (!overwrite && fexists(fp_dst, noprint))
        throw new Error(`${t('已存在')} ${fp_dst}`);
    await fmkdir(fp_dst.fdir, noprint);
    async function copy_and_delete() {
        await fcopy(fp_src, fp_dst, { overwrite, print: false });
        await fdelete(fp_src, noprint);
    }
    if (fp_src[0] !== fp_dst[0] || overwrite)
        await copy_and_delete();
    else
        try {
            await fsp.rename(fp_src, fp_dst);
        }
        catch (error) {
            if (error.code === 'EXDEV')
                await copy_and_delete();
            else
                throw error;
        }
    return fp_dst;
}
/** 重命名文件  rename file
    - fp:  当前文件名／路径  current filename/path
    - fp_: 新的文件名／路径  new filename/path
    - options?:
        - fpd?: fp 和 fp_ 在同一文件夹内  fp and fp_ is in same directory
        - print?: `true`
        - overwrite?: `true`  默认覆盖（不检查效率更高）  better performance without check */
export async function frename(fp, fp_, { fpd, print = true, overwrite = true } = {}) {
    if (fpd) {
        check(fpd.isdir);
        fp = fpd + fp;
        fp_ = fpd + fp_;
    }
    check(path.isAbsolute(fp) && path.isAbsolute(fp_), t('fp 和 fp_ 必须是绝对路径'));
    if (print)
        log_action('重命名', fp, fp_);
    if (!overwrite && fexists(fp_, noprint))
        throw new Error(`${t('已存在')} ${fp}`);
    await fsp.rename(fp, fp_);
}
/**
    递归创建文件夹，确保 fpd 指向的文件夹存在  Create folders recursively, make sure the folder pointed to by fpd exists
    返回首个创建的文件夹或 undefined  Returns the first created folder or undefined
    
    - fpd: 文件夹完整路径  Folder full path
    - options?:
        - print?: `true` */
export async function fmkdir(fpd, { print = true } = {}) {
    check(path.isAbsolute(fpd), `fpd 必须是绝对路径: ${fpd}`);
    check(fpd.isdir, `fpd 必须以 / 结尾: ${fpd}`);
    // 类似 D:/ 这样的根路径，调用 fsp.mkdir 会报错无权限
    if (fpd.length === 3 &&
        'A' <= fpd[0] && fpd[0] <= 'Z' && fpd[1] === ':' && fpd[2] === '/')
        if (fexists(fpd, noprint)) {
            if (print)
                console.log(t('已存在文件夹'), fpd);
            return fpd;
        }
        else
            throw new Error(t('不存在且无法创建文件夹 {{fpd}}', { fpd }));
    // Calling fs.promises.mkdir() when path is a directory that exists results in a rejection only when recursive is false.
    const fpd_ = (await fsp.mkdir(fpd, { recursive: true }))?.fpd;
    if (fpd_) {
        if (print)
            console.log(t('已创建文件夹'), fpd);
    }
    else if (print)
        console.log(t('已存在文件夹'), fpd);
    return fpd_;
}
/** 创建软链接
    - fp_real: 现在真实文件/文件夹的路径
    - fp_link: 目标链接文件/文件夹的路径
    - options?:
        - skip_existing?: `false` 忽略已存在的同名的文件，文件夹
        - print?: `true`
        - junction? */
export async function flink(fp_real, fp_link, { skip_existing = false, print = true, junction = false } = {}) {
    check(path.isAbsolute(fp_real) && path.isAbsolute(fp_link), 'fp 必须是绝对路径');
    const is_fpd_real = fp_real.isdir;
    const is_fpd_link = fp_link.isdir;
    check(is_fpd_real === is_fpd_link, 'fp_real 和 fp_link 必须同为文件路径或文件夹路径');
    if (fexists(fp_link, noprint))
        if (!skip_existing)
            throw new Error(`存在同名${is_fpd_link ? '文件夹' : '文件'}: ${fp_link}，无法创建链接`);
        else {
            if (print)
                console.log('跳过已存在:', fp_link);
            return;
        }
    if (print)
        log_action('已将源文件', fp_real, fp_link, '链接到');
    if (junction)
        await fsp.symlink(fp_real, fp_link, 'junction');
    else
        await fsp.symlink(fp_real, fp_link, is_fpd_real ? 'dir' : 'file');
}
/** 解压 zip 包至文件夹，返回 fpd_out
     - zip:
        - 包路径 (string) 或
        - 包数据 (Uint8Array | ArrayBuffer)
     - fpd_out: 解压到文件夹的完整路径 (string)
    - options?:
        - encoding?: `utf-8` 压缩包文件名编码
        - dryrun?: 只是打印一下文件名列表，看看编码是否正确，不实际解压
        - print?:
            - info?: `true` 开始解压、解压完成
            - files?: `true` 打印解压缩的文件列表
        - strip?: `true` 压缩包内只有一个顶层文件夹时自动去除该层级
        - clean?: `true` 忽略解压 ftrashes 这些垃圾文件
        - filter?: 用于过滤要解压的文件, 返回 falsy 值时忽略这个文件，要注意 file.name 为完整的相对路径（没有 strip） */
export async function funzip(zip, fpd_out, options) {
    await _unzip(zip, fpd_out, options);
    return fpd_out;
}
/** 解压 zip 包至内存，返回解压后的数据 (Record<文件或文件夹 (以 / 结尾) 的相对路径, Uint8Array>)
     - zip:
        - 压缩文件路径 (string) 或
        - 压缩文件内容 (buffer)
    - options?:
        - encoding?: `utf-8` 压缩包文件名编码
        - dryrun?: 只是打印一下文件名列表，看看编码是否正确，不实际解压
        - print?:
            - info?: `true` 开始解压、解压完成
            - files?: `true` 打印解压缩的文件列表
        - strip?: `true` 压缩包内只有一个顶层文件夹时自动去除该层级
        - clean?: `true` 忽略解压 ftrashes 这些垃圾文件
        - filter?: 用于过滤要解压的文件, 返回 falsy 值时忽略这个文件，要注意 file.name 为完整的相对路径（没有 strip） */
export async function unzip(zip, options) {
    return _unzip(zip, undefined, options);
}
async function _unzip(zip, fpd_out, { encoding = 'utf-8', dryrun = false, print = {
    files: true,
    info: true
}, strip = true, clean = true, filter: _filter } = {}) {
    const str_out = typeof fpd_out === 'string' ? fpd_out : '内存';
    let fp_zip = 'buffer';
    let nfiles = 0;
    let size_uncompressed = 0;
    if (isArrayBuffer(zip))
        zip = new Uint8Array(zip);
    else if (typeof zip === 'string') {
        check(['zip', 'apk', 'crx', 'vsix'].includes(zip.fext), 'fp_src 应该以 zip 等后缀结尾');
        fp_zip = zip;
        zip = await fread(zip, { encoding: 'binary', print: false });
    }
    if (print.info)
        log_action(`开始${dryrun ? '测试' : ''}解压`, `${fp_zip} (${zip.length.to_fsize_str()})`, str_out);
    if (!dryrun && fpd_out)
        await fmkdir(fpd_out, { print: print.info });
    let dryrun_unzipped = {};
    const { unzipSync } = await import("./fflate.js");
    let unzipped = unzipSync(zip, {
        decoder: encoding === 'utf-8'
            ? decode
            : (() => {
                let text_decoder = new TextDecoder(encoding);
                return buf => text_decoder.decode(buf);
            })(),
        filter(file) {
            const { name, originalSize } = file;
            // 需要递归的清理 LICENSE, .husky/, .husky/pre-commit 等文件
            if (clean) {
                const lname = name.toLowerCase();
                let to_clean = false;
                // xxx/xxx/xxx/a.txt
                // xxx/xxx/
                // a.txt
                // xxx/
                // 处理每个文件夹部分
                let ileft = 0, iright = 0;
                for (; (iright = lname.indexOf('/', ileft) + 1) !== 0; ileft = iright)
                    if (ftrashes.has(lname.slice(ileft, iright))) {
                        to_clean = true;
                        break;
                    }
                // 处理最后剩下的文件名
                if (!to_clean && ileft < lname.length && ftrashes.has(lname.slice(ileft)))
                    to_clean = true;
                if (to_clean) {
                    if (print.files)
                        console.log(`清理了 ${name}`);
                    return false;
                }
            }
            if (!clean && name.startsWith('__MACOSX/') ||
                _filter && !_filter(file))
                return false;
            else {
                nfiles++;
                size_uncompressed += originalSize;
                if (dryrun) {
                    dryrun_unzipped[name] = null;
                    return false;
                }
                else
                    return true;
            }
        }
    });
    if (dryrun)
        unzipped = dryrun_unzipped;
    if (strip) {
        let top_common_dir = '';
        for (const key in unzipped) {
            const islash = key.indexOf('/');
            if (islash === -1) { // 顶层有文件
                top_common_dir = '';
                break;
            }
            else {
                const top_dir = key.slice(0, islash + 1);
                if (top_common_dir) {
                    if (top_common_dir !== top_dir) {
                        top_common_dir = '';
                        break;
                    }
                }
                else
                    top_common_dir = top_dir;
            }
        }
        if (top_common_dir) {
            let unzipped_ = {};
            for (const key in unzipped) {
                const key_ = key.slice(top_common_dir.length);
                if (key_)
                    unzipped_[key_] = unzipped[key];
            }
            unzipped = unzipped_;
        }
    }
    let pwrites = [];
    for (const key in unzipped) {
        const value = unzipped[key];
        if (fpd_out && !dryrun) {
            const fp = fpd_out + key;
            if (fp.isdir)
                await fmkdir(fp, noprint);
            else {
                let pwrite = fwrite(fp, value, noprint);
                pwrite.catch(noop); // suppress PromiseRejectionHandledWarning: Promise rejection was handled asynchronously
                pwrites.push(pwrite);
            }
        }
        if (print.files)
            console.log(`解压出 ${key}`);
    }
    if (fpd_out && !dryrun)
        await Promise.all(pwrites);
    if (print.info)
        console.log(`解压了 ${nfiles} 个文件，解压后文件总大小为 ${size_uncompressed.to_fsize_str()}`);
    if (!fpd_out || dryrun)
        return unzipped;
}
const ftrashes = new Set([
    '__MACOSX/',
    '.babelrc.js',
    '.babelrc',
    '.ci',
    '.circleci',
    '.dockerignore',
    '.DS_Store',
    '.editorconfig',
    '.eslintignore',
    '.eslintrc.js',
    '.eslintrc.json',
    '.eslintrc.yaml',
    '.eslintrc.yml',
    '.eslintrc',
    '.gitattributes',
    '.github',
    '.gitignore',
    '.husky/',
    '.jsbeautifyrc',
    '.jshintrc',
    '.mailmap',
    '.markdownlint.js',
    '.node-version',
    '.npmignore',
    '.prettierignore',
    '.prettierrc.js',
    '.prettierrc.json',
    '.prettierrc.yaml',
    '.prettierrc',
    '.project',
    '.releaserc.json',
    '.stylelintrc.json',
    '.travis.yml',
    '.vscode/',
    'AUTHORS.txt',
    'AUTHORS',
    'bower',
    'CODE_OF_CONDUCT.md',
    'codecov.yml',
    'commitlint.config.js',
    'CONTRIBUTING.md',
    'COPING.md',
    'husky.config.js',
    'LICENCE.md',
    'LICENCE.txt',
    'LICENCE',
    'LICENSE.md',
    'LICENSE.txt',
    'LICENSE',
    'lint-staged.config.js',
    'open-bot.yaml',
    'package-lock.json',
    'pnpm-lock.yaml',
    'prettier.config.js',
    'stylelint.config.js',
    'tslint.json',
    'yarn.lock',
].map(f => f.toLowerCase()));
export let fwatchers = {};
/** 跟踪文本文件追加的内容，类似 tail -f */
export async function ftail(fp, handler, { print = true } = {}) {
    fwatchers[fp]?.close();
    const { size } = await fstat(fp);
    let pointer = Number(size);
    let lock = new Lock(await fopen(fp));
    let fbuf = Buffer.allocUnsafe(2 ** 20);
    let strbuf = '';
    let decoder = new TextDecoder();
    if (print)
        console.log('开始跟踪追加内容', fp);
    const onchange_throttled = throttle(250, async () => {
        if (lock.locked)
            return;
        await lock.request(async (handle) => {
            const { bytesRead } = await handle.read(fbuf, 0, fbuf.length, pointer);
            pointer += bytesRead;
            const chunk = decoder.decode(fbuf.subarray(0, bytesRead), { stream: true });
            let lines = [];
            let j = 0;
            for (let i = 0; (i = chunk.indexOf('\n', j)) >= 0;) {
                let line = chunk.slice(j, i);
                if (strbuf) {
                    line = strbuf + line;
                    strbuf = '';
                }
                j = i + 1;
                lines.push(line);
            }
            strbuf = chunk.slice(j);
            await handler(lines);
        });
    });
    let watcher = fs.watch(fp, event => {
        if (event === 'change')
            onchange_throttled();
        else {
            console.error(`被监听的文件 ${fp.quote()} 出现了 rename 事件，结束 ftail`);
            watcher.close();
        }
    });
    watcher.on('error', error => {
        console.error(error);
    });
    return fwatchers[fp] = watcher;
}
/** 打开一个文件并搜索替换某个 pattern */
export async function freplace(fp, pattern, replacement, { print = true } = {}) {
    await fwrite(fp, (await fread(fp, { print }))
        .replaceAll(pattern, replacement), { print });
}
/** 同步文件或文件夹 fp_src 到 fp_dst ，返回 fp_dst
    - fp_src: 源文件或文件夹
    - fp_dst: 目标文件或文件夹
    - options?:
        - print?: `print_info`
        - delete?: `false` */
export async function fsync(fp_src, fp_dst, { print = print_files, delete: _delete = false } = {}) {
    if (print.info)
        log_action('同步', fp_src, fp_dst);
    if (fp_src.isdir) {
        let dst_stats;
        try {
            dst_stats = (await flist(fp_dst, { stats: true, deep: true, print: false })).sort(fp_sorter);
        }
        catch (error) {
            if (error.code === 'ENOENT')
                return fcopy(fp_src, fp_dst, { print: print.info });
            else
                throw error;
        }
        const src_stats = (await flist(fp_src, { deep: true, print: false, stats: true }))
            .sort(fp_sorter);
        let p = 0, q = 0;
        let tasks = [];
        let fpd_changeds = [];
        let fpd_deleteds = [];
        const sync_change = ({ fp }, modify) => {
            if (fpd_changeds.some(fpd_created => fp.startsWith(fpd_created)))
                return;
            if (fp.isdir)
                fpd_changeds.push(fp);
            if (print.files)
                log_action(`同步${modify ? '修改' : '新增'}`, `${fp_src}${fp}`, `${fp_dst}${fp}`);
            tasks.push(['fcopy', `${fp_src}${fp}`, `${fp_dst}${fp}`]);
        };
        const sync_delete = ({ fp }) => {
            if (fpd_deleteds.some(fpd_deleted => fp.startsWith(fpd_deleted)))
                return;
            if (fp.isdir)
                fpd_deleteds.push(fp);
            if (print.files)
                console.log('同步删除', `${fp_dst}${fp}`);
            tasks.push(['fdelete', `${fp_dst}${fp}`]);
        };
        for (; p < src_stats.length && q < dst_stats.length;) {
            const src = src_stats[p];
            const dst = dst_stats[q];
            if (src.fp === dst.fp) {
                if (!src.fp.isdir && !compare_stat(src, dst))
                    sync_change(src, true);
                ++p;
                ++q;
            }
            else if (src.fp < dst.fp) { // dst 缺少了文件 / 文件夹
                sync_change(src, false);
                ++p;
            }
            else { // dst 多了文件 / 文件夹
                if (_delete)
                    sync_delete(dst);
                ++q;
            }
        }
        for (; p < src_stats.length; ++p)
            sync_change(src_stats[p], false);
        if (_delete)
            for (; q < dst_stats.length; ++q)
                sync_delete(dst_stats[q]);
        const { default: pmap } = await import('p-map');
        await pmap(tasks, async ([task, arg0, arg1]) => {
            if (task === 'fcopy')
                await fcopy(arg0, arg1, noprint);
            else
                await fdelete(arg0, noprint);
        }, { concurrency: 2 });
    }
    else {
        let same = true;
        const [src_stat, dst_stat] = await Promise.all([
            fstat(fp_src),
            (async () => {
                try {
                    return await fstat(fp_dst);
                }
                catch (error) {
                    if (error.code === 'ENOENT')
                        same = false;
                    else
                        throw error;
                }
            })()
        ]);
        if (same)
            same = compare_stat(src_stat, dst_stat);
        if (same)
            log_action('跳过相同', fp_src, fp_dst);
        else
            await fcopy(fp_src, fp_dst, { print: print.info });
    }
    return fp_dst;
}
/** 根据对象的 fp 属性进行字典序排序 */
export function fp_sorter(l, r) {
    return strcmp(l.fp, r.fp);
}
/** 返回修改时间与大小是否相同 */
function compare_stat(src, dst) {
    // exfat 时间精度为 2s
    return src.size === dst.size && Math.abs(Number(src.mtimeMs) - Number(dst.mtimeMs)) <= 2000;
}
export function log_action(action, fp_src, fp_dst, sep = '->') {
    console.log(`${`${action}  ${fp_src}`.pad(url_width)}  ${sep}  ${fp_dst}`);
}
//# sourceMappingURL=file.js.map