/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs";

const lazy = {};
XPCOMUtils.defineLazyServiceGetters(lazy, {
  WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
});

ChromeUtils.defineESModuleGetters(lazy, {
  ASRouter:
    // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
    "resource:///modules/asrouter/ASRouter.sys.mjs",
  PageActions: "resource:///modules/PageActions.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

import { Rect, Point } from "resource://gre/modules/Geometry.sys.mjs";

const PLAYER_URI = "chrome://global/content/pictureinpicture/player.xhtml";
// Currently, we need titlebar="yes" on macOS in order for the player window
// to be resizable. See bug 1824171.
const TITLEBAR = AppConstants.platform == "macosx" ? "yes" : "no";
const PLAYER_FEATURES = `chrome,alwaysontop,lockaspectratio,resizable,dialog,titlebar=${TITLEBAR}`;

const WINDOW_TYPE = "Toolkit:PictureInPicture";
const TOGGLE_ENABLED_PREF =
  "media.videocontrols.picture-in-picture.video-toggle.enabled";
const TOGGLE_FIRST_SEEN_PREF =
  "media.videocontrols.picture-in-picture.video-toggle.first-seen-secs";
const TOGGLE_HAS_USED_PREF =
  "media.videocontrols.picture-in-picture.video-toggle.has-used";
const TOGGLE_POSITION_PREF =
  "media.videocontrols.picture-in-picture.video-toggle.position";
const TOGGLE_POSITION_RIGHT = "right";
const TOGGLE_POSITION_LEFT = "left";
const RESIZE_MARGIN_PX = 16;
const BACKGROUND_DURATION_HISTOGRAM_ID =
  "FX_PICTURE_IN_PICTURE_BACKGROUND_TAB_PLAYING_DURATION";
const FOREGROUND_DURATION_HISTOGRAM_ID =
  "FX_PICTURE_IN_PICTURE_FOREGROUND_TAB_PLAYING_DURATION";

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "PIP_ENABLED",
  "media.videocontrols.picture-in-picture.enabled",
  false
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "PIP_URLBAR_BUTTON",
  "media.videocontrols.picture-in-picture.urlbar-button.enabled",
  false
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "RESPECT_PIP_DISABLED",
  "media.videocontrols.picture-in-picture.respect-disablePictureInPicture",
  true
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "PIP_WHEN_SWITCHING_TABS",
  "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled",
  true
);

/**
 * To differentiate windows in the Telemetry Event Log, each Picture-in-Picture
 * player window is given a unique ID.
 */
let gNextWindowID = 0;

export class PictureInPictureLauncherParent extends JSWindowActorParent {
  receiveMessage(aMessage) {
    switch (aMessage.name) {
      case "PictureInPicture:Request": {
        let videoData = aMessage.data;
        PictureInPicture.handlePictureInPictureRequest(this.manager, videoData);
        break;
      }
    }
  }
}

export class PictureInPictureToggleParent extends JSWindowActorParent {
  receiveMessage(aMessage) {
    let browsingContext = aMessage.target.browsingContext;
    let browser = browsingContext.top.embedderElement;
    switch (aMessage.name) {
      case "PictureInPicture:OpenToggleContextMenu": {
        let win = browser.ownerGlobal;
        PictureInPicture.openToggleContextMenu(win, aMessage.data);
        break;
      }
      case "PictureInPicture:UpdateEligiblePipVideoCount": {
        let { pipCount, pipDisabledCount } = aMessage.data;
        PictureInPicture.updateEligiblePipVideoCount(browsingContext, {
          pipCount,
          pipDisabledCount,
        });
        PictureInPicture.updateUrlbarToggle(browser);
        break;
      }
      case "PictureInPicture:SetFirstSeen": {
        let { dateSeconds } = aMessage.data;
        PictureInPicture.setFirstSeen(dateSeconds);
        break;
      }
      case "PictureInPicture:SetHasUsed": {
        let { hasUsed } = aMessage.data;
        PictureInPicture.setHasUsed(hasUsed);
        break;
      }
      case "PictureInPicture:VideoTabHidden": {
        if (!lazy.PIP_ENABLED || !lazy.PIP_WHEN_SWITCHING_TABS) {
          break;
        }
        // If the tab is still selected, then we can ignore this event
        if (browser.ownerGlobal.gBrowser.selectedBrowser == browser) {
          break;
        }
        let actor = browsingContext.currentWindowGlobal.getActor(
          "PictureInPictureLauncher"
        );
        actor.sendAsyncMessage("PictureInPicture:AutoToggle");
        break;
      }
      case "PictureInPicture:VideoTabShown": {
        if (!lazy.PIP_ENABLED || !lazy.PIP_WHEN_SWITCHING_TABS) {
          break;
        }
        if (browser.ownerGlobal.gBrowser.selectedBrowser != browser) {
          break;
        }
        for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
          let originatingBrowser = PictureInPicture.weakWinToBrowser.get(win);
          if (browser == originatingBrowser) {
            win.closeFromForeground();
            break;
          }
        }
        break;
      }
    }
  }
}

/**
 * This module is responsible for creating a Picture in Picture window to host
 * a clone of a video element running in web content.
 */
export class PictureInPictureParent extends JSWindowActorParent {
  receiveMessage(aMessage) {
    switch (aMessage.name) {
      case "PictureInPicture:Resize": {
        let videoData = aMessage.data;
        PictureInPicture.resizePictureInPictureWindow(videoData, this);
        break;
      }
      case "PictureInPicture:Close": {
        /**
         * Content has requested that its Picture in Picture window go away.
         */
        let reason = aMessage.data.reason;
        PictureInPicture.closeSinglePipWindow({ reason, actorRef: this });
        break;
      }
      case "PictureInPicture:Playing": {
        let player = PictureInPicture.getWeakPipPlayer(this);
        if (player) {
          player.setIsPlayingState(true);
        }
        break;
      }
      case "PictureInPicture:Paused": {
        let player = PictureInPicture.getWeakPipPlayer(this);
        if (player) {
          player.setIsPlayingState(false);
        }
        break;
      }
      case "PictureInPicture:Muting": {
        let player = PictureInPicture.getWeakPipPlayer(this);
        if (player) {
          player.setIsMutedState(true);
        }
        break;
      }
      case "PictureInPicture:Unmuting": {
        let player = PictureInPicture.getWeakPipPlayer(this);
        if (player) {
          player.setIsMutedState(false);
        }
        break;
      }
      case "PictureInPicture:EnableSubtitlesButton": {
        let player = PictureInPicture.getWeakPipPlayer(this);
        if (player) {
          player.enableSubtitlesButton();
        }
        break;
      }
      case "PictureInPicture:DisableSubtitlesButton": {
        let player = PictureInPicture.getWeakPipPlayer(this);
        if (player) {
          player.disableSubtitlesButton();
        }
        break;
      }
      case "PictureInPicture:SetTimestampAndScrubberPosition": {
        let { timestamp, scrubberPosition } = aMessage.data;
        let player = PictureInPicture.getWeakPipPlayer(this);
        player.setTimestamp(timestamp);
        player.setScrubberPosition(scrubberPosition);
        break;
      }
      case "PictureInPicture:VolumeChange": {
        let { volume } = aMessage.data;
        let player = PictureInPicture.getWeakPipPlayer(this);
        player.setVolume(volume);
        break;
      }
    }
  }
}

