"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DESCRIPT_ION_ALT = exports.DESCRIPT_ION = void 0;
exports.usingDescriptIon = usingDescriptIon;
exports.getCommentFor = getCommentFor;
exports.setCommentFor = setCommentFor;
exports.areCommentsEnabled = areCommentsEnabled;
const config_1 = require("./config");
const path_1 = require("path");
const cross_1 = require("./cross");
const util_files_1 = require("./util-files");
const misc_1 = require("./misc");
const lodash_1 = __importDefault(require("lodash"));
const iconv_lite_1 = __importDefault(require("iconv-lite"));
const promises_1 = require("node:fs/promises");
const expiringCache_1 = require("./expiringCache");
exports.DESCRIPT_ION = 'descript.ion';
exports.DESCRIPT_ION_ALT = 'DESCRIPT.ION';
const commentsStorage = (0, config_1.defineConfig)(cross_1.CFG.comments_storage, '', v => ['', 'attr+ion'].includes(v)); // compiled tell us if we are using descript.ion
(0, config_1.defineConfig)('descript_ion', true, (v, more) => {
    if (!v && more.version?.olderThan('0.57.0-alpha1'))
        commentsStorage.set('attr');
});
const descriptIonEncoding = (0, config_1.defineConfig)('descript_ion_encoding', 'utf8');
function readFromDescriptIon(path) {
    return usingDescriptIon() && readDescriptIon((0, path_1.dirname)(path)).then(x => x.get((0, path_1.basename)(path)), () => undefined);
}
function usingDescriptIon() {
    return commentsStorage.compiled();
}
const COMMENT_ATTR = 'comment';
async function getCommentFor(path) {
    return !path ? undefined : Promise.all([
        commentsStorage.get() ? (0, misc_1.loadFileAttr)(path, COMMENT_ATTR) : undefined,
        readFromDescriptIon(path)
    ]).then(([fromAttr, fromIon]) => fromAttr || fromIon || undefined);
}
async function setCommentFor(path, comment) {
    if (commentsStorage.get()) {
        await (0, misc_1.storeFileAttr)(path, COMMENT_ATTR, comment || undefined);
        return setCommentDescriptIon(path, '');
    }
    return setCommentDescriptIon(path, comment); // should we also remove from file-attr? not sure, but for the time we won't because #1 storeFileAttr is not really deleting, and we would store a lot of empty attributes, #2 more people will switch from descript.ion to attr (because introduced later) than the opposite
}
const setCommentDescriptIon = (0, misc_1.singleWorkerFromBatchWorker)(async (jobs) => {
    const byFolder = lodash_1.default.groupBy(jobs, job => (0, path_1.dirname)(job[0]));
    return Promise.allSettled(lodash_1.default.map(byFolder, async (jobs, folder) => {
        const comments = await readDescriptIon(folder).catch(() => new Map());
        for (const [path, comment] of jobs) {
            const file = path.slice(folder.length + 1);
            if (!comment)
                comments.delete(file);
            else
                comments.set(file, comment);
        }
        const path = await filePathHelper(folder);
        if (!comments.size)
            return (0, promises_1.unlink)(path);
        // encode comments in descript.ion format
        const ws = await (0, util_files_1.createSafeWriteStream)(path);
        comments.forEach((comment, filename) => {
            const multiline = comment.includes('\n');
            const line = (filename.includes(' ') ? `"${filename}"` : filename)
                + ' ' + (multiline ? comment.replaceAll('\n', '\\n') : comment);
            ws.write(iconv_lite_1.default.encode(line, descriptIonEncoding.get()));
            if (multiline)
                ws.write(MULTILINE_SUFFIX, 'binary');
            ws.write('\n');
        });
        await new Promise(res => ws.end(res));
    }));
});
function areCommentsEnabled() {
    return true; // true since we introduced comments in file-attr
}
async function filePathHelper(folder) {
    const main = (0, path_1.join)(folder, exports.DESCRIPT_ION);
    const alt = (0, path_1.join)(folder, exports.DESCRIPT_ION_ALT);
    return await (0, util_files_1.exists)(alt) && !await (0, util_files_1.exists)(main) ? alt : main;
}
const MULTILINE_SUFFIX = Buffer.from([4, 0xC2]);
const pathCache = (0, expiringCache_1.expiringCache)(2_000);
// this can be called many times when listing a folder, and we want to also not check too often as it can be expensive, especially on a networked drive
async function readDescriptIon(path) {
    return (0, util_files_1.parseFile)(await pathCache.try(path, filePathHelper), raw => {
        // for simplicity, we "remove" the sequence MULTILINE_SUFFIX before iconv.decode messes it up
        for (let i = 0; i < raw.length; i++)
            if (raw[i] === MULTILINE_SUFFIX[0] && raw[i + 1] === MULTILINE_SUFFIX[1] && [undefined, 13, 10].includes(raw[i + 2]))
                raw[i] = raw[i + 1] = 10;
        // decoding could also be done with native TextDecoder.decode, but we need iconv for the encoding anyway
        const decoded = iconv_lite_1.default.decode(raw, descriptIonEncoding.get());
        const ret = new Map(decoded.split('\n').map(line => {
            const quoted = line[0] === '"' ? 1 : 0;
            const i = quoted ? line.indexOf('"', 2) + 1 : line.indexOf(' ');
            const fn = line.slice(quoted, i - quoted);
            const comment = line.slice(i + 1).replaceAll('\\n', '\n');
            return [fn, comment];
        }));
        ret.delete('');
        return ret;
    }, 2000);
}
descriptIonEncoding.sub(() => {
    for (const k of util_files_1.parseFileCache.keys())
        if (k.endsWith(exports.DESCRIPT_ION) || k.endsWith(exports.DESCRIPT_ION_ALT))
            util_files_1.parseFileCache.delete(k);
});
