// 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.
/* eslint-disable @devtools/no-imperative-dom-api */

/*
 * Copyright (C) 2012 Research In Motion Limited. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import {BinaryResourceView} from './BinaryResourceView.js';
import {DataGridItem, ResourceChunkView} from './ResourceChunkView.js';

const UIStrings = {
  /**
   * @description Text in Resource Web Socket Frame View of the Network panel. Displays which Opcode
   * is relevant to a particular operation. 'mask' indicates that the Opcode used a mask, which is a
   * way of modifying a value by overlaying another value on top of it, partially covering/changing
   * it, hence 'masking' it.
   * https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
   * @example {Localized name of the Opcode} PH1
   * @example {0} PH2
   */
  sOpcodeSMask: '{PH1} (Opcode {PH2}, mask)',
  /**
   * @description Text in Resource Web Socket Frame View of the Network panel. Displays which Opcode
   * is relevant to a particular operation.
   * @example {Localized name of the Opcode} PH1
   * @example {0} PH2
   */
  sOpcodeS: '{PH1} (Opcode {PH2})',
  /**
   * @description Op codes continuation frame of map in Resource Web Socket Frame View of the Network panel
   */
  continuationFrame: 'Continuation Frame',
  /**
   * @description Op codes text frame of map in Resource Web Socket Frame View of the Network panel
   */
  textMessage: 'Text Message',
  /**
   * @description Op codes binary frame of map in Resource Web Socket Frame View of the Network panel
   */
  binaryMessage: 'Binary Message',
  /**
   * @description Op codes continuation frame of map in Resource Web Socket Frame View of the Network panel indicating that the web socket connection has been closed.
   */
  connectionCloseMessage: 'Connection Close Message',
  /**
   * @description Op codes ping frame of map in Resource Web Socket Frame View of the Network panel
   */
  pingMessage: 'Ping Message',
  /**
   * @description Op codes pong frame of map in Resource Web Socket Frame View of the Network panel
   */
  pongMessage: 'Pong Message',
  /**
   * @description Data grid name for Web Socket Frame data grids
   */
  webSocketFrame: 'Web Socket Frame',
  /**
   * @description Text for something not available
   */
  na: 'N/A',
  /**
   * @description Example for placeholder text
   */
  filterUsingRegex: 'Filter using regex (example: (web)?socket)',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/network/ResourceWebSocketFrameView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);

export class ResourceWebSocketFrameView extends ResourceChunkView<SDK.NetworkRequest.WebSocketFrame> {
  constructor(request: SDK.NetworkRequest.NetworkRequest) {
    super(
        request, 'network-web-socket-message-filter', 'resource-web-socket-frame-split-view-state',
        i18nString(UIStrings.webSocketFrame), i18nString(UIStrings.filterUsingRegex));
    this.element.setAttribute('jslog', `${VisualLogging.pane('web-socket-messages').track({resize: true})}`);
  }

  override getRequestChunks(): SDK.NetworkRequest.WebSocketFrame[] {
    return this.request.frames();
  }
  override createGridItem(frame: SDK.NetworkRequest.WebSocketFrame): DataGridItem {
    return new ResourceFrameNode(frame);
  }

  override chunkFilter(frame: SDK.NetworkRequest.WebSocketFrame): boolean {
    if (this.filterType && frame.type !== this.filterType) {
      return false;
    }
    return !this.filterRegex || this.filterRegex.test(frame.text);
  }

  override wasShown(): void {
    super.wasShown();
    this.refresh();
    this.request.addEventListener(SDK.NetworkRequest.Events.WEBSOCKET_FRAME_ADDED, this.onWebSocketFrameAdded, this);
  }

  override willHide(): void {
    super.willHide();
    this.request.removeEventListener(SDK.NetworkRequest.Events.WEBSOCKET_FRAME_ADDED, this.onWebSocketFrameAdded, this);
  }

  private onWebSocketFrameAdded(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.WebSocketFrame>): void {
    this.chunkAdded(event.data);
  }

  static opCodeDescription(opCode: number, mask: boolean): string {
    const localizedDescription = opCodeDescriptions[opCode] || (() => '');
    if (mask) {
      return i18nString(UIStrings.sOpcodeSMask, {PH1: localizedDescription(), PH2: opCode});
    }
    return i18nString(UIStrings.sOpcodeS, {PH1: localizedDescription(), PH2: opCode});
  }
}

