const path = require('path');
const async = require('async');
const CSON = require('season');
const fs = require('fs-plus');
const { Emitter, CompositeDisposable } = require('event-kit');
const dedent = require('dedent');

const CompileCache = require('./compile-cache');
const ModuleCache = require('./module-cache');
const BufferedProcess = require('./buffered-process');

// Extended: Loads and activates a package's main module and resources such as
// stylesheets, keymaps, grammar, editor properties, and menus.
module.exports = class Package {
  /*
  Section: Construction
  */

  constructor(params) {
    this.config = params.config;
    this.packageManager = params.packageManager;
    this.styleManager = params.styleManager;
    this.commandRegistry = params.commandRegistry;
    this.keymapManager = params.keymapManager;
    this.notificationManager = params.notificationManager;
    this.grammarRegistry = params.grammarRegistry;
    this.themeManager = params.themeManager;
    this.menuManager = params.menuManager;
    this.contextMenuManager = params.contextMenuManager;
    this.deserializerManager = params.deserializerManager;
    this.viewRegistry = params.viewRegistry;
    this.emitter = new Emitter();

    this.mainModule = null;
    this.path = params.path;
    this.preloadedPackage = params.preloadedPackage;
    this.metadata =
      params.metadata || this.packageManager.loadPackageMetadata(this.path);
    this.bundledPackage =
      params.bundledPackage != null
        ? params.bundledPackage
        : this.packageManager.isBundledPackagePath(this.path);
    this.name =
      (this.metadata && this.metadata.name) ||
      params.name ||
      path.basename(this.path);
    this.reset();
  }

  /*
  Section: Event Subscription
  */

  // Essential: Invoke the given callback when all packages have been activated.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidDeactivate(callback) {
    return this.emitter.on('did-deactivate', callback);
  }

  /*
  Section: Instance Methods
  */

  enable() {
    return this.config.removeAtKeyPath('core.disabledPackages', this.name);
  }

  disable() {
    return this.config.pushAtKeyPath('core.disabledPackages', this.name);
  }

  isTheme() {
    return this.metadata && this.metadata.theme;
  }

  measure(key, fn) {
    const startTime = Date.now();
    const value = fn();
    this[key] = Date.now() - startTime;
    return value;
  }

  getType() {
    return 'atom';
  }

  getStyleSheetPriority() {
    return 0;
  }

  preload() {
    this.loadKeymaps();
    this.loadMenus();
    this.registerDeserializerMethods();
    this.activateCoreStartupServices();
    this.registerURIHandler();
    this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata();
    this.requireMainModule();
    this.settingsPromise = this.loadSettings();

    this.activationDisposables = new CompositeDisposable();
    this.activateKeymaps();
    this.activateMenus();
    for (let settings of this.settings) {
      settings.activate(this.config);
    }
    this.settingsActivated = true;
  }

  finishLoading() {
    this.measure('loadTime', () => {
      this.path = path.join(this.packageManager.resourcePath, this.path);
      ModuleCache.add(this.path, this.metadata);

      this.loadStylesheets();
      // Unfortunately some packages are accessing `@mainModulePath`, so we need
      // to compute that variable eagerly also for preloaded packages.
      this.getMainModulePath();
    });
  }

  load() {
    this.measure('loadTime', () => {
      try {
        ModuleCache.add(this.path, this.metadata);

        this.loadKeymaps();
        this.loadMenus();
        this.loadStylesheets();
        this.registerDeserializerMethods();
        this.activateCoreStartupServices();
        this.registerURIHandler();
        this.registerTranspilerConfig();
        this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata();
        this.settingsPromise = this.loadSettings();
        if (this.shouldRequireMainModuleOnLoad() && this.mainModule == null) {
          this.requireMainModule();
        }
      } catch (error) {
        this.handleError(`Failed to load the ${this.name} package`, error);
      }
    });
    return this;
  }

  unload() {
    this.unregisterTranspilerConfig();
  }

  shouldRequireMainModuleOnLoad() {
    return !(
      this.metadata.deserializers ||
      this.metadata.viewProviders ||
      this.metadata.configSchema ||
      this.activationShouldBeDeferred() ||
      localStorage.getItem(this.getCanDeferMainModuleRequireStorageKey()) ===
        'true'
    );
  }

  reset() {
    this.stylesheets = [];
    this.keymaps = [];
    this.menus = [];
    this.grammars = [];
    this.settings = [];
    this.mainInitialized = false;
    this.mainActivated = false;
    this.deserialized = false;
  }

  initializeIfNeeded() {
    if (this.mainInitialized) return;
    this.measure('initializeTime', () => {
      try {
        // The main module's `initialize()` method is guaranteed to be called
        // before its `activate()`. This gives you a chance to handle the
        // serialized package state before the package's derserializers and view
        // providers are used.
        if (!this.mainModule) this.requireMainModule();
        if (typeof this.mainModule.initialize === 'function') {
          this.mainModule.initialize(
            this.packageManager.getPackageState(this.name) || {}
          );
        }
        this.mainInitialized = true;
      } catch (error) {
        this.handleError(
          `Failed to initialize the ${this.name} package`,
          error
        );
      }
    });
  }

  activate() {
    if (!this.grammarsPromise) this.grammarsPromise = this.loadGrammars();
    if (!this.activationPromise) {
      this.activationPromise = new Promise((resolve, reject) => {
        this.resolveActivationPromise = resolve;
        this.measure('activateTime', () => {
          try {
            this.activateResources();
            if (this.activationShouldBeDeferred()) {
              return this.subscribeToDeferredActivation();
            } else {
              return this.activateNow();
            }
          } catch (error) {
            return this.handleError(
              `Failed to activate the ${this.name} package`,
              error
            );
          }
        });
      });
    }

    return Promise.all([
      this.grammarsPromise,
      this.settingsPromise,
      this.activationPromise
    ]);
  }

  activateNow() {
    try {
      if (!this.mainModule) this.requireMainModule();
      this.configSchemaRegisteredOnActivate = this.registerConfigSchemaFromMainModule();
      this.registerViewProviders();
      this.activateStylesheets();
      if (this.mainModule && !this.mainActivated) {
        this.initializeIfNeeded();
        if (typeof this.mainModule.activateConfig === 'function') {
          this.mainModule.activateConfig();
        }
        if (typeof this.mainModule.activate === 'function') {
          this.mainModule.activate(
            this.packageManager.getPackageState(this.name) || {}
          );
        }
        this.mainActivated = true;
        this.activateServices();
      }
      if (this.activationCommandSubscriptions)
        this.activationCommandSubscriptions.dispose();
      if (this.activationHookSubscriptions)
        this.activationHookSubscriptions.dispose();
      if (this.workspaceOpenerSubscriptions)
        this.workspaceOpenerSubscriptions.dispose();
    } catch (error) {
      this.handleError(`Failed to activate the ${this.name} package`, error);
    }

    if (typeof this.resolveActivationPromise === 'function')
      this.resolveActivationPromise();
  }

  registerConfigSchemaFromMetadata() {
    const configSchema = this.metadata.configSchema;
    if (configSchema) {
      this.config.setSchema(this.name, {
        type: 'object',
        properties: configSchema
      });
      return true;
    } else {
      return false;
    }
  }

  registerConfigSchemaFromMainModule() {
    if (this.mainModule && !this.configSchemaRegisteredOnLoad) {
      if (typeof this.mainModule.config === 'object') {
        this.config.setSchema(this.name, {
          type: 'object',
          properties: this.mainModule.config
        });
        return true;
      }
    }
    return false;
  }

  // TODO: Remove. Settings view calls this method currently.
  activateConfig() {
    if (this.configSchemaRegisteredOnLoad) return;
    this.requireMainModule();
    this.registerConfigSchemaFromMainModule();
  }

  activateStylesheets() {
    if (this.stylesheetsActivated) return;

    this.stylesheetDisposables = new CompositeDisposable();

    const priority = this.getStyleSheetPriority();
    for (let [sourcePath, source] of this.stylesheets) {
      const match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./);

      let context;
      if (match) {
        context = match[1];
      } else if (this.metadata.theme === 'syntax') {
        context = 'atom-text-editor';
      }

      this.stylesheetDisposables.add(
        this.styleManager.addStyleSheet(source, {
          sourcePath,
          priority,
          context,
          skipDeprecatedSelectorsTransformation: this.bundledPackage
        })
      );
    }

    this.stylesheetsActivated = true;
  }

  activateResources() {
    if (!this.activationDisposables)
      this.activationDisposables = new CompositeDisposable();

    const packagesWithKeymapsDisabled = this.config.get(
      'core.packagesWithKeymapsDisabled'
    );
    if (
      packagesWithKeymapsDisabled &&
      packagesWithKeymapsDisabled.includes(this.name)
    ) {
      this.deactivateKeymaps();
    } else if (!this.keymapActivated) {
      this.activateKeymaps();
    }

    if (!this.menusActivated) {
      this.activateMenus();
    }

    if (!this.grammarsActivated) {
      for (let grammar of this.grammars) {
        grammar.activate();
      }
      this.grammarsActivated = true;
    }

    if (!this.settingsActivated) {
      for (let settings of this.settings) {
        settings.activate(this.config);
      }
      this.settingsActivated = true;
    }
  }

  activateKeymaps() {
    if (this.keymapActivated) return;

    this.keymapDisposables = new CompositeDisposable();

    const validateSelectors = !this.preloadedPackage;
    for (let [keymapPath, map] of this.keymaps) {
      this.keymapDisposables.add(
        this.keymapManager.add(keymapPath, map, 0, validateSelectors)
      );
    }
    this.menuManager.update();

    this.keymapActivated = true;
  }

  deactivateKeymaps() {
    if (!this.keymapActivated) return;
    if (this.keymapDisposables) {
      this.keymapDisposables.dispose();
    }
    this.menuManager.update();
    this.keymapActivated = false;
  }

  hasKeymaps() {
    for (let [, map] of this.keymaps) {
      if (map.length > 0) return true;
    }
    return false;
  }

  activateMenus() {
    const validateSelectors = !this.preloadedPackage;
    for (const [menuPath, map] of this.menus) {
      if (map['context-menu']) {
        try {
          const itemsBySelector = map['context-menu'];
          this.activationDisposables.add(
            this.contextMenuManager.add(itemsBySelector, validateSelectors)
          );
        } catch (error) {
          if (error.code === 'EBADSELECTOR') {
            error.message += ` in ${menuPath}`;
            error.stack += `\n  at ${menuPath}:1:1`;
          }
          throw error;
        }
      }
    }

    for (const [, map] of this.menus) {
      if (map.menu)
        this.activationDisposables.add(this.menuManager.add(map.menu));
    }

    this.menusActivated = true;
  }

  activateServices() {
    let methodName, version, versions;
    for (var name in this.metadata.providedServices) {
      ({ versions } = this.metadata.providedServices[name]);
      const servicesByVersion = {};
      for (version in versions) {
        methodName = versions[version];
        if (typeof this.mainModule[methodName] === 'function') {
          servicesByVersion[version] = this.mainModule[methodName]();
        }
      }
      this.activationDisposables.add(
        this.packageManager.serviceHub.provide(name, servicesByVersion)
      );
    }

    for (name in this.metadata.consumedServices) {
      ({ versions } = this.metadata.consumedServices[name]);
      for (version in versions) {
        methodName = versions[version];
        if (typeof this.mainModule[methodName] === 'function') {
          this.activationDisposables.add(
            this.packageManager.serviceHub.consume(
              name,
              version,
              this.mainModule[methodName].bind(this.mainModule)
            )
          );
        }
      }
    }
  }

  registerURIHandler() {
    const handlerConfig = this.getURIHandler();
    const methodName = handlerConfig && handlerConfig.method;
    if (methodName) {
      this.uriHandlerSubscription = this.packageManager.registerURIHandlerForPackage(
        this.name,
        (...args) => this.handleURI(methodName, args)
      );
    }
  }

  unregisterURIHandler() {
    if (this.uriHandlerSubscription) this.uriHandlerSubscription.dispose();
  }

  handleURI(methodName, args) {
    this.activate().then(() => {
      if (this.mainModule[methodName])
        this.mainModule[methodName].apply(this.mainModule, args);
    });
    if (!this.mainActivated) this.activateNow();
  }

  registerTranspilerConfig() {
    if (this.metadata.atomTranspilers) {
      CompileCache.addTranspilerConfigForPath(
        this.path,
        this.name,
        this.metadata,
        this.metadata.atomTranspilers
      );
    }
  }

  unregisterTranspilerConfig() {
    if (this.metadata.atomTranspilers) {
      CompileCache.removeTranspilerConfigForPath(this.path);
    }
  }

  loadKeymaps() {
    if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
      this.keymaps = [];
      for (const keymapPath in this.packageManager.packagesCache[this.name]
        .keymaps) {
        const keymapObject = this.packageManager.packagesCache[this.name]
          .keymaps[keymapPath];
        this.keymaps.push([`core:${keymapPath}`, keymapObject]);
      }
    } else {
      this.keymaps = this.getKeymapPaths().map(keymapPath => [
        keymapPath,
        CSON.readFileSync(keymapPath, { allowDuplicateKeys: false }) || {}
      ]);
    }
  }

  loadMenus() {
    if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
      this.menus = [];
      for (const menuPath in this.packageManager.packagesCache[this.name]
        .menus) {
        const menuObject = this.packageManager.packagesCache[this.name].menus[
          menuPath
        ];
        this.menus.push([`core:${menuPath}`, menuObject]);
      }
    } else {
      this.menus = this.getMenuPaths().map(menuPath => [
        menuPath,
        CSON.readFileSync(menuPath) || {}
      ]);
    }
  }

  getKeymapPaths() {
    const keymapsDirPath = path.join(this.path, 'keymaps');
    if (this.metadata.keymaps) {
      return this.metadata.keymaps.map(name =>
        fs.resolve(keymapsDirPath, name, ['json', 'cson', ''])
      );
    } else {
      return fs.listSync(keymapsDirPath, ['cson', 'json']);
    }
  }

  getMenuPaths() {
    const menusDirPath = path.join(this.path, 'menus');
    if (this.metadata.menus) {
      return this.metadata.menus.map(name =>
        fs.resolve(menusDirPath, name, ['json', 'cson', ''])
      );
    } else {
      return fs.listSync(menusDirPath, ['cson', 'json']);
    }
  }

  loadStylesheets() {
    this.stylesheets = this.getStylesheetPaths().map(stylesheetPath => [
      stylesheetPath,
      this.themeManager.loadStylesheet(stylesheetPath, true)
    ]);
  }

  registerDeserializerMethods() {
    if (this.metadata.deserializers) {
      Object.keys(this.metadata.deserializers).forEach(deserializerName => {
        const methodName = this.metadata.deserializers[deserializerName];
        this.deserializerManager.add({
          name: deserializerName,
          deserialize: (state, atomEnvironment) => {
            this.registerViewProviders();
            this.requireMainModule();
            this.initializeIfNeeded();
            if (atomEnvironment.packages.hasActivatedInitialPackages()) {
              // Only explicitly activate the package if initial packages
              // have finished activating. This is because deserialization
              // generally occurs at Atom startup, which happens before the
              // workspace element is added to the DOM and is inconsistent with
              // with when initial package activation occurs. Triggering activation
              // immediately may cause problems with packages that expect to
              // always have access to the workspace element.
              // Otherwise, we just set the deserialized flag and package-manager
              // will activate this package as normal during initial package activation.
              this.activateNow();
            }
            this.deserialized = true;
            return this.mainModule[methodName](state, atomEnvironment);
          }
        });
      });
    }
  }

  activateCoreStartupServices() {
    const directoryProviderService =
      this.metadata.providedServices &&
      this.metadata.providedServices['atom.directory-provider'];
    if (directoryProviderService) {
      this.requireMainModule();
      const servicesByVersion = {};
      for (let version in directoryProviderService.versions) {
        const methodName = directoryProviderService.versions[version];
        if (typeof this.mainModule[methodName] === 'function') {
          servicesByVersion[version] = this.mainModule[methodName]();
        }
      }
      this.packageManager.serviceHub.provide(
        'atom.directory-provider',
        servicesByVersion
      );
    }
  }

  registerViewProviders() {
    if (this.metadata.viewProviders && !this.registeredViewProviders) {
      this.requireMainModule();
      this.metadata.viewProviders.forEach(methodName => {
        this.viewRegistry.addViewProvider(model => {
          this.initializeIfNeeded();
          return this.mainModule[methodName](model);
        });
      });
      this.registeredViewProviders = true;
    }
  }

  getStylesheetsPath() {
    return path.join(this.path, 'styles');
  }

  getStylesheetPaths() {
    if (
      this.bundledPackage &&
      this.packageManager.packagesCache[this.name] &&
      this.packageManager.packagesCache[this.name].styleSheetPaths
    ) {
      const { styleSheetPaths } = this.packageManager.packagesCache[this.name];
      return styleSheetPaths.map(styleSheetPath =>
        path.join(this.path, styleSheetPath)
      );
    } else {
      let indexStylesheet;
      const stylesheetDirPath = this.getStylesheetsPath();
      if (this.metadata.mainStyleSheet) {
        return [fs.resolve(this.path, this.metadata.mainStyleSheet)];
      } else if (this.metadata.styleSheets) {
        return this.metadata.styleSheets.map(name =>
          fs.resolve(stylesheetDirPath, name, ['css', 'less', ''])
        );
      } else if (
        (indexStylesheet = fs.resolve(this.path, 'index', ['css', 'less']))
      ) {
        return [indexStylesheet];
      } else {
        return fs.listSync(stylesheetDirPath, ['css', 'less']);
      }
    }
  }

  loadGrammarsSync() {
    if (this.grammarsLoaded) return;

    let grammarPaths;
    if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
      ({ grammarPaths } = this.packageManager.packagesCache[this.name]);
    } else {
      grammarPaths = fs.listSync(path.join(this.path, 'grammars'), [
        'json',
        'cson'
      ]);
    }

    for (let grammarPath of grammarPaths) {
      if (
        this.preloadedPackage &&
        this.packageManager.packagesCache[this.name]
      ) {
        grammarPath = path.resolve(
          this.packageManager.resourcePath,
          grammarPath
        );
      }

      try {
        const grammar = this.grammarRegistry.readGrammarSync(grammarPath);
        grammar.packageName = this.name;
        grammar.bundledPackage = this.bundledPackage;
        this.grammars.push(grammar);
        grammar.activate();
      } catch (error) {
        console.warn(
          `Failed to load grammar: ${grammarPath}`,
          error.stack || error
        );
      }
    }

    this.grammarsLoaded = true;
    this.grammarsActivated = true;
  }

  loadGrammars() {
    if (this.grammarsLoaded) return Promise.resolve();

    const loadGrammar = (grammarPath, callback) => {
      if (this.preloadedPackage) {
        grammarPath = path.resolve(
          this.packageManager.resourcePath,
          grammarPath
        );
      }

      return this.grammarRegistry.readGrammar(grammarPath, (error, grammar) => {
        if (error) {
          const detail = `${error.message} in ${grammarPath}`;
          const stack = `${error.stack}\n  at ${grammarPath}:1:1`;
          this.notificationManager.addFatalError(
            `Failed to load a ${this.name} package grammar`,
            { stack, detail, packageName: this.name, dismissable: true }
          );
        } else {
          grammar.packageName = this.name;
          grammar.bundledPackage = this.bundledPackage;
          this.grammars.push(grammar);
          if (this.grammarsActivated) grammar.activate();
        }
        return callback();
      });
    };

    return new Promise(resolve => {
      if (
        this.preloadedPackage &&
        this.packageManager.packagesCache[this.name]
      ) {
        const { grammarPaths } = this.packageManager.packagesCache[this.name];
        return async.each(grammarPaths, loadGrammar, () => resolve());
      } else {
        const grammarsDirPath = path.join(this.path, 'grammars');
        fs.exists(grammarsDirPath, grammarsDirExists => {
          if (!grammarsDirExists) return resolve();
          fs.list(grammarsDirPath, ['json', 'cson'], (error, grammarPaths) => {
            if (error || !grammarPaths) return resolve();
            async.each(grammarPaths, loadGrammar, () => resolve());
          });
        });
      }
    });
  }

  loadSettings() {
    this.settings = [];

    const loadSettingsFile = (settingsPath, callback) => {
      return SettingsFile.load(settingsPath, (error, settingsFile) => {
        if (error) {
          const detail = `${error.message} in ${settingsPath}`;
          const stack = `${error.stack}\n  at ${settingsPath}:1:1`;
          this.notificationManager.addFatalError(
            `Failed to load the ${this.name} package settings`,
            { stack, detail, packageName: this.name, dismissable: true }
          );
        } else {
          this.settings.push(settingsFile);
          if (this.settingsActivated) settingsFile.activate(this.config);
        }
        return callback();
      });
    };

    if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) {
      for (let settingsPath in this.packageManager.packagesCache[this.name]
        .settings) {
        const properties = this.packageManager.packagesCache[this.name]
          .settings[settingsPath];
        const settingsFile = new SettingsFile(
          `core:${settingsPath}`,
          properties || {}
        );
        this.settings.push(settingsFile);
        if (this.settingsActivated) settingsFile.activate(this.config);
      }
    } else {
      return new Promise(resolve => {
        const settingsDirPath = path.join(this.path, 'settings');
        fs.exists(settingsDirPath, settingsDirExists => {
          if (!settingsDirExists) return resolve();
          fs.list(settingsDirPath, ['json', 'cson'], (error, settingsPaths) => {
            if (error || !settingsPaths) return resolve();
            async.each(settingsPaths, loadSettingsFile, () => resolve());
          });
        });
      });
    }
  }

  serialize() {
    if (this.mainActivated) {
      if (typeof this.mainModule.serialize === 'function') {
        try {
          return this.mainModule.serialize();
        } catch (error) {
          console.error(
            `Error serializing package '${this.name}'`,
            error.stack
          );
        }
      }
    }
  }

  async deactivate() {
    this.activationPromise = null;
    this.resolveActivationPromise = null;
    if (this.activationCommandSubscriptions)
      this.activationCommandSubscriptions.dispose();
    if (this.activationHookSubscriptions)
      this.activationHookSubscriptions.dispose();
    this.configSchemaRegisteredOnActivate = false;
    this.unregisterURIHandler();
    this.deactivateResources();
    this.deactivateKeymaps();

    if (!this.mainActivated) {
      this.emitter.emit('did-deactivate');
      return;
    }

    if (typeof this.mainModule.deactivate === 'function') {
      try {
        const deactivationResult = this.mainModule.deactivate();
        if (
          deactivationResult &&
          typeof deactivationResult.then === 'function'
        ) {
          await deactivationResult;
        }
      } catch (error) {
        console.error(`Error deactivating package '${this.name}'`, error.stack);
      }
    }

    if (typeof this.mainModule.deactivateConfig === 'function') {
      try {
        await this.mainModule.deactivateConfig();
      } catch (error) {
        console.error(`Error deactivating package '${this.name}'`, error.stack);
      }
    }

    this.mainActivated = false;
    this.mainInitialized = false;
    this.emitter.emit('did-deactivate');
  }

  deactivateResources() {
    for (let grammar of this.grammars) {
      grammar.deactivate();
    }
    for (let settings of this.settings) {
      settings.deactivate(this.config);
    }

    if (this.stylesheetDisposables) this.stylesheetDisposables.dispose();
    if (this.activationDisposables) this.activationDisposables.dispose();
    if (this.keymapDisposables) this.keymapDisposables.dispose();

    this.stylesheetsActivated = false;
    this.grammarsActivated = false;
    this.settingsActivated = false;
    this.menusActivated = false;
  }

  reloadStylesheets() {
    try {
      this.loadStylesheets();
    } catch (error) {
      this.handleError(
        `Failed to reload the ${this.name} package stylesheets`,
        error
      );
    }

    if (this.stylesheetDisposables) this.stylesheetDisposables.dispose();
    this.stylesheetDisposables = new CompositeDisposable();
    this.stylesheetsActivated = false;
    this.activateStylesheets();
  }

  requireMainModule() {
    if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
      if (this.packageManager.packagesCache[this.name].main) {
        this.mainModule = require(this.packageManager.packagesCache[this.name]
          .main);
        return this.mainModule;
      }
    } else if (this.mainModuleRequired) {
      return this.mainModule;
    } else if (!this.isCompatible()) {
      const nativeModuleNames = this.incompatibleModules
        .map(m => m.name)
        .join(', ');
      console.warn(dedent`
        Failed to require the main module of '${
          this.name
        }' because it requires one or more incompatible native modules (${nativeModuleNames}).
        Run \`apm rebuild\` in the package directory and restart Atom to resolve.\
      `);
    } else {
      const mainModulePath = this.getMainModulePath();
      if (fs.isFileSync(mainModulePath)) {
        this.mainModuleRequired = true;

        const previousViewProviderCount = this.viewRegistry.getViewProviderCount();
        const previousDeserializerCount = this.deserializerManager.getDeserializerCount();
        this.mainModule = require(mainModulePath);
        if (
          this.viewRegistry.getViewProviderCount() ===
            previousViewProviderCount &&
          this.deserializerManager.getDeserializerCount() ===
            previousDeserializerCount
        ) {
          localStorage.setItem(
            this.getCanDeferMainModuleRequireStorageKey(),
            'true'
          );
        }
        return this.mainModule;
      }
    }
  }

  getMainModulePath() {
    if (this.resolvedMainModulePath) return this.mainModulePath;
    this.resolvedMainModulePath = true;

    if (this.bundledPackage && this.packageManager.packagesCache[this.name]) {
      if (this.packageManager.packagesCache[this.name].main) {
        this.mainModulePath = path.resolve(
          this.packageManager.resourcePath,
          'static',
          this.packageManager.packagesCache[this.name].main
        );
      } else {
        this.mainModulePath = null;
      }
    } else {
      const mainModulePath = this.metadata.main
        ? path.join(this.path, this.metadata.main)
        : path.join(this.path, 'index');
      this.mainModulePath = fs.resolveExtension(mainModulePath, [
        '',
        ...CompileCache.supportedExtensions
      ]);
    }
    return this.mainModulePath;
  }

  activationShouldBeDeferred() {
    return (
      !this.deserialized &&
      (this.hasActivationCommands() ||
        this.hasActivationHooks() ||
        this.hasWorkspaceOpeners() ||
        this.hasDeferredURIHandler())
    );
  }

  hasActivationHooks() {
    const hooks = this.getActivationHooks();
    return hooks && hooks.length > 0;
  }

  hasWorkspaceOpeners() {
    const openers = this.getWorkspaceOpeners();
    return openers && openers.length > 0;
  }

  hasActivationCommands() {
    const object = this.getActivationCommands();
    for (let selector in object) {
      const commands = object[selector];
      if (commands.length > 0) return true;
    }
    return false;
  }

  hasDeferredURIHandler() {
    const handler = this.getURIHandler();
    return handler && handler.deferActivation !== false;
  }

  subscribeToDeferredActivation() {
    this.subscribeToActivationCommands();
    this.subscribeToActivationHooks();
    this.subscribeToWorkspaceOpeners();
  }

  subscribeToActivationCommands() {
    this.activationCommandSubscriptions = new CompositeDisposable();
    const object = this.getActivationCommands();
    for (let selector in object) {
      const commands = object[selector];
      for (let command of commands) {
        ((selector, command) => {
          // Add dummy command so it appears in menu.
          // The real command will be registered on package activation
          try {
            this.activationCommandSubscriptions.add(
              this.commandRegistry.add(selector, command, function() {})
            );
          } catch (error) {
            if (error.code === 'EBADSELECTOR') {
              const metadataPath = path.join(this.path, 'package.json');
              error.message += ` in ${metadataPath}`;
              error.stack += `\n  at ${metadataPath}:1:1`;
            }
            throw error;
          }

          this.activationCommandSubscriptions.add(
            this.commandRegistry.onWillDispatch(event => {
              if (event.type !== command) return;
              let currentTarget = event.target;
              while (currentTarget) {
                if (currentTarget.webkitMatchesSelector(selector)) {
                  this.activationCommandSubscriptions.dispose();
                  this.activateNow();
                  break;
                }
                currentTarget = currentTarget.parentElement;
              }
            })
          );
        })(selector, command);
      }
    }
  }

  getActivationCommands() {
    if (this.activationCommands) return this.activationCommands;

    this.activationCommands = {};

    if (this.metadata.activationCommands) {
      for (let selector in this.metadata.activationCommands) {
        const commands = this.metadata.activationCommands[selector];
        if (!this.activationCommands[selector])
          this.activationCommands[selector] = [];
        if (typeof commands === 'string') {
          this.activationCommands[selector].push(commands);
        } else if (Array.isArray(commands)) {
          this.activationCommands[selector].push(...commands);
        }
      }
    }

    return this.activationCommands;
  }

  subscribeToActivationHooks() {
    this.activationHookSubscriptions = new CompositeDisposable();
    for (let hook of this.getActivationHooks()) {
      if (typeof hook === 'string' && hook.trim().length > 0) {
        this.activationHookSubscriptions.add(
          this.packageManager.onDidTriggerActivationHook(hook, () =>
            this.activateNow()
          )
        );
      }
    }
  }

  getActivationHooks() {
    if (this.metadata && this.activationHooks) return this.activationHooks;

    if (this.metadata.activationHooks) {
      if (Array.isArray(this.metadata.activationHooks)) {
        this.activationHooks = Array.from(
          new Set(this.metadata.activationHooks)
        );
      } else if (typeof this.metadata.activationHooks === 'string') {
        this.activationHooks = [this.metadata.activationHooks];
      } else {
        this.activationHooks = [];
      }
    } else {
      this.activationHooks = [];
    }

    return this.activationHooks;
  }

  subscribeToWorkspaceOpeners() {
    this.workspaceOpenerSubscriptions = new CompositeDisposable();
    for (let opener of this.getWorkspaceOpeners()) {
      this.workspaceOpenerSubscriptions.add(
        atom.workspace.addOpener(filePath => {
          if (filePath === opener) {
            this.activateNow();
            this.workspaceOpenerSubscriptions.dispose();
            return atom.workspace.createItemForURI(opener);
          }
        })
      );
    }
  }

  getWorkspaceOpeners() {
    if (this.workspaceOpeners) return this.workspaceOpeners;

    if (this.metadata.workspaceOpeners) {
      if (Array.isArray(this.metadata.workspaceOpeners)) {
        this.workspaceOpeners = Array.from(
          new Set(this.metadata.workspaceOpeners)
        );
      } else if (typeof this.metadata.workspaceOpeners === 'string') {
        this.workspaceOpeners = [this.metadata.workspaceOpeners];
      } else {
        this.workspaceOpeners = [];
      }
    } else {
      this.workspaceOpeners = [];
    }

    return this.workspaceOpeners;
  }

  getURIHandler() {
    return this.metadata && this.metadata.uriHandler;
  }

  // Does the given module path contain native code?
  isNativeModule(modulePath) {
    try {
      return (
        fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node'])
          .length > 0
      );
    } catch (error) {
      return false;
    }
  }

  // Get an array of all the native modules that this package depends on.
  //
  // First try to get this information from
  // @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't
  // exist, recurse through all dependencies.
  getNativeModuleDependencyPaths() {
    const nativeModulePaths = [];

    if (this.metadata._atomModuleCache) {
      const relativeNativeModuleBindingPaths =
        (this.metadata._atomModuleCache.extensions &&
          this.metadata._atomModuleCache.extensions['.node']) ||
        [];
      for (let relativeNativeModuleBindingPath of relativeNativeModuleBindingPaths) {
        const nativeModulePath = path.join(
          this.path,
          relativeNativeModuleBindingPath,
          '..',
          '..',
          '..'
        );
        nativeModulePaths.push(nativeModulePath);
      }
      return nativeModulePaths;
    }

    var traversePath = nodeModulesPath => {
      try {
        for (let modulePath of fs.listSync(nodeModulesPath)) {
          if (this.isNativeModule(modulePath))
            nativeModulePaths.push(modulePath);
          traversePath(path.join(modulePath, 'node_modules'));
        }
      } catch (error) {}
    };

    traversePath(path.join(this.path, 'node_modules'));

    return nativeModulePaths;
  }

  /*
  Section: Native Module Compatibility
  */

  // Extended: Are all native modules depended on by this package correctly
  // compiled against the current version of Atom?
  //
  // Incompatible packages cannot be activated.
  //
  // Returns a {Boolean}, true if compatible, false if incompatible.
  isCompatible() {
    if (this.compatible == null) {
      if (this.preloadedPackage) {
        this.compatible = true;
      } else if (this.getMainModulePath()) {
        this.incompatibleModules = this.getIncompatibleNativeModules();
        this.compatible =
          this.incompatibleModules.length === 0 &&
          this.getBuildFailureOutput() == null;
      } else {
        this.compatible = true;
      }
    }
    return this.compatible;
  }

  // Extended: Rebuild native modules in this package's dependencies for the
  // current version of Atom.
  //
  // Returns a {Promise} that resolves with an object containing `code`,
  // `stdout`, and `stderr` properties based on the results of running
  // `apm rebuild` on the package.
  rebuild() {
    return new Promise(resolve =>
      this.runRebuildProcess(result => {
        if (result.code === 0) {
          global.localStorage.removeItem(
            this.getBuildFailureOutputStorageKey()
          );
        } else {
          this.compatible = false;
          global.localStorage.setItem(
            this.getBuildFailureOutputStorageKey(),
            result.stderr
          );
        }
        global.localStorage.setItem(
          this.getIncompatibleNativeModulesStorageKey(),
          '[]'
        );
        resolve(result);
      })
    );
  }

  // Extended: If a previous rebuild failed, get the contents of stderr.
  //
  // Returns a {String} or null if no previous build failure occurred.
  getBuildFailureOutput() {
    return global.localStorage.getItem(this.getBuildFailureOutputStorageKey());
  }

  runRebuildProcess(done) {
    let stderr = '';
    let stdout = '';
    return new BufferedProcess({
      command: this.packageManager.getApmPath(),
      args: ['rebuild', '--no-color'],
      options: { cwd: this.path },
      stderr(output) {
        stderr += output;
      },
      stdout(output) {
        stdout += output;
      },
      exit(code) {
        done({ code, stdout, stderr });
      }
    });
  }

  getBuildFailureOutputStorageKey() {
    return `installed-packages:${this.name}:${
      this.metadata.version
    }:build-error`;
  }

  getIncompatibleNativeModulesStorageKey() {
    const electronVersion = process.versions.electron;
    return `installed-packages:${this.name}:${
      this.metadata.version
    }:electron-${electronVersion}:incompatible-native-modules`;
  }

  getCanDeferMainModuleRequireStorageKey() {
    return `installed-packages:${this.name}:${
      this.metadata.version
    }:can-defer-main-module-require`;
  }

  // Get the incompatible native modules that this package depends on.
  // This recurses through all dependencies and requires all modules that
  // contain a `.node` file.
  //
  // This information is cached in local storage on a per package/version basis
  // to minimize the impact on startup time.
  getIncompatibleNativeModules() {
    if (!this.packageManager.devMode) {
      try {
        const arrayAsString = global.localStorage.getItem(
          this.getIncompatibleNativeModulesStorageKey()
        );
        if (arrayAsString) return JSON.parse(arrayAsString);
      } catch (error1) {}
    }

    const incompatibleNativeModules = [];
    for (let nativeModulePath of this.getNativeModuleDependencyPaths()) {
      try {
        require(nativeModulePath);
      } catch (error) {
        let version;
        try {
          ({ version } = require(`${nativeModulePath}/package.json`));
        } catch (error2) {}
        incompatibleNativeModules.push({
          path: nativeModulePath,
          name: path.basename(nativeModulePath),
          version,
          error: error.message
        });
      }
    }

    global.localStorage.setItem(
      this.getIncompatibleNativeModulesStorageKey(),
      JSON.stringify(incompatibleNativeModules)
    );

    return incompatibleNativeModules;
  }

  handleError(message, error) {
    if (atom.inSpecMode()) throw error;

    let detail, location, stack;
    if (error.filename && error.location && error instanceof SyntaxError) {
      location = `${error.filename}:${error.location.first_line + 1}:${error
        .location.first_column + 1}`;
      detail = `${error.message} in ${location}`;
      stack = 'SyntaxError: ' + error.message + '\n' + 'at ' + location;
    } else if (
      error.less &&
      error.filename &&
      error.column != null &&
      error.line != null
    ) {
      location = `${error.filename}:${error.line}:${error.column}`;
      detail = `${error.message} in ${location}`;
      stack = 'LessError: ' + error.message + '\n' + 'at ' + location;
    } else {
      detail = error.message;
      stack = error.stack || error;
    }

    this.notificationManager.addFatalError(message, {
      stack,
      detail,
      packageName: this.name,
      dismissable: true
    });
  }
};

class SettingsFile {
  static load(path, callback) {
    CSON.readFile(path, (error, properties = {}) => {
      if (error) {
        callback(error);
      } else {
        callback(null, new SettingsFile(path, properties));
      }
    });
  }

  constructor(path, properties) {
    this.path = path;
    this.properties = properties;
  }

  activate(config) {
    for (let selector in this.properties) {
      config.set(null, this.properties[selector], {
        scopeSelector: selector,
        source: this.path
      });
    }
  }

  deactivate(config) {
    for (let selector in this.properties) {
      config.unset(null, { scopeSelector: selector, source: this.path });
    }
  }
}
