/*
 * Copyright 2017-2019 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
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());
    });
};
import Debug from 'debug';
const debug = Debug('core/plugins/scanner');
debug('loading');
import { dirname, join } from 'path';
import { pluginRoot } from './plugins';
import { ImplForPlugins } from '../core/command-tree';
import { isCodedError } from '../models/errors';
import { apiVersion as defaultThemeApiVersion } from '../webapp/themes/Theme';
import { getModel } from '../commands/tree';
/**
 * A data structure to facilitate computation of the Prescan model
 *
 */
class ScanCache {
    constructor(registrar) {
        /** map from command to plugin *name* that defines it */
        this.commandToPlugin = {};
        /**  topological sort of the plugins (order they resolved) */
        this.topological = {};
        /** some plugins override the behavior of others */
        this.overrides = {};
        /** as we scan for plugins, we'll memoize their usage models */
        this.usage = {};
        // TODO document
        this.flat = [];
        this.isSubtreeSynonym = {};
        this.isSynonym = {};
        this.registrar = registrar;
    }
}
/**
 * Override the standard ImplForPlugins with some extra state that we
 * need for scanning.
 *
 */
class CommandRegistrarForScan extends ImplForPlugins {
    constructor(plugin, scanCache) {
        super(plugin);
        this.scanCache = scanCache;
        /** map to plugin *name* */
        this.cmdToPlugin = {};
    }
    subtreeSynonym(route, master) {
        if (route !== master.route) {
            this.scanCache.isSubtreeSynonym[route] = true;
            this.scanCache.isSubtreeSynonym[master.route] = true;
            return super.subtreeSynonym(route, master, {});
        }
    }
    listen(route, handler, options) {
        this.cmdToPlugin[route] = this.plugin;
        return super.listen(route, handler, options);
    }
    subtree(route, options) {
        return super.subtree(route, Object.assign({
            listen: this.listen.bind(this)
        }, options));
    }
    synonym(route, handler, master, options) {
        this.cmdToPlugin[route] = this.plugin;
        this.scanCache.isSynonym[route] = true;
        return super.synonym(route, handler, master, options);
    }
}
/**
 * Load one plugin for the given plugin route, located in the given pluginPath on the local filesystem
 *
 */
const loadPlugin = (route, pluginPath, scanCache) => __awaiter(void 0, void 0, void 0, function* () {
    debug('loadPlugin %s', route);
    // create a CommandRegistrar instance for this one plugin load
    const ctree = new CommandRegistrarForScan(route, scanCache);
    const pluginLoaderRef = yield import(pluginPath);
    function isDirect(loader) {
        return typeof loader === 'function';
    }
    const pluginLoader = isDirect(pluginLoaderRef) ? pluginLoaderRef : pluginLoaderRef.default;
    if (typeof pluginLoader === 'function') {
        // invoke the plugin loader
        scanCache.registrar[route] = yield pluginLoader(ctree, {});
        // generate a mapping from commands (e.g. "/git/status" which is
        // hosted by the bash-like plugin) to plugin (e.g. "bash-like"),
        // which services that command
        const { cmdToPlugin } = ctree;
        for (const k in cmdToPlugin) {
            if (scanCache.commandToPlugin[k]) {
                debug('override', k, cmdToPlugin[k], scanCache.commandToPlugin[k]);
                scanCache.overrides[k] = cmdToPlugin[k];
            }
            else {
                debug('not override', k, cmdToPlugin[k]);
            }
            scanCache.commandToPlugin[k] = cmdToPlugin[k];
        }
    }
});
/**
 * Attempt to load the plugins during a plugin scan
 *
 * @param lastError so we don't repeat making the same mistake 100 times!
 *
 */
