text: /* eslint max-len: 0 */
  
  import * as context from "../../../api/node";
  import type Logger from "../logger";
  import Plugin from "../../plugin";
  import * as messages from "babel-messages";
  import { normaliseOptions } from "./index";
  import resolvePlugin from "../../../helpers/resolve-plugin";
  import resolvePreset from "../../../helpers/resolve-preset";
  import cloneDeepWith from "lodash/cloneDeepWith";
  import clone from "lodash/clone";
  import merge from "../../../helpers/merge";
  import config from "./config";
  import removed from "./removed";
  import buildConfigChain from "./build-config-chain";
  import path from "path";
  
  type PluginObject = {
    pre?: Function;
    post?: Function;
    manipulateOptions?: Function;
  
    visitor: ?{
      [key: string]: Function | {
        enter?: Function | Array<Function>;
        exit?: Function | Array<Function>;
      }
    };
  };
  
  type MergeOptions = {
    options?: Object,
    extending?: Object,
    alias: string,
    loc?: string,
    dirname?: string
  };
  
  export default class OptionManager {
    constructor(log?: Logger) {
      this.resolvedConfigs = [];
      this.options = OptionManager.createBareOptions();
      this.log = log;
    }
  
    resolvedConfigs: Array<string>;
    options: Object;
    log: ?Logger;
  
    static memoisedPlugins: Array<{
      container: Function;
      plugin: Plugin;
    }>;
  
    static memoisePluginContainer(fn, loc, i, alias) {
      for (const cache of (OptionManager.memoisedPlugins: Array<Object>)) {
        if (cache.container === fn) return cache.plugin;
      }
  
      let obj: ?PluginObject;
  
      if (typeof fn === "function") {
        obj = fn(context);
      } else {
        obj = fn;
      }
  
      if (typeof obj === "object") {
        const plugin = new Plugin(obj, alias);
        OptionManager.memoisedPlugins.push({
          container: fn,
          plugin: plugin
        });
        return plugin;
      } else {
        throw new TypeError(messages.get("pluginNotObject", loc, i, typeof obj) + loc + i);
      }
    }
  
    static createBareOptions() {
      const opts = {};
  
      for (const key in config) {
        const opt = config[key];
        opts[key] = clone(opt.default);
      }
  
      return opts;
    }
  
    static normalisePlugin(plugin, loc, i, alias) {
      plugin = plugin.__esModule ? plugin.default : plugin;
  
      if (!(plugin instanceof Plugin)) {
        // allow plugin containers to be specified so they don't have to manually require
        if (typeof plugin === "function" || typeof plugin === "object") {
          plugin = OptionManager.memoisePluginContainer(plugin, loc, i, alias);
        } else {
          throw new TypeError(messages.get("pluginNotFunction", loc, i, typeof plugin));
        }
      }
  
      plugin.init(loc, i);
  
      return plugin;
    }
  
    static normalisePlugins(loc, dirname, plugins) {
      return plugins.map(function (val, i) {
        let plugin, options;
  
        if (!val) {
          throw new TypeError("Falsy value found in plugins");
        }
  
        // destructure plugins
        if (Array.isArray(val)) {
          [plugin, options] = val;
        } else {
          plugin = val;
        }
  
        const alias = typeof plugin === "string" ? plugin : `${loc}$${i}`;
  
        // allow plugins to be specified as strings
        if (typeof plugin === "string") {
          const pluginLoc = resolvePlugin(plugin, dirname);
          if (pluginLoc) {
            plugin = require(pluginLoc);
          } else {
            throw new ReferenceError(messages.get("pluginUnknown", plugin, loc, i, dirname));
          }
        }
  
        plugin = OptionManager.normalisePlugin(plugin, loc, i, alias);
  
        return [plugin, options];
      });
    }
  
    /**
     * This is called when we want to merge the input `opts` into the
     * base options (passed as the `extendingOpts`: at top-level it's the
     * main options, at presets level it's presets options).
     *
     *  - `alias` is used to output pretty traces back to the original source.
     *  - `loc` is used to point to the original config.
     *  - `dirname` is used to resolve plugins relative to it.
     */
  
    mergeOptions({
      options: rawOpts,
      extending: extendingOpts,
      alias,
      loc,
      dirname
    }: MergeOptions) {
      alias = alias || "foreign";
      if (!rawOpts) return;
  
      //
      if (typeof rawOpts !== "object" || Array.isArray(rawOpts)) {
        this.log.error(`Invalid options type for ${alias}`, TypeError);
      }
  
      //
      const opts = cloneDeepWith(rawOpts, (val) => {
        if (val instanceof Plugin) {
          return val;
        }
      });
  
      //
      dirname = dirname || process.cwd();
      loc = loc || alias;
  
      for (const key in opts) {
        const option = config[key];
  
        // check for an unknown option
        if (!option && this.log) {
          if (removed[key]) {
            this.log.error(`Using removed Babel 5 option: ${alias}.${key} - ${removed[key].message}`, ReferenceError);
          } else {
            const unknownOptErr = `Unknown option: ${alias}.${key}. Check out http://babeljs.io/docs/usage/options/ for more information about options.`;
            const presetConfigErr = "A common cause of this error is the presence of a configuration options object without the corresponding preset name. Example:\n\nInvalid:\n  `{ presets: [{option: value}] }`\nValid:\n  `{ presets: [['presetName', {option: value}]] }`\n\nFor more detailed information on preset configuration, please see http://babeljs.io/docs/plugins/#pluginpresets-options.";
  
            this.log.error(`${unknownOptErr}\n\n${presetConfigErr}`, ReferenceError);
          }
        }
      }
  
      // normalise options
      normaliseOptions(opts);
  
      // resolve plugins
      if (opts.plugins) {
        opts.plugins = OptionManager.normalisePlugins(loc, dirname, opts.plugins);
      }
  
      // resolve presets
      if (opts.presets) {
        // If we're in the "pass per preset" mode, we resolve the presets
        // and keep them for further execution to calculate the options.
        if (opts.passPerPreset) {
          opts.presets = this.resolvePresets(opts.presets, dirname, (preset, presetLoc) => {
            this.mergeOptions({
              options: preset,
              extending: preset,
              alias: presetLoc,
              loc: presetLoc,
              dirname: dirname
            });
          });
        } else {
          // Otherwise, just merge presets options into the main options.
          this.mergePresets(opts.presets, dirname);
          delete opts.presets;
        }
      }
  
      // Merge them into current extending options in case of top-level
      // options. In case of presets, just re-assign options which are got
      // normalized during the `mergeOptions`.
      if (rawOpts === extendingOpts) {
        Object.assign(extendingOpts, opts);
      } else {
        merge(extendingOpts || this.options, opts);
      }
    }
  
    /**
     * Merges all presets into the main options in case we are not in the
     * "pass per preset" mode. Otherwise, options are calculated per preset.
     */
    mergePresets(presets: Array<string | Object>, dirname: string) {
      this.resolvePresets(presets, dirname, (presetOpts, presetLoc) => {
        this.mergeOptions({
          options: presetOpts,
          alias: presetLoc,
          loc: presetLoc,
          dirname: path.dirname(presetLoc || "")
        });
      });
    }
  
    /**
     * Resolves presets options which can be either direct object data,
     * or a module name to require.
     */
    resolvePresets(presets: Array<string | Object>, dirname: string, onResolve?) {
      return presets.map((val) => {
        let options;
        if (Array.isArray(val)) {
          if (val.length > 2) {
            throw new Error(`Unexpected extra options ${JSON.stringify(val.slice(2))} passed to preset.`);
          }
  
          [val, options] = val;
        }
  
        let presetLoc;
        try {
          if (typeof val === "string") {
            presetLoc = resolvePreset(val, dirname);
  
            if (!presetLoc) {
              throw new Error(`Couldn't find preset ${JSON.stringify(val)} relative to directory ` +
                JSON.stringify(dirname));
            }
  
            val = require(presetLoc);
          }
  
          // If the imported preset is a transpiled ES2015 module
          if (typeof val === "object" && val.__esModule) {
            // Try to grab the default export.
            if (val.default) {
              val = val.default;
            } else {
              // If there is no default export we treat all named exports as options
              // and just remove the __esModule. This is to support presets that have been
              // exporting named exports in the past, although we definitely want presets to
              // only use the default export (with either an object or a function)
              const { __esModule, ...rest } = val; // eslint-disable-line no-unused-vars
              val = rest;
            }
          }
  
          // For compatibility with babel-core < 6.13.x, allow presets to export an object with a
          // a 'buildPreset' function that will return the preset itself, while still exporting a
          // simple object (rather than a function), for supporting old Babel versions.
          if (typeof val === "object" && val.buildPreset) val = val.buildPreset;
  
  
          if (typeof val !== "function" && options !== undefined) {
            throw new Error(`Options ${JSON.stringify(options)} passed to ` +
              (presetLoc || "a preset") + " which does not accept options.");
          }
  
          if (typeof val === "function") val = val(context, options);
  
          if (typeof val !== "object") {
            throw new Error(`Unsupported preset format: ${val}.`);
          }
  
          onResolve && onResolve(val, presetLoc);
        } catch (e) {
          if (presetLoc) {
            e.message += ` (While processing preset: ${JSON.stringify(presetLoc)})`;
          }
          throw e;
        }
        return val;
      });
    }
  
    normaliseOptions() {
      const opts = this.options;
  
      for (const key in config) {
        const option = config[key];
        const val    = opts[key];
  
        // optional
        if (!val && option.optional) continue;
  
        // aliases
        if (option.alias) {
          opts[option.alias] = opts[option.alias] || val;
        } else {
          opts[key] = val;
        }
      }
    }
  
    init(opts: Object = {}): Object {
      for (const config of buildConfigChain(opts, this.log)) {
        this.mergeOptions(config);
      }
  
      // normalise
      this.normaliseOptions(opts);
  
      return this.options;
    }
  }
  
  OptionManager.memoisedPlugins = [];
  