"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Catalog = void 0;
exports.cleanObsolete = cleanObsolete;
exports.order = order;
exports.writeCompiled = writeCompiled;
exports.orderByMessage = orderByMessage;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const glob_1 = require("glob");
const normalize_path_1 = __importDefault(require("normalize-path"));
const getTranslationsForCatalog_1 = require("./catalog/getTranslationsForCatalog");
const mergeCatalog_1 = require("./catalog/mergeCatalog");
const extractFromFiles_1 = require("./catalog/extractFromFiles");
const utils_1 = require("./utils");
const LOCALE = "{locale}";
const LOCALE_SUFFIX_RE = /\{locale\}.*$/;
class Catalog {
    constructor({ name, path, include, templatePath, format, exclude = [] }, config) {
        this.config = config;
        this.name = name;
        this.path = (0, utils_1.normalizeRelativePath)(path);
        this.include = include.map(utils_1.normalizeRelativePath);
        this.exclude = [this.localeDir, ...exclude.map(utils_1.normalizeRelativePath)];
        this.format = format;
        this.templateFile =
            templatePath ||
                getTemplatePath(this.format.getTemplateExtension(), this.path);
    }
    getFilename(locale) {
        return ((0, utils_1.replacePlaceholders)(this.path, { locale }) +
            this.format.getCatalogExtension());
    }
    async make(options) {
        const [nextCatalog, prevCatalogs] = await Promise.all([
            this.collect({ files: options.files, workerPool: options.workerPool }),
            this.readAll(),
        ]);
        if (!nextCatalog)
            return false;
        const catalogs = this.merge(prevCatalogs, nextCatalog, {
            overwrite: options.overwrite,
            files: options.files,
        });
        // Map over all locales and post-process each catalog
        const sortedCatalogs = Object.fromEntries(Object.entries(catalogs).map(([locale, catalog]) => {
            if (options.clean) {
                catalog = cleanObsolete(catalog);
            }
            catalog = order(options.orderBy, catalog);
            return [locale, catalog];
        }));
        const locales = options.locale ? options.locale : this.locales;
        await Promise.all(locales.map((locale) => this.write(locale, sortedCatalogs[locale])));
        return sortedCatalogs;
    }
    async makeTemplate(options) {
        const catalog = await this.collect({
            files: options.files,
            workerPool: options.workerPool,
        });
        if (!catalog)
            return false;
        const sorted = order(options.orderBy, catalog);
        await this.writeTemplate(sorted);
        return sorted;
    }
    /**
     * Collect messages from source paths. Return a raw message catalog as JSON.
     */
    async collect(options = {}) {
        let paths = this.sourcePaths;
        if (options.files) {
            options.files = options.files.map((p) => (0, utils_1.makePathRegexSafe)((0, normalize_path_1.default)(p, false)));
            const regex = new RegExp(options.files.join("|"), "i");
            paths = paths.filter((path) => regex.test((0, normalize_path_1.default)(path)));
        }
        if (options.workerPool) {
            return await (0, extractFromFiles_1.extractFromFilesWithWorkerPool)(options.workerPool, paths, this.config);
        }
        return await (0, extractFromFiles_1.extractFromFiles)(paths, this.config);
    }
    /*
     *
     * prevCatalogs - map of message catalogs in all available languages with translations
     * nextCatalog - language-agnostic catalog with collected messages
     *
     * Note: if a catalog in prevCatalogs is null it means the language is available, but
     * no previous catalog was generated (usually first run).
     *
     * Orthogonal use-cases
     * --------------------
     *
     * Message IDs:
     * - auto-generated IDs: message is used as a key, `defaults` is not set
     * - custom IDs: message is used as `defaults`, custom ID as a key
     *
     * Source locale (defined by `sourceLocale` in config):
     * - catalog for `sourceLocale`: initially, `translation` is prefilled with `defaults`
     *   (for custom IDs) or `key` (for auto-generated IDs)
     * - all other languages: translation is kept empty
     */
    merge(prevCatalogs, nextCatalog, options) {
        return Object.fromEntries(Object.entries(prevCatalogs).map(([locale, prevCatalog]) => [
            locale,
            (0, mergeCatalog_1.mergeCatalog)(prevCatalog, nextCatalog, this.config.sourceLocale === locale, options),
        ]));
    }
    async getTranslations(locale, options) {
        return await (0, getTranslationsForCatalog_1.getTranslationsForCatalog)(this, locale, options);
    }
    async write(locale, messages) {
        const filename = this.getFilename(locale);
        const created = !fs_1.default.existsSync(filename);
        await this.format.write(filename, messages, locale);
        return [created, filename];
    }
    async writeTemplate(messages) {
        const filename = this.templateFile;
        await this.format.write(filename, messages, undefined);
    }
    async read(locale) {
        return await this.format.read(this.getFilename(locale), locale);
    }
    async readAll(locales = this.locales) {
        const res = {};
        await Promise.all(locales.map(async (locale) => (res[locale] = await this.read(locale))));
        // statement above will save locales in object in undetermined order
        // resort here to have keys order the same as in locales definition
        return this.locales.reduce((acc, locale) => {
            acc[locale] = res[locale];
            return acc;
        }, {});
    }
    async readTemplate() {
        const filename = this.templateFile;
        return await this.format.read(filename, undefined);
    }
    get sourcePaths() {
        const includeGlobs = this.include
            .map((includePath) => {
            const isDir = (0, utils_1.isDirectory)(includePath);
            /**
             * glob library results from absolute patterns such as /foo/* are mounted onto the root setting using path.join.
             * On windows, this will by default result in /foo/* matching C:\foo\bar.txt.
             */
            return isDir
                ? (0, normalize_path_1.default)(path_1.default.resolve(process.cwd(), includePath === "/" ? "" : includePath, "**/*.*"))
                : includePath;
        })
            .map(utils_1.makePathRegexSafe);
        return (0, glob_1.globSync)(includeGlobs, { ignore: this.exclude, mark: true });
    }
    get localeDir() {
        const localePatternIndex = this.path.indexOf(LOCALE);
        if (localePatternIndex === -1) {
            throw Error(`Invalid catalog path: ${LOCALE} variable is missing`);
        }
        return this.path.substring(0, localePatternIndex);
    }
    get locales() {
        return this.config.locales;
    }
}
exports.Catalog = Catalog;
function getTemplatePath(ext, path) {
    return path.replace(LOCALE_SUFFIX_RE, "messages" + ext);
}
function cleanObsolete(messages) {
    return Object.fromEntries(Object.entries(messages).filter(([, message]) => !message.obsolete));
}
function order(by, catalog) {
    return {
        messageId: orderByMessageId,
        message: orderByMessage,
        origin: orderByOrigin,
    }[by](catalog);
}
/**
 * Object keys are in the same order as they were created
 * https://stackoverflow.com/a/31102605/1535540
 */