const topologicalSortForScan = (scanCache, pluginPaths, iter, lastError, lastErrorAlreadyEmitted) => __awaiter(void 0, void 0, void 0, function* () {
    debug('topologicalSortForScan', iter);
    if (iter >= 100) {
        debug('unable to resolve plugins');
        if (lastError) {
            if (!lastErrorAlreadyEmitted) {
                throw lastError;
            } // fallthrough intentional
            throw new Error('Unable to resolve plugins');
        }
    }
    let nUnresolved = 0;
    const unresolved = [];
    for (const route in pluginPaths) {
        debug('resolving %s', route);
        try {
            const module = { route, path: pluginPaths[route] };
            yield loadPlugin(route, pluginPaths[route], scanCache);
            scanCache.flat.push(module);
            delete pluginPaths[route];
        }
        catch (err) {
            const notFound = err.message.indexOf('Module not found') >= 0 || err.message.indexOf('Cannot find module') >= 0;
            if ((!notFound || iter > 10) && lastError && lastError.message !== err.message) {
                //
                // note how we do not print the error if any of three
                // conditions hold (but we still continue iterating)
                //
                // 1. Module not found errors; these are why we have to iterate, because of inter-module dependencies
                //
                // 2. don't print errors for the first 10 iterations; only print errors if they persist
                //
                // 3. if this error is the same as the last; no sense in repeating ourselves
                //
                debug('not retrying');
                console.error(err);
                lastErrorAlreadyEmitted = true;
            }
            //
            // let's try again; here we are implementing a fixpoint
            // computation by iterating till convergence; this fixpoint
            // gives us the topological sort without having to bother
            // computing the topological sort!
            //
            debug('retry on', err);
            lastError = err;
            nUnresolved++;
            unresolved.push(route);
        }
    }
    if (nUnresolved > 0) {
        debug('nUnresolved', nUnresolved, unresolved);
        return topologicalSortForScan(scanCache, pluginPaths, iter + 1, lastError, lastErrorAlreadyEmitted);
    }
    else {
        debug('topologicalSortForScan done');
    }
});
export const scanForModules = (dir, quiet = false, filter = () => true) => __awaiter(void 0, void 0, void 0, function* () {
    debug('scanForModules %s', dir);
    const fs = yield import('fs');
    const path = yield import('path');
    const colors = yield import('colors/safe');
    try {
        const plugins = {};
        const preloads = {};
        const themeSets = [];
        const doScan = ({ modules, moduleDir, parentPath, lookForPlugin = true, lookForPrescan = true }) => {
            debug('doScan', modules);
            modules.forEach(module => {
                const modulePath = path.join(moduleDir, module);
                const name = (parentPath ? `${parentPath}/` : '') + module;
                try {
                    const themePath = path.join(modulePath, 'theme.json');
                    if (fs.existsSync(themePath)) {
                        const themeSet = require(themePath);
                        const nThemes = !themeSet || !themeSet.themes ? 0 : themeSet.themes.length;
                        if (nThemes > 0) {
                            // set these to have the latest apiVersion unless otherwise stated
                            // this decision will be serialized out to the prescan persisted model
                            themeSet.themes.forEach(_ => {
                                if (!_.apiVersion) {
                                    _.apiVersion = defaultThemeApiVersion;
                                }
                            });
                            const msg = colors.bold(colors.rainbow('themes')) + colors.dim(` x${nThemes}`);
                            console.log(colors.green('  \u2713 ') + msg + '\t' + path.basename(module));
                            // add this plugin's themes onto the full list, across all plugins
                            themeSets.push({ themes: themeSet.themes, plugin: module });
                        }
                    }
                }
                catch (err) {
                    console.error('Error registering theme', err);
                }
                if (module.charAt(0) === '@') {
                    // support for @owner/repo style modules; see shell issue #260
                    return doScan({
                        modules: fs.readdirSync(modulePath),
                        moduleDir: modulePath,
                        parentPath: module
                    });
                }
                function lookFor(filename, destMap, colorFn) {
                    const pluginPath = path.join(moduleDir, module, filename);
                    debug('lookFor', filename, pluginPath);
                    /** report a successful find to the console */
                    const ok = () => {
                        const parent = parentPath ? `${parentPath}/` : '';
                        console.log(colors.green('  \u2713 ') + colorFn(filename.replace(/\..*$/, '')) + '\t' + parent + path.basename(module));
                    };
                    if (fs.existsSync(pluginPath)) {
                        if (!quiet) {
                            debug('found', name);
                            ok();
                        }
                        destMap[name] = pluginPath;
                    }
                    else {
                        const backupPluginPath = path.join(modulePath, 'dist', filename);
                        debug('lookFor2', filename, backupPluginPath);
                        if (fs.existsSync(backupPluginPath)) {
                            if (!quiet) {
                                debug('found2', name);
                                ok();
                            }
                            destMap[name] = backupPluginPath;
                        }
                        else {
                            // support for javascript-coded plugins (monorepo)
                            const backupPluginPath = path.join(modulePath, 'src/plugin', filename);
                            debug('lookFor3', filename, backupPluginPath);
                            if (fs.existsSync(backupPluginPath)) {
                                if (!quiet) {
                                    debug('found3', name);
                                    ok();
                                }
                                destMap[name] = backupPluginPath;
                            }
                            else {
                                // support for javascript-coded plugins (external client)
                                const backupPluginPath = path.join(modulePath, 'plugin', filename);
                                debug('lookFor4', filename, backupPluginPath);
                                if (fs.existsSync(backupPluginPath)) {
                                    if (!quiet) {
                                        debug('found4', name);
                                        ok();
                                    }
                                    destMap[name] = backupPluginPath;
                                    // console.error('Skipping plugin, because it does not have a plugin.js', module)
                                }
                            }
                        }
                    }
                    if (fs.statSync(modulePath).isDirectory()) {
                        const subDirs = fs
                            .readdirSync(modulePath)
                            .filter(_ => !/(^m?dist)|(bin)|(web)|(src)|(samples)|(i18n)|(tests)|(node_modules)$/.test(_) &&
                            fs.statSync(path.join(modulePath, _)).isDirectory());
                        if (subDirs.length > 0) {
                            doScan({
                                modules: subDirs,
                                moduleDir: modulePath,
                                parentPath: module,
                                lookForPlugin: filename === 'plugin.js',
                                lookForPrescan: filename === 'preload.js'
                            });
                        }
                    }
                }
                if (lookForPlugin) {
                    lookFor('plugin.js', plugins, colors.bold);
                }
                if (lookForPrescan) {
                    lookFor('preload.js', preloads, colors.dim);
                }
            });
        };
        // scan the app/plugins/modules directory
        const moduleDir = dir; // path.join(dir, 'modules')
        debug('moduleDir', moduleDir);
        doScan({ modules: fs.readdirSync(moduleDir).filter(filter), moduleDir });
        return { plugins, preloads, themeSets };
    }
    catch (err) {
        if (isCodedError(err) && err.code !== 'ENOENT') {
            console.error('Error scanning for external plugins', err);
        }
        return {};
    }
});
/**
 * Scan for plugins hosted by the client itself, rather than npm
 * installed from somewhere else
 *
 */
