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

import '../../ui/components/report_view/report_view.js';
import '../../ui/kit/kit.js';

import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as NetworkForward from '../../panels/network/forward/forward.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js';

import * as ApplicationComponents from './components/components.js';
import serviceWorkersViewStyles from './serviceWorkersView.css.js';
import serviceWorkerUpdateCycleViewStyles from './serviceWorkerUpdateCycleView.css.js';
import {ServiceWorkerUpdateCycleView} from './ServiceWorkerUpdateCycleView.js';

const UIStrings = {
  /**
   * @description Text for linking to other Service Worker registrations
   */
  serviceWorkersFromOtherOrigins: 'Service workers from other origins',
  /**
   * @description Title of update on reload setting in service workers view of the application panel
   */
  updateOnReload: 'Update on reload',
  /**
   * @description Tooltip text that appears on the setting when hovering over it in Service Workers View of the Application panel
   */
  onPageReloadForceTheService: 'On page reload, force the `service worker` to update, and activate it',
  /**
   * @description Title of bypass service worker setting in service workers view of the application panel
   */
  bypassForNetwork: 'Bypass for network',
  /**
   * @description Tooltip text that appears on the setting when hovering over it in Service Workers View of the Application panel
   */
  bypassTheServiceWorkerAndLoad: 'Bypass the `service worker` and load resources from the network',
  /**
   * @description Screen reader title for a section of the Service Workers view of the Application panel
   * @example {https://example.com} PH1
   */
  serviceWorkerForS: '`Service worker` for {PH1}',
  /**
   * @description Text in Service Workers View of the Application panel
   */
  testPushMessageFromDevtools: 'Test push message from DevTools.',
  /**
   * @description Button label for service worker network requests
   */
  networkRequests: 'Network requests',
  /**
   * @description Label for a button in the Service Workers View of the Application panel.
   * Imperative noun. Clicking the button will refresh the list of service worker registrations.
   */
  update: 'Update',
  /**
   * @description Text in Service Workers View of the Application panel
   */
  unregisterServiceWorker: 'Unregister service worker',
  /**
   * @description Text in Service Workers View of the Application panel
   */
  unregister: 'Unregister',
  /**
   * @description Text for the source of something
   */
  source: 'Source',
  /**
   * @description Text for the status of something
   */
  status: 'Status',
  /**
   * @description Text in Service Workers View of the Application panel
   */
  clients: 'Clients',
  /**
   * @description Text in Service Workers View of the Application panel. Label for a section of the
   * tool which allows the developer to send a test push message to the service worker.
   */
  pushString: 'Push',
  /**
   * @description Text in Service Workers View of the Application panel. Placeholder text for where
   * the user can type in the data they want to push to the service worker i.e. the 'push data'. Noun
   * phrase.
   */
  pushData: 'Push data',
  /**
   * @description Text in Service Workers View of the Application panel
   */
  syncString: 'Sync',
  /**
   * @description Placeholder text for the input box where a user is asked for a test tag to sync. This is used as a compound noun, not as a verb.
   */
  syncTag: 'Sync tag',
  /**
   * @description Text for button in Service Workers View of the Application panel that dispatches a periodicsync event
   */
  periodicSync: 'Periodic sync',
  /**
   * @description Default tag for a periodicsync event in Service Workers View of the Application panel
   */
  periodicSyncTag: 'Periodic sync tag',
  /**
   * @description Aria accessible name in Service Workers View of the Application panel
   * @example {3} PH1
   */
  sRegistrationErrors: '{PH1} registration errors',
  /**
   * @description Text in Service Workers View of the Application panel. The Date/time that a service
   * worker version update was received by the webpage.
   * @example {7/3/2019, 3:38:37 PM} PH1
   */
  receivedS: 'Received {PH1}',
  /**
   **@description Text in Service Workers View of the Application panel.
   */
  routers: 'Routers',
  /**
   * @description Text in Service Workers View of the Application panel
   * @example {example.com} PH1
   */
  sDeleted: '{PH1} - deleted',
  /**
   * @description Text in Service Workers View of the Application panel
   * @example {1} PH1
   * @example {stopped} PH2
   */
  sActivatedAndIsS: '#{PH1} activated and is {PH2}',
  /**
   * @description Text in Service Workers View of the Application panel
   */
  stopString: 'Stop',
  /**
   * @description Text in Service Workers View of the Application panel
   */
  startString: 'Start',
  /**
   * @description Text in Service Workers View of the Application panel. Service workers have
   * different versions, which are labelled with numbers e.g. version #2. This text indicates that a
   * particular version is now redundant (it was replaced by a newer version). # means 'number' here.
   * @example {2} PH1
   */
  sIsRedundant: '#{PH1} is redundant',
  /**
   * @description Text in Service Workers View of the Application panel
   * @example {2} PH1
   */
  sWaitingToActivate: '#{PH1} waiting to activate',
  /**
   * @description Text in Service Workers View of the Application panel
   * @example {2} PH1
   */
  sTryingToInstall: '#{PH1} trying to install',
  /**
   * @description Text in Service Workers Update Timeline. Update is a noun.
   */
  updateCycle: 'Update Cycle',
  /**
   * @description Text of a DOM element in Service Workers View of the Application panel
   * @example {example.com} PH1
   */
  workerS: 'Worker: {PH1}',
  /**
   * @description Link text in Service Workers View of the Application panel. When the link is clicked,
   * the focus is moved to the service worker's client page.
   */
  focus: 'focus',
  /**
   * @description Link to view all the Service Workers that have been registered.
   */
  seeAllRegistrations: 'See all registrations',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/ServiceWorkersView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {until} = Directives;
const {widget} = UI.Widget;
const {bindToSetting} = UI.UIUtils;

export interface SectionData {
  manager: SDK.ServiceWorkerManager.ServiceWorkerManager;
  registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration;
}

export interface ServiceWorkersViewInput {
  canManageServiceWorkers: boolean;
  sections: SectionData[];
}

function renderToolbar(): LitTemplate {
  const updateOnReloadSetting =
      Common.Settings.Settings.instance().createSetting('service-worker-update-on-reload', false);
  const bypassServiceWorkerSetting = Common.Settings.Settings.instance().createSetting('bypass-service-worker', false);
  // clang-format off
  return html`<devtools-toolbar class="service-worker-toolbar">
    ${MobileThrottling.ThrottlingManager.throttlingManager().createOfflineToolbarCheckbox().element}
    <devtools-checkbox title=${i18nString(UIStrings.onPageReloadForceTheService)}
                       ${bindToSetting(updateOnReloadSetting)}>
      ${i18nString(UIStrings.updateOnReload)}
    </devtools-checkbox>
    <devtools-checkbox title=${i18nString(UIStrings.bypassTheServiceWorkerAndLoad)}
                       ${bindToSetting(bypassServiceWorkerSetting)}>
      ${i18nString(UIStrings.bypassForNetwork)}
     </devtools-checkbox>
  </devtools-toolbar>`;
  // clang-format on
}

function renderOthersOriginView(): LitTemplate {
  // clang-format off
  return html`<div class="service-workers-other-origin"
                   jslog=${VisualLogging.section('other-origin')}>
    <devtools-report>
      <devtools-report-section-header>
         ${i18nString(UIStrings.serviceWorkersFromOtherOrigins)}
      </devtools-report-section-header>
      <div class="service-worker-section">
         <devtools-link href="chrome://serviceworker-internals"
                        jslogcontext="view-all"
                        .allowPrivileged=${true}>
           ${i18nString(UIStrings.seeAllRegistrations)}
         </devtools-link>
      </div>
    </devtools-report>
  </div>`;
  // clang-format on
}

function getTimeStamp(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): number {
  const versions = registration.versionsByMode();
  let timestamp: number|undefined = 0;
  const active = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.ACTIVE);
  const installing = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.INSTALLING);
  const waiting = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.WAITING);
  const redundant = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.REDUNDANT);

  if (active) {
    timestamp = active.scriptResponseTime;
  } else if (waiting) {
    timestamp = waiting.scriptResponseTime;
  } else if (installing) {
    timestamp = installing.scriptResponseTime;
  } else if (redundant) {
    timestamp = redundant.scriptResponseTime;
  }

  return timestamp || 0;
}

