// Copyright 2018 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.

/* eslint-disable rulesdir/no_underscored_properties */

import * as Common from '../common/common.js';
import * as i18n from '../i18n/i18n.js';
import * as SDK from '../sdk/sdk.js';

import {ProtocolService} from './LighthouseProtocolService.js';  // eslint-disable-line no-unused-vars

export const UIStrings = {
  /**
  *@description Explanation for user that Ligthhouse can only audit HTTP/HTTPS pages
  */
  canOnlyAuditHttphttpsPagesAnd:
      'Can only audit HTTP/HTTPS pages and `Chrome` extensions. Navigate to a different page to start an audit.',
  /**
  *@description Text when stored data in one location may affect Lighthouse run
  *@example {IndexedDB} PH1
  */
  thereMayBeStoredDataAffectingSingular:
      'There may be stored data affecting loading performance in this location: {PH1}. Audit this page in an incognito window to prevent those resources from affecting your scores.',
  /**
  *@description Text when stored data in multiple locations may affect Lighthouse run
  *@example {IndexedDB, WebSQL} PH1
  */
  thereMayBeStoredDataAffectingLoadingPlural:
      'There may be stored data affecting loading performance in these locations: {PH1}. Audit this page in an incognito window to prevent those resources from affecting your scores.',
  /**
  *@description Help text in Lighthouse Controller
  */
  multipleTabsAreBeingControlledBy:
      'Multiple tabs are being controlled by the same `service worker`. Close your other tabs on the same origin to audit this page.',
  /**
  *@description Help text in Lighthouse Controller
  */
  atLeastOneCategoryMustBeSelected: 'At least one category must be selected.',
  /**
  *@description Text in Application Panel Sidebar of the Application panel
  */
  localStorage: 'Local Storage',
  /**
  *@description Text in Application Panel Sidebar of the Application panel
  */
  indexeddb: 'IndexedDB',
  /**
  *@description Text in Application Panel Sidebar of the Application panel
  */
  webSql: 'Web SQL',
  /**
  *@description Text of checkbox to include running the performance audits in Lighthouse
  */
  performance: 'Performance',
  /**
  *@description Tooltip text of checkbox to include running the performance audits in Lighthouse
  */
  howLongDoesThisAppTakeToShow: 'How long does this app take to show content and become usable',
  /**
  *@description Text of checkbox to include running the Progressive Web App audits in Lighthouse
  */
  progressiveWebApp: 'Progressive Web App',
  /**
  *@description Tooltip text of checkbox to include running the Progressive Web App audits in Lighthouse
  */
  doesThisPageMeetTheStandardOfA: 'Does this page meet the standard of a Progressive Web App',
  /**
  *@description Text of checkbox to include running the Best Practices audits in Lighthouse
  */
  bestPractices: 'Best practices',
  /**
  *@description Tooltip text of checkbox to include running the Best Practices audits in Lighthouse
  */
  doesThisPageFollowBestPractices: 'Does this page follow best practices for modern web development',
  /**
  *@description Text of checkbox to include running the Accessibility audits in Lighthouse
  */
  accessibility: 'Accessibility',
  /**
  *@description Tooltip text of checkbox to include running the Accessibility audits in Lighthouse
  */
  isThisPageUsableByPeopleWith: 'Is this page usable by people with disabilities or impairments',
  /**
  *@description Text of checkbox to include running the Search Engine Optimization audits in Lighthouse
  */
  seo: 'SEO',
  /**
  *@description Tooltip text of checkbox to include running the Search Engine Optimization audits in Lighthouse
  */
  isThisPageOptimizedForSearch: 'Is this page optimized for search engine results ranking',
  /**
  *@description Text of checkbox to include running the Ad speed and quality audits in Lighthouse
  */
  publisherAds: 'Publisher Ads',
  /**
  *@description Tooltip text of checkbox to include running the Ad speed and quality audits in Lighthouse
  */
  isThisPageOptimizedForAdSpeedAnd: 'Is this page optimized for ad speed and quality',
  /**
  *@description Text of checkbox to emulate mobile device behavior when running audits in Lighthouse
  */
  applyMobileEmulation: 'Apply mobile emulation',
  /**
  *@description Tooltip text of checkbox to emulate mobile device behavior when running audits in Lighthouse
  */
  applyMobileEmulationDuring: 'Apply mobile emulation during auditing',
  /**
  *@description Text for the mobile platform, as opposed to desktop
  */
  mobile: 'Mobile',
  /**
  *@description Text for the desktop platform, as opposed to mobile
  */
  desktop: 'Desktop',
  /**
  *@description Text for option to enable simulated throttling in Lighthouse Panel
  */
  simulatedThrottling: 'Simulated throttling',
  /**
  *@description Tooltip text that appears when hovering over the 'Simulated Throttling' checkbox in the settings pane opened by clicking the setting cog in the start view of the audits panel
  */
  simulateASlowerPageLoadBasedOn:
      'Simulate a slower page load, based on data from an initial unthrottled load. If disabled, the page is actually slowed with applied throttling.',
  /**
  *@description Text of checkbox to reset storage features prior to running audits in Lighthouse
  */
  clearStorage: 'Clear storage',
  /**
  *@description Tooltip text of checkbox to reset storage features prior to running audits in Lighthouse
  */
  resetStorageLocalstorage:
      'Reset storage (localStorage, IndexedDB, etc) before auditing. (Good for performance & PWA testing)',
};
const str_ = i18n.i18n.registerUIStrings('lighthouse/LighthouseController.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class LighthouseController extends Common.ObjectWrapper.ObjectWrapper implements
    SDK.SDKModel.SDKModelObserver<SDK.ServiceWorkerManager.ServiceWorkerManager> {
  _manager?: SDK.ServiceWorkerManager.ServiceWorkerManager|null;
  _serviceWorkerListeners?: Common.EventTarget.EventDescriptor[];
  _inspectedURL?: string;

  constructor(protocolService: ProtocolService) {
    super();

    protocolService.registerStatusCallback(
        message => this.dispatchEventToListeners(Events.AuditProgressChanged, {message}));

    for (const preset of Presets) {
      preset.setting.addChangeListener(this.recomputePageAuditability.bind(this));
    }

    for (const runtimeSetting of RuntimeSettings) {
      runtimeSetting.setting.addChangeListener(this.recomputePageAuditability.bind(this));
    }

    SDK.SDKModel.TargetManager.instance().observeModels(SDK.ServiceWorkerManager.ServiceWorkerManager, this);
    SDK.SDKModel.TargetManager.instance().addEventListener(
        SDK.SDKModel.Events.InspectedURLChanged, this.recomputePageAuditability, this);
  }

  modelAdded(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void {
    if (this._manager) {
      return;
    }

    this._manager = serviceWorkerManager;
    this._serviceWorkerListeners = [
      this._manager.addEventListener(
          SDK.ServiceWorkerManager.Events.RegistrationUpdated, this.recomputePageAuditability, this),
      this._manager.addEventListener(
          SDK.ServiceWorkerManager.Events.RegistrationDeleted, this.recomputePageAuditability, this),
    ];

    this.recomputePageAuditability();
  }

  modelRemoved(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void {
    if (this._manager !== serviceWorkerManager) {
      return;
    }
    if (this._serviceWorkerListeners) {
      Common.EventTarget.EventTarget.removeEventListeners(this._serviceWorkerListeners);
    }
    this._manager = null;
    this.recomputePageAuditability();
  }

  _hasActiveServiceWorker(): boolean {
    if (!this._manager) {
      return false;
    }

    const mainTarget = this._manager.target();
    if (!mainTarget) {
      return false;
    }

    const inspectedURL = Common.ParsedURL.ParsedURL.fromString(mainTarget.inspectedURL());
    const inspectedOrigin = inspectedURL && inspectedURL.securityOrigin();
    for (const registration of this._manager.registrations().values()) {
      if (registration.securityOrigin !== inspectedOrigin) {
        continue;
      }

      for (const version of registration.versions.values()) {
        if (version.controlledClients.length > 1) {
          return true;
        }
      }
    }

    return false;
  }

  _hasAtLeastOneCategory(): boolean {
    return Presets.some(preset => preset.setting.get());
  }

  _unauditablePageMessage(): string|null {
    if (!this._manager) {
      return null;
    }

    const mainTarget = this._manager.target();
    const inspectedURL = mainTarget && mainTarget.inspectedURL();
    if (inspectedURL && !/^(http|chrome-extension)/.test(inspectedURL)) {
      return i18nString(UIStrings.canOnlyAuditHttphttpsPagesAnd);
    }

    return null;
  }

  async _hasImportantResourcesNotCleared(): Promise<string> {
    const clearStorageSetting =
        RuntimeSettings.find(runtimeSetting => runtimeSetting.setting.name === 'lighthouse.clear_storage');
    if (clearStorageSetting && !clearStorageSetting.setting.get()) {
      return '';
    }
    if (!this._manager) {
      return '';
    }
    const mainTarget = this._manager.target();
    const usageData = await mainTarget.storageAgent().invoke_getUsageAndQuota({origin: mainTarget.inspectedURL()});
    const locations = usageData.usageBreakdown.filter(usage => usage.usage)
                          .map(usage => STORAGE_TYPE_NAMES.get(usage.storageType))
                          .filter(Boolean);
    if (locations.length === 1) {
      return i18nString(UIStrings.thereMayBeStoredDataAffectingSingular, {PH1: locations[0]});
    }
    if (locations.length > 1) {
      return i18nString(UIStrings.thereMayBeStoredDataAffectingLoadingPlural, {PH1: locations.join(', ')});
    }
    return '';
  }

  async _evaluateInspectedURL(): Promise<string> {
    if (!this._manager) {
      return '';
    }
    const mainTarget = this._manager.target();
    const runtimeModel = mainTarget.model(SDK.RuntimeModel.RuntimeModel);
    const executionContext = runtimeModel && runtimeModel.defaultExecutionContext();
    let inspectedURL = mainTarget.inspectedURL();
    if (!executionContext) {
      return inspectedURL;
    }

    // Evaluate location.href for a more specific URL than inspectedURL provides so that SPA hash navigation routes
    // will be respected and audited.
    try {
      const result = await executionContext.evaluate(
          {
            expression: 'window.location.href',
            objectGroup: 'lighthouse',
            includeCommandLineAPI: false,
            silent: false,
            returnByValue: true,
            generatePreview: false,
            allowUnsafeEvalBlockedByCSP: undefined,
            disableBreaks: undefined,
            replMode: undefined,
            throwOnSideEffect: undefined,
            timeout: undefined,
          },
          /* userGesture */ false, /* awaitPromise */ false);
      if ((!('exceptionDetails' in result) || !result.exceptionDetails) && 'object' in result && result.object) {
        inspectedURL = result.object.value;
        result.object.release();
      }
    } catch (err) {
      console.error(err);
    }

    return inspectedURL;
  }

  getFlags(): {internalDisableDeviceScreenEmulation: boolean, emulatedFormFactor: (string|undefined)} {
    const flags = {
      // DevTools handles all the emulation. This tells Lighthouse to not bother with emulation.
      internalDisableDeviceScreenEmulation: true,
    };
    for (const runtimeSetting of RuntimeSettings) {
      runtimeSetting.setFlags(flags, runtimeSetting.setting.get());
    }
    return /** @type {{internalDisableDeviceScreenEmulation: boolean, emulatedFormFactor: (string|undefined)}} */ flags as
        {
          internalDisableDeviceScreenEmulation: boolean,
          emulatedFormFactor: (string | undefined),
        };
  }

  getCategoryIDs(): string[] {
    const categoryIDs = [];
    for (const preset of Presets) {
      if (preset.setting.get()) {
        categoryIDs.push(preset.configID);
      }
    }
    return categoryIDs;
  }

  async getInspectedURL(options?: {force: boolean}): Promise<string> {
    if (options && options.force || !this._inspectedURL) {
      this._inspectedURL = await this._evaluateInspectedURL();
    }
    return this._inspectedURL;
  }

  recomputePageAuditability(): void {
    const hasActiveServiceWorker = this._hasActiveServiceWorker();
    const hasAtLeastOneCategory = this._hasAtLeastOneCategory();
    const unauditablePageMessage = this._unauditablePageMessage();

    let helpText = '';
    if (hasActiveServiceWorker) {
      helpText = i18nString(UIStrings.multipleTabsAreBeingControlledBy);
    } else if (!hasAtLeastOneCategory) {
      helpText = i18nString(UIStrings.atLeastOneCategoryMustBeSelected);
    } else if (unauditablePageMessage) {
      helpText = unauditablePageMessage;
    }

    this.dispatchEventToListeners(Events.PageAuditabilityChanged, {helpText});

    this._hasImportantResourcesNotCleared().then(warning => {
      this.dispatchEventToListeners(Events.PageWarningsChanged, {warning});
    });
  }
}

const STORAGE_TYPE_NAMES = new Map([
  [Protocol.Storage.StorageType.Local_storage, i18nString(UIStrings.localStorage)],
  [Protocol.Storage.StorageType.Indexeddb, i18nString(UIStrings.indexeddb)],
  [Protocol.Storage.StorageType.Websql, i18nString(UIStrings.webSql)],
]);

export const Presets: Preset[] = [
  // configID maps to Lighthouse's Object.keys(config.categories)[0] value
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_perf', true),
    configID: 'performance',
    title: i18nString(UIStrings.performance),
    description: i18nString(UIStrings.howLongDoesThisAppTakeToShow),
    plugin: false,
  },
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_pwa', true),
    configID: 'pwa',
    title: i18nString(UIStrings.progressiveWebApp),
    description: i18nString(UIStrings.doesThisPageMeetTheStandardOfA),
    plugin: false,
  },
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_best_practices', true),
    configID: 'best-practices',
    title: i18nString(UIStrings.bestPractices),
    description: i18nString(UIStrings.doesThisPageFollowBestPractices),
    plugin: false,
  },
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_a11y', true),
    configID: 'accessibility',
    title: i18nString(UIStrings.accessibility),
    description: i18nString(UIStrings.isThisPageUsableByPeopleWith),
    plugin: false,
  },
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_seo', true),
    configID: 'seo',
    title: i18nString(UIStrings.seo),
    description: i18nString(UIStrings.isThisPageOptimizedForSearch),
    plugin: false,
  },
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.cat_pubads', false),
    plugin: true,
    configID: 'lighthouse-plugin-publisher-ads',
    title: i18nString(UIStrings.publisherAds),
    description: i18nString(UIStrings.isThisPageOptimizedForAdSpeedAnd),
  },
];

