// Copyright 2018 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 i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import type * as SDK from '../../core/sdk/sdk.js';
import type * as Logs from '../../models/logs/logs.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import type * as Workspace from '../../models/workspace/workspace.js';
import * as NetworkForward from '../../panels/network/forward/forward.js';
import type * as Search from '../search/search.js';

const UIStrings = {
  /**
   * @description Text for web URLs
   */
  url: 'URL',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/network/NetworkSearchScope.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class NetworkSearchScope implements Search.SearchScope.SearchScope {
  #networkLog: Logs.NetworkLog.NetworkLog;

  constructor(networkLog: Logs.NetworkLog.NetworkLog) {
    this.#networkLog = networkLog;
  }

  performIndexing(progress: Common.Progress.Progress): void {
    queueMicrotask(() => {
      progress.done = true;
    });
  }

  async performSearch(
      searchConfig: Workspace.SearchConfig.SearchConfig, progress: Common.Progress.Progress,
      searchResultCallback: (arg0: Search.SearchScope.SearchResult) => void,
      searchFinishedCallback: (arg0: boolean) => void): Promise<void> {
    const promises = [];
    const requests =
        this.#networkLog.requests().filter(request => searchConfig.filePathMatchesFileQuery(request.url()));
    progress.totalWork = requests.length;
    for (const request of requests) {
      const promise = this.searchRequest(searchConfig, request, progress);
      promises.push(promise);
    }
    const resultsWithNull = await Promise.all(promises);
    const results = (resultsWithNull.filter(result => result !== null));
    if (progress.canceled) {
      searchFinishedCallback(false);
      return;
    }
    for (const result of results.sort((r1, r2) => r1.label().localeCompare(r2.label()))) {
      if (result.matchesCount() > 0) {
        searchResultCallback(result);
      }
    }
    progress.done = true;
    searchFinishedCallback(true);
  }

  private async searchRequest(
      searchConfig: Workspace.SearchConfig.SearchConfig, request: SDK.NetworkRequest.NetworkRequest,
      progress: Common.Progress.Progress): Promise<NetworkSearchResult|null> {
    const bodyMatches = await NetworkSearchScope.#responseBodyMatches(searchConfig, request);
    if (progress.canceled) {
      return null;
    }
    const locations = [];
    if (stringMatchesQuery(request.url())) {
      locations.push(NetworkForward.UIRequestLocation.UIRequestLocation.urlMatch(request));
    }
    for (const header of request.requestHeaders()) {
      if (headerMatchesQuery(header)) {
        locations.push(NetworkForward.UIRequestLocation.UIRequestLocation.requestHeaderMatch(request, header));
      }
    }
    for (const header of request.responseHeaders) {
      if (headerMatchesQuery(header)) {
        locations.push(NetworkForward.UIRequestLocation.UIRequestLocation.responseHeaderMatch(request, header));
      }
    }
    for (const match of bodyMatches) {
      locations.push(NetworkForward.UIRequestLocation.UIRequestLocation.bodyMatch(request, match));
    }
    ++progress.worked;
    return new NetworkSearchResult(request, locations);

    function headerMatchesQuery(header: SDK.NetworkRequest.NameValue): boolean {
      return stringMatchesQuery(`${header.name}: ${header.value}`);
    }

    function stringMatchesQuery(string: string): boolean {
      const flags = searchConfig.ignoreCase() ? 'i' : '';
      const regExps =
          searchConfig.queries().map(query => new RegExp(Platform.StringUtilities.escapeForRegExp(query), flags));
      let pos = 0;
      for (const regExp of regExps) {
        const match = string.substr(pos).match(regExp);
        if (match?.index === undefined) {
          return false;
        }
        pos += match.index + match[0].length;
      }
      return true;
    }
  }

  static async #responseBodyMatches(
      searchConfig: Workspace.SearchConfig.SearchConfig,
      request: SDK.NetworkRequest.NetworkRequest): Promise<TextUtils.ContentProvider.SearchMatch[]> {
    if (!request.contentType().isTextType()) {
      return [];
    }

    let matches: TextUtils.ContentProvider.SearchMatch[] = [];
    for (const query of searchConfig.queries()) {
      const tmpMatches = await request.searchInContent(query, !searchConfig.ignoreCase(), searchConfig.isRegex());
      if (tmpMatches.length === 0) {
        // Mirror file search that all individual queries must produce matches.
        return [];
      }
      matches =
          Platform.ArrayUtilities.mergeOrdered(matches, tmpMatches, TextUtils.ContentProvider.SearchMatch.comparator);
    }
    return matches;
  }

  stopSearch(): void {
  }
}

export class NetworkSearchResult implements Search.SearchScope.SearchResult {
  private readonly request: SDK.NetworkRequest.NetworkRequest;
  private readonly locations: NetworkForward.UIRequestLocation.UIRequestLocation[];

  constructor(
      request: SDK.NetworkRequest.NetworkRequest, locations: NetworkForward.UIRequestLocation.UIRequestLocation[]) {
    this.request = request;
    this.locations = locations;
  }

  matchesCount(): number {
    return this.locations.length;
  }

  label(): string {
    return this.request.displayName;
  }

  description(): string {
    const parsedUrl = this.request.parsedURL;
    if (!parsedUrl) {
      return this.request.url();
    }
    return parsedUrl.urlWithoutScheme();
  }

  matchLineContent(index: number): string {
    const location = this.locations[index];
    if (location.isUrlMatch) {
      return this.request.url();
    }
    const header = location?.header?.header;
    if (header) {
      return header.value;
    }
    return (location.searchMatch as TextUtils.ContentProvider.SearchMatch).lineContent;
  }

  matchRevealable(index: number): Object {
    return this.locations[index];
  }

  matchLabel(index: number): string {
    const location = this.locations[index];
    if (location.isUrlMatch) {
      return i18nString(UIStrings.url);
    }
    const header = location?.header?.header;
    if (header) {
      return `${header.name}:`;
    }

    return ((location.searchMatch as TextUtils.ContentProvider.SearchMatch).lineNumber + 1).toString();
  }

  matchColumn(index: number): number|undefined {
    const location = this.locations[index];
    return location.searchMatch?.columnNumber;
  }

  matchLength(index: number): number|undefined {
    const location = this.locations[index];
    return location.searchMatch?.matchLength;
  }
}