function renderOriginReport(input: ServiceWorkersViewInput): LitTemplate {
  if (!input.canManageServiceWorkers) {
    return nothing;
  }

  const sortedSections = [...input.sections];
  sortedSections.sort((a, b) => {
    const aTimestamp = getTimeStamp(a.registration);
    const bTimestamp = getTimeStamp(b.registration);
    return bTimestamp - aTimestamp;
  });

  // clang-format off
  return html`<div class="service-workers-this-origin" jslog=${VisualLogging.section('this-origin')}>
    <devtools-report .data=${{reportTitle: i18n.i18n.lockedString('Service workers')}}>
      <div class="service-worker-toolbar" slot="toolbar">${renderToolbar()}</div>
      ${sortedSections.map(section => html`<devtools-widget class="service-worker-section-container" ${widget(Section, {section})}></devtools-widget>`)}
    </devtools-report>
  </div>`;
  // clang-format on
}

type View = (input: ServiceWorkersViewInput, output: undefined, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, _output, target): void => {
  // clang-format off
  render(html`
    <!-- This Origin Report -->
    ${renderOriginReport(input)}
    ${renderOthersOriginView()}`,
         target, {
           container: {
             classes: [
               'service-worker-list',
               (input.sections.length > 0 ? 'service-worker-has-current' : 'service-worker-list-empty')
             ]
           }
         });
  // clang-format on
};

