"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SiteGenerationManager = void 0;
const cheerio_1 = __importDefault(require("cheerio"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const walk_sync_1 = __importDefault(require("walk-sync"));
const SiteConfig_1 = require("./SiteConfig");
const VariableProcessor_1 = require("../variables/VariableProcessor");
const ExternalManager_1 = require("../External/ExternalManager");
const SiteLinkManager_1 = require("../html/SiteLinkManager");
const PluginManager_1 = require("../plugins/PluginManager");
const async_1 = require("../utils/async");
const delay_1 = require("../utils/delay");
const fsUtil = __importStar(require("../utils/fsUtil"));
const logger = __importStar(require("../utils/logger"));
const constants_1 = require("./constants");
const Layout_1 = require("../Layout");
const progress_1 = require("../lib/progress");
// Change when migrated to TypeScript
require('../patches/htmlparser2');
const MARKBIND_VERSION = require('../../package.json').version;
const MAX_CONCURRENT_PAGE_GENERATION_PROMISES = 4;
const LAZY_LOADING_BUILD_TIME_RECOMMENDATION_LIMIT = 30000;
const LAZY_LOADING_REBUILD_TIME_RECOMMENDATION_LIMIT = 5000;
const MARKBIND_WEBSITE_URL = 'https://markbind.org/';
const MARKBIND_LINK_HTML = `<a href='${MARKBIND_WEBSITE_URL}'>MarkBind ${MARKBIND_VERSION}</a>`;
/**
 * Orchestrates the site generation process.
 * Manages the build lifecycle, variable processing, plugin management,
 * and rebuilding strategies (lazy/background).
 */
class SiteGenerationManager {
    constructor(rootPath, outputPath, onePagePath, forceReload = false, siteConfigPath = constants_1.SITE_CONFIG_NAME, isDevMode, backgroundBuildMode, postBackgroundBuildFunc) {
        this.rebuildPagesBeingViewed = (0, delay_1.delay)(this._rebuildPagesBeingViewed.bind(this), 1000);
        /**
         * Rebuild pages that are affected by changes in filePaths
         * @param filePaths a single path or an array of paths corresponding to the files that have changed
         */
        this.rebuildAffectedSourceFiles = (0, delay_1.delay)(this._rebuildAffectedSourceFiles.bind(this), 1000);
        /**
         * Rebuild all pages
         */
        this.rebuildSourceFiles = (0, delay_1.delay)(this._rebuildSourceFiles.bind(this), 1000);
        /**
         * Builds pages that are yet to build/rebuild in the background
         */
        this.backgroundBuildNotViewedFiles = (0, delay_1.delay)(this._backgroundBuildNotViewedFiles.bind(this), 1000);
        this.rootPath = rootPath;
        this.outputPath = outputPath;
        this.tempPath = path_1.default.join(rootPath, constants_1.TEMP_FOLDER_NAME);
        this.forceReload = forceReload;
        this.siteConfigPath = siteConfigPath;
        // Background build properties
        this.backgroundBuildMode = onePagePath && backgroundBuildMode;
        this.stopGenerationTimeThreshold = new Date();
        this.postBackgroundBuildFunc = postBackgroundBuildFunc || (() => { });
        // Lazy reload properties
        this.onePagePath = onePagePath;
        this.currentPageViewed = onePagePath
            ? path_1.default.resolve(this.rootPath, fsUtil.removeExtension(onePagePath))
            : '';
        this.currentOpenedPages = [];
        this.toRebuild = new Set();
    }
    configure(siteAssets, sitePages) {
        this.siteAssets = siteAssets;
        this.sitePages = sitePages;
    }
    static rejectHandler(error, removeFolders) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.warn(error);
            try {
                yield Promise.all(removeFolders.map(folder => fs_extra_1.default.remove(folder)));
            }
            catch (err) {
                logger.error(`Failed to remove generated files after error!\n${err.message}`);
            }
        });
    }
    beforeSiteGenerate() {
        this.variableProcessor.invalidateCache();
        this.externalManager.reset();
        this.pluginManager.beforeSiteGenerate();
    }
    /**
     * Changes the site variable of the current page being viewed, building it if necessary.
     * @param normalizedUrl BaseUrl-less and extension-less url of the page
     * @return Boolean of whether the page needed to be rebuilt
     */
    changeCurrentPage(normalizedUrl) {
        this.currentPageViewed = path_1.default.join(this.rootPath, normalizedUrl);
        if (this.toRebuild.has(this.currentPageViewed)) {
            this.beforeSiteGenerate();
            /*
             Lazy loading only builds the page being viewed, but the user may be quick enough
             to trigger multiple page builds before the first one has finished building,
             hence we need to take this into account by using the delayed variant of the method.
             */
            this.rebuildPagesBeingViewed(this.currentPageViewed);
            return true;
        }
        return false;
    }
    /**
     * Changes the list of current opened pages
     * @param normalizedUrls Collection of normalized url of pages taken from the clients
     * ordered from most-to-least recently opened
     */
    changeCurrentOpenedPages(normalizedUrls) {
        if (!this.onePagePath) {
            return;
        }
        const openedPages = normalizedUrls.map(normalizedUrl => path_1.default.join(this.rootPath, normalizedUrl));
        this.currentOpenedPages = constants_1._.uniq(openedPages);
        if (this.currentOpenedPages.length > 0) {
            logger.info('Current opened pages, from most-to-least recent:');
            this.currentOpenedPages.forEach((pagePath, idx) => {
                logger.info(`${idx + 1}. ${fsUtil.ensurePosix(path_1.default.relative(this.rootPath, pagePath))}`);
            });
        }
        else {
            logger.info('No pages are currently opened');
        }
    }
    /**
     * Read and stores the site config from site.json, in Site, SitePages and SiteAssets.
     * Overwrite the default base URL if it's specified by the user.
     * @param baseUrl user defined base URL (if exists)
     */
    readSiteConfig(baseUrl) {
        return __awaiter(this, void 0, void 0, function* () {
            this.siteConfig = yield SiteConfig_1.SiteConfig.readSiteConfig(this.rootPath, this.siteConfigPath, baseUrl);
            this.sitePages.siteConfig = this.siteConfig;
            this.siteAssets.siteConfig = this.siteConfig;
            return this.siteConfig;
        });
    }
    /**
     * Collects the base url map in the site/subsites
     */
    collectBaseUrl() {
        const candidates = (0, walk_sync_1.default)(this.rootPath, { directories: false })
            .filter(x => x.endsWith(this.siteConfigPath))
            .map(x => path_1.default.resolve(this.rootPath, x));
        const baseUrlMap = new Set(candidates.map(candidate => path_1.default.dirname(candidate)));
        this.variableProcessor = new VariableProcessor_1.VariableProcessor(this.rootPath, baseUrlMap);
        this.sitePages.setBaseUrlMap(baseUrlMap);
        this.buildManagers(baseUrlMap);
    }
    /**
     * Set up the managers used with the configurations.
     */
    buildManagers(baseUrlMap) {
        const config = {
            baseUrlMap,
            baseUrl: this.siteConfig.baseUrl,
            rootPath: this.rootPath,
            outputPath: this.outputPath,
            ignore: this.siteConfig.ignore,
            addressablePagesSource: this.sitePages.addressablePagesSource,
            variableProcessor: this.variableProcessor,
            intrasiteLinkValidation: this.siteConfig.intrasiteLinkValidation,
            codeLineNumbers: this.siteConfig.style.codeLineNumbers,
            plantumlCheck: this.siteConfig.plantumlCheck,
            headerIdMap: {},
            siteLinkManager: this.siteLinkManager,
            pluginManager: this.pluginManager,
            externalManager: this.externalManager,
        };
        this.siteLinkManager = new SiteLinkManager_1.SiteLinkManager(config);
        config.siteLinkManager = this.siteLinkManager;
        this.pluginManager = new PluginManager_1.PluginManager(config, this.siteConfig.plugins, this.siteConfig.pluginsContext);
        config.pluginManager = this.pluginManager;
        this.externalManager = new ExternalManager_1.ExternalManager(config);
        config.externalManager = this.externalManager;
        this.layoutManager = new Layout_1.LayoutManager(config);
        // Propagate managers to SitePages
        this.sitePages.variableProcessor = this.variableProcessor;
        this.sitePages.pluginManager = this.pluginManager;
        this.sitePages.siteLinkManager = this.siteLinkManager;
        this.sitePages.externalManager = this.externalManager;
        this.sitePages.layoutManager = this.layoutManager;
    }
    /**
     * Collects the user defined variables map in the site/subsites
     */
    collectUserDefinedVariablesMap() {
        this.variableProcessor.resetUserDefinedVariablesMap();
        this.sitePages.baseUrlMap.forEach((base) => {
            const userDefinedVariablesPath = path_1.default.resolve(base, constants_1.USER_VARIABLES_PATH);
            let content;
            try {
                content = fs_extra_1.default.readFileSync(userDefinedVariablesPath, 'utf8');
            }
            catch (e) {
                content = '';
                logger.warn(e.message);
            }
            /*
             We retrieve the baseUrl of the (sub)site by appending the relative to the configured base url
             i.e. We ignore the configured baseUrl of the sub sites.
             */
            const siteRelativePathFromRoot = fsUtil.ensurePosix(path_1.default.relative(this.rootPath, base));
            const siteBaseUrl = siteRelativePathFromRoot === ''
                ? this.siteConfig.baseUrl
                : path_1.default.posix.join(this.siteConfig.baseUrl || '/', siteRelativePathFromRoot);
            this.variableProcessor.addUserDefinedVariable(base, 'baseUrl', siteBaseUrl);
            this.variableProcessor.addUserDefinedVariable(base, 'MarkBind', MARKBIND_LINK_HTML);
            const $ = cheerio_1.default.load(content, { decodeEntities: false });
            $('variable,span').each((_index, element) => {
                const name = $(element).attr('name') || $(element).attr('id');
                this.variableProcessor.renderAndAddUserDefinedVariable(base, name, $(element).html());
            });
        });
    }
    /**
     * Collects the user defined variables map in the site/subsites
     * if there is a change in the variables file
     * @param filePaths array of paths corresponding to files that have changed
     */
    collectUserDefinedVariablesMapIfNeeded(filePaths) {
        const variablesPath = path_1.default.resolve(this.rootPath, constants_1.USER_VARIABLES_PATH);
        if (filePaths.includes(variablesPath)) {
            this.collectUserDefinedVariablesMap();
            return true;
        }
        return false;
    }
    /**
     * Generate the website.
     * @param baseUrl user defined base URL (if exists)
     */
    generate(baseUrl) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            // Create the .tmp folder for storing intermediate results.
            fs_extra_1.default.emptydirSync(this.tempPath);
            // Clean the output folder; create it if not exist.
            fs_extra_1.default.emptydirSync(this.outputPath);
            const lazyWebsiteGenerationString = this.onePagePath ? '(lazy) ' : '';
            logger.info(`Website generation ${lazyWebsiteGenerationString}started at ${startTime.toLocaleTimeString()}`);
            try {
                yield this.readSiteConfig(baseUrl);
                this.sitePages.collectAddressablePages();
                this.collectBaseUrl();
                this.collectUserDefinedVariablesMap();
                yield this.siteAssets.buildAssets();
                yield (this.onePagePath ? this.lazyBuildSourceFiles() : this.buildSourceFiles());
                yield this.siteAssets.copyCoreWebAsset();
                yield this.siteAssets.copyBootstrapIconsAsset();
                yield this.siteAssets.copyBootstrapTheme(false);
                yield this.siteAssets.copyFontAwesomeAsset();
                yield this.siteAssets.copyOcticonsAsset();
                yield this.siteAssets.copyMaterialIconsAsset();
                yield this.writeSiteData();
                this.calculateBuildTimeForGenerate(startTime, lazyWebsiteGenerationString);
                if (this.backgroundBuildMode) {
                    this.backgroundBuildNotViewedFiles();
                }
            }
            catch (error) {
                yield SiteGenerationManager.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Helper function for generate().
     */
    calculateBuildTimeForGenerate(startTime, lazyWebsiteGenerationString) {
        const endTime = new Date();
        const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000;
        logger.info(`Website generation ${lazyWebsiteGenerationString}complete! Total build time: ${totalBuildTime}s`);
        if (!this.onePagePath && totalBuildTime > LAZY_LOADING_BUILD_TIME_RECOMMENDATION_LIMIT) {
            logger.info('Your site took quite a while to build...'
                + ' Have you considered using markbind serve -o when writing content to speed things up?');
        }
    }
    /**
     * Build all pages of the site
     */
    buildSourceFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            this.beforeSiteGenerate();
            logger.info('Generating pages...');
            try {
                yield this.generatePages();
                yield fs_extra_1.default.remove(this.tempPath);
                logger.info('Pages built');
            }
            catch (error) {
                yield SiteGenerationManager.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Adds all pages except the viewed pages to toRebuild, flagging them for lazy building later.
     */
    lazyBuildAllPagesNotViewed(viewedPages) {
        return __awaiter(this, void 0, void 0, function* () {
            const viewedPagesArray = Array.isArray(viewedPages) ? viewedPages : [viewedPages];
            this.sitePages.pages.forEach((page) => {
                const normalizedUrl = fsUtil.removeExtension(page.pageConfig.sourcePath);
                if (!viewedPagesArray.some(viewedPage => normalizedUrl === viewedPage)) {
                    this.toRebuild.add(normalizedUrl);
                }
            });
        });
    }
    /**
     * Only build landing page of the site, building more as the author goes to different links.
     */
    lazyBuildSourceFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            this.beforeSiteGenerate();
            logger.info('Generating landing page...');
            try {
                yield this.generateLandingPage();
                yield this.copyLazySourceFiles();
                yield fs_extra_1.default.remove(this.tempPath);
                yield this.lazyBuildAllPagesNotViewed(this.currentPageViewed);
                logger.info('Landing page built, other pages will be built as you navigate to them!');
            }
            catch (error) {
                yield SiteGenerationManager.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Helper function for lazyBuildSourceFiles().
     */
    copyLazySourceFiles() {
        const lazyLoadingSpinnerHtmlFilePath = path_1.default.join(__dirname, constants_1.LAZY_LOADING_SITE_FILE_NAME);
        const outputSpinnerHtmlFilePath = path_1.default.join(this.outputPath, constants_1.LAZY_LOADING_SITE_FILE_NAME);
        return fs_extra_1.default.copy(lazyLoadingSpinnerHtmlFilePath, outputSpinnerHtmlFilePath);
    }
    _rebuildAffectedSourceFiles(filePaths) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.backgroundBuildMode) {
                this.stopOngoingBuilds();
            }
            const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths];
            const uniquePaths = constants_1._.uniq(filePathArray);
            this.beforeSiteGenerate();
            try {
                yield this.layoutManager.updateLayouts(filePathArray);
                yield this.regenerateAffectedPages(uniquePaths);
                yield fs_extra_1.default.remove(this.tempPath);
                if (this.backgroundBuildMode) {
                    this.backgroundBuildNotViewedFiles();
                }
            }
            catch (error) {
                yield SiteGenerationManager.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    _rebuildPagesBeingViewed(normalizedUrls) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            const normalizedUrlArray = Array.isArray(normalizedUrls) ? normalizedUrls : [normalizedUrls];
            const uniqueUrls = constants_1._.uniq(normalizedUrlArray);
            uniqueUrls.forEach(normalizedUrl => logger.info(`Building ${normalizedUrl} as some of its dependencies were changed since the last visit`));
            const pagesToRebuild = this.sitePages.pages.filter(page => uniqueUrls.some(pageUrl => fsUtil.removeExtension(page.pageConfig.sourcePath) === pageUrl));
            const pageGenerationTask = {
                mode: 'async',
                pages: pagesToRebuild,
            };
            try {
                this._setTimestampVariable();
                yield this.runPageGenerationTasks([pageGenerationTask]);
                yield this.writeSiteData();
                SiteGenerationManager.calculateBuildTimeForRebuildPagesBeingViewed(startTime);
            }
            catch (err) {
                yield SiteGenerationManager.rejectHandler(err, [this.tempPath, this.outputPath]);
            }
            yield fs_extra_1.default.remove(this.tempPath);
        });
    }
    /**
     * Helper function for _rebuildPagesBeingViewed().
     */
    static calculateBuildTimeForRebuildPagesBeingViewed(startTime) {
        const endTime = new Date();
        const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000;
        return logger.info(`Lazy website regeneration complete! Total build time: ${totalBuildTime}s`);
    }
    _backgroundBuildNotViewedFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.toRebuild.size === 0) {
                return;
            }
            logger.info('Building files that are not viewed in the background...');
            const isCompleted = yield this.generatePagesMarkedToRebuild();
            if (isCompleted) {
                logger.info('Background building completed!');
                this.postBackgroundBuildFunc();
            }
        });
    }
    /**
     * Generates pages that are marked to be built/rebuilt.
     * @returns A Promise that resolves once all pages are generated.
     */
    generatePagesMarkedToRebuild() {
        return __awaiter(this, void 0, void 0, function* () {
            const pagesToRebuild = this.sitePages.pages.filter((page) => {
                const normalizedUrl = fsUtil.removeExtension(page.pageConfig.sourcePath);
                return this.toRebuild.has(normalizedUrl);
            });
            const pageRebuildTask = {
                mode: 'async',
                pages: pagesToRebuild,
            };
            return this.runPageGenerationTasks([pageRebuildTask]);
        });
    }
    _rebuildSourceFiles() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.backgroundBuildMode) {
                this.stopOngoingBuilds();
            }
            logger.info('Pages or site config modified, updating pages...');
            this.beforeSiteGenerate();
            this.layoutManager.removeLayouts();
            const removedPageFilePaths = this.sitePages.updateAddressablePages();
            try {
                yield this.siteAssets.removeAsset(removedPageFilePaths);
                yield this.rebuildRequiredPages();
                if (this.backgroundBuildMode) {
                    this.backgroundBuildNotViewedFiles();
                }
            }
            catch (error) {
                yield SiteGenerationManager.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Helper function for _rebuildSourceFiles().
     */
    rebuildRequiredPages() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.onePagePath) {
                this.sitePages
                    .mapAddressablePagesToPages(this.sitePages.addressablePages || [], this.sitePages.getFavIconUrl());
                yield this._rebuildPagesBeingViewed(this.currentOpenedPages);
                yield this.lazyBuildAllPagesNotViewed(this.currentOpenedPages);
                return;
            }
            logger.warn('Rebuilding all pages...');
            yield this.buildSourceFiles();
        });
    }
    /**
     * Writes the site data to siteData.json
     * @param verbose Flag to emit logs of the operation
     */
    writeSiteData() {
        return __awaiter(this, arguments, void 0, function* (verbose = true) {
            const siteDataPath = path_1.default.join(this.outputPath, constants_1.SITE_DATA_NAME);
            const siteData = {
                enableSearch: this.siteConfig.enableSearch,
                pages: this.sitePages.pages.filter(page => page.pageConfig.searchable && page.headings)
                    .map(page => ({
                    src: page.pageConfig.src,
                    title: page.title,
                    headings: page.headings,
                    headingKeywords: page.keywords,
                    frontmatterKeywords: page.frontmatter.keywords,
                })),
            };
            try {
                yield fs_extra_1.default.outputJson(siteDataPath, siteData, { spaces: 2 });
                if (verbose) {
                    logger.info('Site data built');
                }
            }
            catch (error) {
                yield SiteGenerationManager.rejectHandler(error, [this.tempPath, this.outputPath]);
            }
        });
    }
    stopOngoingBuilds() {
        this.stopGenerationTimeThreshold = new Date();
    }
    /**
     * Runs the supplied page generation tasks according to the specified mode of each task.
     * A page generation task can be a sequential generation or an asynchronous generation.
     * @param pageGenerationTasks Array of page generation tasks
     * @returns A Promise that resolves to a boolean which indicates whether the generation
     * ran to completion
     */
    runPageGenerationTasks(pageGenerationTasks) {
        return __awaiter(this, void 0, void 0, function* () {
            const pagesCount = pageGenerationTasks.reduce((acc, task) => acc + task.pages.length, 0);
            const progressBar = new progress_1.ProgressBar(`[:bar] :current / ${pagesCount} pages built`, { total: pagesCount });
            progressBar.render();
            logger.setProgressBar(progressBar);
            const startTime = new Date();
            let isCompleted = true;
            yield (0, async_1.sequentialAsyncForEach)(pageGenerationTasks, (task) => __awaiter(this, void 0, void 0, function* () {
                if (this.backgroundBuildMode && startTime < this.stopGenerationTimeThreshold) {
                    logger.info('Page generation stopped');
                    logger.debug('Page generation stopped at generation task queue');
                    isCompleted = false;
                    return;
                }
                if (task.mode === 'sequential') {
                    isCompleted = yield this.generatePagesSequential(task.pages, progressBar);
                }
                else {
                    isCompleted = (yield this.generatePagesAsyncThrottled(task.pages, progressBar));
                }
                logger.removeProgressBar();
                this.siteLinkManager.validateAllIntralinks();
            }));
            return isCompleted;
        });
    }
    /**
     * Generate pages sequentially. That is, the pages are generated
     * one-by-one in order.
     * @param pages Pages to be generated
     * @param progressBar Progress bar of the overall generation process
     * @returns A Promise that resolves to a boolean which indicates whether the generation
     * ran to completion
     */
    generatePagesSequential(pages, progressBar) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            let isCompleted = true;
            yield (0, async_1.sequentialAsyncForEach)(pages, (page) => __awaiter(this, void 0, void 0, function* () {
                if (this.backgroundBuildMode && startTime < this.stopGenerationTimeThreshold) {
                    logger.info('Page generation stopped');
                    logger.debug('Page generation stopped at sequential generation');
                    isCompleted = false;
                    return;
                }
                try {
                    yield page.generate(this.externalManager);
                    this.toRebuild.delete(fsUtil.removeExtension(page.pageConfig.sourcePath));
                    if (this.backgroundBuildMode) {
                        yield this.writeSiteData(false);
                    }
                    progressBar.tick();
                }
                catch (err) {
                    throw new Error(`Error while generating ${page.pageConfig.sourcePath}: ${err}`);
                }
            }));
            return isCompleted;
        });
    }
    /**
     * Creates the supplied pages' page generation promises at a throttled rate.
     * This is done to avoid pushing too many callbacks into the event loop at once. (#1245)
     * @param pages Pages to be generated
     * @param progressBar Progress bar of the overall generation process
     * @returns A Promise that resolves to a boolean which indicates whether the generation
     * ran to completion
     */
    generatePagesAsyncThrottled(pages, progressBar) {
        return new Promise((resolve, reject) => {
            const context = {
                startTime: new Date(),
                numPagesGenerated: 0,
                numPagesToGenerate: pages.length,
                isCompleted: true,
            };
            // Map pages into array of callbacks for delayed execution
            const pageGenerationQueue = pages.map(page => () => __awaiter(this, void 0, void 0, function* () {
                // Pre-generate guard to ensure no newly executed callbacks start on stop
                if (this.backgroundBuildMode && context.startTime < this.stopGenerationTimeThreshold) {
                    if (context.isCompleted) {
                        logger.info('Page generation stopped');
                        logger.debug('Page generation stopped at asynchronous generation');
                        context.isCompleted = false;
                        resolve(false);
                    }
                    return;
                }
                try {
                    yield page.generate(this.externalManager);
                    this.toRebuild.delete(fsUtil.removeExtension(page.pageConfig.sourcePath));
                    if (this.backgroundBuildMode) {
                        yield this.writeSiteData(false);
                    }
                    this.generateProgressBarStatus(progressBar, context, pageGenerationQueue, resolve);
                }
                catch (err) {
                    logger.error(err);
                    reject(new Error(`Error while generating ${page.pageConfig.sourcePath}`));
                }
            }));
            /*
             Take the first MAX_CONCURRENT_PAGE_GENERATION_PROMISES callbacks and execute them.
             Whenever a page generation callback resolves,
             it pops the next unprocessed callback off pageGenerationQueue and executes it.
             */
            pageGenerationQueue.splice(0, MAX_CONCURRENT_PAGE_GENERATION_PROMISES)
                .forEach(generatePage => generatePage());
        });
    }
    /**
     * Helper function for generatePagesAsyncThrottled().
     */
    generateProgressBarStatus(progressBar, context, pageGenerationQueue, resolve) {
        // Post-generate guard to ensure no new callbacks are executed on stop
        if (this.backgroundBuildMode && context.startTime < this.stopGenerationTimeThreshold) {
            if (context.isCompleted) {
                logger.info('Page generation stopped');
                logger.debug('Page generation stopped at asynchronous generation');
                context.isCompleted = false;
                resolve(false);
            }
            return;
        }
        progressBar.tick();
        context.numPagesGenerated += 1;
        if (pageGenerationQueue.length) {
            pageGenerationQueue.pop()();
        }
        else if (context.numPagesGenerated === context.numPagesToGenerate) {
            resolve(true);
        }
    }
    /**
     * Renders all pages specified in site configuration file to the output folder
     */
    generatePages() {
        // Run MarkBind include and render on each source file.
        // Render the final rendered page to the output folder.
        const addressablePages = this.sitePages.addressablePages || [];
        const faviconUrl = this.sitePages.getFavIconUrl();
        this._setTimestampVariable();
        this.sitePages.mapAddressablePagesToPages(addressablePages, faviconUrl);
        const pageGenerationTask = {
            mode: 'async',
            pages: this.sitePages.pages,
        };
        return this.runPageGenerationTasks([pageGenerationTask]);
    }
    /**
     * Renders only the starting page for lazy loading to the output folder.
     */
    generateLandingPage() {
        return __awaiter(this, void 0, void 0, function* () {
            const addressablePages = this.sitePages.addressablePages || [];
            const faviconUrl = this.sitePages.getFavIconUrl();
            this._setTimestampVariable();
            this.sitePages.mapAddressablePagesToPages(addressablePages, faviconUrl);
            const landingPage = this.sitePages.pages.find(page => page.pageConfig.src === this.onePagePath);
            if (!landingPage) {
                throw new Error(`${this.onePagePath} is not specified in the site configuration.`);
            }
            yield landingPage.generate(this.externalManager);
        });
    }
    regenerateAffectedPages(filePaths) {
        return __awaiter(this, void 0, void 0, function* () {
            const startTime = new Date();
            const shouldRebuildAllPages = this.collectUserDefinedVariablesMapIfNeeded(filePaths) || this.forceReload;
            if (shouldRebuildAllPages) {
                logger.warn('Rebuilding all pages as variables file was changed, or the --force-reload flag was set');
            }
            this._setTimestampVariable();
            let openedPagesToRegenerate = [];
            const asyncPagesToRegenerate = this.sitePages.pages.filter((page) => {
                const doFilePathsHaveSourceFiles = filePaths.some(filePath => page.isDependency(filePath));
                if (shouldRebuildAllPages || doFilePathsHaveSourceFiles) {
                    if (this.onePagePath) {
                        const normalizedSource = fsUtil.removeExtension(page.pageConfig.sourcePath);
                        const openIdx = this.currentOpenedPages.findIndex(pagePath => pagePath === normalizedSource);
                        const isRecentlyViewed = openIdx !== -1;
                        if (!isRecentlyViewed) {
                            this.toRebuild.add(normalizedSource);
                        }
                        else {
                            openedPagesToRegenerate[openIdx] = page;
                        }
                        return false;
                    }
                    return true;
                }
                return false;
            });
            /*
             * As a side effect of doing assignment to an empty array, some elements might be
             * undefined if it has not been assigned to anything. We filter those out here.
             */
            openedPagesToRegenerate = openedPagesToRegenerate.filter(page => page);
            const totalPagesToRegenerate = openedPagesToRegenerate.length + asyncPagesToRegenerate.length;
            if (totalPagesToRegenerate === 0) {
                logger.info('No pages needed to be rebuilt');
                return;
            }
            logger.info(`Rebuilding ${totalPagesToRegenerate} pages`);
            const pageGenerationTasks = [];
            if (openedPagesToRegenerate.length > 0) {
                const recentPagesGenerationTask = {
                    mode: 'sequential',
                    pages: openedPagesToRegenerate,
                };
                pageGenerationTasks.push(recentPagesGenerationTask);
            }
            if (asyncPagesToRegenerate.length > 0) {
                const asyncPagesGenerationTask = {
                    mode: 'async',
                    pages: asyncPagesToRegenerate,
                };
                pageGenerationTasks.push(asyncPagesGenerationTask);
            }
            try {
                yield this.runPageGenerationTasks(pageGenerationTasks);
                yield this.writeSiteData();
                logger.info('Pages rebuilt');
                this.calculateBuildTimeForRegenerateAffectedPages(startTime);
            }
            catch (err) {
                yield SiteGenerationManager.rejectHandler(err, [this.tempPath, this.outputPath]);
            }
        });
    }
    /**
     * Helper function for regenerateAffectedPages().
     */
    calculateBuildTimeForRegenerateAffectedPages(startTime) {
        const endTime = new Date();
        const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000;
        logger.info(`Website regeneration complete! Total build time: ${totalBuildTime}s`);
        if (!this.onePagePath && totalBuildTime > LAZY_LOADING_REBUILD_TIME_RECOMMENDATION_LIMIT) {
            logger.info('Your pages took quite a while to rebuild...'
                + ' Have you considered using markbind serve -o when writing content to speed things up?');
        }
    }
    _setTimestampVariable() {
        const options = {
            weekday: 'short',
            year: 'numeric',
            month: 'short',
            day: 'numeric',
            timeZone: this.siteConfig.timeZone,
            timeZoneName: 'short',
        };
        const time = new Date().toLocaleTimeString(this.siteConfig.locale, options);
        this.variableProcessor.addUserDefinedVariableForAllSites('timestamp', time);
    }
    reloadSiteConfig() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.backgroundBuildMode) {
                this.stopOngoingBuilds();
            }
            const oldSiteConfig = this.siteConfig;
            const oldAddressablePages = this.sitePages.addressablePages.slice();
            const oldPagesSrc = oldAddressablePages.map(page => page.src);
            yield this.readSiteConfig();
            yield this.siteAssets.handleIgnoreReload(oldSiteConfig.ignore);
            yield this.handlePageReload(oldAddressablePages, oldPagesSrc, oldSiteConfig);
            yield this.siteAssets.handleStyleReload(oldSiteConfig.style);
            if (this.backgroundBuildMode) {
                this.backgroundBuildNotViewedFiles();
            }
        });
    }
    /**
     * Handles the rebuilding of modified pages
     */
    handlePageReload(oldAddressablePages, oldPagesSrc, oldSiteConfig) {
        return __awaiter(this, void 0, void 0, function* () {
            this.sitePages.collectAddressablePages();
            // Comparator for the _differenceWith comparison below
            const isNewPage = (newPage, oldPage) => constants_1._.isEqual(newPage, oldPage) || newPage.src === oldPage.src;
            const addedPages = constants_1._.differenceWith(this.sitePages.addressablePages, oldAddressablePages, isNewPage);
            const removedPages = constants_1._.differenceWith(oldAddressablePages, this.sitePages.addressablePages, isNewPage)
                .map(filePath => fsUtil.setExtension(filePath.src, '.html'));
            // Checks if any attributes of site.json requiring a global rebuild are modified
            const isGlobalConfigModified = () => !constants_1._.isEqual(oldSiteConfig.faviconPath, this.siteConfig.faviconPath)
                || !constants_1._.isEqual(oldSiteConfig.titlePrefix, this.siteConfig.titlePrefix)
                || !constants_1._.isEqual(oldSiteConfig.titleSuffix, this.siteConfig.titleSuffix)
                || !constants_1._.isEqual(oldSiteConfig.style, this.siteConfig.style)
                || !constants_1._.isEqual(oldSiteConfig.externalScripts, this.siteConfig.externalScripts)
                || !constants_1._.isEqual(oldSiteConfig.globalOverride, this.siteConfig.globalOverride)
                || !constants_1._.isEqual(oldSiteConfig.plugins, this.siteConfig.plugins)
                || !constants_1._.isEqual(oldSiteConfig.pluginsContext, this.siteConfig.pluginsContext)
                || !constants_1._.isEqual(oldSiteConfig.headingIndexingLevel, this.siteConfig.headingIndexingLevel)
                || !constants_1._.isEqual(oldSiteConfig.enableSearch, this.siteConfig.enableSearch)
                || !constants_1._.isEqual(oldSiteConfig.timeZone, this.siteConfig.timeZone)
                || !constants_1._.isEqual(oldSiteConfig.locale, this.siteConfig.locale)
                || !constants_1._.isEqual(oldSiteConfig.intrasiteLinkValidation, this.siteConfig.intrasiteLinkValidation)
                || !constants_1._.isEqual(oldSiteConfig.plantumlCheck, this.siteConfig.plantumlCheck);
            if (isGlobalConfigModified() || !constants_1._.isEmpty(addedPages) || !constants_1._.isEmpty(removedPages)) {
                yield this.siteAssets.removeAsset(removedPages);
                this.buildManagers(this.sitePages.baseUrlMap);
                yield this.rebuildSourceFiles();
                yield this.writeSiteData();
            }
            else {
                // Get pages with edited attributes but with the same src
                const editedPages = constants_1._.differenceWith(this.sitePages.addressablePages, oldAddressablePages, (newPage, oldPage) => constants_1._.isEqual(newPage, oldPage) || !oldPagesSrc.includes(newPage.src));
                this.sitePages.updatePages(editedPages);
                const siteConfigDirectory = path_1.default.dirname(path_1.default.join(this.rootPath, this.siteConfigPath));
                this.regenerateAffectedPages(editedPages.map(page => path_1.default.join(siteConfigDirectory, page.src)));
            }
        });
    }
}
exports.SiteGenerationManager = SiteGenerationManager;
//# sourceMappingURL=SiteGenerationManager.js.map