"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.assemble = exports.generatePrescanModel = exports.scanForModules = void 0;

var _debug = _interopRequireDefault(require("debug"));

var _path = require("path");

var _plugins = require("./plugins");

var _commandTree = require("../core/command-tree");

var _errors = require("../models/errors");

var _Theme = require("../webapp/themes/Theme");

var _tree = require("../commands/tree");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/*
 * 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 = void 0 && (void 0).__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());
  });
};

const debug = (0, _debug.default)('core/plugins/scanner');
debug('loading');

/**
 * 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 _commandTree.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 Promise.resolve().then(() => require(`${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');
  }
});

const scanForModules = (dir, quiet = false, filter = () => true) => __awaiter(void 0, void 0, void 0, function* () {
  debug('scanForModules %s', dir);
  const fs = yield Promise.resolve().then(() => require('fs'));
  const path = yield Promise.resolve().then(() => require('path'));
  const colors = yield Promise.resolve().then(() => require('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 = _Theme.apiVersion;
                }
              });
              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 ((0, _errors.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
 *
 */


exports.scanForModules = scanForModules;

function clientHosted(opts) {
  return __awaiter(this, void 0, void 0, function* () {
    const pluginRootAbsolute = process.env.PLUGIN_ROOT || opts.pluginRoot || (0, _path.join)(__dirname, _plugins.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 = (0, _path.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
 *
 */


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: (0, _tree.getModel)().catchalls,
    docs: undefined // assembler.ts will fill this in

  };
});
/**
 * Assemble the plugins for faster loading
 *
 */


exports.generatePrescanModel = generatePrescanModel;

const assemble = (registrar, opts) => {
  return generatePrescanModel(registrar, Object.assign({
    assembly: true
  }, opts));
};

exports.assemble = assemble;