function orderByMessageId(messages) {
    return Object.keys(messages)
        .sort()
        .reduce((acc, key) => {
        ;
        acc[key] = messages[key];
        return acc;
    }, {});
}
function orderByOrigin(messages) {
    function getFirstOrigin(messageKey) {
        const sortedOrigins = messages[messageKey].origin.sort((a, b) => {
            if (a[0] < b[0])
                return -1;
            if (a[0] > b[0])
                return 1;
            return 0;
        });
        return sortedOrigins[0];
    }
    return Object.keys(messages)
        .sort((a, b) => {
        const [aFile, aLineNumber] = getFirstOrigin(a);
        const [bFile, bLineNumber] = getFirstOrigin(b);
        if (aFile < bFile)
            return -1;
        if (aFile > bFile)
            return 1;
        if (aLineNumber < bLineNumber)
            return -1;
        if (aLineNumber > bLineNumber)
            return 1;
        return 0;
    })
        .reduce((acc, key) => {
        ;
        acc[key] = messages[key];
        return acc;
    }, {});
}
async function writeCompiled(path, locale, compiledCatalog, namespace) {
    let ext;
    switch (namespace) {
        case "es":
            ext = "mjs";
            break;
        case "ts":
        case "json":
            ext = namespace;
            break;
        default:
            ext = "js";
    }
    const filename = `${(0, utils_1.replacePlaceholders)(path, { locale })}.${ext}`;
    await (0, utils_1.writeFile)(filename, compiledCatalog);
    return filename;
}
function orderByMessage(messages) {
    // hardcoded en-US locale to have consistent sorting
    // @see https://github.com/lingui/js-lingui/pull/1808
    const collator = new Intl.Collator("en-US");
    return Object.keys(messages)
        .sort((a, b) => {
        const aMsg = messages[a].message || "";
        const bMsg = messages[b].message || "";
        const aCtxt = messages[a].context || "";
        const bCtxt = messages[b].context || "";
        return collator.compare(aMsg, bMsg) || collator.compare(aCtxt, bCtxt);
    })
        .reduce((acc, key) => {
        ;
        acc[key] = messages[key];
        return acc;
    }, {});
}
