// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('extensions', function() {
  'use strict';

  /**
   * @implements {extensions.ActivityLogDelegate}
   * @implements {extensions.ActivityLogEventDelegate}
   * @implements {extensions.ErrorPageDelegate}
   * @implements {extensions.ItemDelegate}
   * @implements {extensions.KeyboardShortcutDelegate}
   * @implements {extensions.LoadErrorDelegate}
   * @implements {extensions.PackDialogDelegate}
   * @implements {extensions.ToolbarDelegate}
   */
  class Service {
    constructor() {
      /** @private {boolean} */
      this.isDeleting_ = false;

      /** @private {!Set<string>} */
      this.eventsToIgnoreOnce_ = new Set();
    }

    getProfileConfiguration() {
      return new Promise(function(resolve, reject) {
        chrome.developerPrivate.getProfileConfiguration(resolve);
      });
    }

    getItemStateChangedTarget() {
      return chrome.developerPrivate.onItemStateChanged;
    }

    /**
     * @param {string} extensionId
     * @param {!chrome.developerPrivate.EventType} eventType
     * @return {boolean}
     */
    shouldIgnoreUpdate(extensionId, eventType) {
      return this.eventsToIgnoreOnce_.delete(`${extensionId}_${eventType}`);
    }

    /**
     * @param {string} extensionId
     * @param {!chrome.developerPrivate.EventType} eventType
     */
    ignoreNextEvent(extensionId, eventType) {
      this.eventsToIgnoreOnce_.add(`${extensionId}_${eventType}`);
    }

    getProfileStateChangedTarget() {
      return chrome.developerPrivate.onProfileStateChanged;
    }

    getExtensionsInfo() {
      return new Promise(function(resolve, reject) {
        chrome.developerPrivate.getExtensionsInfo(
            {includeDisabled: true, includeTerminated: true}, resolve);
      });
    }

    /** @override */
    getExtensionSize(id) {
      return new Promise(function(resolve, reject) {
        chrome.developerPrivate.getExtensionSize(id, resolve);
      });
    }

    /** @override */
    addRuntimeHostPermission(id, host) {
      return new Promise((resolve, reject) => {
        chrome.developerPrivate.addHostPermission(id, host, () => {
          if (chrome.runtime.lastError) {
            reject(chrome.runtime.lastError.message);
            return;
          }
          resolve();
        });
      });
    }

    /** @override */
    removeRuntimeHostPermission(id, host) {
      return new Promise((resolve, reject) => {
        chrome.developerPrivate.removeHostPermission(id, host, () => {
          if (chrome.runtime.lastError) {
            reject(chrome.runtime.lastError.message);
            return;
          }
          resolve();
        });
      });
    }

    /**
     * Opens a file browser dialog for the user to select a file (or directory).
     * @param {chrome.developerPrivate.SelectType} selectType
     * @param {chrome.developerPrivate.FileType} fileType
     * @return {Promise<string>} The promise to be resolved with the selected
     *     path.
     */
    chooseFilePath_(selectType, fileType) {
      return new Promise(function(resolve, reject) {
        chrome.developerPrivate.choosePath(
            selectType, fileType, function(path) {
              if (chrome.runtime.lastError &&
                  chrome.runtime.lastError != 'File selection was canceled.') {
                reject(chrome.runtime.lastError);
              } else {
                resolve(path || '');
              }
            });
      });
    }

    /** @override */
    updateExtensionCommandKeybinding(extensionId, commandName, keybinding) {
      chrome.developerPrivate.updateExtensionCommand({
        extensionId: extensionId,
        commandName: commandName,
        keybinding: keybinding,
      });
    }

    /** @override */
    updateExtensionCommandScope(extensionId, commandName, scope) {
      // The COMMAND_REMOVED event needs to be ignored since it is sent before
      // the command is added back with the updated scope but can be handled
      // after the COMMAND_ADDED event.
      this.ignoreNextEvent(
          extensionId, chrome.developerPrivate.EventType.COMMAND_REMOVED);
      chrome.developerPrivate.updateExtensionCommand({
        extensionId: extensionId,
        commandName: commandName,
        scope: scope,
      });
    }


    /** @override */
    setShortcutHandlingSuspended(isCapturing) {
      chrome.developerPrivate.setShortcutHandlingSuspended(isCapturing);
    }

    /**
     * @param {chrome.developerPrivate.LoadUnpackedOptions=} opt_options
     * @return {!Promise} A signal that loading finished, rejected if any error
     *     occurred.
     * @private
     */
    loadUnpackedHelper_(opt_options) {
      return new Promise(function(resolve, reject) {
        const options = Object.assign(
            {
              failQuietly: true,
              populateError: true,
            },
            opt_options);

        chrome.developerPrivate.loadUnpacked(
            options, (loadError) => {
              if (chrome.runtime.lastError &&
                  chrome.runtime.lastError.message !=
                      'File selection was canceled.') {
                throw new Error(chrome.runtime.lastError.message);
              }
              if (loadError) {
                return reject(loadError);
              }

              resolve();
            });
      });
    }

    /** @override */
    deleteItem(id) {
      if (this.isDeleting_) {
        return;
      }
      this.isDeleting_ = true;
      chrome.management.uninstall(id, {showConfirmDialog: true}, () => {
        // The "last error" was almost certainly the user canceling the dialog.
        // Do nothing. We only check it so we don't get noisy logs.
        /** @suppress {suspiciousCode} */
        chrome.runtime.lastError;
        this.isDeleting_ = false;
      });
    }

    /** @override */
    setItemEnabled(id, isEnabled) {
      chrome.management.setEnabled(id, isEnabled);
    }

    /** @override */
    setItemAllowedIncognito(id, isAllowedIncognito) {
      chrome.developerPrivate.updateExtensionConfiguration({
        extensionId: id,
        incognitoAccess: isAllowedIncognito,
      });
    }

    /** @override */
    setItemAllowedOnFileUrls(id, isAllowedOnFileUrls) {
      chrome.developerPrivate.updateExtensionConfiguration({
        extensionId: id,
        fileAccess: isAllowedOnFileUrls,
      });
    }

    /** @override */
    setItemHostAccess(id, hostAccess) {
      chrome.developerPrivate.updateExtensionConfiguration({
        extensionId: id,
        hostAccess: hostAccess,
      });
    }

    /** @override */
    setItemCollectsErrors(id, collectsErrors) {
      chrome.developerPrivate.updateExtensionConfiguration({
        extensionId: id,
        errorCollection: collectsErrors,
      });
    }

    /** @override */
    inspectItemView(id, view) {
      chrome.developerPrivate.openDevTools({
        extensionId: id,
        renderProcessId: view.renderProcessId,
        renderViewId: view.renderViewId,
        incognito: view.incognito,
      });
    }

    /**
     * @param {string} url
     * @override
     */
    openUrl(url) {
      window.open(url);
    }

    /** @override */
    reloadItem(id) {
      return new Promise(function(resolve, reject) {
        chrome.developerPrivate.reload(
            id, {failQuietly: true, populateErrorForUnpacked: true},
            (loadError) => {
              if (loadError) {
                reject(loadError);
                return;
              }

              resolve();
            });
      });
    }

    /** @override */
    repairItem(id) {
      chrome.developerPrivate.repairExtension(id);
    }

    /** @override */
    showItemOptionsPage(extension) {
      assert(extension && extension.optionsPage);
      if (extension.optionsPage.openInTab) {
        chrome.developerPrivate.showOptions(extension.id);
      } else {
        extensions.navigation.navigateTo({
          page: extensions.Page.DETAILS,
          subpage: extensions.Dialog.OPTIONS,
          extensionId: extension.id,
        });
      }
    }

    /** @override */
    setProfileInDevMode(inDevMode) {
      chrome.developerPrivate.updateProfileConfiguration(
          {inDeveloperMode: inDevMode});
    }

    /** @override */
    loadUnpacked() {
      return this.loadUnpackedHelper_();
    }

    /** @override */
    retryLoadUnpacked(retryGuid) {
      // Attempt to load an unpacked extension, optionally as another attempt at
      // a previously-specified load.
      return this.loadUnpackedHelper_({retryGuid: retryGuid});
    }

    /** @override */
    choosePackRootDirectory() {
      return this.chooseFilePath_(
          chrome.developerPrivate.SelectType.FOLDER,
          chrome.developerPrivate.FileType.LOAD);
    }

    /** @override */
    choosePrivateKeyPath() {
      return this.chooseFilePath_(
          chrome.developerPrivate.SelectType.FILE,
          chrome.developerPrivate.FileType.PEM);
    }

    /** @override */
    packExtension(rootPath, keyPath, flag, callback) {
      chrome.developerPrivate.packDirectory(rootPath, keyPath, flag, callback);
    }

    /** @override */
    updateAllExtensions() {
      return new Promise((resolve) => {
        chrome.developerPrivate.autoUpdate(resolve);
        chrome.metricsPrivate.recordUserAction('Options_UpdateExtensions');
      });
    }

    /** @override */
    deleteErrors(extensionId, errorIds, type) {
      chrome.developerPrivate.deleteExtensionErrors({
        extensionId: extensionId,
        errorIds: errorIds,
        type: type,
      });
    }

    /** @override */
    requestFileSource(args) {
      return new Promise(function(resolve, reject) {
        chrome.developerPrivate.requestFileSource(args, resolve);
      });
    }

    /** @override */
    showInFolder(id) {
      chrome.developerPrivate.showPath(id);
    }

    /** @override */
    getExtensionActivityLog(extensionId) {
      return new Promise(function(resolve, reject) {
        chrome.activityLogPrivate.getExtensionActivities(
            {
              activityType:
                  chrome.activityLogPrivate.ExtensionActivityFilter.ANY,
              extensionId: extensionId
            },
            resolve);
      });
    }

    /** @override */
    getFilteredExtensionActivityLog(extensionId, searchTerm) {
      const anyType = chrome.activityLogPrivate.ExtensionActivityFilter.ANY;

      // Construct one filter for each API call we will make: one for substring
      // search by api call, one for substring search by page URL, and one for
      // substring search by argument URL. % acts as a wildcard.
      const activityLogFilters = [
        {
          activityType: anyType,
          extensionId: extensionId,
          apiCall: `%${searchTerm}%`,
        },
        {
          activityType: anyType,
          extensionId: extensionId,
          pageUrl: `%${searchTerm}%`,
        },
        {
          activityType: anyType,
          extensionId: extensionId,
          argUrl: `%${searchTerm}%`
        }
      ];

      const promises = activityLogFilters.map(
          filter => new Promise(function(resolve, reject) {
            chrome.activityLogPrivate.getExtensionActivities(filter, resolve);
          }));

      return Promise.all(promises).then(results => {
        // We may have results that are present in one or more searches, so
        // we merge them here. We also assume that every distinct activity
        // id corresponds to exactly one activity.
        const activitiesById = new Map();
        for (const result of results) {
          for (const activity of result.activities) {
            activitiesById.set(activity.activityId, activity);
          }
        }

        return {activities: Array.from(activitiesById.values())};
      });
    }

    /** @override */
    deleteActivitiesById(activityIds) {
      return new Promise(function(resolve, reject) {
        chrome.activityLogPrivate.deleteActivities(activityIds, resolve);
      });
    }

    /** @override */
    deleteActivitiesFromExtension(extensionId) {
      return new Promise(function(resolve, reject) {
        chrome.activityLogPrivate.deleteActivitiesByExtension(
            extensionId, resolve);
      });
    }

    /** @override */
    getOnExtensionActivity() {
      return chrome.activityLogPrivate.onExtensionActivity;
    }

    /** @override */
    downloadActivities(rawActivityData, fileName) {
      const blob = new Blob([rawActivityData], {type: 'application/json'});
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = fileName;
      a.click();
    }

    /**
     * Attempts to load an unpacked extension via a drag-n-drop gesture.
     * @return {!Promise}
     */
    loadUnpackedFromDrag() {
      return this.loadUnpackedHelper_({useDraggedPath: true});
    }

    installDroppedFile() {
      chrome.developerPrivate.installDroppedFile();
    }

    notifyDragInstallInProgress() {
      chrome.developerPrivate.notifyDragInstallInProgress();
    }
  }

  cr.addSingletonGetter(Service);

  return {Service: Service};
});
