/**
 * The Camera UI View that displays all the camera feeds as individual video elements.
 * @type {Application}
 *
 * @param {WebRTC} webrtc                 The WebRTC Implementation to display
 * @param {ApplicationOptions} [options]  Application configuration options.
 */
class CameraViews extends Application {
  constructor(options={}) {
    if ( !("width" in options) ) options.width = game.webrtc?.settings.client.dockWidth || 240;
    super(options);
    if ( game.webrtc?.settings.client.dockPosition === AVSettings.DOCK_POSITIONS.RIGHT ) {
      this.options.resizable.rtl = true;
    }
  }

  /** @override */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      id: "camera-views",
      template: "templates/hud/camera-views.html",
      popOut: false,
      width: 240,
      resizable: {selector: ".camera-view-width-control", resizeY: false}
    });
  }

  /* -------------------------------------------- */

  /**
   * A reference to the master AV orchestrator instance
   * @type {AVMaster}
   */
  get webrtc() {
    return game.webrtc;
  }

  /* -------------------------------------------- */

  /**
   * If all camera views are popped out, hide the dock.
   * @type {boolean}
   */
  get hidden() {
    return this.webrtc.client.getConnectedUsers().reduce((hidden, u) => {
      const settings = this.webrtc.settings.users[u];
      return hidden && (settings.blocked || settings.popout);
    }, true);
  }

  /* -------------------------------------------- */
  /* Public API                                   */
  /* -------------------------------------------- */

  /**
   * Obtain a reference to the div.camera-view which is used to portray a given Foundry User.
   * @param {string} userId     The ID of the User document
   * @return {HTMLElement|null}
   */
  getUserCameraView(userId) {
    return this.element.find(`.camera-view[data-user=${userId}]`)[0] || null;
  }

  /* -------------------------------------------- */

  /**
   * Obtain a reference to the video.user-camera which displays the video channel for a requested Foundry User.
   * If the user is not broadcasting video this will return null.
   * @param {string} userId     The ID of the User document
   * @return {HTMLVideoElement|null}
   */
  getUserVideoElement(userId) {
    return this.element.find(`.camera-view[data-user=${userId}] video.user-camera`)[0] || null;
  }

  /* -------------------------------------------- */

  /**
   * Sets whether a user is currently speaking or not
   *
   * @param {string} userId     The ID of the user
   * @param {boolean} speaking  Whether the user is speaking
   */
  setUserIsSpeaking(userId, speaking) {
    const view = this.getUserCameraView(userId);
    if ( view ) view.classList.toggle("speaking", speaking);
  }

  /* -------------------------------------------- */
  /*  Application Rendering                       */
  /* -------------------------------------------- */

  /**
   * Extend the render logic to first check whether a render is necessary based on the context
   * If a specific context was provided, make sure an update to the navigation is necessary before rendering
   */
  render(force, context={}) {
    const { renderContext, renderData } = context;
    if ( this.webrtc.mode === AVSettings.AV_MODES.DISABLED ) return this;
    if ( renderContext ) {
      if ( renderContext !== "updateUser" ) return this;
      const updateKeys = ["name", "permissions", "role", "active", "color", "sort", "character", "avatar"];
      if ( !updateKeys.some(k => renderData.hasOwnProperty(k)) ) return this;
    }
    return super.render(force, context);
  }

  /* -------------------------------------------- */

  /** @override */
  async _render(force = false, options = {}) {
    await super._render(force, options);
    this.webrtc.onRender();
  }

  /* -------------------------------------------- */

  /** @inheritdoc */
  setPosition({left, top, width, scale} = {}) {
    const position = super.setPosition({left, top, width, height: "auto", scale});
    if ( foundry.utils.isEmpty(position) ) return position;
    const clientSettings = game.webrtc.settings.client;
    if ( game.webrtc.settings.verticalDock ) {
      clientSettings.dockWidth = width;
      game.webrtc.settings.set("client", "dockWidth", width);
    }
    return position;
  }

  /* -------------------------------------------- */

  /** @override */
  getData(options={}) {
    const settings = this.webrtc.settings;
    const userSettings = settings.users;

    // Get the sorted array of connected users
    const connectedIds = this.webrtc.client.getConnectedUsers();
    const users = connectedIds.reduce((users, u) => {
      const data = this._getDataForUser(u, userSettings[u]);
      if ( data && !userSettings[u].blocked ) users.push(data);
      return users;
    }, []);
    users.sort(this.constructor._sortUsers);

    // Maximum Z of all user popout windows
    this.maxZ = Math.max(...users.map(u => userSettings[u.user.id].z));

    // Define a dynamic class for the camera dock container which affects its rendered style
    const dockClass = [`camera-position-${settings.client.dockPosition}`];
    if ( !users.some(u => !u.settings.popout) ) dockClass.push("webrtc-dock-empty");
    if ( settings.client.hideDock ) dockClass.push("webrtc-dock-minimized");
    if ( this.hidden ) dockClass.push("hidden");

    // Alter the body class depending on whether the players list is hidden
    const playersVisible = !settings.client.hidePlayerList || settings.client.hideDock;
    document.body.classList.toggle("players-hidden", playersVisible);

    const nameplateModes = AVSettings.NAMEPLATE_MODES;
    const nameplateSetting = settings.client.nameplates ?? nameplateModes.BOTH;

    const nameplates = {
      cssClass: [
        nameplateSetting === nameplateModes.OFF ? "hidden" : "",
        [nameplateModes.PLAYER_ONLY, nameplateModes.CHAR_ONLY].includes(nameplateSetting) ? "noanimate" : ""
      ].filterJoin(" "),
      playerName: [nameplateModes.BOTH, nameplateModes.PLAYER_ONLY].includes(nameplateSetting),
      charname: [nameplateModes.BOTH, nameplateModes.CHAR_ONLY].includes(nameplateSetting)
    };

    // Return data for rendering
    return {
      self: game.user,
      muteAll: settings.muteAll,
      borderColors: settings.client.borderColors,
      dockClass: dockClass.join(" "),
      hidden: this.hidden,
      users, nameplates
    };
  }

  /* -------------------------------------------- */

  /**
   * Prepare rendering data for a single user
   * @private
   */
  _getDataForUser(userId, settings) {
    const user = game.users.get(userId);
    if ( !user || !user.active ) return null;
    const charname = user.character ? user.character.name.split(" ")[0] : "";

    // CSS classes for the frame
    const frameClass = settings.popout ? "camera-box-popout" : "camera-box-dock";
    const audioClass = this.webrtc.canUserShareAudio(userId) ? null : "no-audio";
    const videoClass = this.webrtc.canUserShareVideo(userId) ? null : "no-video";

    // Return structured User data
    return {
      user, settings,
      local: user.isSelf,
      charname: user.isGM ? game.i18n.localize("USER.GM") : charname,
      volume: foundry.audio.AudioHelper.volumeToInput(settings.volume),
      cameraViewClass: [frameClass, videoClass, audioClass].filterJoin(" ")
    };
  }

  /* -------------------------------------------- */

  /**
   * A custom sorting function that orders/arranges the user display frames
   * @return {number}
   * @private
   */
  static _sortUsers(a, b) {
    const as = a.settings;
    const bs = b.settings;
    if (as.popout && bs.popout) return as.z - bs.z; // Sort popouts by z-index
    if (as.popout) return -1;                       // Show popout feeds first
    if (bs.popout) return 1;
    if (a.user.isSelf) return -1;                   // Show local feed first
    if (b.user.isSelf) return 1;
    if (a.hasVideo && !b.hasVideo) return -1;       // Show remote users with a camera before those without
    if (b.hasVideo && !a.hasVideo) return 1;
    return a.user.sort - b.user.sort;               // Sort according to user order
  }

  /* -------------------------------------------- */
  /*  Event Listeners and Handlers                */
  /* -------------------------------------------- */

  /** @override */
  activateListeners(html) {

    // Display controls when hovering over the video container
    let cvh = this._onCameraViewHover.bind(this);
    html.find(".camera-view").hover(cvh, cvh);

    // Handle clicks on AV control buttons
    html.find(".av-control").click(this._onClickControl.bind(this));

    // Handle volume changes
    html.find(".webrtc-volume-slider").change(this._onVolumeChange.bind(this));

    // Handle user controls.
    this._refreshView(html.find(".user-controls")[0]?.dataset.user);

    // Hide Global permission icons depending on the A/V mode
    const mode = this.webrtc.mode;
    if ( mode === AVSettings.AV_MODES.VIDEO ) html.find('[data-action="toggle-audio"]').hide();
    if ( mode === AVSettings.AV_MODES.AUDIO ) html.find('[data-action="toggle-video"]').hide();

    // Make each popout window draggable
    for ( let popout of this.element.find(".app.camera-view-popout") ) {
      let box = popout.querySelector(".camera-view");
      new CameraPopoutAppWrapper(this, box.dataset.user, $(popout));
    }

    // Listen to the video's srcObjectSet event to set the display mode of the user.
    for ( let video of this.element.find("video") ) {
      const view = video.closest(".camera-view");
      this._refreshView(view.dataset.user);
      video.addEventListener("webrtcVideoSet", ev => {
        const view = video.closest(".camera-view");
        if ( view.dataset.user !== ev.detail ) return;
        this._refreshView(view.dataset.user);
      });
    }
  }

  /* -------------------------------------------- */

  /**
   * On hover in a camera container, show/hide the controls.
   * @event {Event} event   The original mouseover or mouseout hover event
   * @private
   */
  _onCameraViewHover(event) {
    this._toggleControlVisibility(event.currentTarget, event.type === "mouseenter", null);
  }

  /* -------------------------------------------- */

  /**
   * On clicking on a toggle, disable/enable the audio or video stream.
   * @event {MouseEvent} event   The originating click event
   * @private
   */
  async _onClickControl(event) {
    event.preventDefault();

    // Reference relevant data
    const button = event.currentTarget;
    const action = button.dataset.action;
    const userId = button.closest(".camera-view, .user-controls")?.dataset.user;
    const user = game.users.get(userId);
    const settings = this.webrtc.settings;
    const userSettings = settings.getUser(user.id);

    // Handle different actions
    switch ( action ) {

      // Globally block video
      case "block-video":
        if ( !game.user.isGM ) return;
        await user.update({"permissions.BROADCAST_VIDEO": !userSettings.canBroadcastVideo});
        return this._refreshView(userId);

      // Globally block audio
      case "block-audio":
        if ( !game.user.isGM ) return;
        await user.update({"permissions.BROADCAST_AUDIO": !userSettings.canBroadcastAudio});
        return this._refreshView(userId);

      // Hide the user
      case "hide-user":
        if ( user.isSelf ) return;
        await settings.set("client", `users.${user.id}.blocked`, !userSettings.blocked);
        return this.render();

      // Toggle video display
      case "toggle-video":
        if ( !user.isSelf ) return;
        if ( userSettings.hidden && !userSettings.canBroadcastVideo ) {
          return ui.notifications.warn("WEBRTC.WarningCannotEnableVideo", {localize: true});
        }
        await settings.set("client", `users.${user.id}.hidden`, !userSettings.hidden);
        return this._refreshView(userId);

      // Toggle audio output
      case "toggle-audio":
        if ( !user.isSelf ) return;
        if ( userSettings.muted && !userSettings.canBroadcastAudio ) {
          return ui.notifications.warn("WEBRTC.WarningCannotEnableAudio", {localize: true});
        }
        await settings.set("client", `users.${user.id}.muted`, !userSettings.muted);
        return this._refreshView(userId);

      // Toggle mute all peers
      case "mute-peers":
        if ( !user.isSelf ) return;
        await settings.set("client", "muteAll", !settings.client.muteAll);
        return this._refreshView(userId);

      // Disable sending and receiving video
      case "disable-video":
        if ( !user.isSelf ) return;
        await settings.set("client", "disableVideo", !settings.client.disableVideo);
        return this._refreshView(userId);

      // Configure settings
      case "configure":
        return this.webrtc.config.render(true);

      // Toggle popout
      case "toggle-popout":
        await settings.set("client", `users.${user.id}.popout`, !userSettings.popout);
        return this.render();

      // Hide players
      case "toggle-players":
        await settings.set("client", "hidePlayerList", !settings.client.hidePlayerList);
        return this.render();

      // Minimize the dock
      case "toggle-dock":
        await settings.set("client", "hideDock", !settings.client.hideDock);
        return this.render();
    }
  }

  /* -------------------------------------------- */

  /**
   * Change volume control for a stream
   * @param {Event} event   The originating change event from interaction with the range input
   * @private
   */
  _onVolumeChange(event) {
    const input = event.currentTarget;
    const box = input.closest(".camera-view");
    const userId = box.dataset.user;
    let volume = foundry.audio.AudioHelper.inputToVolume(input.value);
    box.getElementsByTagName("video")[0].volume = volume;
    this.webrtc.settings.set("client", `users.${userId}.volume`, volume);
  }

  /* -------------------------------------------- */
  /*  Internal Helpers                            */
  /* -------------------------------------------- */

  /**
   * Dynamically refresh the state of a single camera view
   * @param {string} userId  The ID of the user whose view we want to refresh.
   * @protected
   */
  _refreshView(userId) {
    const view = this.element[0].querySelector(`.camera-view[data-user="${userId}"]`);
    const isSelf = game.user.id === userId;
    const clientSettings = game.webrtc.settings.client;
    const userSettings = game.webrtc.settings.getUser(userId);
    const minimized = clientSettings.hideDock;
    const isVertical = game.webrtc.settings.verticalDock;

    // Identify permissions
    const cbv = game.webrtc.canUserBroadcastVideo(userId);
    const csv = game.webrtc.canUserShareVideo(userId);
    const cba = game.webrtc.canUserBroadcastAudio(userId);
    const csa = game.webrtc.canUserShareAudio(userId);

    // Refresh video display
    const video = view.querySelector("video.user-camera");
    const avatar = view.querySelector("img.user-avatar");
    if ( video && avatar ) {
      const showVideo = csv && (isSelf || !clientSettings.disableVideo) && (!minimized || userSettings.popout);
      video.style.visibility = showVideo ? "visible" : "hidden";
      video.style.display = showVideo ? "block" : "none";
      avatar.style.display = showVideo ? "none" : "unset";
    }

    // Hidden and muted status icons
    view.querySelector(".status-hidden")?.classList.toggle("hidden", csv);
    view.querySelector(".status-muted")?.classList.toggle("hidden", csa);

    // Volume bar and video output volume
    if ( video ) {
      video.volume = userSettings.volume;
      video.muted = isSelf || clientSettings.muteAll; // Mute your own video
    }
    const volBar = this.element[0].querySelector(`[data-user="${userId}"] .volume-bar`);
    if ( volBar ) {
      const displayBar = (userId !== game.user.id) && cba;
      volBar.style.display = displayBar ? "block" : "none";
      volBar.disabled = !displayBar;
    }

    // Control toggle states
    const actions = {
      "block-video": {state: !cbv, display: game.user.isGM && !isSelf},
      "block-audio": {state: !cba, display: game.user.isGM && !isSelf},
      "hide-user": {state: !userSettings.blocked, display: !isSelf},
      "toggle-video": {state: !csv, display: isSelf && !minimized},
      "toggle-audio": {state: !csa, display: isSelf},
      "mute-peers": {state: clientSettings.muteAll, display: isSelf},
      "disable-video": {state: clientSettings.disableVideo, display: isSelf && !minimized},
      "toggle-players": {state: !clientSettings.hidePlayerList, display: isSelf && !minimized && isVertical},
      "toggle-dock": {state: !clientSettings.hideDock, display: isSelf}
    };
    const toggles = this.element[0].querySelectorAll(`[data-user="${userId}"] .av-control.toggle`);
    for ( let button of toggles ) {
      const action = button.dataset.action;
      if ( !(action in actions) ) continue;
      const state = actions[action].state;
      const displayed = actions[action].display;
      button.style.display = displayed ? "block" : "none";
      button.enabled = displayed;
      button.children[0].classList.remove(this._getToggleIcon(action, !state));
      button.children[0].classList.add(this._getToggleIcon(action, state));
      button.dataset.tooltip = this._getToggleTooltip(action, state);
    }
  }

  /* -------------------------------------------- */

  /**
   * Render changes needed to the PlayerList ui.
   * Show/Hide players depending on option.
   * @private
   */
  _setPlayerListVisibility() {
    const hidePlayerList = this.webrtc.settings.client.hidePlayerList;
    const players = document.getElementById("players");
    const top = document.getElementById("ui-top");
    if ( players ) players.classList.toggle("hidden", hidePlayerList);
    if ( top ) top.classList.toggle("offset", !hidePlayerList);
  }

  /* -------------------------------------------- */

  /**
   * Get the icon class that should be used for various action buttons with different toggled states.
   * The returned icon should represent the visual status of the NEXT state (not the CURRENT state).
   *
   * @param {string} action     The named av-control button action
   * @param {boolean} state     The CURRENT action state.
   * @returns {string}          The icon that represents the NEXT action state.
   * @protected
   */
  _getToggleIcon(action, state) {
    const clientSettings = game.webrtc.settings.client;
    const dockPositions = AVSettings.DOCK_POSITIONS;
    const dockIcons = {
      [dockPositions.TOP]: {collapse: "down", expand: "up"},
      [dockPositions.RIGHT]: {collapse: "left", expand: "right"},
      [dockPositions.BOTTOM]: {collapse: "up", expand: "down"},
      [dockPositions.LEFT]: {collapse: "right", expand: "left"}
    }[clientSettings.dockPosition];
    const actionMapping = {
      "block-video": ["fa-video", "fa-video-slash"],            // True means "blocked"
      "block-audio": ["fa-microphone", "fa-microphone-slash"],  // True means "blocked"
      "hide-user": ["fa-eye", "fa-eye-slash"],
      "toggle-video": ["fa-camera-web", "fa-camera-web-slash"], // True means "enabled"
      "toggle-audio": ["fa-microphone", "fa-microphone-slash"], // True means "enabled"
      "mute-peers": ["fa-volume-up", "fa-volume-mute"],         // True means "muted"
      "disable-video": ["fa-video", "fa-video-slash"],
      "toggle-players": ["fa-caret-square-right", "fa-caret-square-left"], // True means "displayed"
      "toggle-dock": [`fa-caret-square-${dockIcons.collapse}`, `fa-caret-square-${dockIcons.expand}`]
    };
    const icons = actionMapping[action];
    return icons ? icons[state ? 1: 0] : null;
  }

  /* -------------------------------------------- */

  /**
   * Get the text title that should be used for various action buttons with different toggled states.
   * The returned title should represent the tooltip of the NEXT state (not the CURRENT state).
   *
   * @param {string} action     The named av-control button action
   * @param {boolean} state     The CURRENT action state.
   * @returns {string}          The icon that represents the NEXT action state.
   * @protected
   */
  _getToggleTooltip(action, state) {
    const actionMapping = {
      "block-video": ["BlockUserVideo", "AllowUserVideo"],      // True means "blocked"
      "block-audio": ["BlockUserAudio", "AllowUserAudio"],      // True means "blocked"
      "hide-user": ["ShowUser", "HideUser"],
      "toggle-video": ["DisableMyVideo", "EnableMyVideo"],      // True means "enabled"
      "toggle-audio": ["DisableMyAudio", "EnableMyAudio"],      // True means "enabled"
      "mute-peers": ["MutePeers", "UnmutePeers"],               // True means "muted"
      "disable-video": ["DisableAllVideo", "EnableVideo"],
      "toggle-players": ["ShowPlayers", "HidePlayers"],         // True means "displayed"
      "toggle-dock": ["ExpandDock", "MinimizeDock"]
    };
    const labels = actionMapping[action];
    return game.i18n.localize(`WEBRTC.Tooltip${labels ? labels[state ? 1 : 0] : ""}`);
  }

  /* -------------------------------------------- */

  /**
   * Show or hide UI control elements
   * This replaces the use of jquery.show/hide as it simply adds a class which has display:none
   * which allows us to have elements with display:flex which can be hidden then shown without
   * breaking their display style.
   * This will show/hide the toggle buttons, volume controls and overlay sidebars
   * @param {jQuery} container    The container for which to show/hide control elements
   * @param {boolean} show        Whether to show or hide the controls
   * @param {string} selector     Override selector to specify which controls to show or hide
   * @private
   */
  _toggleControlVisibility(container, show, selector) {
    selector = selector || `.control-bar`;
    container.querySelectorAll(selector).forEach(c => c.classList.toggle("hidden", !show));
  }
}
