"use strict";
/*
 * Copyright 2018, collaboration Factory AG. All rights reserved.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Scheduler = void 0;
const JobTracker_1 = require("./JobTracker");
const path = require("path");
const chokidar = require("chokidar");
const utils_1 = require("../utils");
const interfaces_1 = require("../compiler/interfaces");
const CplaceVersion_1 = require("../model/CplaceVersion");
class Scheduler {
    /**
     * Creates a new scheduler to run compilation jobs.
     *
     * @param executor The executor to run jobs
     * @param plugins All plugins that are currently in compilation scope
     * @param rootRepository The name of the repository the compilation is started from
     * @param mainRepoDir Path to the `main` repository
     * @param isProduction Whether to compile for production
     * @param noParents Whether to exclude parent repositories from compilation
     * @param watchFiles Whether to watch files for changes
     * @param updateDetails Details of a potentially available version update
     */
    constructor(executor, plugins, rootRepository, mainRepoDir, isProduction, noParents, watchFiles, withYaml, updateDetails) {
        this.executor = executor;
        this.plugins = plugins;
        this.rootRepository = rootRepository;
        this.mainRepoDir = mainRepoDir;
        this.isProduction = isProduction;
        this.noParents = noParents;
        this.watchFiles = watchFiles;
        this.withYaml = withYaml;
        this.updateDetails = updateDetails;
        this.watchers = {
            ts: new Map(),
            tsE2E: new Map(),
            less: new Map(),
            css: new Map(),
            openAPIYaml: new Map(),
            vendor: new Map(),
        };
        this.completed = false;
        this.vendorJobs = this.createVendorJobTracker();
        this.tsJobs = this.createTsJobTracker();
        this.tsE2EJobs = this.createTsE2EJobTracker();
        this.lessJobs = this.createLessJobTracker();
        this.openAPIYamlJobs = this.createOpenAPIYamlJobTracker();
        this.compressCssJobs = this.createCompressCssJobTracker();
        this.combineJsJobs = this.createCombineJsJobTracker();
    }
    start() {
        const p = new Promise((resolve, reject) => {
            this.finishedResolver = resolve;
            this.finishedRejecter = reject;
            this.scheduleNext();
        });
        p.then(() => {
            this.cleanup();
        }, () => {
            this.cleanup();
        });
        return p;
    }
    stop() {
        this.completed = true;
        this.cleanup();
    }
    scheduleNext() {
        if (this.completed) {
            return;
        }
        // vendor compilation should be requested first
        const vendorSchedulingResult = this.getAndScheduleNextJob(this.vendorJobs, 'vendor', 'vendor');
        if (vendorSchedulingResult.backoff) {
            return;
        }
        const nextVendorPlugin = vendorSchedulingResult.scheduledPlugin;
        const lessSchedulingResult = this.getAndScheduleNextJob(this.lessJobs, 'less', 'less');
        if (lessSchedulingResult.backoff) {
            return;
        }
        const nextLessPlugin = lessSchedulingResult.scheduledPlugin;
        const openAPIYamlSchedulingResult = this.getAndScheduleNextJob(this.openAPIYamlJobs, 'openAPIYaml', 'openAPIYaml');
        if (openAPIYamlSchedulingResult.backoff) {
            return;
        }
        const nextOpenAPIYamlPlugin = openAPIYamlSchedulingResult.scheduledPlugin;
        const compressCssSchedulingResult = this.getAndScheduleNextJob(this.compressCssJobs, 'compressCss', 'css');
        if (compressCssSchedulingResult.backoff) {
            return;
        }
        const nextCompressCssPlugin = compressCssSchedulingResult.scheduledPlugin;
        let nextTsE2EPlugin = null;
        if (!this.isProduction) {
            const tsE2ESchedulingResult = this.getAndScheduleNextJob(this.tsE2EJobs, 'tsE2E', 'tsE2E');
            if (tsE2ESchedulingResult.backoff) {
                return;
            }
            nextTsE2EPlugin = tsE2ESchedulingResult.scheduledPlugin;
        }
        const tsSchedulingResult = this.getAndScheduleNextJob(this.tsJobs, 'ts', 'ts');
        if (tsSchedulingResult.backoff) {
            return;
        }
        const nextTsPlugin = tsSchedulingResult.scheduledPlugin;
        let nextCombineJsPlugin = null;
        if (this.isProduction) {
            const combineJsSchedulingResult = this.getAndScheduleNextJob(this.combineJsJobs, 'combineJs', 'combineJs');
            if (combineJsSchedulingResult.backoff) {
                return;
            }
            nextCombineJsPlugin = combineJsSchedulingResult.scheduledPlugin;
        }
        if (nextTsPlugin === null &&
            nextTsE2EPlugin == null &&
            nextLessPlugin === null &&
            nextCompressCssPlugin === null &&
            nextOpenAPIYamlPlugin === null &&
            nextVendorPlugin === null &&
            nextCombineJsPlugin === null) {
            if (!this.watchFiles && !this.completed) {
                utils_1.printUpdateDetails(this.updateDetails);
                this.completed = true;
                this.finishedResolver && this.finishedResolver();
            }
            else if (this.watchFiles) {
                console.log();
                console.log(utils_1.csucc `Compilation completed - watching files...`);
                console.log();
                utils_1.printUpdateDetails(this.updateDetails);
            }
        }
        else if (nextTsPlugin ||
            nextTsE2EPlugin ||
            nextLessPlugin ||
            nextCompressCssPlugin ||
            nextOpenAPIYamlPlugin ||
            nextVendorPlugin ||
            nextCombineJsPlugin) {
            if (this.executor.hasCapacity()) {
                this.scheduleNext();
            }
        }
    }
    getAndScheduleNextJob(jobTracker, compileType, watchType) {
        const nextPlugin = jobTracker.getNextKey();
        if (nextPlugin) {
            const plugin = this.getPlugin(nextPlugin);
            const compileRequest = {
                pluginName: plugin.pluginName,
                dependencyPaths: plugin.pluginDescriptor.dependencies.map((pluginDescriptor) => this.plugins.get(pluginDescriptor.name).pluginDir),
                assetsPath: plugin.assetsDir,
                mainRepoDir: this.mainRepoDir,
                isProduction: this.isProduction,
                verbose: utils_1.isDebugEnabled(),
            };
            compileRequest[compileType] = true;
            utils_1.debug(`(Scheduler) scheduling ${compileType} compile step for ${plugin.pluginName}`);
            jobTracker.markProcessing(nextPlugin);
            this.executor.run(compileRequest, this.watchFiles).then((result) => {
                const compilationResultChanged = result !== interfaces_1.CompilationResult.UNCHANGED;
                const firstCompletion = jobTracker.markCompleted(nextPlugin, compilationResultChanged);
                if (firstCompletion && this.watchFiles) {
                    this.registerWatch(nextPlugin, watchType);
                }
                this.scheduleNext();
            }, (e) => {
                const firstCompletion = jobTracker.markCompleted(nextPlugin, true);
                if (firstCompletion && this.watchFiles) {
                    this.registerWatch(nextPlugin, watchType);
                }
                // when watching we don't kill the compiler here but just continue
                if (this.watchFiles) {
                    this.scheduleNext();
                    return;
                }
                this.completed = true;
                this.finishedRejecter && this.finishedRejecter(e);
            });
            if (!this.executor.hasCapacity()) {
                return { backoff: true };
            }
        }
        return {
            scheduledPlugin: nextPlugin,
        };
    }
    cleanup() {
        this.watchers.ts.forEach((watcher) => {
            watcher.close();
        });
        this.watchers.tsE2E.forEach((watcher) => {
            watcher.close();
        });
        this.watchers.less.forEach((watcher) => {
            watcher.close();
        });
        this.watchers.css.forEach((watcher) => {
            watcher.close();
        });
        this.watchers.openAPIYaml.forEach((watcher) => {
            watcher.close();
        });
        this.watchers.vendor.forEach((watcher) => {
            watcher.close();
        });
    }
    createTsJobTracker() {
        const tsPlugins = [];
        this.plugins.forEach((plugin) => {
            if (plugin.hasTypeScriptAssets &&
                this.isInCompilationScope(plugin)) {
                tsPlugins.push(plugin);
            }
        });
        const jobs = tsPlugins.map((plugin) => new JobTracker_1.JobDetails(plugin.pluginName, this.filterTypeScriptPlugins(plugin.pluginDescriptor.dependencies), this.filterTypeScriptPlugins(plugin.dependents), [this.vendorJobs.isJobDone.bind(this.vendorJobs)]));
        return new JobTracker_1.JobTracker(jobs);
    }
    createTsE2EJobTracker() {
        const tsE2EPlugins = [];
        this.plugins.forEach((plugin) => {
            const shouldCompileE2EAssets = plugin.hasTypeScriptE2EAssets &&
                this.isInCompilationScope(plugin);
            if (shouldCompileE2EAssets &&
                CplaceVersion_1.CplaceVersion.isVersionLowerThen23()) {
                tsE2EPlugins.push(plugin);
            }
            if (shouldCompileE2EAssets &&
                !CplaceVersion_1.CplaceVersion.isVersionLowerThen23()) {
                console.log(utils_1.cwarn `[${plugin.pluginName}] E2E assets are no longer compiled! Starting from the cplace release 23.1 all the E2E tests should be moved into a dedicated E2E repository. In addition, E2E tests must be written using Cypress!`);
            }
        });
        const jobs = tsE2EPlugins.map((plugin) => new JobTracker_1.JobDetails(plugin.pluginName, this.filterTypeScriptE2EPlugins(plugin.pluginDescriptor.dependencies), this.filterTypeScriptE2EPlugins(plugin.dependents), []));
        return new JobTracker_1.JobTracker(jobs);
    }
    createLessJobTracker() {
        const lessPlugins = [];
        this.plugins.forEach((plugin) => {
            if (plugin.hasLessAssets && this.isInCompilationScope(plugin)) {
                lessPlugins.push(plugin);
            }
        });
        const jobs = lessPlugins.map((plugin) => new JobTracker_1.JobDetails(plugin.pluginName, this.filterLessPlugins(plugin.pluginDescriptor.dependencies), this.filterLessPlugins(plugin.dependents), []));
        return new JobTracker_1.JobTracker(jobs);
    }
    createVendorJobTracker() {
        const vendorPlugins = [];
        this.plugins.forEach((plugin) => {
            if (plugin.hasVendors && this.isInCompilationScope(plugin)) {
                vendorPlugins.push(plugin);
            }
        });
        const jobs = vendorPlugins.map((plugin) => new JobTracker_1.JobDetails(plugin.pluginName, this.filterVendorPlugins(plugin.pluginDescriptor.dependencies), this.filterVendorPlugins(plugin.dependents), []));
        return new JobTracker_1.JobTracker(jobs);
    }
    filterVendorPlugins(plugins) {
        return plugins
            .map((p) => this.getPlugin(p.name))
            .filter((p) => p.hasVendors && this.isInCompilationScope(p))
            .map((p) => p.pluginName);
    }
    createCombineJsJobTracker() {
        const combineJsPlugins = [];
        this.plugins.forEach((plugin) => {
            if (plugin.hasCombineJs && this.isInCompilationScope(plugin)) {
                combineJsPlugins.push(plugin);
            }
        });
        const jobs = combineJsPlugins.map((plugin) => new JobTracker_1.JobDetails(plugin.pluginName, this.filterCombineJsPlugins(plugin.pluginDescriptor.dependencies), this.filterCombineJsPlugins(plugin.dependents), [
            this.vendorJobs.isJobDone.bind(this.vendorJobs),
            this.tsJobs.isJobDone.bind(this.tsJobs),
        ]));
        return new JobTracker_1.JobTracker(jobs);
    }
    createOpenAPIYamlJobTracker() {
        const openAPIYamlPlugins = [];
        this.plugins.forEach((plugin) => {
            if (plugin.hasOpenAPIYamlAssets &&
                this.isInCompilationScope(plugin) &&
                this.withYaml) {
                openAPIYamlPlugins.push(plugin);
            }
        });
        const jobs = openAPIYamlPlugins.map((plugin) => new JobTracker_1.JobDetails(plugin.pluginName, this.filterOpenAPIYamlPlugins(plugin.pluginDescriptor.dependencies), this.filterOpenAPIYamlPlugins(plugin.dependents), []));
        return new JobTracker_1.JobTracker(jobs);
    }
    createCompressCssJobTracker() {
        const compressPlugins = [];
        this.plugins.forEach((plugin) => {
            // if the plugin has vendors, the compress css task will also be called from the VendorCompiler
            // To prevent the task from running twice, skip compress css task if plugin has vendors.
            if (plugin.hasCompressCssAssets &&
                !plugin.hasVendors &&
                this.isInCompilationScope(plugin)) {
                compressPlugins.push(plugin);
            }
        });
        const jobs = compressPlugins.map((plugin) => new JobTracker_1.JobDetails(plugin.pluginName, [], [], []));
        return new JobTracker_1.JobTracker(jobs);
    }
    isInCompilationScope(plugin) {
        return !this.noParents || plugin.repo === this.rootRepository;
    }
    filterTypeScriptPlugins(plugins) {
        return plugins
            .map((p) => this.getPlugin(p.name))
            .filter((p) => p.hasTypeScriptAssets && this.isInCompilationScope(p))
            .map((p) => p.pluginName);
    }
    filterTypeScriptE2EPlugins(plugins) {
        return plugins
            .map((p) => this.getPlugin(p.name))
            .filter((p) => p.hasTypeScriptE2EAssets && this.isInCompilationScope(p))
            .map((p) => p.pluginName);
    }
    filterLessPlugins(plugins) {
        return plugins
            .map((p) => this.getPlugin(p.name))
            .filter((p) => p.hasLessAssets && this.isInCompilationScope(p))
            .map((p) => p.pluginName);
    }
    filterOpenAPIYamlPlugins(plugins) {
        return plugins
            .map((p) => this.getPlugin(p.name))
            .filter((p) => p.hasOpenAPIYamlAssets && this.isInCompilationScope(p))
            .map((p) => p.pluginName);
    }
    filterCombineJsPlugins(plugins) {
        return plugins
            .map((p) => this.getPlugin(p.name))
            .filter((p) => p.hasCombineJs && this.isInCompilationScope(p))
            .map((p) => p.pluginName);
    }
    registerWatch(pluginName, type) {
        if (!this.watchFiles) {
            return;
        }
        const plugin = this.getPlugin(pluginName);
        const pattern = Scheduler.WATCH_PATTERNS[type];
        let glob;
        let watchDir = path.join(plugin.assetsDir, type);
        let jobTracker;
        switch (type) {
            case 'tsE2E':
                watchDir = path.join(plugin.assetsDir, 'e2e');
                jobTracker = this.tsE2EJobs;
                break;
            case 'ts':
                jobTracker = this.tsJobs;
                break;
            case 'less':
                jobTracker = this.lessJobs;
                break;
            case 'css':
                jobTracker = this.compressCssJobs;
                break;
            case 'openAPIYaml':
                watchDir = path.join(plugin.pluginDir, 'api');
                jobTracker = this.openAPIYamlJobs;
                break;
            case 'vendor':
                watchDir = path.join(plugin.pluginDir, 'assets');
                glob = Scheduler.convertToUnixPath(`${watchDir}/*(${pattern})`);
                jobTracker = this.vendorJobs;
                break;
            case 'combineJs':
                jobTracker = this.combineJsJobs;
                break;
        }
        if (!glob) {
            glob = Scheduler.convertToUnixPath(`${watchDir}/**/*.(${pattern})`);
        }
        const watcher = chokidar.watch(glob);
        this.watchers[type].set(pluginName, watcher);
        let ready = false;
        let debounce;
        const handleEvent = () => {
            if (!ready) {
                return;
            }
            console.log('===> Recompiling', pluginName);
            debounce && clearTimeout(debounce);
            debounce = setTimeout(() => {
                jobTracker.markDirty(pluginName);
                this.scheduleNext();
            }, 500);
        };
        watcher
            .on('ready', () => (ready = true))
            .on('add', handleEvent)
            .on('change', handleEvent)
            .on('unlink', handleEvent)
            .on('unlinkDir', handleEvent)
            .on('error', (e) => {
            console.error(utils_1.cerr `[${pluginName}] watcher failed: ${e}`);
            watcher.close();
        });
    }
    getPlugin(pluginName) {
        const plugin = this.plugins.get(pluginName);
        if (!plugin) {
            throw Error(`unknown plugin: ${plugin}`);
        }
        return plugin;
    }
    // code from some github project
    static convertToUnixPath(input) {
        const isExtendedLengthPath = /^\\\\\?\\/.test(input);
        const hasNonAscii = /[^\u0000-\u0080]+/.test(input);
        if (isExtendedLengthPath || hasNonAscii) {
            return input;
        }
        return input.replace(/\\/g, '/');
    }
}
exports.Scheduler = Scheduler;
Scheduler.WATCH_PATTERNS = {
    ts: 'ts|htm?(l)',
    tsE2E: 'ts',
    less: 'less',
    css: 'css',
    openAPIYaml: 'yaml',
    vendor: 'package-lock.json|index.ts',
};
//# sourceMappingURL=Scheduler.js.map