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

import type * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import {createIcon} from '../../ui/kit/kit.js';
import * as UI from '../../ui/legacy/legacy.js';

import {ApplicationPanelTreeElement, ExpandableApplicationPanelTreeElement} from './ApplicationPanelTreeElement.js';
import type {ResourcesPanel} from './ResourcesPanel.js';
import {ServiceWorkerCacheView} from './ServiceWorkerCacheViews.js';

const UIStrings = {
  /**
   * @description Text in Application Panel Sidebar of the Application panel
   */
  cacheStorage: 'Cache storage',
  /**
   * @description Text in Application Panel if no cache storage was detected.
   */
  noCacheStorage: 'No cache storage detected',
  /**
   * @description Description text in Application Panel describing the cache storage tab
   */
  cacheStorageDescription: 'On this page you can view and delete cache data.',
  /**
   * @description A context menu item in the Application Panel Sidebar of the Application panel
   */
  refreshCaches: 'Refresh Caches',
  /**
   * @description Text to delete something
   */
  delete: 'Delete',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/ServiceWorkerCacheTreeElement.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class ServiceWorkerCacheTreeElement extends ExpandableApplicationPanelTreeElement {
  private swCacheModels: Set<SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel>;
  private swCacheTreeElements: Set<SWCacheTreeElement>;
  private storageBucket?: Protocol.Storage.StorageBucket;

  constructor(resourcesPanel: ResourcesPanel, storageBucket?: Protocol.Storage.StorageBucket) {
    super(
        resourcesPanel, i18nString(UIStrings.cacheStorage), i18nString(UIStrings.noCacheStorage),
        i18nString(UIStrings.cacheStorageDescription), 'cache-storage');
    const icon = createIcon('database');
    this.setLink('https://developer.chrome.com/docs/devtools/storage/cache/' as Platform.DevToolsPath.UrlString);
    this.setLeadingIcons([icon]);
    this.swCacheModels = new Set();
    this.swCacheTreeElements = new Set();
    this.storageBucket = storageBucket;
  }

  initialize(): void {
    this.swCacheModels.clear();
    this.swCacheTreeElements.clear();
    SDK.TargetManager.TargetManager.instance().observeModels(SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel, {
      modelAdded: (model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel) =>
          this.serviceWorkerCacheModelAdded(model),
      modelRemoved: (model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel) =>
          this.serviceWorkerCacheModelRemoved(model),
    });
  }

  override onattach(): void {
    super.onattach();
    this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
  }

  private handleContextMenuEvent(event: MouseEvent): void {
    const contextMenu = new UI.ContextMenu.ContextMenu(event);
    contextMenu.defaultSection().appendItem(
        i18nString(UIStrings.refreshCaches), this.refreshCaches.bind(this), {jslogContext: 'refresh-caches'});
    void contextMenu.show();
  }

  private refreshCaches(): void {
    for (const swCacheModel of this.swCacheModels) {
      swCacheModel.refreshCacheNames();
    }
  }

  private serviceWorkerCacheModelAdded(model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel): void {
    model.enable();
    this.swCacheModels.add(model);
    for (const cache of model.caches()) {
      this.addCache(model, cache);
    }
    model.addEventListener(SDK.ServiceWorkerCacheModel.Events.CACHE_ADDED, this.cacheAdded, this);
    model.addEventListener(SDK.ServiceWorkerCacheModel.Events.CACHE_REMOVED, this.cacheRemoved, this);
  }

  private serviceWorkerCacheModelRemoved(model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel): void {
    for (const cache of model.caches()) {
      this.removeCache(model, cache);
    }
    model.removeEventListener(SDK.ServiceWorkerCacheModel.Events.CACHE_ADDED, this.cacheAdded, this);
    model.removeEventListener(SDK.ServiceWorkerCacheModel.Events.CACHE_REMOVED, this.cacheRemoved, this);
    this.swCacheModels.delete(model);
  }

  private cacheAdded(event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerCacheModel.CacheEvent>): void {
    const {model, cache} = event.data;
    this.addCache(model, cache);
  }

  private cacheInTree(cache: SDK.ServiceWorkerCacheModel.Cache): boolean {
    if (this.storageBucket) {
      return cache.inBucket(this.storageBucket);
    }
    return true;
  }

  private addCache(
      model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel, cache: SDK.ServiceWorkerCacheModel.Cache): void {
    if (this.cacheInTree(cache)) {
      const swCacheTreeElement =
          new SWCacheTreeElement(this.resourcesPanel, model, cache, this.storageBucket === undefined);
      this.swCacheTreeElements.add(swCacheTreeElement);
      this.appendChild(swCacheTreeElement);
    }
  }

  private cacheRemoved(event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerCacheModel.CacheEvent>): void {
    const {model, cache} = event.data;
    if (this.cacheInTree(cache)) {
      this.removeCache(model, cache);
    }
  }

  private removeCache(
      model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel, cache: SDK.ServiceWorkerCacheModel.Cache): void {
    const swCacheTreeElement = this.cacheTreeElement(model, cache);
    if (!swCacheTreeElement) {
      return;
    }

    this.removeChild(swCacheTreeElement);
    this.swCacheTreeElements.delete(swCacheTreeElement);
    this.setExpandable(this.childCount() > 0);
  }

  private cacheTreeElement(
      model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel,
      cache: SDK.ServiceWorkerCacheModel.Cache): SWCacheTreeElement|null {
    for (const cacheTreeElement of this.swCacheTreeElements) {
      if (cacheTreeElement.hasModelAndCache(model, cache)) {
        return cacheTreeElement;
      }
    }
    return null;
  }
}

export class SWCacheTreeElement extends ApplicationPanelTreeElement {
  private readonly model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel;
  private cache: SDK.ServiceWorkerCacheModel.Cache;
  private view: ServiceWorkerCacheView|null;

  constructor(
      resourcesPanel: ResourcesPanel, model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel,
      cache: SDK.ServiceWorkerCacheModel.Cache, appendStorageKey: boolean) {
    let cacheName;
    if (appendStorageKey) {
      cacheName = cache.cacheName + ' - ' + cache.storageKey;
    } else {
      cacheName = cache.cacheName;
    }
    super(resourcesPanel, cacheName, false, 'cache-storage-instance');
    this.model = model;
    this.cache = cache;
    this.view = null;
    const icon = createIcon('table');
    this.setLeadingIcons([icon]);
  }

  override get itemURL(): Platform.DevToolsPath.UrlString {
    // I don't think this will work at all.
    return 'cache://' + this.cache.cacheId as Platform.DevToolsPath.UrlString;
  }

  override onattach(): void {
    super.onattach();
    this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
  }

  private handleContextMenuEvent(event: MouseEvent): void {
    const contextMenu = new UI.ContextMenu.ContextMenu(event);
    contextMenu.defaultSection().appendItem(
        i18nString(UIStrings.delete), this.clearCache.bind(this), {jslogContext: 'delete'});
    void contextMenu.show();
  }

  private clearCache(): void {
    void this.model.deleteCache(this.cache);
  }

  update(cache: SDK.ServiceWorkerCacheModel.Cache): void {
    this.cache = cache;
    if (this.view) {
      this.view.update(cache);
    }
  }

  override onselect(selectedByUser: boolean|undefined): boolean {
    super.onselect(selectedByUser);
    if (!this.view) {
      this.view = new ServiceWorkerCacheView(this.model, this.cache);
    }

    this.showView(this.view);
    Host.userMetrics.panelShown('service-worker-cache');
    return false;
  }

  hasModelAndCache(
      model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel, cache: SDK.ServiceWorkerCacheModel.Cache): boolean {
    return this.cache.equals(cache) && this.model === model;
  }
}
