"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FSItem = void 0;
exports.fsitem = fsitem;
const timeutil_1 = require("@epdoc/timeutil");
const typeutil_1 = require("@epdoc/typeutil");
const checksum_1 = __importDefault(require("checksum"));
const fx = __importStar(require("fs-extra"));
const node_fs_1 = __importStar(require("node:fs"));
const promises_1 = require("node:fs/promises");
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const pdf2json_1 = __importDefault(require("pdf2json"));
const fsbytes_1 = require("./fsbytes");
const fsstats_1 = require("./fsstats");
const types_1 = require("./types");
const REG = {
    pdf: /\.pdf$/i,
    xml: /\.xml$/i,
    json: /\.json$/i,
    txt: /\.(txt|text)$/i,
    leadingDot: new RegExp(/^\./),
    BOM: new RegExp(/^\uFEFF/)
};
/**
 * Create a new FSItem object.
 * @param {(FSItem | FolderPath | FilePath)[])} args - An FSItem, a path, or a spread of paths to be used with path.resolve
 * @returns {FSItem} - A new FSItem object
 */
function fsitem(...args) {
    return new FSItem(...args);
}
/**
 * An object representing a file system entry, which may be either a file or a
 * folder.
 *
 * Has methods to:
 *  - Retrieve properties of an existing file or folder.
 *  - Manipulate file paths.
 *  - Recursive support for reading the contents of folders
 *  - Safe copy and backup methods for an existing file or folder
 *  - Reading and writing files
 *  - Getting the creation dates of files, including using the metadata of some file formats
 *  - Testing files for equality
 */