let throttleDisabledForDebugging = false;
export const setThrottleDisabledForDebugging = (enable: boolean): void => {
  throttleDisabledForDebugging = enable;
};

export class ServiceWorkersView extends UI.Widget.VBox implements
    SDK.TargetManager.SDKModelObserver<SDK.ServiceWorkerManager.ServiceWorkerManager> {
  private readonly sections: Map<SDK.ServiceWorkerManager.ServiceWorkerRegistration, SectionData>;
  private manager: SDK.ServiceWorkerManager.ServiceWorkerManager|null;
  private securityOriginManager: SDK.SecurityOriginManager.SecurityOriginManager|null;
  private readonly eventListeners:
      Map<SDK.ServiceWorkerManager.ServiceWorkerManager, Common.EventTarget.EventDescriptor[]>;
  readonly #output = undefined;

  #view: (input: ServiceWorkersViewInput, output: undefined, target: HTMLElement) => void;

  constructor(view: View = DEFAULT_VIEW) {
    super({
      jslog: `${VisualLogging.pane('service-workers')}`,
      useShadowDom: true,
    });
    this.#view = view;
    this.registerRequiredCSS(serviceWorkersViewStyles);

    this.sections = new Map();

    this.manager = null;
    this.securityOriginManager = null;

    this.eventListeners = new Map();
    SDK.TargetManager.TargetManager.instance().observeModels(SDK.ServiceWorkerManager.ServiceWorkerManager, this);
  }

  override wasShown(): void {
    super.wasShown();
    this.requestUpdate();
  }

  override async performUpdate(): Promise<void> {
    if (this.manager) {
      for (const registration of this.manager.registrations().values()) {
        const isCurrent = this.isOriginCurrent(registration.securityOrigin);
        if (isCurrent && !this.sections.has(registration)) {
          this.sections.set(registration, {manager: this.manager, registration});
        } else if (!isCurrent && this.sections.has(registration)) {
          this.sections.delete(registration);
        }
      }
    }

    const input: ServiceWorkersViewInput = {
      canManageServiceWorkers: this.manager !== null,
      sections: Array.from(this.sections.values()).map(data => ({...data})),
    };

    this.#view(input, this.#output, this.contentElement);
  }

  modelAdded(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void {
    if (serviceWorkerManager.target() !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) {
      return;
    }
    this.manager = serviceWorkerManager;
    this.securityOriginManager =
        (serviceWorkerManager.target().model(SDK.SecurityOriginManager.SecurityOriginManager) as
         SDK.SecurityOriginManager.SecurityOriginManager);

    for (const registration of this.manager.registrations().values()) {
      this.updateRegistration(registration);
    }

    this.eventListeners.set(serviceWorkerManager, [
      this.manager.addEventListener(SDK.ServiceWorkerManager.Events.REGISTRATION_UPDATED, this.registrationUpdated,
                                    this),
      this.manager.addEventListener(SDK.ServiceWorkerManager.Events.REGISTRATION_DELETED, this.registrationDeleted,
                                    this),
      this.securityOriginManager.addEventListener(SDK.SecurityOriginManager.Events.SecurityOriginAdded,
                                                  this.requestUpdate, this),
      this.securityOriginManager.addEventListener(SDK.SecurityOriginManager.Events.SecurityOriginRemoved,
                                                  this.requestUpdate, this),
    ]);
  }

  modelRemoved(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void {
    if (!this.manager || this.manager !== serviceWorkerManager) {
      return;
    }

    Common.EventTarget.removeEventListeners(this.eventListeners.get(serviceWorkerManager) || []);
    this.eventListeners.delete(serviceWorkerManager);
    this.manager = null;
    this.securityOriginManager = null;
  }

  private registrationUpdated(
      event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerManager.ServiceWorkerRegistration>): void {
    this.updateRegistration(event.data);
    this.gcRegistrations();
  }

  private gcRegistrations(): void {
    if (!this.manager || !this.securityOriginManager) {
      return;
    }
    let hasNonDeletedRegistrations = false;
    const securityOrigins = new Set<string>(this.securityOriginManager.securityOrigins());
    for (const registration of this.manager.registrations().values()) {
      if (!securityOrigins.has(registration.securityOrigin) && !this.isRegistrationVisible(registration)) {
        continue;
      }
      if (!registration.canBeRemoved()) {
        hasNonDeletedRegistrations = true;
        break;
      }
    }

    if (!hasNonDeletedRegistrations) {
      return;
    }

    for (const registration of this.manager.registrations().values()) {
      const visible = securityOrigins.has(registration.securityOrigin) || this.isRegistrationVisible(registration);
      if (!visible && registration.canBeRemoved()) {
        this.removeRegistrationFromList(registration);
      }
    }
  }

  private isOriginCurrent(origin: string): boolean {
    if (this.securityOriginManager &&
        (this.securityOriginManager.securityOrigins().includes(origin) ||
         this.securityOriginManager.unreachableMainSecurityOrigin() === origin)) {
      return true;
    }
    return false;
  }

  private updateRegistration(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration,
                             skipUpdate?: boolean): void {
    if (!this.manager) {
      return;
    }
    let sectionData = this.sections.get(registration);
    if (!sectionData) {
      if (!this.isOriginCurrent(registration.securityOrigin)) {
        return;
      }
      sectionData = {manager: this.manager, registration};
      this.sections.set(registration, sectionData);
    }
    if (skipUpdate) {
      return;
    }
    this.requestUpdate();
  }

  private registrationDeleted(
      event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerManager.ServiceWorkerRegistration>): void {
    this.removeRegistrationFromList(event.data);
  }

  private removeRegistrationFromList(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration,
                                     skipVisibilityUpdate = false): void {
    this.sections.delete(registration);
    if (!skipVisibilityUpdate) {
      this.requestUpdate();
    }
  }

  private isRegistrationVisible(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): boolean {
    if (!registration.scopeURL) {
      return true;
    }
    return false;
  }
}

export interface SectionViewInput {
  title: string;
  isDeleted: boolean;
  errorsLength: number;
  pushData: string;
  syncTag: string;
  periodicSyncTag: string;
  updateCycleTable: HTMLElement;
  activeVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion;
  waitingVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion;
  installingVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion;
  redundantVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion;
  renderClientInfo: (clientId: Protocol.Target.TargetID) => Promise<LitTemplate|typeof nothing>;
  onNetworkRequests: () => void;
  onUpdate: () => void;
  onUnregister: () => void;
  onPush: (data: string) => void;
  onSync: (tag: string) => void;
  onPeriodicSync: (tag: string) => void;
  onStop: (versionId: string) => void;
  onStart: () => void;
  onSkipWaiting: () => void;
}

function renderHeaderButtons(input: SectionViewInput): LitTemplate {
  // clang-format off
  return html`
    <devtools-button .data=${{
          variant: Buttons.Button.Variant.TEXT,
          title: i18nString(UIStrings.networkRequests),
          jslogContext: 'show-network-requests'
        } as Buttons.Button.ButtonData}
        .disabled=${input.isDeleted}
        @click=${input.onNetworkRequests}>
      ${i18nString(UIStrings.networkRequests)}
    </devtools-button>
    <devtools-button .data=${{
          variant: Buttons.Button.Variant.TEXT,
          title: i18nString(UIStrings.update),
          jslogContext: 'update'
        } as Buttons.Button.ButtonData}
        .disabled=${input.isDeleted}
        @click=${input.onUpdate}>
      ${i18nString(UIStrings.update)}
    </devtools-button>
    <devtools-button .data=${{
          variant: Buttons.Button.Variant.TEXT,
          title: i18nString(UIStrings.unregisterServiceWorker),
          jslogContext: 'unregister'
        } as Buttons.Button.ButtonData}
        .disabled=${input.isDeleted}
        @click=${input.onUnregister}>
      ${i18nString(UIStrings.unregister)}
    </devtools-button>`;
  // clang-format on
}

function renderSyncNotificationField(label: string, initialValue: string, placeholder: string,
                                     callback: (arg0: string) => void, jslogContext: string): LitTemplate {
  // clang-format off
  return html`
    <div class="report-field">
    <div class="report-field-name">${label}</div>
      <div class="report-field-value">
      <form class="service-worker-editor-with-button" @submit=${(e: Event) => {
        const {editor} = e.target as HTMLFormElement;
        callback(editor.value || '');
        e.consume(true);
      }}>
        <input name="editor" class="source-code service-worker-notification-editor harmony-input" type="text"
          .value=${initialValue}
          placeholder=${placeholder}
          aria-label=${label}
          .spellcheck=${false}
          jslog=${VisualLogging.textField().track({change: true}).context(jslogContext)}
        >
        <devtools-button .data=${{
            type: 'submit',
            variant: Buttons.Button.Variant.OUTLINED,
            jslogContext} as Buttons.Button.ButtonData}>
          ${label}
        </devtools-button>
      </form>
      </div>
    </div>`;
  // clang-format on
}

function renderVersion(icon: string, label: string, content: LitTemplate = nothing): LitTemplate {
  // clang-format off
  return html`
    <div class="service-worker-version">
      <div class=${icon}></div>
      <span class="service-worker-version-string" role="alert" aria-live="polite">
        ${label}
      </span>
      ${content}
    </div>`;
  // clang-format on
}

function renderClientsField(input: SectionViewInput,
                            version?: SDK.ServiceWorkerManager.ServiceWorkerVersion): LitTemplate {
  if (!version?.controlledClients?.length) {
    return html`<div class="report-field">
      <div class="report-field-name">${i18nString(UIStrings.clients)}</div>
      <div class="report-field-value"></div>
    </div>`;
  }
  // clang-format off
  return html`<div class="report-field">
      <div class="report-field-name">${i18nString(UIStrings.clients)}</div>
      <div class="report-field-value">
      ${version.controlledClients.map(client => html`
        <div class="service-worker-client">
          ${until(input.renderClientInfo(client))}
       </div>`)}
    </div>
  </div>`;
  // clang-format on
}

function renderSourceField(input: SectionViewInput,
                           version?: SDK.ServiceWorkerManager.ServiceWorkerVersion): LitTemplate {
  if (!version) {
    return html`<div class="report-field">
      <div class="report-field-name">${i18nString(UIStrings.source)}</div>
      <div class="report-field-value"></div>
    </div>`;
  }
  const fileName = Common.ParsedURL.ParsedURL.extractName(version.scriptURL);
  // clang-format off

  return html`<div class="report-field">
    <div class="report-field-name">${i18nString(UIStrings.source)}</div>
    <div class="report-field-value">
      <div class="report-field-value-filename">
        ${Components.Linkifier.Linkifier.renderLinkifiedUrl(version.scriptURL, {
          text: fileName, tabStop: true, jslogContext: 'source-location'})}
        ${input.errorsLength ? html`
          <button
              class="devtools-link link"
              tabindex="0"
              aria-label=${i18nString(UIStrings.sRegistrationErrors, {PH1: input.errorsLength})}
              @click=${() => Common.Console.Console.instance().show()}>
            <devtools-icon name="cross-circle-filled" class="error-icon">
            </devtools-icon>
            ${input.errorsLength}
          </button>` : nothing}
      </div>
      ${version.scriptResponseTime !== undefined ? html`
        <div class="report-field-value-subtitle">
          ${i18nString(UIStrings.receivedS, {PH1: new Date(version.scriptResponseTime * 1000).toLocaleString()})}
        </div>
      ` : nothing}
    </div>
  </div>`;
  // clang-format on
}

function renderStatusField(input: SectionViewInput, active?: SDK.ServiceWorkerManager.ServiceWorkerVersion,
                           waiting?: SDK.ServiceWorkerManager.ServiceWorkerVersion,
                           installing?: SDK.ServiceWorkerManager.ServiceWorkerVersion,
                           redundant?: SDK.ServiceWorkerManager.ServiceWorkerVersion): LitTemplate {
  // clang-format off

  return html`<div class="report-field">
    <div class="report-field-name">${i18nString(UIStrings.status)}</div>
    <div class="report-field-value">
      <div class="service-worker-version-stack">
        <div class="service-worker-version-stack-bar"></div>
        ${active ? renderVersion(
            'service-worker-active-circle',
            i18nString(UIStrings.sActivatedAndIsS, {
              PH1: active.id,
              PH2: SDK.ServiceWorkerManager.ServiceWorkerVersion.RunningStatus[active.currentState.runningStatus](),
            }),
            active.isRunning() || active.isStarting() ? html`
              <devtools-button .data=${{jslogContext: 'stop', variant: Buttons.Button.Variant.OUTLINED} as Buttons.Button.ButtonData}
                              @click=${() => input.onStop(active.id)}>
                  ${i18nString(UIStrings.stopString)}
              </devtools-button>`
            : active.isStartable() ? html`
              <devtools-button .data=${{jslogContext: 'start', variant: Buttons.Button.Variant.OUTLINED} as Buttons.Button.ButtonData}
                              @click=${input.onStart}>
                  ${i18nString(UIStrings.startString)}
              </devtools-button>`
            : nothing)
        : redundant ? renderVersion(
            'service-worker-redundant-circle',
            i18nString(UIStrings.sIsRedundant, {PH1: redundant.id}))
        : nothing}
        ${waiting ? renderVersion(
            'service-worker-waiting-circle',
            i18nString(UIStrings.sWaitingToActivate, {PH1: waiting.id}), html`
              <devtools-button .data=${{
                    jslogContext: 'skip-waiting',
                    title: i18n.i18n.lockedString('skipWaiting'),
                    variant: Buttons.Button.Variant.OUTLINED} as Buttons.Button.ButtonData}
                  @click=${input.onSkipWaiting}>
                ${i18n.i18n.lockedString('skipWaiting')}
              </devtools-button>
              ${waiting.scriptResponseTime !== undefined ? html`
                <div class="service-worker-subtitle">
                  ${i18nString(UIStrings.receivedS, {PH1: new Date(waiting.scriptResponseTime * 1000).toLocaleString()})}
                </div>
              ` : nothing}
          `,
        ) : nothing}
        ${installing ? renderVersion(
          'service-worker-installing-circle',
          i18nString(UIStrings.sTryingToInstall, {PH1: installing.id}),
          installing.scriptResponseTime !== undefined ? html`
            <div class="service-worker-subtitle">
              ${i18nString(UIStrings.receivedS, {PH1: new Date(installing.scriptResponseTime * 1000).toLocaleString()})}
            </div>` : nothing) : nothing}
      </div>
    </div>
  </div>`;
  // clang-format on
}

function renderUpdateCycleField(input: SectionViewInput): LitTemplate {
  return html`
    <div class="report-field">
      <div class="report-field-name">${i18nString(UIStrings.updateCycle)}</div>
      <div class="report-field-value">
        ${input.updateCycleTable}
      </div>
    </div>`;
}

function renderRouterField(input: SectionViewInput): LitTemplate {
  const active = input.activeVersion;
  const title = i18nString(UIStrings.routers);
  if (active?.routerRules && active.routerRules.length > 0) {
    // If there is at least one registered rule in the active version, append the router filed.
    // clang-format off
    return html`
      <div class="report-field">
        <div class="report-field-name">${title}</div>
        <div class="report-field-value">
          ${widget(ApplicationComponents.ServiceWorkerRouterView.ServiceWorkerRouterView, { rules: active.routerRules })}
        </div>
      </div>`;
    // clang-format on
  }
  return nothing;
}

type SectionView = (input: SectionViewInput, _output: undefined, target: HTMLElement) => void;

export const DEFAULT_SECTION_VIEW: SectionView =
    (input: SectionViewInput, _output: undefined, target: HTMLElement): void => {
      // clang-format off
  render(html`
      <style>${serviceWorkersViewStyles}</style>
      <style>${serviceWorkerUpdateCycleViewStyles}</style>
      <devtools-report-section-header role="heading" aria-level="2"
              aria-label=${i18nString(UIStrings.serviceWorkerForS, { PH1: input.title })}>
        <span style="flex: 1 1 auto">${input.title}</span>
        ${renderHeaderButtons(input)}
      </devtools-report-section-header>
      <div class="service-worker-section">
         ${renderSourceField(input, input.activeVersion ?? input.redundantVersion)}
         ${renderStatusField(input, input.activeVersion, input.waitingVersion, input.installingVersion, input.redundantVersion)}
         ${renderClientsField(input, input.activeVersion ?? input.redundantVersion)}
         ${renderSyncNotificationField(i18nString(UIStrings.pushString), input.pushData,
                                   i18nString(UIStrings.pushData), input.onPush, 'push-message')}
         ${renderSyncNotificationField(i18nString(UIStrings.syncString), input.syncTag,
                                   i18nString(UIStrings.syncTag), input.onSync, 'sync-tag')}
         ${renderSyncNotificationField(i18nString(UIStrings.periodicSync), input.periodicSyncTag,
                                   i18nString(UIStrings.periodicSyncTag), input.onPeriodicSync,
                                   'periodic-sync-tag')}
         ${renderUpdateCycleField(input)}
         ${renderRouterField(input)}
      </div>
  `, target);
      // clang-format on
    };

export class Section extends UI.Widget.VBox {
  private manager!: SDK.ServiceWorkerManager.ServiceWorkerManager;
  registration!: SDK.ServiceWorkerManager.ServiceWorkerRegistration;
  private sectionInternal!: SectionData;
  private fingerprint: symbol|null;
  private pushNotificationDataSetting!: Common.Settings.Setting<string>;
  private syncTagNameSetting!: Common.Settings.Setting<string>;
  private periodicSyncTagNameSetting!: Common.Settings.Setting<string>;
  private updateCycleView!: ServiceWorkerUpdateCycleView;
  private readonly clientInfoCache: Map<string, Protocol.Target.TargetInfo>;
  private readonly throttler: Common.Throttler.Throttler;
  #view: SectionView;

  constructor(element: HTMLElement, view = DEFAULT_SECTION_VIEW) {
    super(element);
    this.fingerprint = null;
    this.clientInfoCache = new Map();
    this.throttler = new Common.Throttler.Throttler(500);
    this.#view = view;
  }

  set section(data: SectionData) {
    const registrationChanged = !this.registration || this.registration !== data.registration;
    this.sectionInternal = data;
    this.manager = data.manager;
    this.registration = data.registration;

    if (!this.pushNotificationDataSetting) {
      this.pushNotificationDataSetting = Common.Settings.Settings.instance().createLocalSetting(
          'push-data', i18nString(UIStrings.testPushMessageFromDevtools));
      this.syncTagNameSetting =
          Common.Settings.Settings.instance().createLocalSetting('sync-tag-name', 'test-tag-from-devtools');
      this.periodicSyncTagNameSetting =
          Common.Settings.Settings.instance().createLocalSetting('periodic-sync-tag-name', 'test-tag-from-devtools');
    }

    if (registrationChanged) {
      this.updateCycleView = new ServiceWorkerUpdateCycleView(this.registration);
      this.clientInfoCache.clear();
    }
  }

  get section(): SectionData {
    return this.sectionInternal;
  }

  getTitle(): string {
    const scopeURL = this.registration.scopeURL;
    return this.registration.isDeleted ? i18nString(UIStrings.sDeleted, {PH1: scopeURL}) : scopeURL;
  }

  override requestUpdate(): void {
    if (throttleDisabledForDebugging) {
      super.requestUpdate();
      return;
    }
    void this.throttler.schedule(() => {
      super.requestUpdate();
      return Promise.resolve();
    });
  }

  override performUpdate(): Promise<void> {
    const fingerprint = this.registration.fingerprint();
    if (fingerprint === this.fingerprint) {
      return Promise.resolve();
    }
    this.fingerprint = fingerprint;

    const versions = this.registration.versionsByMode();

    const active = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.ACTIVE);
    const waiting = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.WAITING);
    const installing = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.INSTALLING);
    const redundant = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.REDUNDANT);

    const title = this.getTitle();

    const input: SectionViewInput = {
      title,
      isDeleted: this.registration.isDeleted,
      errorsLength: this.registration.errors?.length ?? 0,
      pushData: this.pushNotificationDataSetting.get(),
      syncTag: this.syncTagNameSetting.get(),
      periodicSyncTag: this.periodicSyncTagNameSetting.get(),
      updateCycleTable: this.updateCycleView.tableElement,
      activeVersion: active,
      waitingVersion: waiting,
      installingVersion: installing,
      redundantVersion: redundant,
      renderClientInfo: this.renderClientInfo.bind(this),
      onNetworkRequests: this.networkRequestsClicked.bind(this),
      onUpdate: this.updateButtonClicked.bind(this),
      onUnregister: this.unregisterButtonClicked.bind(this),
      onPush: this.push.bind(this),
      onSync: this.sync.bind(this),
      onPeriodicSync: this.periodicSync.bind(this),
      onStop: this.stopButtonClicked.bind(this),
      onStart: this.startButtonClicked.bind(this),
      onSkipWaiting: this.skipButtonClicked.bind(this),
    };

    this.#view(input, undefined, this.contentElement);
    this.updateCycleView.refresh();

    return Promise.resolve();
  }

  private unregisterButtonClicked(): void {
    this.manager.deleteRegistration(this.registration.id);
  }

  private updateButtonClicked(): void {
    void this.manager.updateRegistration(this.registration.id);
  }

  private networkRequestsClicked(): void {
    void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([
      {
        filterType: NetworkForward.UIFilter.FilterType.Is,
        filterValue: NetworkForward.UIFilter.IsFilterType.SERVICE_WORKER_INTERCEPTED,
      },
    ]));

    Host.userMetrics.actionTaken(Host.UserMetrics.Action.ServiceWorkerNetworkRequestClicked);
  }

  private push(data: string): void {
    this.pushNotificationDataSetting.set(data);
    void this.manager.deliverPushMessage(this.registration.id, data);
  }

  private sync(tag: string): void {
    this.syncTagNameSetting.set(tag);
    void this.manager.dispatchSyncEvent(this.registration.id, tag, true);
  }

  private periodicSync(tag: string): void {
    this.periodicSyncTagNameSetting.set(tag);
    void this.manager.dispatchPeriodicSyncEvent(this.registration.id, tag);
  }

  private async renderClientInfo(clientId: Protocol.Target.TargetID): Promise<LitTemplate|typeof nothing> {
    let targetInfo = this.clientInfoCache.get(clientId);
    if (!targetInfo) {
      const response = await this.manager.target().targetAgent().invoke_getTargetInfo({targetId: clientId});
      if (!response.targetInfo) {
        return nothing;
      }
      targetInfo = response.targetInfo;
      this.clientInfoCache.set(clientId, targetInfo);
    }

    if (targetInfo.type !== 'page' && targetInfo.type !== 'iframe') {
      // clang-format off
      return html`<span class="service-worker-client-string">
        ${i18nString(UIStrings.workerS, {PH1: targetInfo.url})}
      </span>`;
      // clang-format on
    }

    // clang-format off
    return html`
      <span class="service-worker-client-string">${targetInfo.url}</span>
      <devtools-button
        .data=${{
          iconName: 'select-element',
          variant: Buttons.Button.Variant.ICON,
          size: Buttons.Button.Size.SMALL,
          title: i18nString(UIStrings.focus),
          jslogContext: 'client-focus',
        } as Buttons.Button.ButtonData}
        class="service-worker-client-focus-link"
        @click=${this.activateTarget.bind(this, targetInfo.targetId)}
      ></devtools-button>`;
    // clang-format on
  }

  private activateTarget(targetId: Protocol.Target.TargetID): void {
    void this.manager.target().targetAgent().invoke_activateTarget({targetId});
  }

  private startButtonClicked(): void {
    void this.manager.startWorker(this.registration.scopeURL);
  }

  private skipButtonClicked(): void {
    void this.manager.skipWaiting(this.registration.scopeURL);
  }

  private stopButtonClicked(versionId: string): void {
    void this.manager.stopWorker(versionId);
  }
}
