// Copyright 2019 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 * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import type * as Protocol from '../../generated/protocol.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import type {MainView, TriggerDispatcher} from './MainView.js';
import type {PlayerEvent} from './MediaModel.js';
import playerListViewStyles from './playerListView.css.js';
import {PlayerPropertyKeys} from './PlayerPropertiesView.js';

const {classMap} = Directives;

const UIStrings = {
  /**
   * @description A right-click context menu entry which when clicked causes the menu entry for that player to be removed.
   */
  hidePlayer: 'Hide player',
  /**
   * @description A right-click context menu entry which should keep the element selected, while hiding all other entries.
   */
  hideAllOthers: 'Hide all others',
  /**
   * @description Context menu entry which downloads the json dump when clicked
   */
  savePlayerInfo: 'Save player info',
  /**
   * @description Side-panel entry title text for the players section.
   */
  players: 'Players',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/media/PlayerListView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

interface PlayerStatus {
  playerTitle: string;
  frameTitle: string;
  playerID: string;
  exists: boolean;
  playing: boolean;
  titleEdited: boolean;
  iconName: string;
}

export interface ViewInput {
  players: PlayerStatus[];
  selectedPlayerID: string|null;
  onPlayerClick: (playerID: string) => void;
  onPlayerContextMenu: (playerID: string, event: Event) => void;
}
export type View = (input: ViewInput, output: object, target: HTMLElement) => void;

const DEFAULT_VIEW: View = (input, _output, target) => {
  // clang-format off
  render(
    html`
      <style>${playerListViewStyles}</style>
      <div class="player-entry-header" id="players-header">${i18nString(UIStrings.players)}</div>
      <div role="listbox" aria-labelledby="players-header">
      ${input.players.map(player => {
        const isSelected = player.playerID === input.selectedPlayerID;
        return html`
          <div class=${classMap({
            'player-entry-row': true,
            hbox: true,
            selected: isSelected,
            'force-white-icons': isSelected,
          })}
               tabindex="0"
               @click=${() => input.onPlayerClick(player.playerID)}
               @keydown=${(e: KeyboardEvent) => {
                 if (Platform.KeyboardUtilities.isEnterOrSpaceKey(e)) {
                   e.preventDefault();
                   input.onPlayerClick(player.playerID);
                 }
               }}
               @contextmenu=${(e: Event) => input.onPlayerContextMenu(player.playerID, e)}
               role="option"
               aria-selected=${isSelected}
               jslog=${VisualLogging.item('player').track({click: true, resize: true})}>
            <div class="player-entry-status-icon vbox">
              <div class="player-entry-status-icon-centering">
                <devtools-icon name=${player.iconName}></devtools-icon>
              </div>
            </div>
            <div class="player-entry-frame-title">${player.frameTitle}</div>
            <div class="player-entry-player-title">${player.playerTitle}</div>
          </div>
        `;
      })}
      </div>
    `,
    target
  );
  // clang-format on
};

export class PlayerListView extends UI.Widget.VBox implements TriggerDispatcher {
  #view: View;
  private readonly playerStatuses: Map<string, PlayerStatus>;
  private readonly playerEntriesWithHostnameFrameTitle: Set<string>;
  private readonly mainContainer: MainView;
  private currentlySelectedPlayerID: string|null;

  constructor(mainContainer: MainView, view: View = DEFAULT_VIEW) {
    super({useShadowDom: true});
    this.#view = view;

    this.playerStatuses = new Map();
    this.playerEntriesWithHostnameFrameTitle = new Set();

    // Container where new panels can be added based on clicks.
    this.mainContainer = mainContainer;

    this.currentlySelectedPlayerID = null;
    this.requestUpdate();
  }

  override performUpdate(): void {
    const input: ViewInput = {
      players: Array.from(this.playerStatuses.values()),
      selectedPlayerID: this.currentlySelectedPlayerID,
      onPlayerClick: this.selectPlayer.bind(this),
      onPlayerContextMenu: this.rightClickPlayer.bind(this),
    };
    this.#view(input, {}, this.contentElement);
  }

  selectPlayerById(playerID: string): void {
    if (this.playerStatuses.has(playerID)) {
      this.selectPlayer(playerID);
    }
  }

  private selectPlayer(playerID: string): void {
    this.mainContainer.renderMainPanel(playerID);
    this.currentlySelectedPlayerID = playerID;
    this.requestUpdate();
  }

  private rightClickPlayer(playerID: string, event: Event): void {
    const contextMenu = new UI.ContextMenu.ContextMenu(event);
    contextMenu.headerSection().appendItem(
        i18nString(UIStrings.hidePlayer), this.mainContainer.markPlayerForDeletion.bind(this.mainContainer, playerID),
        {jslogContext: 'hide-player'});
    contextMenu.headerSection().appendItem(
        i18nString(UIStrings.hideAllOthers),
        this.mainContainer.markOtherPlayersForDeletion.bind(this.mainContainer, playerID),
        {jslogContext: 'hide-all-others'});
    contextMenu.headerSection().appendItem(
        i18nString(UIStrings.savePlayerInfo), this.mainContainer.exportPlayerData.bind(this.mainContainer, playerID),
        {jslogContext: 'save-player-info'});
    void contextMenu.show();
  }

  private setMediaElementFrameTitle(playerID: string, frameTitle: string, isHostname: boolean): void {
    // Only remove the title from the set if we aren't setting a hostname title.
    // Otherwise, if it has a non-hostname title, and the requested new title is
    // a hostname, just drop it.
    if (this.playerEntriesWithHostnameFrameTitle.has(playerID)) {
      if (!isHostname) {
        this.playerEntriesWithHostnameFrameTitle.delete(playerID);
      }
    } else if (isHostname) {
      return;
    }

    if (!this.playerStatuses.has(playerID)) {
      return;
    }
    const playerStatus = this.playerStatuses.get(playerID);
    if (playerStatus) {
      playerStatus.frameTitle = frameTitle;
      this.requestUpdate();
    }
  }

  private setMediaElementPlayerTitle(playerID: string, playerTitle: string): void {
    if (!this.playerStatuses.has(playerID)) {
      return;
    }
    const playerStatus = this.playerStatuses.get(playerID);
    if (playerStatus) {
      playerStatus.playerTitle = playerTitle;
      this.requestUpdate();
    }
  }

  private setMediaElementPlayerIcon(playerID: string, iconName: string): void {
    if (!this.playerStatuses.has(playerID)) {
      return;
    }
    const playerStatus = this.playerStatuses.get(playerID);
    if (playerStatus) {
      playerStatus.iconName = iconName;
      this.requestUpdate();
    }
  }

  private formatAndEvaluate(
      playerID: string, func: (...args: any[]) => unknown, candidate: string, min: number, max: number): void {
    if (candidate.length <= min) {
      return;
    }
    if (candidate.length >= max) {
      candidate = candidate.substring(0, max - 1) + '…';
    }
    func.bind(this)(playerID, candidate);
  }

  addMediaElementItem(playerID: string): void {
    this.playerStatuses.set(playerID, {
      playerTitle: 'PlayerTitle',
      frameTitle: 'FrameTitle',
      playerID,
      exists: true,
      playing: false,
      titleEdited: false,
      iconName: 'pause',
    });
    this.playerEntriesWithHostnameFrameTitle.add(playerID);
    this.requestUpdate();
  }

  deletePlayer(playerID: string): void {
    if (!this.playerStatuses.has(playerID)) {
      return;
    }
    this.playerStatuses.delete(playerID);
    this.requestUpdate();
  }

  onEvent(playerID: string, event: PlayerEvent): void {
    const parsed = JSON.parse(event.value);
    const eventType = parsed.event;

    // Load events provide the actual underlying URL for the video, which makes
    // a great way to identify a specific video within a page that potentially
    // may have many videos. MSE videos have a special blob:http(s) protocol
    // that we'd like to keep mind of, so we do prepend blob:
    if (eventType === 'kLoad') {
      const url = parsed.url as string;
      const videoName = url.substring(url.lastIndexOf('/') + 1);
      this.formatAndEvaluate(playerID, this.setMediaElementPlayerTitle, videoName, 1, 20);
      return;
    }

    if (eventType === 'kPlay') {
      this.setMediaElementPlayerIcon(playerID, 'play');
      return;
    }

    if (eventType === 'kPause' || eventType === 'kEnded') {
      this.setMediaElementPlayerIcon(playerID, 'pause');
      return;
    }

    if (eventType === 'kWebMediaPlayerDestroyed') {
      this.setMediaElementPlayerIcon(playerID, 'cross');
      return;
    }
  }

  onProperty(playerID: string, property: Protocol.Media.PlayerProperty): void {
    // FrameUrl is always present, and we can generate a basic frame title from
    // it by grabbing the hostname. It's not possible to generate a "good" player
    // title from the FrameUrl though, since the page location itself might not
    // have any relevance to the video being played, and would be shared by all
    // videos on the page.
    if (property.name === PlayerPropertyKeys.FRAME_URL) {
      const frameTitle = new URL(property.value).hostname;
      this.formatAndEvaluate(playerID, this.setMediaElementFrameTitle, frameTitle, 1, 20);
      return;
    }

    // On the other hand, the page may set a title, which usually makes for a
    // better frame title than a hostname. Unfortunately, its only "usually",
    // since the site is free to set the title to _anything_, it might just be
    // junk, or it might be super long. If it's empty, or 1 character, It's
    // preferable to just drop it. Titles longer than 20 will have the first
    // 17 characters kept and an ellipsis appended.
    if (property.name === PlayerPropertyKeys.FRAME_TITLE && property.value) {
      this.formatAndEvaluate(playerID, this.setMediaElementFrameTitle, property.value, 1, 20);
      return;
    }
  }

  onError(_playerID: string, _error: Protocol.Media.PlayerError): void {
    // TODO(tmathmeyer) show an error icon next to the player name
  }

  onMessage(_playerID: string, _message: Protocol.Media.PlayerMessage): void {
    // TODO(tmathmeyer) show a message count number next to the player name.
  }
}