class FSItem {
    _isFSItem = true;
    // @ts-ignore
    _f;
    // @ts-ignore
    _stats = new fsstats_1.FSStats();
    // Test to see if _folders and _files have been read
    _haveReadFolderContents = false;
    // If this is a folder, contains a filtered list of folders within this folder
    _folders = [];
    // If this is a folder, contains a filtered list of files within this folder
    _files = [];
    // Stores the strings that were used to create the path. This property may be deprecated at any time.
    _args = [];
    /**
     * Create a new FSItem object from an existing FSItem object, a file path or
     * an array of file path parts that can be merged using node:path#resolve.
     * @param {(FSItem | FolderPath | FilePath)[])} args - An FSItem, a path, or a spread of paths to be used with path.resolve
     */
    constructor(...args) {
        if (args.length === 1) {
            const arg = args[0];
            if (FSItem.isInstance(arg)) {
                this._f = arg._f;
                this._args = arg._args.map((item) => {
                    return item;
                });
                this._stats = arg._stats.copy();
                this._haveReadFolderContents = arg._haveReadFolderContents;
                this._folders = arg._folders.map((item) => {
                    return item.copy();
                });
                this._files = arg._files.map((item) => {
                    return item.copy();
                });
            }
            else if ((0, typeutil_1.isArray)(arg)) {
                if (arg.find((item) => {
                    return !(0, typeutil_1.isString)(item);
                })) {
                    throw new Error('Invalid parameter');
                }
                else {
                    this._f = node_path_1.default.resolve(arg);
                    this._args = arg;
                }
            }
            else if ((0, typeutil_1.isString)(arg)) {
                this._f = arg;
                this._args = [arg];
            }
        }
        else if (args.length > 1) {
            args.forEach((arg) => {
                if ((0, typeutil_1.isString)(arg)) {
                    this._args.push(arg);
                }
                else {
                    throw new Error('Invalid parameter');
                }
            });
            this._f = node_path_1.default.resolve(...args);
        }
    }
    /**
     * Return a copy of this object. Does not copy the file.
     * @see FSItem#copyTo
     */
    copy() {
        return new FSItem(this);
    }
    /**
     * Test if val is an FSItem object. Can be used as a typescript type gate.
     * @param val Any object
     * @returns
     */
    static isInstance(val) {
        return (0, typeutil_1.isDict)(val) && val._isFSItem === true;
    }
    /**
     * Append a file or folder name to this.f.
     * @param args A file name or array of file names.
     * @returns This
     */
    add(...args) {
        if (args.length === 1) {
            if ((0, typeutil_1.isArray)(args[0])) {
                this._f = node_path_1.default.resolve(this._f, ...args[0]);
                args[0].forEach((arg) => {
                    this._args.push(arg);
                });
            }
            else {
                this._f = node_path_1.default.resolve(this._f, args[0]);
                this._args.push(args[0]);
            }
        }
        else if (args.length > 1) {
            this._f = node_path_1.default.resolve(this._f, ...args);
            args.forEach((arg) => {
                this._args.push(arg);
            });
        }
        return this;
    }
    /**
     * Set the path to the home dir
     */
    home(...args) {
        this._f = node_os_1.default.userInfo().homedir;
        this._args = [this._f];
        if (args) {
            this.add(...args);
        }
        return this;
    }
    get path() {
        return this._f;
    }
    /**
     * Return the original parts that were used to make this.f. The value may
     * become out of sync with the actual value of this.f if too many operations
     * were performed on the path.
     * Use with caution. This may be deprecated.
     */
    get parts() {
        return this._args;
    }
    /**
     * Returns the file's base file name, minus it's extension. For example, for
     * '/path/to/file.name.html', this method will return 'file.name'. Unlike
     * node:path
     * [basename](https://nodejs.org/api/path.html#pathbasenamepath-suffix)
     * method, this does NOT include the extension.
     * @return {string} The base portion of the filename, which excludes the file's extension.
     */
    get basename() {
        return node_path_1.default.basename(this._f).replace(/\.[^\.]*$/, '');
    }
    /**
     * Returns '/path/to' portion of /path/to/file.name.html'
     */
    get dirname() {
        return node_path_1.default.dirname(this._f);
    }
    /**
     * Returns the file extension, exluding the decimal character. For example,
     * '/path/to/file.name.html' will return 'html'.
     * @return {string} File extension, exluding the decimal character.
     */
    get extname() {
        return node_path_1.default.extname(this._f);
    }
    /**
     * Returns the full filename of the file or folder, including it's extension.
     * For example, '/path/to/file.name.html' would return 'file.name.html'.
     * @return {string} - The full file or folder name, including it's extension, if any.
     */
    get filename() {
        return node_path_1.default.basename(this._f);
    }
    /**
     * For folders, indicates if we have read the folder's contents.
     * @returns {boolean} - true if this is a folder and we have read the folder's contents.
     */
    haveReadFolderContents() {
        return this._haveReadFolderContents;
    }
    /**
     * Get the list of FSItem files that matched a previous call to getFiles() or
     * getChildren().
     * @returns {FSItem[]} Array of FSItem objects representing files.
     */
    get files() {
        return this._files;
    }
    /**
     * Get the list of filenames that matched a previous call to getFolders() or
     * getChildren().
     * @returns {FileName[]} Array of filenames.
     */
    get filenames() {
        return this._files.map((fs) => {
            return fs.filename;
        });
    }
    /**
     * Get the list of FSItem folders that matched a previous call to getFolders() or
     * getChildren().
     * @returns {FSItem[]} Array of FSItem objects representing folders.
     */
    get folders() {
        return this._folders;
    }
    /**
     * Get the list of folder names that matched a previous call to getFolders() or
     * getChildren().
     * @returns {FolderName[]} Array of folder names.
     */
    get folderNames() {
        return this._folders.map((fs) => {
            return fs.filename;
        });
    }
    /**
     * Looks at the extension of the filename to determine if it is one of the
     * listed types.
     * @param type List of types (eg. 'jpg', 'png')
     * @returns
     */
    isType(...type) {
        const lowerCaseExt = this.extname.toLowerCase().replace(/^\./, '');
        for (const entry of type) {
            if ((0, typeutil_1.isRegExp)(entry)) {
                if (entry.test(lowerCaseExt)) {
                    return true;
                }
            }
            else if ((0, typeutil_1.isString)(entry)) {
                if (entry.toLowerCase() === lowerCaseExt) {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * Tests the extension to see if this is a PDF file.
     * @param {boolean} [testContents=false] If true, tests the file contents as well (not implemented).
     * @returns {boolean} True if the extension indicates this is a PDF file.
     */
    isPdf(testContents = false) {
        return REG.pdf.test(this.extname);
    }
    /**
     * Tests the extension to see if this is an XML file.
     * @returns  {boolean} True if the extension indicates this is an XML file.
     */
    isXml() {
        return REG.xml.test(this.extname);
    }
    /**
     * Tests the extension to see if this is a text file.
     * @returns {boolean} True if the extension indicates this is a text file.
     */
    isTxt() {
        return REG.txt.test(this.extname);
    }
    /**
     * Tests the extension to see if this is a JSON file.
     * @returns {boolean} True if the extension indicates this is a JSON file.
     */
    isJson() {
        return REG.json.test(this.extname);
    }
    /**
   * Asynchronously reads a specified number of bytes from the file and returns
   * them as an FSBytes instance. In order to determine what type of file this is,
   * at least 24 bytes must be read.
  
   * @param {number} [length=24] The number of bytes to read from the file.
   * Defaults to 24.
   * @returns {Promise<FSBytes>} A promise that resolves with an FSBytes instance
   * containing the read bytes, or rejects with an error.
   */
    getBytes(length = 24) {
        return this.readBytes(length).then((buffer) => {
            return new fsbytes_1.FSBytes(buffer);
        });
    }
    /**
     * Set or change the extension of this file. `This` must be a file.
     * @param {string} ext The extension. The string may or may not include a leading '.'.
     * @returns {this} The current FSItem instance.
     */
    setExt(ext) {
        if (!REG.leadingDot.test(ext)) {
            ext = '.' + ext;
        }
        if (ext !== this.extname) {
            this._f = node_path_1.default.format({ ...node_path_1.default.parse(this._f), base: '', ext: ext });
            this._stats.clear();
        }
        return this;
    }
    /**
     * Set or change the basename of this file. `This` must be a file.
     * @param {string} val The new basename for the file.
     * @returns {this} The current FSItem instance.
     */
    setBasename(val) {
        if (val !== this.basename) {
            this._f = node_path_1.default.format({ dir: this.dirname, name: val, ext: this.extname });
            this._stats.clear();
        }
        return this;
    }
    /**
     * Return the FSSTATS for this file, retrieving the stats and referencing them
     * with this._stats if they have not been previously read. FSSTATS can become
     * stale and should be reread if a file is manipulated.
     *
     * Example `fsutil('mypath/file.txt').getStats().isFile()`.
     *
     * @param {boolean} force Force retrieval of the states, even if they have
     * already been retrieved.
     * @returns {Promise<FSStats>} A promise with an FSStats object
     */
    getStats(force = false) {
        if (force || !this._stats.isInitialized()) {
            return node_fs_1.default.promises
                .stat(this._f)
                .then((resp) => {
                this._stats = new fsstats_1.FSStats(resp);
                return Promise.resolve(this._stats);
            })
                .catch((err) => {
                this._stats = new fsstats_1.FSStats();
                return Promise.resolve(this._stats);
            });
        }
        else {
            return Promise.resolve(this._stats);
        }
    }
    /**
     * Getter returns the FSStats object associated with this file. A previous
     * call to getStats() is needed in order to read stats from disk.
     * @return {FSStats} - The FSStats for this file, if they have been read.
     */
    get stats() {
        return this._stats;
    }
    /**
     * Is this a folder? Will retrieve the FSStats for the file system entry if
     * they haven't been previously read.
     * @returns a promise with value true if this is a folder.
     */
    async isDirectory() {
        return this.getStats().then((resp) => {
            return this._stats.isDirectory();
        });
    }
    /**
     * Calls the FSItem#isDirectory method.
     * @returns {Prommise<boolean>}
     * @see FSItem#isDirectory
     */
    async isDir() {
        return this.isDirectory();
    }
    /**
     * Calls the FSItem#isDirectory method.
     * @returns {Prommise<boolean>}
     * @see FSItem#isDirectory
     */
    async isFolder() {
        return this.isDirectory();
    }
    /**
     * Is this a file? Will retrieve the FSStats for the file system entry if they
     * haven't been previously read.
     * @returns a promise with value true if this is a file.
     */
    async isFile() {
        return this.getStats().then((resp) => {
            return this._stats.isFile();
        });
    }
    /**
     * Does this file or folder exist? Will retrieve the FSStats for the file
     * system entry if they haven't been previously read.
     * @returns a promise with value true if this exists.
     */
    async exists() {
        return this.getStats().then((resp) => {
            return this._stats.isDirectory() || this._stats.isFile();
        });
    }
    /**
     * Is this a folder? Will retrieve the FSStats for the file system entry if
     * they haven't been previously read.
     * @returns a promise with value true if this is a folder.
     * @deprecated Use isDir() method instead.
     */
    async dirExists() {
        return this.isDir();
    }
    /**
     * Is this a file? Will retrieve the FSStats for the file system entry if they
     * haven't been previously read.
     * @returns a promise with value true if this is a file.
     * @deprecated Use isFile() method instead.
     */
    async fileExists() {
        return this.isFile();
    }
    /**
     * When was this file system entry created? Will retrieve the FSStats for the
     * file system entry if they haven't been previously read.
     * @returns a promise with the Date this file was created.
     * @deprecated Use isFile() method instead.
     */
    async createdAt() {
        return this.getStats().then((resp) => {
            return this._stats.createdAt();
        });
    }
    /**
     * Test for equality with the basename of this file.
     * @param {string} name
     * @returns {boolean} True if equal
     */
    isNamed(name) {
        return name === this.basename;
    }
    /**
     * Ensures there is a folder with this path.
     * @param {fx.EnsureDirOptions | number} [options] Options for ensuring the directory.
     * @returns {Promise<unknown>} A promise that resolves when the directory is ensured.
     */
    async ensureDir(options) {
        return fx.ensureDir(this._f, options);
    }
    /**
     * Synchronous version of `ensureDir`.
     * @param {fx.EnsureDirOptions | number} [options] Options for ensuring the directory.
     * @returns {this} The current FSItem instance.
     */
    ensureDirSync(options) {
        fx.ensureDirSync(this._f, options);
        return this;
    }
    /**
     * Removes this file or folder.
     * @returns {Promise<void>} A promise that resolves when the file or folder is removed.
     */
    async remove() {
        return fx.remove(this._f);
    }
    /**
     * Copy this file or folder to the location `dest`.
     * @param dest
     * @param options An fx.CopyOptions object
     * @returns
     */
    async copyTo(dest, options) {
        const p = FSItem.isInstance(dest) ? dest.path : dest;
        return fx.copy(this._f, p, options);
    }
    /**
     * Syncronous version of `copyTo` method.
     * @param dest
     * @param options An fx.CopyOptionsSync object
     * @returns
     */
    copySync(dest, options) {
        const p = FSItem.isInstance(dest) ? dest.path : dest;
        fx.copySync(this._f, p, options);
        return this;
    }
    /**
     * Move `this` file or folder to the location `dest`.
     * @param {FilePath | FSItem} dest - The new path for the file
     * @param {fx.MoveOptions} options - Options to `overwrite` and `dereference` symlinks.
     * @returns {Promise<void>}
     */
    async moveTo(dest, options) {
        const p = FSItem.isInstance(dest) ? dest.path : dest;
        return fx.move(this._f, p, options);
    }
    /**
     * If this is a folder, retrieves the list of matching files in this folder.
     * Repopulates this._files and this._folders in the process. Returns just the
     * filenames, not the full path.
     * @param regex (optional) Use to constrain results
     * @return Array of files within the folder
     */
    async getFiles(regex) {
        return this.getChildren({ match: regex }).then(() => {
            return Promise.resolve(this._files);
        });
    }
    /**
     * If this is a folder, retrieves the list of matching folders in this folder.
     * Repopulates this._files and this._folders in the process. Returns just the
     * folder names, not the full path.
     * @param regex (optional) Use to constrain results
     * @return Array of folders with the folder
     */
    async getFolders(regex) {
        return this.getChildren({ match: regex }).then(() => {
            return Promise.resolve(this._folders);
        });
    }
    /**
     * If this is a folder, retrieves the list of matching files and folders in
     * this folder and stores the lists as this._files and this._folders.
     * @param opts.match (Optional) File or folder names must match this string or
     * RegExp. If not specified then file and folder names are not filtered.
     * @return {Promise<FSItem[]> - Array of all files and folders within this folder
     */
    async getChildren(options = { levels: 1 }) {
        const opts = {
            match: options.match,
            levels: (0, typeutil_1.isNumber)(options.levels) ? options.levels - 1 : 0,
            callback: options.callback,
            sort: (0, typeutil_1.isDict)(options.sort) ? options.sort : {}
        };
        const all = [];
        this._folders = [];
        this._files = [];
        this._haveReadFolderContents = false;
        return node_fs_1.default.promises
            .readdir(this._f)
            .then((entries) => {
            const jobs = [];
            for (const entry of entries) {
                const fs = fsitem(this._f, entry);
                let bMatch = false;
                if (opts.match) {
                    if ((0, typeutil_1.isString)(opts.match) && entry === opts.match) {
                        bMatch = true;
                    }
                    else if ((0, typeutil_1.isRegExp)(opts.match) && opts.match.test(entry)) {
                        bMatch = true;
                    }
                }
                else {
                    bMatch = true;
                }
                if (bMatch) {
                    const job = fs.getStats().then((stat) => {
                        all.push(fs);
                        if (opts.callback) {
                            const job1 = opts.callback(fs);
                            jobs.push(job1);
                        }
                        if (stat.isDirectory()) {
                            this._folders.push(fs);
                            if (opts.levels > 0) {
                                const job2 = fs.getChildren(opts);
                                jobs.push(job2);
                            }
                        }
                        else if (stat.isFile()) {
                            this._files.push(fs);
                        }
                    });
                    jobs.push(job);
                }
            }
            return Promise.all(jobs);
        })
            .then((resp) => {
            this._haveReadFolderContents = true;
            if ((0, typeutil_1.isDict)(opts.sort)) {
                this.sortChildren(opts.sort);
            }
            return Promise.resolve(all);
        });
    }
    /**
     * Sorts the children (files and folders) of this FSItem.
     * @param {FSSortOpts} [opts={}] - Sorting options.
     * @returns {void}
     */
    sortChildren(opts = {}) {
        this.sortFolders();
        if (opts.type === 'size') {
            this.sortFilesBySize();
        }
        else {
            this.sortFiles();
        }
        if (opts.direction === 'descending') {
            this.folders.reverse();
            this.files.reverse();
        }
    }
    /**
     * Sorts the folders of this FSItem alphabetically. Run getChildren() first.
     * @returns {this} The current FSItem instance.
     */
    sortFolders() {
        this.folders.sort((a, b) => {
            return (0, typeutil_1.compareDictValue)(a, b, 'filename');
        });
        return this;
    }
    /**
     * Sorts the files of this FSItem alphabetically.
     * @returns {this} The current FSItem instance.
     */
    sortFiles() {
        this.files.sort((a, b) => {
            return (0, typeutil_1.compareDictValue)(a, b, 'filename');
        });
        return this;
    }
    /**
     * Sorts the files of this FSItem by size. Run getChildren() first.
     * @returns {this} The current FSItem instance.
     */
    sortFilesBySize() {
        this.files.sort((a, b) => {
            return (0, typeutil_1.compareDictValue)(a, b, 'size');
        });
        return this;
    }
    /**
     * For files, calculate the checksum of this file
     * @returns {Promise<string>} A promise that resolves with the checksum of the file.
     */
    async checksum() {
        return new Promise((resolve, reject) => {
            // @ts-ignore
            checksum_1.default.file(this._f, (err, sum) => {
                if (err) {
                    reject(this.newError(err));
                }
                else {
                    resolve(sum);
                }
            });
        });
    }
    /**
     * For PDF files, gets the Creation Date of this file file by reading it's
     * metadata.
     * @returns {Promise<Date | undefined>} A promise that resolves with the creation date of the PDF file, or undefined if not found.
     */
    async getPdfDate() {
        return new Promise((resolve, reject) => {
            const pdfParser = new pdf2json_1.default();
            pdfParser.on('readable', (resp) => {
                if (resp && resp.Meta && resp.Meta.CreationDate) {
                    // const d = new Date(p[1], p[2], p[3], p[4], p[5], p[6]);
                    // d.tim;
                    const d = timeutil_1.DateUtil.fromPdfDate(resp.Meta.CreationDate);
                    resolve(d ? d.date : undefined);
                }
                resolve(new Date(0));
            });
            pdfParser.on('pdfParser_dataError', (errObj) => {
                reject(this.newError(errObj.parserError));
            });
            pdfParser.loadPDF(this._f);
        });
    }
    /**
     * Use checksums to test if this file is equal to path2
     * @param path2
     * @returns {Promise<boolean>} A promise that resolves with true if the files are equal, false otherwise.
     */
    async filesEqual(path2) {
        return new Promise((resolve, reject) => {
            const job1 = this.isFile();
            const job2 = fsitem(path2).isFile();
            return Promise.all([job1, job2]).then((resps) => {
                if (resps && resps.length === 2 && resps[0] === true && resps[1] === true) {
                    const job3 = this.checksum();
                    const job4 = new FSItem(path2).checksum();
                    return Promise.all([job3, job4]).then((resps) => {
                        if (resps && resps.length === 2 && resps[0] === resps[1]) {
                            resolve(true);
                        }
                        else {
                            resolve(false);
                        }
                    });
                }
                else {
                    resolve(false);
                }
            });
        });
    }
    /**
     * Asynchronously reads a specified number of bytes from a file.
     *
     * @param {number} length The number of bytes to read from the file.
     * @param {Buffer} [buffer] An optional buffer to store the read bytes. If not provided, a new buffer will be allocated with the specified length.
     * @param {number} [offset=0] The offset within the buffer where to start storing the read bytes. Defaults to 0.
     * @param {number} [position=0] The offset within the file from where to start reading (optional). Defaults to 0.
     * @returns {Promise<Buffer>} A promise that resolves with the buffer containing the read bytes, or rejects with an error.
     * @throws {Error} Rejects the promise with any error encountered during the file opening, reading, or closing operations.
     */
    async readBytes(length, buffer, offset = 0, position = 0) {
        return new Promise((resolve, reject) => {
            node_fs_1.default.open(this.path, 'r', (err, fd) => {
                if (err) {
                    reject(err);
                }
                else {
                    const buf = buffer ? buffer : Buffer.alloc(length);
                    node_fs_1.default.read(fd, buf, offset, length, position, (err2, bytesRead, resultBuffer) => {
                        (0, node_fs_1.close)(fd, (err3) => {
                            if (err2) {
                                reject(err2);
                            }
                            else if (err3) {
                                reject(err3);
                            }
                            else {
                                resolve(resultBuffer);
                            }
                        });
                    });
                }
            });
        });
    }
    /**
     * Reads the entire file as a buffer.
     * @returns {Promise<Buffer>} A promise that resolves with the file contents as a buffer.
     */
    async readAsBuffer() {
        return (0, promises_1.readFile)(this._f).catch((err) => {
            throw this.newError(err);
        });
    }
    /**
     * Reads the entire file as a string.
     * @returns {Promise<string>} A promise that resolves with the file contents as a string.
     */
    async readAsString() {
        return new Promise((resolve, reject) => {
            node_fs_1.default.readFile(this._f, 'utf8', (err, data) => {
                if (err) {
                    reject(this.newError(err));
                }
                else {
                    // Remove BOM, if present
                    resolve(data.replace(REG.BOM, '').toString());
                }
            });
        });
    }
    /**
     * Reads the file as JSON and parses it.
     * @returns {Promise<any>} A promise that resolves with the parsed JSON content.
     */
    async readJson() {
        return new Promise((resolve, reject) => {
            node_fs_1.default.readFile(this._f, 'utf8', (err, data) => {
                if (err) {
                    reject(this.newError(err));
                }
                else {
                    try {
                        const json = JSON.parse(data.toString());
                        resolve(json);
                    }
                    catch (error) {
                        reject(this.newError(error));
                    }
                }
            });
        });
    }
    /**
     * Reads the file as JSON, parses it, and performs a deep copy with optional transformations.
     * @param {FsDeepCopyOpts} [opts={}] - Options for the deep copy operation.
     * @returns {Promise<any>} A promise that resolves with the deeply copied and transformed JSON content.
     */
    async deepReadJson(opts = {}) {
        return this.readJson().then((resp) => {
            return this.deepCopy(resp, opts);
        });
    }
    /**
     * Performs a deep copy of the given data with optional transformations.
     * @param {any} a - The data to deep copy.
     * @param {FsDeepCopyOpts} [options] - Options for the deep copy operation.
     * @returns {Promise<any>} A promise that resolves with the deeply copied and transformed data.
     */
    async deepCopy(a, options) {
        let opts = (0, typeutil_1.deepCopySetDefaultOpts)(options);
        const urlTest = new RegExp(`^${opts.pre}(file|http|https):\/\/(.+)${opts.post}$`, 'i');
        if (opts.includeUrl && (0, typeutil_1.isNonEmptyString)(a) && urlTest.test(a)) {
            const p = a.match(urlTest);
            if ((0, typeutil_1.isNonEmptyArray)(p) && (0, types_1.isFilePath)(p[2])) {
                const fs = new FSItem(this.dirname, p[2]);
                return fs.deepReadJson(opts).then((resp) => {
                    return Promise.resolve(resp);
                });
            }
            else {
                return Promise.resolve(a);
            }
        }
        else if ((0, typeutil_1.isObject)(a)) {
            // @ts-ignore
            const re = opts && opts.detectRegExp ? asRegExp(a) : undefined;
            if (re) {
                return Promise.resolve(re);
            }
            else {
                const jobs = [];
                const result2 = {};
                Object.keys(a).forEach((key) => {
                    let job = this.deepCopy(a[key], opts).then((resp) => {
                        result2[key] = resp;
                    });
                    jobs.push(job);
                });
                return Promise.all(jobs).then((resp) => {
                    return Promise.resolve(result2);
                });
            }
        }
        else {
            return Promise.resolve((0, typeutil_1.deepCopy)(a, opts));
        }
    }
    /**
     * Writes JSON data to the file.
     * @param {any} data - The data to write as JSON.
     * @returns {Promise<void>} A promise that resolves when the write operation is complete.
     */
    async writeJson(data) {
        const buf = Buffer.from(JSON.stringify(data, null, 2), 'utf8');
        return node_fs_1.default.promises.writeFile(this._f, buf);
    }
    /**
     * Writes base64-encoded data to the file.
     * @param {string} data - The base64-encoded data to write.
     * @returns {Promise<void>} A promise that resolves when the write operation is complete.
     */
    async writeBase64(data) {
        return this.write(data, 'base64');
    }
    /**
     * Writes data to the file with the specified encoding.
     * @param {string | string[]} data - The data to write.
     * @param {BufferEncoding} [type='utf8'] - The encoding to use.
     * @returns {Promise<void>} A promise that resolves when the write operation is complete.
     */
    async write(data, type = 'utf8') {
        if ((0, typeutil_1.isArray)(data)) {
            data = data.join('\n');
        }
        const buf = Buffer.from(data, type);
        return node_fs_1.default.promises.writeFile(this._f, buf);
    }
    /**
     * 'Backup' a file by moving it to a new filename. Use when copying a file to
     * the same location or creating a new file at the same location.
     * @param {BackupOpts} opts
     * @returns {Promise<FilePath | boolean>} - Path to file if file was backed
     * up, or true if the file didn't exist
     */
    async backup(opts = { type: 'renameWithTilde', errorIfExists: false }) {
        await this.getStats();
        if (this._stats && this._stats.exists()) {
            // this file already exists. Deal with it by renaming it.
            let newPath = undefined;
            if (opts.type === types_1.fileConflictStrategyType.renameWithTilde) {
                newPath = this.path + '~';
            }
            else if (opts.type === types_1.fileConflictStrategyType.renameWithNumber) {
                const limit = (0, typeutil_1.isInteger)(opts.limit) ? opts.limit : 32;
                newPath = await this.findAvailableIndexFilename(limit, opts.separator);
                if (!newPath && opts.errorIfExists) {
                    throw this.newError('EEXIST', 'File exists');
                }
            }
            else if (opts.type === 'overwrite') {
                newPath = this.path;
            }
            else {
                if (opts.errorIfExists) {
                    throw this.newError('EEXIST', 'File exists');
                }
            }
            if (newPath) {
                return this.moveTo(newPath, { overwrite: true })
                    .then((resp) => {
                    return Promise.resolve(newPath);
                })
                    .catch((err) => {
                    throw this.newError('ENOENT', 'File could not be renamed');
                });
            }
        }
        else {
            // The caller should have previously tested if the file exists, so we
            // should not hit this
            throw this.newError('ENOENT', 'File does not exist');
        }
        return Promise.resolve(true);
    }
    /**
     * Finds the next available indexed filename. For example, for `filename.ext`,
     * tries `filename-01.ext`, `filename-02.ext`, etc until it finds a filename
     * that is not used.
     * @param {Integer} [limit=32] - The maximum number of attempts to find an available filename.
     * @param {string} [sep='-'] - The separator to use between the filename and the index.
     * @returns {Promise<FilePath | undefined>} A promise that resolves with an available file path, or undefined if not found.
     */
    async findAvailableIndexFilename(limit = 32, sep = '-') {
        let newFsDest;
        let count = 0;
        let looking = true;
        while (looking) {
            newFsDest = fsitem(this.dirname, this.basename + sep + (0, typeutil_1.pad)(++count, 2) + this.extname);
            looking = await newFsDest.exists();
        }
        if (!looking && FSItem.isInstance(newFsDest)) {
            return newFsDest.path;
        }
    }
    /**
     * Copy an existing file or directory to a new location. Optionally creates a
     * backup if there is an existing file or directory at `destFile`.
     * @param {FilePath | FSItem} destFile - The destination file or directory.
     * @param {SafeCopyOpts} [opts={}] - Options for the copy or move operation.
     * @returns {Promise<boolean | undefined>} A promise that resolves with true if the file was copied or moved, false otherwise.
     */
    async safeCopy(destFile, opts = {}) {
        await this.getStats();
        if (this._stats && this._stats.exists()) {
            const fsDest = FSItem.isInstance(destFile) ? destFile : fsitem(destFile);
            await fsDest.getStats();
            let bGoAhead = true;
            if (fsDest._stats.exists()) {
                bGoAhead = false;
                // The dest already exists. Deal with it
                bGoAhead = await fsDest.backup(opts.conflictStrategy);
            }
            if (bGoAhead) {
                if (opts.ensureParentDirs) {
                    await fsDest.ensureDir();
                }
                if (opts.move) {
                    return this.moveTo(fsDest.path, { overwrite: true }).then((resp) => {
                        // console.log(`  Moved ${srcFile} to ${destPath}`);
                        return Promise.resolve(true);
                    });
                }
                else {
                    return this.copyTo(fsDest.path, { overwrite: true }).then((resp) => {
                        // console.log(`  Copied ${srcFile} to ${destPath}`);
                        return Promise.resolve(true);
                    });
                }
            }
            else {
                return Promise.resolve(false);
            }
        }
        else {
            // This shouldn't happen. The caller should know the file exists before
            // calling this method.
            throw this.newError('ENOENT', 'File does not exist');
        }
        return Promise.resolve(false);
    }
    /**
     * Creates a new Error with the specified code and message, including the file path.
     * @param {any} code - The error code or Error object.
     * @param {string} [message] - The error message.
     * @returns {Error} A new Error object.
     */
    newError(code, message) {
        if ((0, typeutil_1.isError)(code)) {
            code.message = `${code.message}: ${this._f}`;
            return code;
        }
        let err = new Error(`${message}: ${this._f}`);
        // @ts-ignore
        err.code = code;
        return err;
    }
}
exports.FSItem = FSItem;
//# sourceMappingURL=fsitem.js.map