/* vim: set ts=2 sw=2 sts=2 et tw=80: */
/* 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/. */

export class DOMFullscreenParent extends JSWindowActorParent {
  // These properties get set by browser-fullScreenAndPointerLock.js.
  // TODO: Bug 1743703 - Consider moving the messaging component of
  //       browser-fullScreenAndPointerLock.js into the actor
  waitingForChildEnterFullscreen = false;
  waitingForChildExitFullscreen = false;
  // Cache the next message recipient actor and in-process browsing context that
  // is computed by _getNextMsgRecipientActor() of
  // browser-fullScreenAndPointerLock.js, this is used to ensure the fullscreen
  // cleanup messages goes the same route as fullscreen request, especially for
  // the cleanup that happens after actor is destroyed.
  // TODO: Bug 1743703 - Consider moving the messaging component of
  //       browser-fullScreenAndPointerLock.js into the actor
  nextMsgRecipient = null;

  updateFullscreenWindowReference(aWindow) {
    if (aWindow.document.documentElement.hasAttribute("inDOMFullscreen")) {
      this._fullscreenWindow = aWindow;
    } else {
      delete this._fullscreenWindow;
    }
  }

  cleanupDomFullscreen(aWindow) {
    if (!aWindow.FullScreen) {
      return;
    }

    // If we don't need to wait for child reply, i.e. cleanupDomFullscreen
    // doesn't message to child, and we've exit the fullscreen, there won't be
    // DOMFullscreen:Painted message from child and it is possible that no more
    // paint would be triggered, so just notify fullscreen-painted observer.
    if (
      !aWindow.FullScreen.cleanupDomFullscreen(this) &&
      !aWindow.document.fullscreen
    ) {
      Services.obs.notifyObservers(aWindow, "fullscreen-painted");
    }
  }

  /**
   * Clean up fullscreen state and resume chrome UI if window is in fullscreen
   * and this actor is the one where the original fullscreen enter or
   * exit request comes.
   */
  _cleanupFullscreenStateAndResumeChromeUI(aWindow) {
    this.cleanupDomFullscreen(aWindow);
    if (this.requestOrigin == this && aWindow.document.fullscreen) {
      aWindow.windowUtils.remoteFrameFullscreenReverted();
    }
  }

  didDestroy() {
    this._didDestroy = true;

    let window = this._fullscreenWindow;
    if (!window) {
      let topBrowsingContext = this.browsingContext.top;
      let browser = topBrowsingContext.embedderElement;
      if (!browser) {
        return;
      }

      if (
        this.waitingForChildExitFullscreen ||
        this.waitingForChildEnterFullscreen
      ) {
        this.waitingForChildExitFullscreen = false;
        this.waitingForChildEnterFullscreen = false;
        // We were destroyed while waiting for our DOMFullscreenChild to exit
        // or enter fullscreen, run cleanup steps anyway.
        this._cleanupFullscreenStateAndResumeChromeUI(browser.ownerGlobal);
      }

      if (this != this.requestOrigin) {
        // The current fullscreen requester should handle the fullsceen event
        // if any.
        this.removeListeners(browser.ownerGlobal);
      }
      return;
    }

    if (this.waitingForChildEnterFullscreen) {
      this.waitingForChildEnterFullscreen = false;
      if (window.document.fullscreen) {
        // We were destroyed while waiting for our DOMFullscreenChild
        // to transition to fullscreen so we abort the entire
        // fullscreen transition to prevent getting stuck in a
        // partial fullscreen state. We need to go through the
        // document since window.Fullscreen could be undefined
        // at this point.
        //
        // This could reject if we're not currently in fullscreen
        // so just ignore rejection.
        window.document.exitFullscreen().catch(() => {});
        return;
      }
      this.cleanupDomFullscreen(window);
    }

    // Need to resume Chrome UI if the window is still in fullscreen UI
    // to avoid the window stays in fullscreen problem. (See Bug 1620341)
    if (window.document.documentElement.hasAttribute("inDOMFullscreen")) {
      this.cleanupDomFullscreen(window);
      if (window.windowUtils) {
        window.windowUtils.remoteFrameFullscreenReverted();
      }
    } else if (this.waitingForChildExitFullscreen) {
      this.waitingForChildExitFullscreen = false;
      // We were destroyed while waiting for our DOMFullscreenChild to exit
      // run cleanup steps anyway.
      this._cleanupFullscreenStateAndResumeChromeUI(window);
    }
    this.updateFullscreenWindowReference(window);
  }

