// Copyright 2021 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/kit/kit.js';
import '../../ui/components/icon_button/icon_button.js';

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Formatter from '../../models/formatter/formatter.js';
import * as Persistence from '../../models/persistence/persistence.js';
import type * as Workspace from '../../models/workspace/workspace.js';
import type * as Diff from '../../third_party/diff/diff.js';
import * as DiffView from '../../ui/components/diff_view/diff_view.js';
import {Directives, html, type TemplateResult} from '../../ui/lit/lit.js';
import * as PanelCommon from '../common/common.js';
import * as Snippets from '../snippets/snippets.js';

const {ref, styleMap, ifDefined} = Directives;

const UIStrings = {
  /**
   * @description Tooltip to explain the resource's overridden status
   */
  requestContentHeadersOverridden: 'Both request content and headers are overridden',
  /**
   * @description Tooltip to explain the resource's overridden status
   */
  requestContentOverridden: 'Request content is overridden',
  /**
   * @description Tooltip to explain the resource's overridden status
   */
  requestHeadersOverridden: 'Request headers are overridden',
  /**
   * @description Tooltip to explain why the request has warning icon
   */
  thirdPartyPhaseout:
      'Cookies for this request are blocked either because of Chrome flags or browser configuration. Learn more in the Issues panel.',
  /**
   * @description Tooltip to explain that a request was throttled
   * @example {Image} PH1
   * @example {3G} PH2
   */
  resourceTypeWithThrottling: '{PH1} (throttled to {PH2})',
  /**
   * @description Tooltip for a failed request
   * @example {Document} PH1
   */
  requestFailed: '{PH1} request failed',
  /**
   * @description Tooltip for a failed request
   * @example {Document} PH1
   */
  prefetchFailed: '{PH1} prefetch request failed',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/utils/utils.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

/** These utilities are packaged in a class to allow unittests to stub or spy the implementation. **/
export class PanelUtils {
  static isFailedNetworkRequest(request: SDK.NetworkRequest.NetworkRequest|null): boolean {
    if (!request) {
      return false;
    }
    if (request.failed && !request.statusCode) {
      return true;
    }
    if (request.statusCode >= 400) {
      return true;
    }
    const signedExchangeInfo = request.signedExchangeInfo();
    if (signedExchangeInfo !== null && Boolean(signedExchangeInfo.errors)) {
      return true;
    }
    if (request.corsErrorStatus()) {
      return true;
    }
    return false;
  }

  static getIconForNetworkRequest(request: SDK.NetworkRequest.NetworkRequest): TemplateResult {
    let type = request.resourceType();

    if (PanelUtils.isFailedNetworkRequest(request)) {
      let iconName: string;
      let title: string;
      // Failed prefetch network requests are displayed as warnings instead of errors.
      if (request.resourceType() === Common.ResourceType.resourceTypes.Prefetch) {
        title = i18nString(UIStrings.prefetchFailed, {PH1: type.title()});
        iconName = 'warning-filled';
      } else {
        title = i18nString(UIStrings.requestFailed, {PH1: type.title()});
        iconName = 'cross-circle-filled';
      }

      // clang-format off
      return html`<devtools-icon
          class="icon"
          name=${iconName}
          title=${title}
          role=img
        ></devtools-icon>`;
      // clang-format on
    }

    if (request.hasThirdPartyCookiePhaseoutIssue()) {
      // clang-format off
      return html`<devtools-icon
        class="icon"
        name="warning-filled"
        role=img
        title=${i18nString(UIStrings.thirdPartyPhaseout)}
      ></devtools-icon>`;
      // clang-format on
    }

    const isHeaderOverridden = request.hasOverriddenHeaders();
    const isContentOverridden = request.hasOverriddenContent;
    if (isHeaderOverridden || isContentOverridden) {
      let title: Common.UIString.LocalizedString;
      if (isHeaderOverridden && isContentOverridden) {
        title = i18nString(UIStrings.requestContentHeadersOverridden);
      } else if (isContentOverridden) {
        title = i18nString(UIStrings.requestContentOverridden);
      } else {
        title = i18nString(UIStrings.requestHeadersOverridden);
      }

      // clang-format off
      return html`<div class="network-override-marker">
          <devtools-icon class="icon" name="document" role=img title=${title}></devtools-icon>
        </div>`;
      // clang-format on
    }

    // Pick icon based on MIME type in the following cases:
    // - If the MIME type is 'image': some images have request type of 'fetch' or etc.
    // - If the request type is 'fetch': everything fetched by service worker has request type 'fetch'.
    // - If the request type is 'other' and MIME type is 'script', e.g. for wasm files
    const typeFromMime = Common.ResourceType.ResourceType.fromMimeType(request.mimeType);

    if (typeFromMime !== type && typeFromMime !== Common.ResourceType.resourceTypes.Other) {
      if (type === Common.ResourceType.resourceTypes.Fetch) {
        type = typeFromMime;
      } else if (typeFromMime === Common.ResourceType.resourceTypes.Image) {
        type = typeFromMime;
      } else if (
          type === Common.ResourceType.resourceTypes.Other &&
          typeFromMime === Common.ResourceType.resourceTypes.Script) {
        type = typeFromMime;
      }
    }

    if (type === Common.ResourceType.resourceTypes.Image) {
      // clang-format off
      return html`<div class="image icon">
        <img
          class="image-network-icon-preview"
          title=${iconTitleForRequest(request)}
          alt=${iconTitleForRequest(request)}
          ${ref(el => {
            if (el) {
             void request.populateImageSource(el as HTMLImageElement);
            }
          })}
        />
      </div>`;
      // clang-format on
    }

    // Exclude Manifest here because it has mimeType:application/json but it has its own icon
    if (type !== Common.ResourceType.resourceTypes.Manifest &&
        Common.ResourceType.ResourceType.simplifyContentType(request.mimeType) === 'application/json') {
      // clang-format off
      return html`<devtools-icon
          class="icon" name="file-json" title=${iconTitleForRequest(request)} role=img
          style="color:var(--icon-file-script)">
        </devtools-icon>`;
      // clang-format on
    }

    // Others
    const {iconName, color} = PanelUtils.iconDataForResourceType(type);
    // clang-format off
    return html`<devtools-icon
        class="icon" name=${iconName} title=${iconTitleForRequest(request)}
        style=${styleMap({color})}>
      </devtools-icon>`;
    // clang-format on

    function iconTitleForRequest(request: SDK.NetworkRequest.NetworkRequest): string {
      const throttlingConditions =
          SDK.NetworkManager.MultitargetNetworkManager.instance().appliedRequestConditions(request);
      if (!throttlingConditions?.urlPattern) {
        return request.resourceType().title();
      }
      const title = typeof throttlingConditions?.conditions.title === 'string' ?
          throttlingConditions?.conditions.title :
          throttlingConditions?.conditions.title();
      return i18nString(UIStrings.resourceTypeWithThrottling, {PH1: request.resourceType().title(), PH2: title});
    }
  }

  static iconDataForResourceType(resourceType: Common.ResourceType.ResourceType): {iconName: string, color?: string} {
    if (resourceType.isDocument()) {
      return {iconName: 'file-document'};
    }
    if (resourceType.isImage()) {
      return {iconName: 'file-image', color: 'var(--icon-file-image)'};
    }
    if (resourceType.isFont()) {
      return {iconName: 'file-font'};
    }
    if (resourceType.isScript()) {
      return {iconName: 'file-script'};
    }
    if (resourceType.isStyleSheet()) {
      return {iconName: 'file-stylesheet'};
    }
    if (resourceType.name() === Common.ResourceType.resourceTypes.Manifest.name()) {
      return {iconName: 'file-manifest'};
    }
    if (resourceType.name() === Common.ResourceType.resourceTypes.Wasm.name()) {
      return {iconName: 'file-wasm'};
    }
    if (resourceType.name() === Common.ResourceType.resourceTypes.WebSocket.name() ||
        resourceType.name() === Common.ResourceType.resourceTypes.DirectSocket.name()) {
      return {iconName: 'file-websocket'};
    }
    if (resourceType.name() === Common.ResourceType.resourceTypes.Media.name()) {
      return {iconName: 'file-media'};
    }

    if (resourceType.name() === Common.ResourceType.resourceTypes.Fetch.name() ||
        resourceType.name() === Common.ResourceType.resourceTypes.XHR.name()) {
      return {iconName: 'file-fetch-xhr'};
    }

    return {iconName: 'file-generic'};
  }

  static getIconForSourceFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): TemplateResult {
    const binding = Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode);
    const networkPersistenceManager = Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance();
    let iconType = 'document';
    let hasDotBadge = false;
    let isDotPurple = false;
    if (binding) {
      if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(binding.fileSystem)) {
        iconType = 'snippet';
      }
      hasDotBadge = true;
      isDotPurple = networkPersistenceManager.project() === binding.fileSystem.project();
    } else if (networkPersistenceManager.isActiveHeaderOverrides(uiSourceCode)) {
      hasDotBadge = true;
      isDotPurple = true;
    } else if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(uiSourceCode)) {
      iconType = 'snippet';
    }

    const title =
        binding ? PanelCommon.PersistenceUtils.PersistenceUtils.tooltipForUISourceCode(uiSourceCode) : undefined;
    // clang-format off
    return html`<devtools-file-source-icon
        class="icon"
        name=${iconType} 
        title=${ifDefined(title)} 
        .data=${{
          contentType: uiSourceCode.contentType().name(), hasDotBadge, isDotPurple, iconType}
        }></devtools-file-source-icon>`;
    // clang-format on
  }

  static async formatCSSChangesFromDiff(diff: Diff.Diff.DiffArray): Promise<string> {
    const indent = '  ';
    const {originalLines, currentLines, rows} = DiffView.DiffView.buildDiffRows(diff);
    const originalRuleMaps = await buildStyleRuleMaps(originalLines.join('\n'));
    const currentRuleMaps = await buildStyleRuleMaps(currentLines.join('\n'));

    let changes = '';
    let recordedOriginalSelector, recordedCurrentSelector;
    let hasOpenDeclarationBlock = false;
    for (const {currentLineNumber, originalLineNumber, type} of rows) {
      if (type !== DiffView.DiffView.RowType.DELETION && type !== DiffView.DiffView.RowType.ADDITION) {
        continue;
      }

      const isDeletion = type === DiffView.DiffView.RowType.DELETION;
      const lines = isDeletion ? originalLines : currentLines;
      // Diff line arrays starts at 0, but line numbers start at 1.
      const lineIndex = isDeletion ? originalLineNumber - 1 : currentLineNumber - 1;
      const line = lines[lineIndex].trim();
      const {declarationIDToStyleRule, styleRuleIDToStyleRule} = isDeletion ? originalRuleMaps : currentRuleMaps;
      let styleRule;
      let prefix = '';
      if (declarationIDToStyleRule.has(lineIndex)) {
        styleRule = declarationIDToStyleRule.get(lineIndex) as FormattableStyleRule;
        const selector = styleRule.selector;
        // Use the equality of selector strings as a best-effort check for the equality of style rules.
        if (selector !== recordedOriginalSelector && selector !== recordedCurrentSelector) {
          prefix += `${selector} {\n`;
        }
        prefix += indent;
        hasOpenDeclarationBlock = true;
      } else {
        if (hasOpenDeclarationBlock) {
          prefix = '}\n\n';
          hasOpenDeclarationBlock = false;
        }
        if (styleRuleIDToStyleRule.has(lineIndex)) {
          styleRule = styleRuleIDToStyleRule.get(lineIndex);
        }
      }

      const processedLine = isDeletion ? `/* ${line} */` : line;
      changes += prefix + processedLine + '\n';
      if (isDeletion) {
        recordedOriginalSelector = styleRule?.selector;
      } else {
        recordedCurrentSelector = styleRule?.selector;
      }
    }

    if (changes.length > 0) {
      changes += '}';
    }
    return changes;
  }

  static highlightElement(element: HTMLElement): void {
    element.scrollIntoViewIfNeeded();
    element.animate(
        [
          {offset: 0, backgroundColor: 'rgba(255, 255, 0, 0.2)'},
          {offset: 0.1, backgroundColor: 'rgba(255, 255, 0, 0.7)'},
          {offset: 1, backgroundColor: 'transparent'},
        ],
        {duration: 2000, easing: 'cubic-bezier(0, 0, 0.2, 1)'});
  }
}