/**
 * This module is responsible for creating a Picture in Picture window to host
 * a clone of a video element running in web content.
 */
export var PictureInPicture = {
  // Maps PictureInPictureParent actors to their corresponding PiP player windows
  weakPipToWin: new WeakMap(),

  // Maps PiP player windows to their originating content's browser
  weakWinToBrowser: new WeakMap(),

  // Maps a browser to the number of PiP windows it has
  browserWeakMap: new WeakMap(),

  // Maps an AppWindow to the number of PiP windows it has
  originatingWinWeakMap: new WeakMap(),

  // Maps a WindowGlobal to count of eligible PiP videos
  weakGlobalToEligiblePipCount: new WeakMap(),

  // Tracks the number of open player windows for Telemetry tracking.
  currentPlayerCount: 0,
  maxConcurrentPlayerCount: 0,

  /**
   * Returns the player window if one exists and if it hasn't yet been closed.
   *
   * @param {PictureInPictureParent} pipActorRef
   *   Reference to the calling PictureInPictureParent actor
   *
   * @returns {Window} the player window if it exists and is not in the
   * process of being closed. Returns null otherwise.
   */
  getWeakPipPlayer(pipActorRef) {
    let playerWin = this.weakPipToWin.get(pipActorRef);
    if (!playerWin || playerWin.closed) {
      return null;
    }
    return playerWin;
  },

  /**
   * Get the PiP panel for a browser. Create the panel if needed.
   * @param {Browser} browser The current browser
   * @returns panel The panel element
   */
  getPanelForBrowser(browser) {
    let panel = browser.ownerDocument.querySelector("#PictureInPicturePanel");

    if (!panel) {
      let template = browser.ownerDocument.querySelector(
        "#PictureInPicturePanelTemplate"
      );
      let clone = template.content.cloneNode(true);
      template.replaceWith(clone);

      panel = this.getPanelForBrowser(browser);
      this._attachEventListeners(panel);
    }
    return panel;
  },

  _attachEventListeners(panel) {
    panel.addEventListener("popupshown", this);
    panel.addEventListener("popuphidden", this);
    panel
      .querySelector("#respect-pipDisabled-switch")
      .addEventListener("click", this);
  },

  handleEvent(event) {
    switch (event.type) {
      case "TabSwapPictureInPicture": {
        this.onPipSwappedBrowsers(event);
        break;
      }
      case "TabSelect": {
        this.updatePlayingDurationHistograms();
        break;
      }
      case "popupshown":
        this.onPipPanelShown(event);
        break;
      case "popuphidden":
        this.onPipPanelHidden(event);
        break;
      case "click":
        this.toggleRespectDisablePip(event);
        break;
    }
  },

  /**
   * Increase the count of PiP windows for a given browser
   * @param browser The browser to increase PiP count in browserWeakMap
   */
  addPiPBrowserToWeakMap(browser) {
    let count = this.browserWeakMap.has(browser)
      ? this.browserWeakMap.get(browser)
      : 0;
    this.browserWeakMap.set(browser, count + 1);

    // If a browser is being added to the browserWeakMap, that means its
    // probably a good time to make sure the playing duration histograms
    // are up-to-date, as it means that we've either opened a new PiP
    // player window, or moved the originating tab to another window.
    this.updatePlayingDurationHistograms();
  },

  /**
   * Increase the count of PiP windows for a given AppWindow.
   *
   * @param {Browser} browser
   *   The content browser that the originating video lives in and from which
   *   we'll read its parent window to increase PiP window count in originatingWinWeakMap.
   */
  addOriginatingWinToWeakMap(browser) {
    let parentWin = browser.ownerGlobal;
    let count = this.originatingWinWeakMap.get(parentWin);
    if (!count || count == 0) {
      this.setOriginatingWindowActive(parentWin.browsingContext, true);
      this.originatingWinWeakMap.set(parentWin, 1);

      let gBrowser = browser.getTabBrowser();
      if (gBrowser) {
        gBrowser.tabContainer.addEventListener("TabSelect", this);
      }
    } else {
      this.originatingWinWeakMap.set(parentWin, count + 1);
    }
  },

  /**
   * Decrease the count of PiP windows for a given browser.
   * If the count becomes 0, we will remove the browser from the WeakMap
   * @param browser The browser to decrease PiP count in browserWeakMap
   */
  removePiPBrowserFromWeakMap(browser) {
    let count = this.browserWeakMap.get(browser);
    if (count <= 1) {
      this.browserWeakMap.delete(browser);
      let tabbrowser = browser.getTabBrowser();
      if (tabbrowser && !tabbrowser.shouldActivateDocShell(browser)) {
        browser.docShellIsActive = false;
      }
    } else {
      this.browserWeakMap.set(browser, count - 1);
    }
  },

  /**
   * Decrease the count of PiP windows for a given AppWindow.
   * If the count becomes 0, we will remove the AppWindow from the WeakMap.
   *
   * @param {Browser} browser
   *   The content browser that the originating video lives in and from which
   *   we'll read its parent window to decrease PiP window count in originatingWinWeakMap.
   */
  removeOriginatingWinFromWeakMap(browser) {
    let parentWin = browser?.ownerGlobal;

    if (!parentWin) {
      return;
    }

    let count = this.originatingWinWeakMap.get(parentWin);
    if (!count || count <= 1) {
      this.originatingWinWeakMap.delete(parentWin, 0);
      this.setOriginatingWindowActive(parentWin.browsingContext, false);

      let gBrowser = browser.getTabBrowser();
      if (gBrowser) {
        gBrowser.tabContainer.removeEventListener("TabSelect", this);
      }
    } else {
      this.originatingWinWeakMap.set(parentWin, count - 1);
    }
  },

  onPipSwappedBrowsers(event) {
    let otherTab = event.detail;
    if (otherTab) {
      for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
        if (this.weakWinToBrowser.get(win) === event.target.linkedBrowser) {
          this.weakWinToBrowser.set(win, otherTab.linkedBrowser);
          this.removePiPBrowserFromWeakMap(event.target.linkedBrowser);
          this.removeOriginatingWinFromWeakMap(event.target.linkedBrowser);
          this.addPiPBrowserToWeakMap(otherTab.linkedBrowser);
          this.addOriginatingWinToWeakMap(otherTab.linkedBrowser);
        }
      }
      otherTab.addEventListener("TabSwapPictureInPicture", this);
    }
  },

  updatePlayingDurationHistograms() {
    // A tab switch occurred in a browser window with one more tabs that have
    // PiP player windows associated with them.
    for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
      let browser = this.weakWinToBrowser.get(win);
      let gBrowser = browser.getTabBrowser();
      if (gBrowser?.selectedBrowser == browser) {
        // If there are any background stopwatches running for this window, finish
        // them and switch to foreground.
        if (TelemetryStopwatch.running(BACKGROUND_DURATION_HISTOGRAM_ID, win)) {
          TelemetryStopwatch.finish(BACKGROUND_DURATION_HISTOGRAM_ID, win);
        }
        if (
          !TelemetryStopwatch.running(FOREGROUND_DURATION_HISTOGRAM_ID, win)
        ) {
          TelemetryStopwatch.start(FOREGROUND_DURATION_HISTOGRAM_ID, win, {
            inSeconds: true,
          });
        }
      } else {
        // If there are any foreground stopwatches running for this window, finish
        // them and switch to background.
        if (TelemetryStopwatch.running(FOREGROUND_DURATION_HISTOGRAM_ID, win)) {
          TelemetryStopwatch.finish(FOREGROUND_DURATION_HISTOGRAM_ID, win);
        }

        if (
          !TelemetryStopwatch.running(BACKGROUND_DURATION_HISTOGRAM_ID, win)
        ) {
          TelemetryStopwatch.start(BACKGROUND_DURATION_HISTOGRAM_ID, win, {
            inSeconds: true,
          });
        }
      }
    }
  },

  /**
   * Called when the browser UI handles the View:PictureInPicture command via
   * the keyboard.
   *
   * @param {Event} event
   */
  onCommand(event) {
    if (!lazy.PIP_ENABLED) {
      return;
    }

    let win = event.target.ownerGlobal;
    let bc = Services.focus.focusedContentBrowsingContext;
    if (bc.top == win.gBrowser.selectedBrowser.browsingContext) {
      let actor = bc.currentWindowGlobal.getActor("PictureInPictureLauncher");
      actor.sendAsyncMessage("PictureInPicture:KeyToggle");
    }
  },

  async focusTabAndClosePip(window, pipActor) {
    let browser = this.weakWinToBrowser.get(window);
    if (!browser) {
      return;
    }

    let gBrowser = browser.getTabBrowser();
    let tab = gBrowser.getTabForBrowser(browser);

    // focus the tab's window
    tab.ownerGlobal.focus();

    gBrowser.selectedTab = tab;
    await this.closeSinglePipWindow({ reason: "Unpip", actorRef: pipActor });
  },

  /**
   * Update the respect PiPDisabled pref value when the toggle is clicked.
   * @param {Event} event The event from toggling the respect
   *   PiPDisabled in the PiP panel
   */
  toggleRespectDisablePip(event) {
    let toggle = event.target;
    let respectPipDisabled = !toggle.pressed;

    Services.prefs.setBoolPref(
      "media.videocontrols.picture-in-picture.respect-disablePictureInPicture",
      respectPipDisabled
    );

    Glean.pictureinpicture.disrespectDisableUrlBar.record();
  },

  /**
   * Updates the PiP count and PiPDisabled count of eligible PiP videos for a
   * respective WindowGlobal.
   * @param {BrowsingContext} browsingContext The BrowsingContext with eligible videos
   * @param {Object} object
   *    pipCount: The number of eligible videos for the respective WindowGlobal
   *    pipDisabledCount: The number of disablePiP videos for the respective WindowGlobal
   */
  updateEligiblePipVideoCount(browsingContext, object) {
    let windowGlobal = browsingContext.currentWindowGlobal;

    if (windowGlobal) {
      this.weakGlobalToEligiblePipCount.set(windowGlobal, object);
    }
  },

  /**
   * A generator function that yeilds a WindowGlobal, it's respective PiP
   * count, and if any of the videos have PiPDisabled set.
   * @param {Browser} browser The selected browser
   */
  *windowGlobalPipCountGenerator(browser) {
    let contextsToVisit = [browser.browsingContext];
    while (contextsToVisit.length) {
      let currentBC = contextsToVisit.pop();
      let windowGlobal = currentBC.currentWindowGlobal;

      if (!windowGlobal) {
        continue;
      }

      let { pipCount, pipDisabledCount } =
        this.weakGlobalToEligiblePipCount.get(windowGlobal) || {
          pipCount: 0,
          pipDisabledCount: 0,
        };

      contextsToVisit.push(...currentBC.children);

      yield { windowGlobal, pipCount, pipDisabledCount };
    }
  },

  /**
   * Gets the total eligible video count and total PiPDisabled count for a
   * given browser.
   * @param {Browser} browser The selected browser
   * @returns Total count of eligible PiP videos for the selected broser
   */
  getEligiblePipVideoCount(browser) {
    let totalPipCount = 0;
    let totalPipDisabled = 0;

    for (let {
      pipCount,
      pipDisabledCount,
    } of this.windowGlobalPipCountGenerator(browser)) {
      totalPipCount += pipCount;
      totalPipDisabled += pipDisabledCount;
    }

    return { totalPipCount, totalPipDisabled };
  },

  /**
   * This function updates the hover text on the urlbar PiP button when we enter or exit PiP
   * @param {Document} document The window document
   * @param {Element} pipToggle The urlbar PiP button
   * @param {String} dataL10nId The data l10n id of the string we want to show
   */
  updateUrlbarHoverText(document, pipToggle, dataL10nId) {
    let shortcut = document.getElementById("key_togglePictureInPicture");

    document.l10n.setAttributes(pipToggle, dataL10nId, {
      shortcut: ShortcutUtils.prettifyShortcut(shortcut),
    });
  },

  /**
   * Toggles the visibility of the PiP urlbar button. If the total video count
   * is 1, then we will show the button. If any eligible video has PiPDisabled,
   * then the button will show. Otherwise the button is hidden.
   * @param {Browser} browser The selected browser
   */
  updateUrlbarToggle(browser) {
    if (!lazy.PIP_ENABLED || !lazy.PIP_URLBAR_BUTTON) {
      return;
    }

    let win = browser.ownerGlobal;
    if (win.closed || win.gBrowser?.selectedBrowser !== browser) {
      return;
    }

    let { totalPipCount, totalPipDisabled } =
      this.getEligiblePipVideoCount(browser);

    let pipToggle = win.document.getElementById("picture-in-picture-button");
    if (
      totalPipCount === 1 ||
      (totalPipDisabled > 0 && lazy.RESPECT_PIP_DISABLED)
    ) {
      pipToggle.hidden = false;
      lazy.PageActions.sendPlacedInUrlbarTrigger(pipToggle);
    } else {
      pipToggle.hidden = true;
    }

    let browserHasPip = !!this.browserWeakMap.get(browser);
    if (browserHasPip) {
      this.setUrlbarPipIconActive(browser.ownerGlobal);
    } else {
      this.setUrlbarPipIconInactive(browser.ownerGlobal);
    }
  },

  /**
   * Open the PiP panel if any video has PiPDisabled, otherwise finds the
   * correct WindowGlobal to open the eligible PiP video.
   * @param {Event} event Event from clicking the PiP urlbar button
   */
  toggleUrlbar(event) {
    let win = event.target.ownerGlobal;
    let browser = win.gBrowser.selectedBrowser;

    let pipPanel = this.getPanelForBrowser(browser);

    for (let {
      windowGlobal,
      pipCount,
      pipDisabledCount,
    } of this.windowGlobalPipCountGenerator(browser)) {
      if (
        (pipDisabledCount > 0 && lazy.RESPECT_PIP_DISABLED) ||
        (pipPanel && pipPanel.state !== "closed")
      ) {
        this.togglePipPanel(browser);
        return;
      } else if (pipCount === 1) {
        let eventExtraKeys = {};
        if (
          !Services.prefs.getBoolPref(TOGGLE_HAS_USED_PREF) &&
          lazy.ASRouter.initialized
        ) {
          let { messages, messageImpressions } = lazy.ASRouter.state;
          let pipCallouts = messages.filter(
            message =>
              message.template === "feature_callout" &&
              message.content.screens.some(screen =>
                screen.anchors.some(anchor =>
                  anchor.selector.includes("picture-in-picture-button")
                )
              )
          );
          if (pipCallouts.length) {
            // Has one of the callouts been seen in the last 48 hours?
            let now = Date.now();
            let callout = pipCallouts.some(message =>
              messageImpressions[message.id]?.some(
                impression => now - impression < 48 * 60 * 60 * 1000
              )
            );
            if (callout) {
              eventExtraKeys.callout = true;
            }
          }
        }
        let actor = windowGlobal.getActor("PictureInPictureToggle");
        actor.sendAsyncMessage("PictureInPicture:UrlbarToggle", eventExtraKeys);
        return;
      }
    }
  },

  /**
   * Set the toggle for PiPDisabled when the panel is shown.
   * If the pref is set from about:config, we need to update
   * the toggle switch in the panel to match the pref.
   * @param {Event} event The panel shown event
   */
  onPipPanelShown(event) {
    let toggle = event.target.querySelector("#respect-pipDisabled-switch");
    toggle.pressed = !lazy.RESPECT_PIP_DISABLED;
  },

  /**
   * Update the visibility of the urlbar PiP button when the panel is hidden.
   * The button will show when there is more than 1 video and at least 1 video
   * has PiPDisabled. If we no longer want to respect PiPDisabled then we
   * need to check if the urlbar button should still be visible.
   * @param {Event} event The panel hidden event
   */
  onPipPanelHidden(event) {
    this.updateUrlbarToggle(event.view.gBrowser.selectedBrowser);
  },

  /**
   * Create the PiP panel if needed and toggle the display of the panel
   * @param {Browser} browser The current browser
   */
  togglePipPanel(browser) {
    let pipPanel = this.getPanelForBrowser(browser);

    if (pipPanel.state === "closed") {
      let anchor = browser.ownerDocument.querySelector(
        "#picture-in-picture-button"
      );

      pipPanel.openPopup(anchor, "bottomright topright");
      Glean.pictureinpicture.openedMethodUrlBar.record({
        disableDialog: true,
      });
    } else {
      pipPanel.hidePopup();
    }
  },

  /**
   * Sets the PiP urlbar to an active state. This changes the icon in the
   * urlbar button to the unpip icon.
   * @param {Window} win The current Window
   */
  setUrlbarPipIconActive(win) {
    let pipToggle = win.document.getElementById("picture-in-picture-button");
    pipToggle.toggleAttribute("pipactive", true);

    this.updateUrlbarHoverText(
      win.document,
      pipToggle,
      "picture-in-picture-urlbar-button-close"
    );
  },

  /**
   * Sets the PiP urlbar to an inactive state. This changes the icon in the
   * urlbar button to the open pip icon.
   * @param {Window} win The current window
   */
  setUrlbarPipIconInactive(win) {
    if (!win) {
      return;
    }
    let pipToggle = win.document.getElementById("picture-in-picture-button");
    pipToggle.toggleAttribute("pipactive", false);

    this.updateUrlbarHoverText(
      win.document,
      pipToggle,
      "picture-in-picture-urlbar-button-open"
    );
  },

  /**
   * Remove attribute which enables pip icon in tab
   *
   * @param {Window} window
   *   A PictureInPicture player's window, used to resolve the player's
   *   associated originating content browser
   */
  clearPipTabIcon(window) {
    const browser = this.weakWinToBrowser.get(window);
    if (!browser) {
      return;
    }

    // see if no other pip windows are open for this content browser
    for (let win of Services.wm.getEnumerator(WINDOW_TYPE)) {
      if (
        win !== window &&
        this.weakWinToBrowser.has(win) &&
        this.weakWinToBrowser.get(win) === browser
      ) {
        return;
      }
    }

    let gBrowser = browser.getTabBrowser();
    let tab = gBrowser?.getTabForBrowser(browser);
    if (tab) {
      tab.removeAttribute("pictureinpicture");
    }
  },

  /**
   * Closes and waits for passed PiP player window to finish closing.
   *
   * @param {Window} pipWin
   *   Player window to close
   */
  async closePipWindow(pipWin) {
    if (pipWin.closed) {
      return;
    }
    let closedPromise = new Promise(resolve => {
      pipWin.addEventListener("unload", resolve, { once: true });
    });
    pipWin.close();
    await closedPromise;
  },

  /**
   * Closes a single PiP window. Used exclusively in conjunction with support
   * for multiple PiP windows
   *
   * @param {Object} closeData
   *   Additional data required to complete a close operation on a PiP window
   * @param {PictureInPictureParent} closeData.actorRef
   *   The PictureInPictureParent actor associated with the PiP window being closed
   * @param {string} closeData.reason
   *   The reason for closing this PiP window
   */
  async closeSinglePipWindow(closeData) {
    const { reason, actorRef } = closeData;
    const win = this.getWeakPipPlayer(actorRef);
    if (!win) {
      return;
    }
    this.removePiPBrowserFromWeakMap(this.weakWinToBrowser.get(win));

    Glean.pictureinpicture["closedMethod" + reason].record();
    await this.closePipWindow(win);
  },

  /**
   * A request has come up from content to open a Picture in Picture
   * window.
   *
   * @param {WindowGlobalParent} wgps
   *   The WindowGlobalParent that is requesting the Picture in Picture
   *   window.
   *
   * @param {object} videoData
   *   An object containing the following properties:
   *
   *   videoHeight (int):
   *     The preferred height of the video.
   *
   *   videoWidth (int):
   *     The preferred width of the video.
   *
   * @returns {Promise}
   *   Resolves once the Picture in Picture window has been created, and
   *   the player component inside it has finished loading.
   */
  async handlePictureInPictureRequest(wgp, videoData) {
    this.currentPlayerCount += 1;
    this.maxConcurrentPlayerCount = Math.max(
      this.maxConcurrentPlayerCount,
      this.currentPlayerCount
    );
    Glean.pictureinpicture.mostConcurrentPlayers.set(
      this.maxConcurrentPlayerCount
    );

    let browser = wgp.browsingContext.top.embedderElement;
    let parentWin = browser.ownerGlobal;

    let win = await this.openPipWindow(parentWin, videoData);
    win.setIsPlayingState(videoData.playing);
    win.setIsMutedState(videoData.isMuted);

    // set attribute which shows pip icon in tab
    let tab = parentWin.gBrowser.getTabForBrowser(browser);
    tab.setAttribute("pictureinpicture", true);

    this.setUrlbarPipIconActive(parentWin);

    tab.addEventListener("TabSwapPictureInPicture", this);

    let pipId = gNextWindowID.toString();
    win.setupPlayer(pipId, wgp, videoData.videoRef, videoData.autoFocus);
    gNextWindowID++;

    this.weakWinToBrowser.set(win, browser);
    this.addPiPBrowserToWeakMap(browser);
    this.addOriginatingWinToWeakMap(browser);

    win.setScrubberPosition(videoData.scrubberPosition);
    win.setTimestamp(videoData.timestamp);
    win.setVolume(videoData.volume);

    Services.prefs.setBoolPref(TOGGLE_HAS_USED_PREF, true);

    Glean.pictureinpicture.createPlayer.record({
      value: pipId,
      width: win.innerWidth,
      height: win.innerHeight,
      screenX: win.screenX,
      screenY: win.screenY,
      ccEnabled: videoData.ccEnabled,
      webVTTSubtitles: videoData.webVTTSubtitles,
    });
  },

  /**
   * Calls the browsingContext's `forceAppWindowActive` flag to determine if the
   * the top level chrome browsingContext should be forcefully set as active or not.
   * When the originating window's browsing context is set to active, captions on the
   * PiP window are properly updated. Forcing active while a PiP window is open ensures
   * that captions are still updated when the originating window is occluded.
   *
   * @param {BrowsingContext} browsingContext
   *   The browsing context of the originating window
   * @param {boolean} isActive
   *   True to force originating window as active, or false to not enforce it
   * @see CanonicalBrowsingContext
   */
  setOriginatingWindowActive(browsingContext, isActive) {
    browsingContext.forceAppWindowActive = isActive;
  },

  /**
   * unload event has been called in player.js, cleanup our preserved
   * browser object.
   *
   * @param {Window} window
   */
  unload(window) {
    TelemetryStopwatch.finish(
      "FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION",
      window
    );

    if (TelemetryStopwatch.running(BACKGROUND_DURATION_HISTOGRAM_ID, window)) {
      TelemetryStopwatch.finish(BACKGROUND_DURATION_HISTOGRAM_ID, window);
    } else if (
      TelemetryStopwatch.running(FOREGROUND_DURATION_HISTOGRAM_ID, window)
    ) {
      TelemetryStopwatch.finish(FOREGROUND_DURATION_HISTOGRAM_ID, window);
    }

    let browser = this.weakWinToBrowser.get(window);
    this.removeOriginatingWinFromWeakMap(browser);

    this.currentPlayerCount -= 1;
    // Saves the location of the Picture in Picture window
    this.savePosition(window);
    this.clearPipTabIcon(window);
    this.setUrlbarPipIconInactive(browser?.ownerGlobal);
  },

  /**
   * Open a Picture in Picture window on the same screen as parentWin,
   * sized based on the information in videoData.
   *
   * @param {ChromeWindow} parentWin
   *   The window hosting the browser that requested the Picture in
   *   Picture window.
   *
   * @param {object} videoData
   *   An object containing the following properties:
   *
   *   videoHeight (int):
   *     The preferred height of the video.
   *
   *   videoWidth (int):
   *     The preferred width of the video.
   *
   * @param {PictureInPictureParent} actorReference
   *   Reference to the calling PictureInPictureParent
   *
   * @returns {Promise}
   *   Resolves once the window has opened and loaded the player component.
   */
  async openPipWindow(parentWin, videoData) {
    let { top, left, width, height } = this.fitToScreen(parentWin, videoData);

    let { left: resolvedLeft, top: resolvedTop } = this.resolveOverlapConflicts(
      left,
      top,
      width,
      height
    );

    top = Math.round(resolvedTop);
    left = Math.round(resolvedLeft);
    width = Math.round(width);
    height = Math.round(height);

    let features =
      `${PLAYER_FEATURES},top=${top},left=${left},outerWidth=${width},` +
      `outerHeight=${height}`;
    let isPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(parentWin);

    if (isPrivate) {
      features += ",private";
    }

    let pipWindow = Services.ww.openWindow(
      parentWin,
      PLAYER_URI,
      null,
      features,
      null
    );

    TelemetryStopwatch.start(
      "FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION",
      pipWindow,
      {
        inSeconds: true,
      }
    );

    pipWindow.windowUtils.setResizeMargin(RESIZE_MARGIN_PX);

    // If the window is Private the icon will have already been set when
    // it was opened.
    if (Services.appinfo.OS == "WINNT" && !isPrivate) {
      lazy.WindowsUIUtils.setWindowIconNoData(pipWindow);
    }

    return new Promise(resolve => {
      pipWindow.addEventListener(
        "load",
        () => {
          resolve(pipWindow);
        },
        { once: true }
      );
    });
  },

  /**
   * This function tries to restore the last known Picture-in-Picture location
   * and size. If those values are unknown or offscreen, then a default
   * location and size is used.
   *
   * @param {ChromeWindow|PlayerWindow} requestingWin
   *   The window hosting the browser that requested the Picture in
   *   Picture window. If this is an existing player window then the returned
   *   player size and position will be determined based on the existing
   *   player window's size and position.
   *
   * @param {object} videoData
   *   An object containing the following properties:
   *
   *   videoHeight (int):
   *     The preferred height of the video.
   *
   *   videoWidth (int):
   *     The preferred width of the video.
   *
   * @returns {object}
   *   The size and position for the player window, in CSS pixels relative to
   *   requestingWin.
   *
   *   top (int):
   *     The top position for the player window.
   *
   *   left (int):
   *     The left position for the player window.
   *
   *   width (int):
   *     The width of the player window.
   *
   *   height (int):
   *     The height of the player window.
   */
  fitToScreen(requestingWin, videoData) {
    let { videoHeight, videoWidth } = videoData;

    const isPlayer = requestingWin.document.location.href == PLAYER_URI;

    let requestingCssToDesktopScale =
      requestingWin.devicePixelRatio / requestingWin.desktopToDeviceScale;

    let top, left, width, height;
    if (!isPlayer) {
      // requestingWin is a content window, load last PiP's dimensions
      ({ top, left, width, height } = this.loadPosition());
    } else if (requestingWin.windowState === requestingWin.STATE_FULLSCREEN) {
      // `requestingWin` is a PiP window and in fullscreen. We stored the size
      // and position before entering fullscreen and we will use that to
      // calculate the new position
      ({ top, left, width, height } = requestingWin.getDeferredResize());
      left *= requestingCssToDesktopScale;
      top *= requestingCssToDesktopScale;
    } else {
      // requestingWin is a PiP player, conserve its dimensions in this case
      left = requestingWin.screenX * requestingCssToDesktopScale;
      top = requestingWin.screenY * requestingCssToDesktopScale;
      width = requestingWin.outerWidth;
      height = requestingWin.outerHeight;
    }

    // Check that previous location and size were loaded.
    // Note that at this point left and top are in desktop pixels, while width
    // and height are in CSS pixels.
    if (!isNaN(top) && !isNaN(left) && !isNaN(width) && !isNaN(height)) {
      // Get the screen of the last PiP window. PiP screen will be the default
      // screen if the point was not on a screen.
      let PiPScreen = this.getWorkingScreen(left, top);

      // Center position of PiP window.
      let PipScreenCssToDesktopScale =
        PiPScreen.defaultCSSScaleFactor / PiPScreen.contentsScaleFactor;
      let centerX = left + (width * PipScreenCssToDesktopScale) / 2;
      let centerY = top + (height * PipScreenCssToDesktopScale) / 2;

      // We have the screen, now we will get the dimensions of the screen
      let [PiPScreenLeft, PiPScreenTop, PiPScreenWidth, PiPScreenHeight] =
        this.getAvailScreenSize(PiPScreen);

      // Check that the center of the last PiP location is within the screen limits
      // If it's not, then we will use the default size and position
      if (
        PiPScreenLeft <= centerX &&
        centerX <= PiPScreenLeft + PiPScreenWidth &&
        PiPScreenTop <= centerY &&
        centerY <= PiPScreenTop + PiPScreenHeight
      ) {
        let oldWidthDesktopPix = width * PipScreenCssToDesktopScale;

        // The new PiP window will keep the height of the old
        // PiP window and adjust the width to the correct ratio
        width = Math.round((height * videoWidth) / videoHeight);

        // Minimum window size on Windows is 136
        if (AppConstants.platform == "win") {
          width = 136 > width ? 136 : width;
        }

        let widthDesktopPix = width * PipScreenCssToDesktopScale;
        let heightDesktopPix = height * PipScreenCssToDesktopScale;

        // WIGGLE_ROOM allows the PiP window to be within 5 pixels of the right
        // side of the screen to stay snapped to the right side
        const WIGGLE_ROOM = 5;
        // If the PiP window was right next to the right side of the screen
        // then move the PiP window to the right the same distance that
        // the width changes from previous width to current width
        let rightScreen = PiPScreenLeft + PiPScreenWidth;
        let distFromRight = rightScreen - (left + widthDesktopPix);
        if (
          0 < distFromRight &&
          distFromRight <= WIGGLE_ROOM + (oldWidthDesktopPix - widthDesktopPix)
        ) {
          left += distFromRight;
        }

        // Checks if some of the PiP window is off screen and
        // if so it will adjust to move everything on screen
        if (left < PiPScreenLeft) {
          // off the left of the screen
          // slide right
          left = PiPScreenLeft;
        }
        if (top < PiPScreenTop) {
          // off the top of the screen
          // slide down
          top = PiPScreenTop;
        }
        if (left + widthDesktopPix > PiPScreenLeft + PiPScreenWidth) {
          // off the right of the screen
          // slide left
          left = PiPScreenLeft + PiPScreenWidth - widthDesktopPix;
        }
        if (top + heightDesktopPix > PiPScreenTop + PiPScreenHeight) {
          // off the bottom of the screen
          // slide up
          top = PiPScreenTop + PiPScreenHeight - heightDesktopPix;
        }
        // Convert top / left from desktop to requestingWin-relative CSS pixels.
        top /= requestingCssToDesktopScale;
        left /= requestingCssToDesktopScale;
        return { top, left, width, height };
      }
    }

    // We don't have the size or position of the last PiP window, so fall
    // back to calculating the default location.
    let screen = this.getWorkingScreen(
      requestingWin.screenX * requestingCssToDesktopScale,
      requestingWin.screenY * requestingCssToDesktopScale,
      requestingWin.outerWidth * requestingCssToDesktopScale,
      requestingWin.outerHeight * requestingCssToDesktopScale
    );
    let [screenLeft, screenTop, screenWidth, screenHeight] =
      this.getAvailScreenSize(screen);

    let screenCssToDesktopScale =
      screen.defaultCSSScaleFactor / screen.contentsScaleFactor;

    // The Picture in Picture window will be a maximum of a quarter of
    // the screen height, and a third of the screen width.
    const MAX_HEIGHT = screenHeight / 4;
    const MAX_WIDTH = screenWidth / 3;

    width = videoWidth * screenCssToDesktopScale;
    height = videoHeight * screenCssToDesktopScale;
    let aspectRatio = videoWidth / videoHeight;

    if (videoHeight > MAX_HEIGHT || videoWidth > MAX_WIDTH) {
      // We're bigger than the max.
      // Take the largest dimension and clamp it to the associated max.
      // Recalculate the other dimension to maintain aspect ratio.
      if (videoWidth >= videoHeight) {
        // We're clamping the width, so the height must be adjusted to match
        // the original aspect ratio. Since aspect ratio is width over height,
        // that means we need to _divide_ the MAX_WIDTH by the aspect ratio to
        // calculate the appropriate height.
        width = MAX_WIDTH;
        height = Math.round(MAX_WIDTH / aspectRatio);
      } else {
        // We're clamping the height, so the width must be adjusted to match
        // the original aspect ratio. Since aspect ratio is width over height,
        // this means we need to _multiply_ the MAX_HEIGHT by the aspect ratio
        // to calculate the appropriate width.
        height = MAX_HEIGHT;
        width = Math.round(MAX_HEIGHT * aspectRatio);
      }
    }

    // Now that we have the dimensions of the video, we need to figure out how
    // to position it in the bottom right corner. Since we know the width of the
    // available rect, we need to subtract the dimensions of the window we're
    // opening to get the top left coordinates that openWindow expects.
    //
    // In event that the user has multiple displays connected, we have to
    // calculate the top-left coordinate of the new window in absolute
    // coordinates that span the entire display space, since this is what the
    // openWindow expects for its top and left feature values.
    //
    // The screenWidth and screenHeight values only tell us the available
    // dimensions on the screen that the parent window is on. We add these to
    // the screenLeft and screenTop values, which tell us where this screen is
    // located relative to the "origin" in absolute coordinates.
    let isRTL = Services.locale.isAppLocaleRTL;
    left = isRTL ? screenLeft : screenLeft + screenWidth - width;
    top = screenTop + screenHeight - height;

    // Convert top/left from desktop pixels to requestingWin-relative CSS
    // pixels, and width / height to the target screen's CSS pixels, which is
    // what we've made the size calculation against.
    top /= requestingCssToDesktopScale;
    left /= requestingCssToDesktopScale;
    width /= screenCssToDesktopScale;
    height /= screenCssToDesktopScale;

    return { top, left, width, height };
  },

  /**
   * This function will take the size and potential location of a new
   * Picture-in-Picture player window, and try to return the location
   * coordinates that will best ensure that the player window will not overlap
   * with other pre-existing player windows.
   *
   * @param {int} left
   *  x position of left edge for Picture-in-Picture window that is being
   *  opened
   * @param {int} top
   *  y position of top edge for Picture-in-Picture window that is being
   *  opened
   * @param {int} width
   *  Width of Picture-in-Picture window that is being opened
   * @param {int} height
   *  Height of Picture-in-Picture window that is being opened
   *
   * @returns {object}
   *  An object with the following properties:
   *
   *   top (int):
   *     The recommended top position for the player window.
   *
   *   left (int):
   *     The recommended left position for the player window.
   */
  resolveOverlapConflicts(left, top, width, height) {
    // This algorithm works by first identifying the possible candidate
    // locations that the new player window could be placed without overlapping
    // other player windows (assuming that a confict is discovered at all of
    // course). The optimal candidate is then selected by its distance to the
    // original conflict, shorter distances are better.
    //
    // Candidates are discovered by iterating over each of the sides of every
    // pre-existing player window. One candidate is collected for each side.
    // This is done to ensure that the new player window will be opened to
    // tightly fit along the edge of another player window.
    //
    // These candidates are then pruned for candidates that will introduce
    // further conflicts. Finally the ideal candidate is selected from this
    // pool of remaining candidates, optimized for minimizing distance to
    // the original conflict.
    let playerRects = [];

    for (let playerWin of Services.wm.getEnumerator(WINDOW_TYPE)) {
      playerRects.push(
        new Rect(
          playerWin.screenX,
          playerWin.screenY,
          playerWin.outerWidth,
          playerWin.outerHeight
        )
      );
    }

    const newPlayerRect = new Rect(left, top, width, height);
    let conflictingPipRect = playerRects.find(rect =>
      rect.intersects(newPlayerRect)
    );

    if (!conflictingPipRect) {
      // no conflicts found
      return { left, top };
    }

    const conflictLoc = conflictingPipRect.center();

    // Will try to resolve a better placement only on the screen where
    // the conflict occurred
    const conflictScreen = this.getWorkingScreen(conflictLoc.x, conflictLoc.y);

    const [screenTop, screenLeft, screenWidth, screenHeight] =
      this.getAvailScreenSize(conflictScreen);

    const screenRect = new Rect(
      screenTop,
      screenLeft,
      screenWidth,
      screenHeight
    );

    const getEdgeCandidates = rect => {
      return [
        // left edge's candidate
        new Point(rect.left - newPlayerRect.width, rect.top),
        // top edge's candidate
        new Point(rect.left, rect.top - newPlayerRect.height),
        // right edge's candidate
        new Point(rect.right + newPlayerRect.width, rect.top),
        // bottom edge's candidate
        new Point(rect.left, rect.bottom),
      ];
    };

    let candidateLocations = [];
    for (const playerRect of playerRects) {
      for (let candidateLoc of getEdgeCandidates(playerRect)) {
        const candidateRect = new Rect(
          candidateLoc.x,
          candidateLoc.y,
          width,
          height
        );

        if (!screenRect.contains(candidateRect)) {
          continue;
        }

        // test that no PiPs conflict with this candidate box
        if (playerRects.some(rect => rect.intersects(candidateRect))) {
          continue;
        }

        const candidateCenter = candidateRect.center();
        const candidateDistanceToConflict =
          Math.abs(conflictLoc.x - candidateCenter.x) +
          Math.abs(conflictLoc.y - candidateCenter.y);

        candidateLocations.push({
          distanceToConflict: candidateDistanceToConflict,
          location: candidateLoc,
        });
      }
    }

    if (!candidateLocations.length) {
      // if no suitable candidates can be found, return the original location
      return { left, top };
    }

    // sort candidates by distance to the conflict, select the closest
    const closestCandidate = candidateLocations.sort(
      (firstCand, secondCand) =>
        firstCand.distanceToConflict - secondCand.distanceToConflict
    )[0];

    if (!closestCandidate) {
      // can occur if there were no valid candidates, return original location
      return { left, top };
    }

    const resolvedX = closestCandidate.location.x;
    const resolvedY = closestCandidate.location.y;

    return { left: resolvedX, top: resolvedY };
  },

  /**
   * Resizes the the PictureInPicture player window.
   *
   * @param {object} videoData
   *    The source video's data.
   * @param {PictureInPictureParent} actorRef
   *    Reference to the PictureInPicture parent actor.
   */
  resizePictureInPictureWindow(videoData, actorRef) {
    let win = this.getWeakPipPlayer(actorRef);

    if (!win) {
      return;
    }

    win.resizeToVideo(this.fitToScreen(win, videoData));
  },

  /**
   * Opens the context menu for toggling PictureInPicture.
   *
   * @param {Window} window
   * @param {object} data
   *  Message data from the PictureInPictureToggleParent
   */
  openToggleContextMenu(window, data) {
    let document = window.document;
    let popup = document.getElementById("pictureInPictureToggleContextMenu");
    let contextMoveToggle = document.getElementById(
      "context_MovePictureInPictureToggle"
    );

    // Set directional string for toggle position
    let position = Services.prefs.getStringPref(
      TOGGLE_POSITION_PREF,
      TOGGLE_POSITION_RIGHT
    );
    switch (position) {
      case TOGGLE_POSITION_RIGHT:
        document.l10n.setAttributes(
          contextMoveToggle,
          "picture-in-picture-move-toggle-left"
        );
        break;
      case TOGGLE_POSITION_LEFT:
        document.l10n.setAttributes(
          contextMoveToggle,
          "picture-in-picture-move-toggle-right"
        );
        break;
    }

    // We synthesize a new MouseEvent to propagate the inputSource to the
    // subsequently triggered popupshowing event.
    let newEvent = new PointerEvent("contextmenu", {
      bubbles: true,
      cancelable: true,
      screenX: data.screenXDevPx / window.devicePixelRatio,
      screenY: data.screenYDevPx / window.devicePixelRatio,
      pointerType: (() => {
        switch (data.inputSource) {
          case MouseEvent.MOZ_SOURCE_MOUSE:
            return "mouse";
          case MouseEvent.MOZ_SOURCE_PEN:
            return "pen";
          case MouseEvent.MOZ_SOURCE_ERASER:
            return "eraser";
          case MouseEvent.MOZ_SOURCE_CURSOR:
            return "cursor";
          case MouseEvent.MOZ_SOURCE_TOUCH:
            return "touch";
          case MouseEvent.MOZ_SOURCE_KEYBOARD:
            return "keyboard";
          default:
            return "";
        }
      })(),
    });
    popup.openPopupAtScreen(newEvent.screenX, newEvent.screenY, true, newEvent);
  },

  hideToggle() {
    Services.prefs.setBoolPref(TOGGLE_ENABLED_PREF, false);
    Glean.pictureinpictureSettings.disablePlayer.record();
  },

  /**
   * This is used in AsyncTabSwitcher.sys.mjs and tabbrowser.js to check if the browser
   * currently has a PiP window.
   * If the browser has a PiP window we want to keep the browser in an active state because
   * the browser is still partially visible.
   * @param browser The browser to check if it has a PiP window
   * @returns true if browser has PiP window else false
   */
  isOriginatingBrowser(browser) {
    return this.browserWeakMap.has(browser);
  },

  moveToggle() {
    // Get the current position
    let position = Services.prefs.getStringPref(
      TOGGLE_POSITION_PREF,
      TOGGLE_POSITION_RIGHT
    );
    let newPosition = "";
    // Determine what the opposite position would be for that preference
    switch (position) {
      case TOGGLE_POSITION_RIGHT:
        newPosition = TOGGLE_POSITION_LEFT;
        break;
      case TOGGLE_POSITION_LEFT:
        newPosition = TOGGLE_POSITION_RIGHT;
        break;
    }
    if (newPosition) {
      Services.prefs.setStringPref(TOGGLE_POSITION_PREF, newPosition);
    }
  },

  /**
   * This function takes a screen and will return the left, top, width and
   * height of the screen
   * @param {Screen} screen
   * The screen we need to get the size and coordinates of
   *
   * @returns {array}
   * Size and location of screen in desktop pixels.
   *
   *   screenLeft.value (int):
   *     The left position for the screen.
   *
   *   screenTop.value (int):
   *     The top position for the screen.
   *
   *   screenWidth.value (int):
   *     The width of the screen.
   *
   *   screenHeight.value (int):
   *     The height of the screen.
   */
  getAvailScreenSize(screen) {
    let screenLeft = {},
      screenTop = {},
      screenWidth = {},
      screenHeight = {};
    screen.GetAvailRectDisplayPix(
      screenLeft,
      screenTop,
      screenWidth,
      screenHeight
    );
    return [
      screenLeft.value,
      screenTop.value,
      screenWidth.value,
      screenHeight.value,
    ];
  },

  /**
   * This function takes in a rect in desktop pixels, and returns the screen it
   * is located on.
   *
   * If the left and top are not on any screen, it will return the default
   * screen.
   *
   * @param {int} left
   *  left or x coordinate
   *
   * @param {int} top
   *  top or y coordinate
   *
   * @param {int} width
   *  top or y coordinate
   *
   * @param {int} height
   *  top or y coordinate
   *
   * @returns {Screen} screen
   *  the screen the left and top are on otherwise, default screen
   */
  getWorkingScreen(left, top, width = 1, height = 1) {
    // Get the screen manager
    let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
      Ci.nsIScreenManager
    );
    // use screenForRect to get screen
    // this returns the default screen if left and top are not
    // on any screen
    return screenManager.screenForRect(left, top, width, height);
  },

  /**
   * Saves position and size of Picture-in-Picture window
   * @param {Window} win The Picture-in-Picture window
   */
  savePosition(win) {
    let xulStore = Services.xulStore;

    // We store left / top position in desktop pixels, like SessionStore does,
    // so that we can restore them properly (as CSS pixels need to be relative
    // to a screen, and we won't have a target screen to restore).
    let cssToDesktopScale = win.devicePixelRatio / win.desktopToDeviceScale;

    let left = win.screenX * cssToDesktopScale;
    let top = win.screenY * cssToDesktopScale;
    let width = win.outerWidth;
    let height = win.outerHeight;

    xulStore.setValue(PLAYER_URI, "picture-in-picture", "left", left);
    xulStore.setValue(PLAYER_URI, "picture-in-picture", "top", top);
    xulStore.setValue(PLAYER_URI, "picture-in-picture", "width", width);
    xulStore.setValue(PLAYER_URI, "picture-in-picture", "height", height);
  },

  /**
   * Load last Picture in Picture location and size
   * @returns {object}
   *   The size and position of the last Picture in Picture window.
   *
   *   top (int):
   *     The top position for the last player window.
   *     Otherwise NaN
   *
   *   left (int):
   *     The left position for the last player window.
   *     Otherwise NaN
   *
   *   width (int):
   *     The width of the player last window.
   *     Otherwise NaN
   *
   *   height (int):
   *     The height of the player last window.
   *     Otherwise NaN
   */
  loadPosition() {
    let xulStore = Services.xulStore;

    let left = parseInt(
      xulStore.getValue(PLAYER_URI, "picture-in-picture", "left")
    );
    let top = parseInt(
      xulStore.getValue(PLAYER_URI, "picture-in-picture", "top")
    );
    let width = parseInt(
      xulStore.getValue(PLAYER_URI, "picture-in-picture", "width")
    );
    let height = parseInt(
      xulStore.getValue(PLAYER_URI, "picture-in-picture", "height")
    );

    return { top, left, width, height };
  },

  setFirstSeen(dateSeconds) {
    if (!dateSeconds) {
      return;
    }

    Services.prefs.setIntPref(TOGGLE_FIRST_SEEN_PREF, dateSeconds);
  },

  setHasUsed(hasUsed) {
    Services.prefs.setBoolPref(TOGGLE_HAS_USED_PREF, !!hasUsed);
  },
};