export type Flags = {
  [flag: string]: string|boolean,
};

export const RuntimeSettings: RuntimeSetting[] = [
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.device_type', 'mobile'),
    title: i18nString(UIStrings.applyMobileEmulation),
    description: i18nString(UIStrings.applyMobileEmulationDuring),
    setFlags: (flags: Flags, value: string|boolean): void => {
      // See Audits.AuditsPanel._setupEmulationAndProtocolConnection()
      flags.emulatedFormFactor = value;
    },
    options: [
      {label: i18nString(UIStrings.mobile), value: 'mobile'},
      {label: i18nString(UIStrings.desktop), value: 'desktop'},
    ],
    learnMore: undefined,
  },
  {
    // This setting is disabled, but we keep it around to show in the UI.
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.throttling', true),
    title: i18nString(UIStrings.simulatedThrottling),
    // We will disable this when we have a Lantern trace viewer within DevTools.
    learnMore:
        'https://github.com/GoogleChrome/lighthouse/blob/master/docs/throttling.md#devtools-lighthouse-panel-throttling',
    description: i18nString(UIStrings.simulateASlowerPageLoadBasedOn),
    setFlags: (flags: Flags, value: string|boolean): void => {
      flags.throttlingMethod = value ? 'simulate' : 'devtools';
    },
    options: undefined,
  },
  {
    setting: Common.Settings.Settings.instance().createSetting('lighthouse.clear_storage', true),
    title: i18nString(UIStrings.clearStorage),
    description: i18nString(UIStrings.resetStorageLocalstorage),
    setFlags: (flags: Flags, value: string|boolean): void => {
      flags.disableStorageReset = !value;
    },
    options: undefined,
    learnMore: undefined,
  },
];

export const Events = {
  PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
  PageWarningsChanged: Symbol('PageWarningsChanged'),
  AuditProgressChanged: Symbol('AuditProgressChanged'),
  RequestLighthouseStart: Symbol('RequestLighthouseStart'),
  RequestLighthouseCancel: Symbol('RequestLighthouseCancel'),
};
export interface Preset {
  setting: Common.Settings.Setting<boolean>;
  configID: string;
  title: string;
  description: string;
  plugin: boolean;
}
export interface RuntimeSetting {
  setting: Common.Settings.Setting<string|boolean>;
  description: string;
  setFlags: (flags: Flags, value: string|boolean) => void;
  options?: {label: string, value: string}[];
  title?: string;
  learnMore?: string;
}