interface FormattableStyleRule {
  rule: Formatter.FormatterWorkerPool.CSSRule;
  selector: string;
}

async function buildStyleRuleMaps(content: string): Promise<{
  declarationIDToStyleRule: Map<number, FormattableStyleRule>,
  styleRuleIDToStyleRule: Map<number, FormattableStyleRule>,
}> {
  const rules = await new Promise<Formatter.FormatterWorkerPool.CSSRule[]>(res => {
    const rules: Formatter.FormatterWorkerPool.CSSRule[] = [];
    Formatter.FormatterWorkerPool.formatterWorkerPool().parseCSS(content, (isLastChunk, currentRules) => {
      rules.push(...currentRules);
      if (isLastChunk) {
        res(rules);
      }
    });
  });

  // We use line numbers as unique IDs for rules and declarations
  const declarationIDToStyleRule = new Map<number, FormattableStyleRule>();
  const styleRuleIDToStyleRule = new Map<number, FormattableStyleRule>();
  for (const rule of rules) {
    if ('styleRange' in rule) {
      const selector = rule.selectorText.split('\n').pop()?.trim();
      if (!selector) {
        continue;
      }
      const styleRule = {rule, selector};
      styleRuleIDToStyleRule.set(rule.styleRange.startLine, styleRule);
      for (const property of rule.properties) {
        declarationIDToStyleRule.set(property.range.startLine, styleRule);
      }
    }
  }
  return {declarationIDToStyleRule, styleRuleIDToStyleRule};
}