function clientHosted(opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const pluginRootAbsolute = process.env.PLUGIN_ROOT || opts.pluginRoot || join(__dirname, pluginRoot); // filesystem path for the plugins
        debug('pluginRootAbsolute', pluginRootAbsolute);
        const topOfScan = opts.pluginRoot || pluginRootAbsolute;
        debug('using clientHosted scan', topOfScan);
        return scanForModules(topOfScan);
    });
}
/**
 * Scan for plugins that clients incorporate via `npm install`
 *
 */
function clientRequired() {
    return __awaiter(this, void 0, void 0, function* () {
        const topOfScan = dirname(require.resolve('@kui-shell/prescan.json'));
        debug('using clientRequired scan', topOfScan);
        return scanForModules(topOfScan, false, (filename) => /^plugin-/.test(filename) || /^client$/.test(filename));
    });
}
/**
 * Look for plugins by scanning the local filesystem
 *
 */
const resolveFromLocalFilesystem = (scanCache, opts = {}) => __awaiter(void 0, void 0, void 0, function* () {
    debug('resolveFromLocalFilesystem');
    const { plugins, preloads, themeSets } = opts.externalOnly ? yield clientHosted(opts) : yield clientRequired();
    debug('availablePlugins %s', JSON.stringify(plugins));
    // then, we load the plugins
    yield topologicalSortForScan(scanCache, plugins, 0);
    return { preloads, themeSets };
});
/**
 * Generate a prescan model
 *
 */
export const generatePrescanModel = (registrar, opts) => __awaiter(void 0, void 0, void 0, function* () {
    debug('generatePrescanModel', opts);
    // this will store the state of a scan
    const scanCache = new ScanCache(registrar);
    // do the scan, which will populate that StateCache instance
    const { preloads, themeSets } = yield resolveFromLocalFilesystem(scanCache, opts);
    const disambiguator = {};
    for (const route in scanCache.commandToPlugin) {
        const A = route.split('/');
        for (let idx = 1; idx < A.length; idx++) {
            const cmd = `/${A.slice(idx).join('/')}`;
            if (!disambiguator[cmd] && (route === cmd || !scanCache.commandToPlugin[route])) {
                // this is, so far, an unambiguous resolution, and we don't
                // have a specific resolution for this cmd
                disambiguator[cmd] = route;
                scanCache.commandToPlugin[cmd] = scanCache.commandToPlugin[route];
            }
            else if (disambiguator[cmd]) {
                // a conflict. is it a subtree-synonym conflcit? if so ignore the conflict
                const subtree = route.substring(0, route.lastIndexOf('/'));
                if (!scanCache.isSubtreeSynonym[subtree]) {
                    if (disambiguator[cmd] === cmd) {
                        // rule in favor of what we have
                    }
                    else if (route === cmd) {
                        // rule in favor of the new one
                        disambiguator[cmd] = route;
                        scanCache.commandToPlugin[cmd] = scanCache.commandToPlugin[route];
                    }
                    else {
                        // collision, remove the previous disambiguator
                        disambiguator[cmd] = true;
                        delete scanCache.commandToPlugin[cmd];
                    }
                }
            }
        }
    }
    return {
        preloads: Object.keys(preloads).map(route => ({
            route,
            path: preloads[route]
        })),
        themeSets,
        commandToPlugin: scanCache.commandToPlugin,
        topological: scanCache.topological,
        flat: scanCache.flat,
        overrides: scanCache.overrides,
        usage: scanCache.usage,
        disambiguator: undefined,
        catchalls: getModel().catchalls,
        docs: undefined // assembler.ts will fill this in
    };
});
/**
 * Assemble the plugins for faster loading
 *
 */
export const assemble = (registrar, opts) => {
    return generatePrescanModel(registrar, Object.assign({ assembly: true }, opts));
};
//# sourceMappingURL=scanner.js.map