const enum OpCodes {
  CONTINUATION_FRAME = 0,
  TEXT_FRAME = 1,
  BINARY_FRAME = 2,
  CONNECTION_CLOSE_FRAME = 8,
  PING_FRAME = 9,
  PONG_FRAME = 10,
}

const opCodeDescriptions: Array<() => string> = (function(): Array<() => Common.UIString.LocalizedString> {
  const map = [];
  map[OpCodes.CONTINUATION_FRAME] = i18nLazyString(UIStrings.continuationFrame);
  map[OpCodes.TEXT_FRAME] = i18nLazyString(UIStrings.textMessage);
  map[OpCodes.BINARY_FRAME] = i18nLazyString(UIStrings.binaryMessage);
  map[OpCodes.CONNECTION_CLOSE_FRAME] = i18nLazyString(UIStrings.connectionCloseMessage);
  map[OpCodes.PING_FRAME] = i18nLazyString(UIStrings.pingMessage);
  map[OpCodes.PONG_FRAME] = i18nLazyString(UIStrings.pongMessage);
  return map;
})();

class ResourceFrameNode extends DataGridItem {
  readonly frame: SDK.NetworkRequest.WebSocketFrame;
  private readonly isTextFrame: boolean;
  #dataText: string;
  #binaryView: BinaryResourceView|null;

  constructor(frame: SDK.NetworkRequest.WebSocketFrame) {
    let length = String(frame.text.length);
    const time = new Date(frame.time * 1000);
    const timeText = ('0' + time.getHours()).substr(-2) + ':' + ('0' + time.getMinutes()).substr(-2) + ':' +
        ('0' + time.getSeconds()).substr(-2) + '.' + ('00' + time.getMilliseconds()).substr(-3);
    const timeNode = document.createElement('div');
    UI.UIUtils.createTextChild(timeNode, timeText);
    UI.Tooltip.Tooltip.install(timeNode, time.toLocaleString());

    let dataText: string = frame.text;
    let description = ResourceWebSocketFrameView.opCodeDescription(frame.opCode, frame.mask);
    const isTextFrame = frame.opCode === OpCodes.TEXT_FRAME;

    if (frame.type === SDK.NetworkRequest.WebSocketFrameType.Error) {
      description = dataText;
      length = i18nString(UIStrings.na);

    } else if (isTextFrame) {
      description = dataText;

    } else if (frame.opCode === OpCodes.BINARY_FRAME) {
      length = i18n.ByteUtilities.bytesToString(Platform.StringUtilities.base64ToSize(frame.text));
      description = opCodeDescriptions[frame.opCode]();

    } else {
      dataText = description;
    }

    super({data: description, length, time: timeNode});

    this.frame = frame;
    this.isTextFrame = isTextFrame;
    this.#dataText = dataText;

    this.#binaryView = null;
  }

  override createCells(element: Element): void {
    element.classList.toggle(
        'resource-chunk-view-row-error', this.frame.type === SDK.NetworkRequest.WebSocketFrameType.Error);
    element.classList.toggle(
        'resource-chunk-view-row-send', this.frame.type === SDK.NetworkRequest.WebSocketFrameType.Send);
    element.classList.toggle(
        'resource-chunk-view-row-receive', this.frame.type === SDK.NetworkRequest.WebSocketFrameType.Receive);
    super.createCells(element);
  }

  override nodeSelfHeight(): number {
    return 21;
  }

  override dataText(): string {
    return this.#dataText;
  }

  override binaryView(): BinaryResourceView|null {
    if (this.isTextFrame || this.frame.type === SDK.NetworkRequest.WebSocketFrameType.Error) {
      return null;
    }

    if (!this.#binaryView) {
      if (this.#dataText.length > 0) {
        this.#binaryView = new BinaryResourceView(
            TextUtils.StreamingContentData.StreamingContentData.from(
                new TextUtils.ContentData.ContentData(this.#dataText, true, 'applicaiton/octet-stream')),
            Platform.DevToolsPath.EmptyUrlString, Common.ResourceType.resourceTypes.WebSocket);
      }
    }
    return this.#binaryView;
  }

  override getTime(): number {
    return this.frame.time;
  }
}