  receiveMessage(aMessage) {
    let topBrowsingContext = this.browsingContext.top;
    let browser = topBrowsingContext.embedderElement;

    if (!browser) {
      // No need to go further when the browser is not accessible anymore
      // (which can happen when the tab is closed for instance),
      return;
    }

    let window = browser.ownerGlobal;
    switch (aMessage.name) {
      case "DOMFullscreen:Request": {
        this.manager.fullscreen = true;
        this.waitingForChildExitFullscreen = false;
        this.requestOrigin = this;
        this.addListeners(window);
        window.windowUtils.remoteFrameFullscreenChanged(browser);
        break;
      }
      case "DOMFullscreen:NewOrigin": {
        // Don't show the warning if we've already exited fullscreen.
        if (window.document.fullscreen) {
          window.PointerlockFsWarning.showFullScreen(
            aMessage.data.originNoSuffix
          );
        }
        this.updateFullscreenWindowReference(window);
        break;
      }
      case "DOMFullscreen:Entered": {
        this.manager.fullscreen = true;
        this.nextMsgRecipient = null;
        this.waitingForChildEnterFullscreen = false;
        window.FullScreen.enterDomFullscreen(browser, this);
        this.updateFullscreenWindowReference(window);
        break;
      }
      case "DOMFullscreen:Exit": {
        this.manager.fullscreen = false;
        this.waitingForChildEnterFullscreen = false;
        window.windowUtils.remoteFrameFullscreenReverted();
        break;
      }
      case "DOMFullscreen:Exited": {
        this.manager.fullscreen = false;
        this.waitingForChildExitFullscreen = false;
        this.cleanupDomFullscreen(window);
        this.updateFullscreenWindowReference(window);
        break;
      }
      case "DOMFullscreen:Painted": {
        this.waitingForChildExitFullscreen = false;
        Services.obs.notifyObservers(window, "fullscreen-painted");
        this.sendAsyncMessage("DOMFullscreen:Painted", {});
        Glean.fullscreen.change.stopAndAccumulate(this.timerId);
        this.timerId = null;
        break;
      }
    }
  }

  handleEvent(aEvent) {
    let window = aEvent.currentTarget.ownerGlobal;
    // We can not get the corresponding browsing context from actor if the actor
    // has already destroyed, so use event target to get browsing context
    // instead.
    let requestOrigin = window.browsingContext.fullscreenRequestOrigin?.get();
    if (this != requestOrigin) {
      // The current fullscreen requester should handle the fullsceen event,
      // ignore them if we are not the current requester.
      this.removeListeners(window);
      return;
    }

    switch (aEvent.type) {
      case "MozDOMFullscreen:Entered": {
        // The event target is the element which requested the DOM
        // fullscreen. If we were entering DOM fullscreen for a remote
        // browser, the target would be the browser which was the parameter of
        // `remoteFrameFullscreenChanged` call. If the fullscreen
        // request was initiated from an in-process browser, we need
        // to get its corresponding browser here.
        let browser;
        if (aEvent.target.ownerGlobal == window) {
          browser = aEvent.target;
        } else {
          browser = aEvent.target.ownerGlobal.docShell.chromeEventHandler;
        }

        // Addon installation should be cancelled when entering fullscreen for security and usability reasons.
        // Installation prompts in fullscreen can trick the user into installing unwanted addons.
        // In fullscreen the notification box does not have a clear visual association with its parent anymore.
        if (window.gXPInstallObserver) {
          window.gXPInstallObserver.removeAllNotifications(browser);
        }

        this.timerId = Glean.fullscreen.change.start();
        window.FullScreen.enterDomFullscreen(browser, this);
        this.updateFullscreenWindowReference(window);

        if (!this.hasBeenDestroyed() && this.requestOrigin) {
          window.PointerlockFsWarning.showFullScreen(
            this.requestOrigin.manager.documentPrincipal.originNoSuffix
          );
        }
        break;
      }
      case "MozDOMFullscreen:Exited": {
        this.timerId = Glean.fullscreen.change.start();

        // Make sure that the actor has not been destroyed before
        // accessing its browsing context. Otherwise, a error may
        // occur and hence cleanupDomFullscreen not executed, resulting
        // in the browser window being in an unstable state.
        // (Bug 1590138).
        if (!this.hasBeenDestroyed() && !this.requestOrigin) {
          this.requestOrigin = this;
        }
        this.cleanupDomFullscreen(window);
        this.updateFullscreenWindowReference(window);

        // If the document is supposed to be in fullscreen, keep the listener to wait for
        // further events.
        if (!this.manager.fullscreen) {
          this.removeListeners(window);
        }
        break;
      }
    }
  }

  addListeners(aWindow) {
    aWindow.addEventListener(
      "MozDOMFullscreen:Entered",
      this,
      /* useCapture */ true,
      /* wantsUntrusted */
      false
    );
    aWindow.addEventListener(
      "MozDOMFullscreen:Exited",
      this,
      /* useCapture */ true,
      /* wantsUntrusted */ false
    );
  }

  removeListeners(aWindow) {
    aWindow.removeEventListener("MozDOMFullscreen:Entered", this, true);
    aWindow.removeEventListener("MozDOMFullscreen:Exited", this, true);
  }

  /**
   * Get the actor where the original fullscreen
   * enter or exit request comes from.
   */
  get requestOrigin() {
    let chromeBC = this.browsingContext.topChromeWindow?.browsingContext;
    let requestOrigin = chromeBC?.fullscreenRequestOrigin;
    return requestOrigin && requestOrigin.get();
  }

  /**
   * Store the actor where the original fullscreen
   * enter or exit request comes from in the top level
   * browsing context.
   */
  set requestOrigin(aActor) {
    let chromeBC = this.browsingContext.topChromeWindow?.browsingContext;
    if (!chromeBC) {
      console.error("not able to get browsingContext for chrome window.");
      return;
    }

    if (aActor) {
      chromeBC.fullscreenRequestOrigin = Cu.getWeakReference(aActor);
    } else {
      delete chromeBC.fullscreenRequestOrigin;
    }
  }

  hasBeenDestroyed() {
    if (this._didDestroy) {
      return true;
    }

    // The 'didDestroy' callback is not always getting called.
    // So we can't rely on it here. Instead, we will try to access
    // the browsing context to judge wether the actor has
    // been destroyed or not.
    try {
      return !this.browsingContext;
    } catch {
      return true;
    }
  }
}
