/*
 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
 *           (C) 1999 Antti Koivisto (koivisto@kde.org)
 *           (C) 2001 Dirk Mueller (mueller@kde.org)
 *           (C) 2006 Alexey Proskuryakov (ap@webkit.org)
 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All
 * rights reserved.
 * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved.
 * (http://www.torchmobile.com/)
 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
 * Copyright (C) 2013 Google Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include "third_party/blink/renderer/core/fullscreen/fullscreen.h"

#include "base/containers/adapters.h"
#include "base/time/time.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom-blink.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_fullscreen_options.h"
#include "third_party/blink/renderer/core/css/style_change_reason.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/execution_context/agent.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/frame/visual_viewport.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen_request_type.h"
#include "third_party/blink/renderer/core/fullscreen/scoped_allow_fullscreen.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_dialog_element.h"
#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/core/html_element_type_helpers.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/svg/svg_svg_element.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/scheduler/public/event_loop.h"

namespace blink {

namespace {

void FullscreenElementChanged(Document& document,
                              Element* old_element,
                              Element* new_element,
                              FullscreenRequestType new_request_type,
                              const FullscreenOptions* new_options) {
  DCHECK_NE(old_element, new_element);

  if (new_element) {
    document.GetStyleEngine().EnsureUAStyleForFullscreen(*new_element);
  }

  if (old_element) {
    DCHECK_NE(old_element, Fullscreen::FullscreenElementFrom(document));

    old_element->PseudoStateChanged(CSSSelector::kPseudoFullScreen);
    old_element->PseudoStateChanged(CSSSelector::kPseudoFullscreen);
    old_element->PseudoStateChanged(CSSSelector::kPseudoModal);

    old_element->SetContainsFullScreenElement(false);
    old_element->SetContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(
        false);
  }

  if (new_element) {
    DCHECK_EQ(new_element, Fullscreen::FullscreenElementFrom(document));
    // FullscreenOptions should be provided for incoming fullscreen element.
    CHECK(new_options);

    new_element->PseudoStateChanged(CSSSelector::kPseudoFullScreen);
    new_element->PseudoStateChanged(CSSSelector::kPseudoFullscreen);
    new_element->PseudoStateChanged(CSSSelector::kPseudoModal);

    // OOPIF: For RequestType::kForCrossProcessDescendant, |new_element|
    // is the iframe element for the out-of-process frame that contains the
    // fullscreen element. Hence, it must match :-webkit-full-screen-ancestor.
    if (new_request_type & FullscreenRequestType::kForCrossProcessDescendant) {
      DCHECK(IsA<HTMLIFrameElement>(new_element));
      new_element->SetContainsFullScreenElement(true);
    }
    new_element->SetContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(
        true);
  }

  // Update IsInert() flags.
  const StyleChangeReasonForTracing& reason =
      StyleChangeReasonForTracing::Create(style_change_reason::kFullscreen);
  if (old_element && new_element) {
    old_element->SetNeedsStyleRecalc(kLocalStyleChange, reason);
    new_element->SetNeedsStyleRecalc(kLocalStyleChange, reason);
  } else if (Element* root = document.documentElement()) {
    root->SetNeedsStyleRecalc(kLocalStyleChange, reason);
  }

  // Any element not contained by the fullscreen element is inert (see
  // |Node::IsInert()|), so changing the fullscreen element will typically
  // change the inertness of most elements. Reserialize the entire document.
  document.RefreshAccessibilityTree();

  if (LocalFrame* frame = document.GetFrame()) {
    // TODO(foolip): Synchronize hover state changes with animation frames.
    // https://crbug.com/668758
    frame->GetEventHandler().ScheduleHoverStateUpdate();
    frame->GetChromeClient().FullscreenElementChanged(
        old_element, new_element, new_options, new_request_type);

    // Update paint properties on the visual viewport since
    // user-input-scrollable bits will change based on fullscreen state.
    if (Page* page = frame->GetPage()) {
      if (page->GetVisualViewport().IsActiveViewport())
        page->GetVisualViewport().SetNeedsPaintPropertyUpdate();
    }
  }
}

class MetaParams : public GarbageCollected<MetaParams> {
 public:
  MetaParams() = default;
  MetaParams(FullscreenRequestType request_type,
             const FullscreenOptions* options,
             const base::TimeTicks& fullscreen_enter_time)
      : request_type_(request_type),
        options_(options),
        fullscreen_enter_time_(fullscreen_enter_time) {}
  MetaParams(const MetaParams&) = delete;
  MetaParams& operator=(const MetaParams&) = delete;

  virtual ~MetaParams() = default;

  virtual void Trace(Visitor* visitor) const { visitor->Trace(options_); }

  FullscreenRequestType request_type() const { return request_type_; }
  const FullscreenOptions* options() const { return options_.Get(); }
  const base::TimeTicks& fullscreen_enter_time() const {
    return fullscreen_enter_time_;
  }

 private:
  FullscreenRequestType request_type_;
  Member<const FullscreenOptions> options_;
  const base::TimeTicks fullscreen_enter_time_;
};

using ElementMetaParamsMap =
    HeapHashMap<WeakMember<const Element>, Member<const MetaParams>>;

ElementMetaParamsMap& FullscreenParamsMap() {
  using ElementMetaParamsMapHolder = DisallowNewWrapper<ElementMetaParamsMap>;
  DEFINE_STATIC_LOCAL(Persistent<ElementMetaParamsMapHolder>, holder,
                      (MakeGarbageCollected<ElementMetaParamsMapHolder>()));
  return holder->Value();
}

bool HasFullscreenFlag(const Element& element) {
  return FullscreenParamsMap().Contains(&element);
}

void SetFullscreenFlag(const Element& element,
                       FullscreenRequestType request_type,
                       const FullscreenOptions* options) {
  FullscreenParamsMap().insert(
      &element, MakeGarbageCollected<MetaParams>(request_type, options,
                                                 base::TimeTicks::Now()));
}

void UnsetFullscreenFlag(const Element& element) {
  FullscreenParamsMap().erase(&element);
}

// https://fullscreen.spec.whatwg.org/#iframe-fullscreen-flag
// All iframe elements have an associated iframe fullscreen flag. Unless stated
// otherwise it is unset. Stored separately from FullscreenParamsMap since only
// iframe elements can have this flag, and it must be cleared independently.
using IframeFullscreenFlagSet =
    HeapHashSet<WeakMember<const HTMLIFrameElement>>;

IframeFullscreenFlagSet& IframeFullscreenFlagElements() {
  using Holder = DisallowNewWrapper<IframeFullscreenFlagSet>;
  DEFINE_STATIC_LOCAL(Persistent<Holder>, holder,
                      (MakeGarbageCollected<Holder>()));
  return holder->Value();
}

bool HasIframeFullscreenFlag(const HTMLIFrameElement& element) {
  return IframeFullscreenFlagElements().Contains(&element);
}

void SetIframeFullscreenFlag(const HTMLIFrameElement& element) {
  IframeFullscreenFlagElements().insert(&element);
}

void UnsetIframeFullscreenFlag(const HTMLIFrameElement& element) {
  IframeFullscreenFlagElements().erase(&element);
}

FullscreenRequestType GetRequestType(const Element& element) {
  return FullscreenParamsMap().find(&element)->value->request_type();
}

const MetaParams* GetParams(Element& element) {
  return FullscreenParamsMap().find(&element)->value.Get();
}

// https://fullscreen.spec.whatwg.org/#fullscreen-an-element
void GoFullscreen(Element& element,
                  FullscreenRequestType request_type,
                  const FullscreenOptions* options) {
  Document& document = element.GetDocument();
  Element* old_element = Fullscreen::FullscreenElementFrom(document);

  // If |element| is already in top layer remove it so it will
  // be appended to the end.
  if (element.IsInTopLayer()) {
    document.RemoveFromTopLayerImmediately(&element);
  } else {
    DCHECK(!HasFullscreenFlag(element));
  }

  // Proposed new behavior: top layer elements like dialogs and fullscreen
  // elements can be nested inside popovers.
  // Old/existing behavior: showing a modal dialog or fullscreen
  // element should hide all open popovers.
  auto* hide_until = HTMLElement::TopLayerElementPopoverAncestor(
      element, TopLayerElementType::kFullscreen);
  HTMLElement::HideAllPopoversUntil(
      hide_until, document, HidePopoverFocusBehavior::kNone,
      HidePopoverTransitionBehavior::kFireEventsAndWaitForTransitions);

  // To fullscreen an |element| within a |document|, set the |element|'s
  // fullscreen flag and add it to |document|'s top layer.
  SetFullscreenFlag(element, request_type, options);
  document.AddToTopLayer(&element);

  DCHECK_EQ(&element, Fullscreen::FullscreenElementFrom(document));
  FullscreenElementChanged(document, old_element, &element, request_type,
                           options);
}

// https://fullscreen.spec.whatwg.org/#unfullscreen-an-element
void Unfullscreen(Element& element) {
  Document& document = element.GetDocument();
  Element* old_element = Fullscreen::FullscreenElementFrom(document);

  // To unfullscreen an |element| within a |document|, unset the element's
  // fullscreen flag and iframe fullscreen flag (if any), and remove it from
  // |document|'s top layer.
  DCHECK(element.IsInTopLayer());
  DCHECK(HasFullscreenFlag(element));
  UnsetFullscreenFlag(element);
  if (auto* iframe = DynamicTo<HTMLIFrameElement>(element)) {
    UnsetIframeFullscreenFlag(*iframe);
  }
  document.ScheduleForTopLayerRemoval(&element,
                                      Document::TopLayerReason::kFullscreen);

  // WebXR DOM Overlay mode doesn't allow changing the fullscreen element, this
  // is enforced in AllowedToRequestFullscreen. In this mode, unfullscreening
  // should only be happening via ExitFullscreen. This may involve previous
  // nested fullscreen elements being unfullscreened first, ignore those. This
  // matches kPseudoXrOverlay rules in SelectorChecker::CheckPseudoClass().
  if (document.IsXrOverlay() && element == old_element) {
    // If this was the active fullscreen element, we're exiting fullscreen mode,
    // and this also ends WebXR DOM Overlay mode.
    document.SetIsXrOverlay(false, &element);
  }

  Element* new_element = Fullscreen::FullscreenElementFrom(document);
  if (old_element != new_element) {
    FullscreenRequestType new_request_type;
    const FullscreenOptions* new_options;
    if (new_element) {
      const MetaParams* params = GetParams(*new_element);
      new_request_type = params->request_type();
      new_options = params->options();
    } else {
      new_request_type = FullscreenRequestType::kUnprefixed;
      new_options = FullscreenOptions::Create();
    }
    FullscreenElementChanged(document, old_element, new_element,
                             new_request_type, new_options);
  }
}

// https://fullscreen.spec.whatwg.org/#unfullscreen-a-document
void Unfullscreen(Document& document) {
  // To unfullscreen a |document|, unfullscreen all elements, within
  // |document|'s top layer, whose fullscreen flag is set.

  HeapVector<Member<Element>> fullscreen_elements;
  for (Element* element : document.TopLayerElements()) {
    if (HasFullscreenFlag(*element))
      fullscreen_elements.push_back(element);
  }

  for (Element* element : fullscreen_elements)
    Unfullscreen(*element);
}

// https://html.spec.whatwg.org/C/#allowed-to-use
bool AllowedToUseFullscreen(const Document& document,
                            ReportOptions report_on_failure) {
  // To determine whether a Document object |document| is allowed to use the
  // feature indicated by attribute name |allowattribute|, run these steps:

  // 1. If |document| has no browsing context, then return false.
  if (!document.GetFrame())
    return false;

  // 2. If Permissions Policy is enabled, return the policy for "fullscreen"
  // feature.
  return document.GetExecutionContext()->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kFullscreen, report_on_failure);
}

// https://fullscreen.spec.whatwg.org/#fullscreen-is-supported
bool FullscreenIsSupported(const Document& document) {
  LocalFrame* frame = document.GetFrame();
  if (!frame)
    return false;

  // Fullscreen is not currently supported in document pip.
  // TODO(crbug.com/1402928): Figure out the correct way of handling fullscreen
  // element in picture-in-picture window.
  if (RuntimeEnabledFeatures::DocumentPictureInPictureAPIEnabled(
          document.GetExecutionContext()) &&
      frame->LocalFrameRoot().DomWindow() &&
      frame->LocalFrameRoot().DomWindow()->IsPictureInPictureWindow()) {
    return false;
  }

  // Fullscreen is supported if there is no previously-established user
  // preference, security risk, or platform limitation.
  return !document.GetSettings() ||
         document.GetSettings()->GetFullscreenSupported();
}

// https://fullscreen.spec.whatwg.org/#fullscreen-element-ready-check
RequestFullscreenError FullscreenElementReadyCheck(
    const Element& element,
    ReportOptions report_on_failure) {
  // A fullscreen element ready check for an element `element` returns true if
  // all of the following are true, and false otherwise:

  // `element` is in a document.
  if (!element.isConnected())
    return RequestFullscreenError::kElementNotConnected;

  // `element`'s node document is allowed to use the "fullscreen" feature.
  if (!AllowedToUseFullscreen(element.GetDocument(), report_on_failure))
    return RequestFullscreenError::kDisallowedByPermissionsPolicy;

  // `element` namespace is not the HTML namespace or `element`’s popover
  // visibility state is hidden.
  if (auto* html_element = DynamicTo<HTMLElement>(element);
      html_element && html_element->popoverOpen()) {
    return RequestFullscreenError::kElementOpenAsPopover;
  }

  return RequestFullscreenError::kNone;
}

// RequestFullscreenScope is allocated at the top of |RequestFullscreen()| and
// used to avoid synchronously changing any state within that method, by
// deferring changes in |DidEnterFullscreen()|.
class RequestFullscreenScope {
  STACK_ALLOCATED();

 public:
  RequestFullscreenScope() {
    DCHECK(!running_request_fullscreen_);
    running_request_fullscreen_ = true;
  }
  RequestFullscreenScope(const RequestFullscreenScope&) = delete;
  RequestFullscreenScope& operator=(const RequestFullscreenScope&) = delete;

  ~RequestFullscreenScope() {
    DCHECK(running_request_fullscreen_);
    running_request_fullscreen_ = false;
  }

  static bool RunningRequestFullscreen() { return running_request_fullscreen_; }

 private:
  static bool running_request_fullscreen_;
};

bool RequestFullscreenScope::running_request_fullscreen_ = false;

// Walks the frame tree and returns the first local ancestor frame, if any.
LocalFrame* NextLocalAncestor(Frame& frame) {
  Frame* parent = frame.Tree().Parent();
  if (!parent)
    return nullptr;
  if (auto* parent_local_frame = DynamicTo<LocalFrame>(parent))
    return parent_local_frame;
  return NextLocalAncestor(*parent);
}

// Walks the document's frame tree and returns the document of the first local
// ancestor frame, if any.
Document* NextLocalAncestor(Document& document) {
  LocalFrame* frame = document.GetFrame();
  if (!frame)
    return nullptr;
  LocalFrame* next = NextLocalAncestor(*frame);
  if (!next)
    return nullptr;
  DCHECK(next->GetDocument());
  return next->GetDocument();
}

// Helper to walk the ancestor chain and return the Document of the topmost
// local ancestor frame. Note that this is not the same as the topmost frame's
// Document, which might be unavailable in OOPIF scenarios. For example, with
// OOPIFs, when called on the bottom frame's Document in a A-B-C-B hierarchy in
// process B, this will skip remote frame C and return this frame: A-[B]-C-B.
Document& TopmostLocalAncestor(Document& document) {
  if (Document* next = NextLocalAncestor(document))
    return TopmostLocalAncestor(*next);
  return document;
}

size_t CountFullscreenInTopLayer(const Document& document) {
  size_t count = 0;
  for (Element* element : document.TopLayerElements()) {
    if (HasFullscreenFlag(*element))
      ++count;
  }
  return count;
}

// https://fullscreen.spec.whatwg.org/#simple-fullscreen-document
bool IsSimpleFullscreenDocument(const Document& document) {
  return CountFullscreenInTopLayer(document) == 1;
}

// https://fullscreen.spec.whatwg.org/#collect-documents-to-unfullscreen
HeapVector<Member<Document>> CollectDocumentsToUnfullscreen(Document& doc) {
  // 1. Let |docs| be an ordered set consisting of |doc|.
  HeapVector<Member<Document>> docs;
  docs.push_back(&doc);

  // 2. While true:
  for (Document* lastDoc = &doc;;) {
    // 2.1. Let |lastDoc| be |docs|'s last document.

    // 2.2. Assert: |lastDoc|'s fullscreen element is not null.
    DCHECK(Fullscreen::FullscreenElementFrom(*lastDoc));

    // 2.3. If |lastDoc| is not a simple fullscreen document, break.
    if (!IsSimpleFullscreenDocument(*lastDoc))
      break;

    // 2.4. Let |container| be |lastDoc|'s browsing context container, if any,
    // and otherwise break.
    Frame* frame = lastDoc->GetFrame();
    if (!frame) {
      break;
    }
    Element* container = DynamicTo<HTMLFrameOwnerElement>(frame->Owner());
    if (!container) {
      lastDoc = NextLocalAncestor(*lastDoc);
      // OOPIF: Skip over remote frames, assuming that they have exactly one
      // element in their fullscreen element stacks, thereby erring on the side
      // of exiting fullscreen.
      if (!lastDoc) {
        break;
      }
      docs.push_back(lastDoc);
      continue;
    }

    // 2.5. If |container|'s iframe fullscreen flag is set, break.
    if (auto* iframe = DynamicTo<HTMLIFrameElement>(container)) {
      if (HasIframeFullscreenFlag(*iframe)) {
        break;
      }
    }

    // 2.6. Append |container|'s node document to |docs|.
    Document& parentDoc = container->GetDocument();
    docs.push_back(&parentDoc);
    lastDoc = &parentDoc;
  }

  // 3. Return |docs|.
  return docs;
}

// https://fullscreen.spec.whatwg.org/#run-the-fullscreen-rendering-steps
void FireEvent(const AtomicString& type, Element* element, Document* document) {
  if (!document || !element)
    return;

  // |Document::EnqueueAnimationFrameTask()| is used instead of a "list of
  // pending fullscreen events", so only the body of the "run the fullscreen
  // rendering steps" loop appears here:

  // 3.1. Let |target| be |element| if |element| is connected and its node
  // document is |document|, and otherwise let |target| be |document|.
  EventTarget* target =
      element->isConnected() && &element->GetDocument() == document
          ? static_cast<EventTarget*>(element)
          : static_cast<EventTarget*>(document);

  // 3.2. Fire an event named |type|, with its bubbles and composed attributes
  // set to true, at |target|.
  Event* event = Event::CreateBubble(type);
  event->SetComposed(true);
  target->DispatchEvent(*event);
}

const AtomicString& AdjustEventType(const AtomicString& type,
                                    FullscreenRequestType request_type) {
  DCHECK(type == event_type_names::kFullscreenchange ||
         type == event_type_names::kFullscreenerror);

  if (!(request_type & FullscreenRequestType::kPrefixed))
    return type;
  return type == event_type_names::kFullscreenchange
             ? event_type_names::kWebkitfullscreenchange
             : event_type_names::kWebkitfullscreenerror;
}

void EnqueueEvent(const AtomicString& type,
                  Element& element,
                  Document& document,
                  FullscreenRequestType request_type) {
  const AtomicString& adjusted_type = AdjustEventType(type, request_type);
  document.EnqueueAnimationFrameTask(BindOnce(FireEvent, adjusted_type,
                                              WrapWeakPersistent(&element),
                                              WrapWeakPersistent(&document)));
}

const char* GetErrorString(RequestFullscreenError error) {
  switch (error) {
    case RequestFullscreenError::kNone:
      NOTREACHED();
    case RequestFullscreenError::kElementTypeNotHTMLNorSVG:
      return "Element is not an HTML or SVG element";
    case RequestFullscreenError::kElementTypeDialog:
      return "Dialog elements are invalid";
    case RequestFullscreenError::kElementNotConnected:
      return "Element is not connected";
    case RequestFullscreenError::kDisallowedByPermissionsPolicy:
      return "Disallowed by permissions policy";
    case RequestFullscreenError::kElementOpenAsPopover:
      return "The element is already open as a Popover, and therefore cannot "
             "be opened via the fullscreen API.";
    case RequestFullscreenError::kFullscreenNotSupported:
      return "Fullscreen is not supported";
    case RequestFullscreenError::kPermissionCheckFailed:
      return "Permissions check failed";
    case RequestFullscreenError::kDocumentIncorrect:
      return "Incorrect document";
    case RequestFullscreenError::kNotGranted:
      return "not granted";
  }
}

}  // anonymous namespace

const char Fullscreen::kSupplementName[] = "Fullscreen";

Fullscreen& Fullscreen::From(LocalDOMWindow& window) {
  Fullscreen* fullscreen = Supplement<LocalDOMWindow>::From<Fullscreen>(window);
  if (!fullscreen) {
    fullscreen = MakeGarbageCollected<Fullscreen>(window);
    ProvideTo(window, fullscreen);
  }
  return *fullscreen;
}

// static
bool Fullscreen::HasFullscreenElements() {
  return !FullscreenParamsMap().empty();
}

Element* Fullscreen::FullscreenElementFrom(Document& document) {
  // The fullscreen element is the topmost element in the document's top layer
  // whose fullscreen flag is set, if any, and null otherwise.

  const auto& elements = document.TopLayerElements();
  for (const auto& element : base::Reversed(elements)) {
    if (HasFullscreenFlag(*element))
      return element.Get();
  }

  return nullptr;
}

// https://fullscreen.spec.whatwg.org/#fullscreen-element
Element* Fullscreen::FullscreenElementForBindingFrom(TreeScope& scope) {
  Element* element = FullscreenElementFrom(scope.GetDocument());
  if (!element)
    return nullptr;
  return scope.AdjustedElement(*element);
}

bool Fullscreen::IsInFullscreenElementStack(const Element& element) {
  return HasFullscreenFlag(const_cast<Element&>(element));
}

Fullscreen::Fullscreen(LocalDOMWindow& window)
    : Supplement<LocalDOMWindow>(window),
      ExecutionContextLifecycleObserver(&window) {}

Fullscreen::~Fullscreen() = default;

void Fullscreen::ContextDestroyed() {
  pending_requests_.clear();
  pending_exits_.clear();
}

// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
void Fullscreen::RequestFullscreen(Element& pending) {
  FullscreenOptions* options = FullscreenOptions::Create();
  options->setNavigationUI(V8FullscreenNavigationUI::Enum::kHide);
  RequestFullscreen(pending, options, FullscreenRequestType::kUnprefixed);
}

ScriptPromise<IDLUndefined> Fullscreen::RequestFullscreen(
    Element& pending,
    const FullscreenOptions* options,
    FullscreenRequestType request_type,
    ScriptState* script_state,
    ExceptionState* exception_state) {
  RequestFullscreenScope scope;

  // 1. Let `pendingDoc` be `this`'s node document.
  // The context object for `this` comes from `pending`.
  Document& document = pending.GetDocument();

  // 2. Let `promise` be a new promise.
  // For optimization allocate the ScriptPromiseResolver later.
  ScriptPromiseResolver<IDLUndefined>* resolver = nullptr;

  // 3. If `pendingDoc` is not fully active, then reject `promise` with a
  // TypeError exception and return `promise`.
  if (!document.IsActive() || !document.GetFrame()) {
    if (!exception_state)
      return EmptyPromise();
    exception_state->ThrowTypeError("Document not active");
    return EmptyPromise();
  }

  if (script_state) {
    // We should only be creating promises for unprefixed variants.
    DCHECK(!(request_type & FullscreenRequestType::kPrefixed));
    resolver =
        MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(script_state);
  }

  bool for_cross_process_descendant =
      request_type & FullscreenRequestType::kForCrossProcessDescendant;

  // Only increment use counters in the actual fullscreen element's process.
  LocalDOMWindow& window = *document.domWindow();
  if (!for_cross_process_descendant) {
    if (window.IsSecureContext())
      UseCounter::Count(window, WebFeature::kFullscreenSecureOrigin);
    else
      UseCounter::Count(window, WebFeature::kFullscreenInsecureOrigin);
    // Coarsely measure whether this request may be specifying another screen.
    if (options->hasScreen())
      UseCounter::Count(window, WebFeature::kFullscreenCrossScreen);
  }

  // 4. Let `error` be false.
  RequestFullscreenError error = RequestFullscreenError::kNone;

  // 5. If any of the following conditions are false, then set `error` to true:
  // OOPIF: If `RequestFullscreen()` was already called in a descendant frame
  // and passed the conditions enforcement, do not check again here.
  if (for_cross_process_descendant) {
    ContinueRequestFullscreenAfterConditionsEnforcement(
        &pending, request_type, options, resolver, error);
  } else {
    EnforceRequestFullscreenConditions(
        pending, document,
        BindOnce(
            &Fullscreen::ContinueRequestFullscreenAfterConditionsEnforcement,
            WrapPersistent(&pending), request_type, WrapPersistent(options),
            WrapPersistent(resolver)));
  }

  // 7. Return |promise|, and run the remaining steps in parallel.
  return resolver ? resolver->Promise() : ScriptPromise<IDLUndefined>();
}

void Fullscreen::EnforceRequestFullscreenConditions(
    Element& pending,
    Document& document,
    base::OnceCallback<void(RequestFullscreenError)> callback) {
  // This’s namespace is the HTML namespace or this is an SVG svg or MathML math
  // element.
  if (!pending.IsHTMLElement() && !IsA<SVGSVGElement>(pending)) {
    std::move(callback).Run(RequestFullscreenError::kElementTypeNotHTMLNorSVG);
    return;
  }

  // This is not a dialog element.
  if (IsA<HTMLDialogElement>(pending)) {
    std::move(callback).Run(RequestFullscreenError::kElementTypeDialog);
    return;
  }

  // The fullscreen element ready check for this returns true.
  RequestFullscreenError ready_error =
      FullscreenElementReadyCheck(pending, ReportOptions::kReportOnFailure);
  if (ready_error != RequestFullscreenError::kNone) {
    std::move(callback).Run(ready_error);
    return;
  }

  // Fullscreen is supported.
  if (!FullscreenIsSupported(document)) {
    std::move(callback).Run(RequestFullscreenError::kFullscreenNotSupported);
    return;
  }

  //  WebXR DOM Overlay integration, cf.
  //  https://immersive-web.github.io/dom-overlays/
  //
  // The current implementation of WebXR's "dom-overlay" mode internally uses
  // the Fullscreen API to show a single DOM element based on configuration at
  // XR session start. In addition, for WebXR sessions without "dom-overlay"
  // the renderer may need to force the page to fullscreen to ensure that
  // browser UI hides/responds accordingly. In either case, requesting a WebXR
  // Session does require a user gesture, but it has likely expired by the time
  // the renderer actually gets the XR session from the device and attempts
  // to fullscreen the page.
  if (ScopedAllowFullscreen::FullscreenAllowedReason() ==
          ScopedAllowFullscreen::kXrOverlay ||
      ScopedAllowFullscreen::FullscreenAllowedReason() ==
          ScopedAllowFullscreen::kXrSession) {
    DVLOG(1) << __func__ << ": allowing fullscreen element setup for XR";
    std::move(callback).Run(RequestFullscreenError::kNone);
    return;
  }

  // The WebXR API doesn't support changing elements during the session if the
  // dom-overlay feature is in use (indicated by the IsXrOverlay property). To
  // avoid inconsistencies between implementations we need to block changes via
  // Fullscreen API while the XR session is active, while still allowing the XR
  // code to set up fullscreen mode on session start.
  if (document.IsXrOverlay()) {
    DVLOG(1) << __func__
             << ": rejecting change of fullscreen element for XR DOM overlay";
    std::move(callback).Run(RequestFullscreenError::kPermissionCheckFailed);
    return;
  }

  // An algorithm is allowed to request fullscreen if one of the following is
  // true:

  // The algorithm is triggered by a user activation.
  if (LocalFrame::HasTransientUserActivation(document.GetFrame())) {
    std::move(callback).Run(RequestFullscreenError::kNone);
    return;
  }

  // The algorithm is triggered by a fullscreen request capability delegation.
  if (document.domWindow()->IsFullscreenRequestTokenActive()) {
    std::move(callback).Run(RequestFullscreenError::kNone);
    return;
  }

  // The algorithm is triggered by a user-generated orientation change.
  if (ScopedAllowFullscreen::FullscreenAllowedReason() ==
      ScopedAllowFullscreen::kOrientationChange) {
    UseCounter::Count(document,
                      WebFeature::kFullscreenAllowedByOrientationChange);
    std::move(callback).Run(RequestFullscreenError::kNone);
    return;
  }

  // The context has permission to use fullscreen without transient activation,
  // and no blocking cooldown period is in effect from a recent fullscreen exit.
  //
  // The supplement may be null before this window ever enters fullscreen, but
  // the browser enforces broader per-origin cooldowns with FullscreenUserData.
  if (Fullscreen* fullscreen =
          Supplement<LocalDOMWindow>::From<Fullscreen>(*document.domWindow());
      fullscreen && base::TimeTicks::Now() <=
                        fullscreen->block_automatic_fullscreen_until()) {
    std::move(callback).Run(RequestFullscreenError::kPermissionCheckFailed);
    return;
  }

  // Check permission and wait for the async result.
  auto* permission_service =
      document.GetPermissionService(document.GetExecutionContext());
  CHECK(permission_service);
  auto descriptor = mojom::blink::PermissionDescriptor::New();
  descriptor->name = mojom::blink::PermissionName::FULLSCREEN;
  descriptor->extension =
      mojom::blink::PermissionDescriptorExtension::NewFullscreen(
          mojom::blink::FullscreenPermissionDescriptor::New(
              /*allow_without_user_gesture=*/true));
  permission_service->HasPermission(
      std::move(descriptor),
      blink::BindOnce(
          [](base::OnceCallback<void(RequestFullscreenError)> callback,
             Document* document,
             mojom::blink::PermissionStatusWithDetailsPtr result) {
            if (result->status == mojom::blink::PermissionStatus::GRANTED) {
              UseCounter::Count(document,
                                WebFeature::kFullscreenAllowedByContentSetting);
              std::move(callback).Run(RequestFullscreenError::kNone);
            } else {
              std::move(callback).Run(
                  RequestFullscreenError::kPermissionCheckFailed);
            }
          },
          std::move(callback), WrapPersistent(&document)));
}

void Fullscreen::ContinueRequestFullscreenAfterConditionsEnforcement(
    Element* pending,
    FullscreenRequestType request_type,
    const FullscreenOptions* options,
    ScriptPromiseResolver<IDLUndefined>* resolver,
    RequestFullscreenError error) {
  CHECK(pending);
  Document& document = pending->GetDocument();
  if (error != RequestFullscreenError::kNone) {
    // TODO: Surface more errors in the console with added precision.
    if (error == RequestFullscreenError::kPermissionCheckFailed) {
      String message = ExceptionMessages::FailedToExecute(
          "requestFullscreen", "Element",
          "API can only be initiated by a user gesture.");
      document.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
          mojom::blink::ConsoleMessageSource::kJavaScript,
          mojom::blink::ConsoleMessageLevel::kWarning, message));
    }
    // Note: Although we are past the "in parallel" point, it's OK to continue
    // synchronously because when `error` is true, `ContinueRequestFullscreen()`
    // will only queue a task and return. This is indistinguishable from, e.g.,
    // enqueueing a microtask to continue at step 9.
    ContinueRequestFullscreen(document, *pending, request_type, options,
                              resolver, error);
    return;
  }

  LocalDOMWindow& window = *document.domWindow();

  // 8. If `error` is false: then resize `pendingDoc`’s node navigable’s
  // top-level traversable’s active document’s viewport’s dimensions, optionally
  // taking into account options["navigationUI"].
  // Optionally display a message how the end user can revert this.
  if (From(window).pending_requests_.size()) {
    UseCounter::Count(window, WebFeature::kFullscreenRequestWithPendingElement);
  }

  From(window).pending_requests_.push_back(MakeGarbageCollected<PendingRequest>(
      pending, request_type, options, resolver));
  LocalFrame& frame = *window.GetFrame();
  frame.GetChromeClient().EnterFullscreen(frame, options, request_type);

  // 6. If `error` is false, then consume user activation given `pendingDoc`’s
  // relevant global object.
  // TODO: Reorder implementation to match the specified algorithm steps.
  // This does, at least, correctly consume activation before RequestFullscreen
  // returns its promise. This callback is run synchronously before the promise
  // is returned when conditions are met and the frame has transient activation.
  // This callback is only run asynchronously when a frame requests fullscreen
  // without transient activation, which requires a permission service check.
  if ((request_type & FullscreenRequestType::kForCrossProcessDescendant) == 0) {
    LocalFrame::ConsumeTransientUserActivation(window.GetFrame());
    window.ConsumeFullscreenRequestToken();
  }
}

void Fullscreen::DidResolveEnterFullscreenRequest(Document& document,
                                                  bool granted) {
  if (!document.domWindow())
    return;

  // We may be called synchronously from within
  // |FullscreenController::EnterFullscreen()| if we were already fullscreen,
  // but must still not synchronously change the fullscreen element. Instead
  // enqueue a microtask to continue.
  if (RequestFullscreenScope::RunningRequestFullscreen()) {
    document.GetAgent().event_loop()->EnqueueMicrotask(BindOnce(
        [](Document* document, bool granted) {
          DCHECK(document);
          DidResolveEnterFullscreenRequest(*document, granted);
        },
        WrapPersistent(&document), granted));
    return;
  }

  PendingRequests requests;
  requests.swap(From(*document.domWindow()).pending_requests_);
  const RequestFullscreenError error =
      granted ? RequestFullscreenError::kNone
              : RequestFullscreenError::kNotGranted;
  for (const Member<PendingRequest>& request : requests) {
    ContinueRequestFullscreen(document, *request->element(), request->type(),
                              request->options(), request->resolver(), error);
  }
}

void Fullscreen::ContinueRequestFullscreen(
    Document& document,
    Element& pending,
    FullscreenRequestType request_type,
    const FullscreenOptions* options,
    ScriptPromiseResolver<IDLUndefined>* resolver,
    RequestFullscreenError error) {
  DCHECK(document.IsActive());
  DCHECK(document.GetFrame());

  // 9. If any of the following conditions are false, then set `error` to true:
  //     * `pending`'s node document is `pendingDoc`.
  //     * The fullscreen element ready check for `pending` returns true.
  if (error == RequestFullscreenError::kNone) {
    if (pending.GetDocument() != document) {
      error = RequestFullscreenError::kDocumentIncorrect;
    } else {
      error = FullscreenElementReadyCheck(pending, ReportOptions::kDoNotReport);
    }
  }

  // 10. If `error` is true:
  if (error != RequestFullscreenError::kNone) {
    // 10.1. Append (fullscreenerror, `pending`) to `pendingDoc`'s list of
    // pending fullscreen events.
    EnqueueEvent(event_type_names::kFullscreenerror, pending, document,
                 request_type);

    // 10.2. Reject `promise` with a TypeError exception and terminate these
    // steps.
    if (resolver && resolver->GetScriptState()->ContextIsValid()) {
      ScriptState::Scope scope(resolver->GetScriptState());
      resolver->Reject(V8ThrowException::CreateTypeError(
          resolver->GetScriptState()->GetIsolate(), GetErrorString(error)));
    }
    return;
  }

  // 11. Let |fullscreenElements| be an ordered set initially consisting of
  // |pending|.
  HeapVector<Member<Element>> fullscreen_elements;
  fullscreen_elements.push_back(pending);

  // 12. While the first element in |fullscreenElements| is in a nested browsing
  // context: append its browsing context container to |fullscreenElements|.
  //
  // OOPIF: |fullscreenElements| will only contain elements for local ancestors,
  // and remote ancestors will be processed in their respective processes. This
  // preserves the spec's event firing order for local ancestors, but not for
  // remote ancestors. However, that difference shouldn't be observable in
  // practice: a fullscreenchange event handler would need to postMessage a
  // frame in another renderer process, where the message should be queued up
  // and processed after the IPC that dispatches fullscreenchange.
  for (Frame* frame = pending.GetDocument().GetFrame(); frame;
       frame = frame->Tree().Parent()) {
    Element* element = DynamicTo<HTMLFrameOwnerElement>(frame->Owner());
    if (!element)
      continue;
    fullscreen_elements.push_back(element);
  }

  // 13. For each |element| in |fullscreenElements|:
  for (Element* element : fullscreen_elements) {
    // 13.1. Let |doc| be |element|'s node document.
    Document& doc = element->GetDocument();

    // If this fullscreen request is for WebXR DOM Overlay mode, apply that
    // property to the document. This updates styling (setting the background
    // transparent) and adds the :xr-overlay pseudoclass.
    if (request_type & FullscreenRequestType::kForXrOverlay) {
      // There's never more than one overlay element per document. (It's either
      // the actual overlay element, or a containing iframe element if the
      // actual element is in a different document.) It can't be changed during
      // the session, that's enforced by AllowedToRequestFullscreen().
      DCHECK(!doc.IsXrOverlay());
      doc.SetIsXrOverlay(true, element);
    }

    // 13.2. If |element| is |doc|'s fullscreen element, continue.
    if (element == FullscreenElementFrom(doc))
      continue;

    // 13.3. If |element| is |pending| and |pending| is an iframe element, set
    // |element|'s iframe fullscreen flag.
    if (element == &pending) {
      if (auto* iframe = DynamicTo<HTMLIFrameElement>(pending)) {
        SetIframeFullscreenFlag(*iframe);
      }
    }

    // 13.4. Fullscreen |element| within |doc|.
    GoFullscreen(*element, request_type, options);

    // 13.5. Append (fullscreenchange, |element|) to |doc|'s list of pending
    // fullscreen events.
    EnqueueEvent(event_type_names::kFullscreenchange, *element, doc,
                 request_type);
  }

  // 14. Resolve |promise| with undefined.
  if (resolver) {
    ScriptState::Scope scope(resolver->GetScriptState());
    resolver->Resolve();
  }
}

// https://fullscreen.spec.whatwg.org/#fully-exit-fullscreen
void Fullscreen::FullyExitFullscreen(Document& document, bool ua_originated) {
  // TODO(foolip): The spec used to have a first step saying "Let |doc| be the
  // top-level browsing context's document" which was removed in
  // https://github.com/whatwg/fullscreen/commit/3243119d027a8ff5b80998eb1f17f8eba148a346.
  // Remove it here as well.
  Document& doc = TopmostLocalAncestor(document);

  // 1. If |document|'s fullscreen element is null, terminate these steps.
  Element* fullscreen_element = FullscreenElementFrom(doc);
  if (!fullscreen_element)
    return;

  // 2. Unfullscreen elements whose fullscreen flag is set, within
  // |document|'s top layer, except for |document|'s fullscreen element.
  HeapVector<Member<Element>> unfullscreen_elements;
  for (Element* element : doc.TopLayerElements()) {
    if (HasFullscreenFlag(*element) && element != fullscreen_element)
      unfullscreen_elements.push_back(element);
  }
  for (Element* element : unfullscreen_elements)
    Unfullscreen(*element);
  DCHECK(IsSimpleFullscreenDocument(doc));

  // 3. Exit fullscreen |document|.
  ExitFullscreen(doc, nullptr, nullptr, ua_originated);
}

// https://fullscreen.spec.whatwg.org/#exit-fullscreen
ScriptPromise<IDLUndefined> Fullscreen::ExitFullscreen(
    Document& doc,
    ScriptState* script_state,
    ExceptionState* exception_state,
    bool ua_originated) {
  // 1. Let |promise| be a new promise.
  // For optimization allocate the ScriptPromiseResolver later.
  ScriptPromiseResolver<IDLUndefined>* resolver = nullptr;

  // 2. If |doc| is not fully active or |doc|'s fullscreen element is null, then
  // reject |promise| with a TypeError exception and return |promise|.
  if (!doc.IsActive() || !doc.GetFrame() || !FullscreenElementFrom(doc)) {
    if (!exception_state)
      return EmptyPromise();
    exception_state->ThrowTypeError("Document not active");
    return EmptyPromise();
  }

  if (script_state) {
    resolver =
        MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(script_state);
  }

  // 3. Let |resize| be false.
  bool resize = false;

  // 4. Let |docs| be the result of collecting documents to unfullscreen given
  // |doc|.
  HeapVector<Member<Document>> docs = CollectDocumentsToUnfullscreen(doc);

  // 5. Let |topLevelDoc| be |doc|'s top-level browsing context's active
  // document.
  //
  // OOPIF: Let |topLevelDoc| be the topmost local ancestor instead. If the main
  // frame is in another process, we will still fully exit fullscreen even
  // though that's wrong if the main frame was in nested fullscreen.
  // TODO(alexmos): Deal with nested fullscreen cases, see
  // https://crbug.com/617369.
  Document& top_level_doc = TopmostLocalAncestor(doc);

  // 6. If |topLevelDoc| is in |docs|, and it is a simple fullscreen document,
  // then set |doc| to |topLevelDoc| and |resize| to true.
  //
  // Note: |doc| is not set here, but |doc| will be the topmost local ancestor
  // in |Fullscreen::ContinueExitFullscreen| if |resize| is true.
  if (!docs.empty() && docs.back() == &top_level_doc &&
      IsSimpleFullscreenDocument(top_level_doc)) {
    resize = true;
  }

  Element* element = FullscreenElementFrom(doc);

  const MetaParams* element_params = GetParams(*element);
  FullscreenRequestType request_type = element_params
                                           ? element_params->request_type()
                                           : FullscreenRequestType::kUnprefixed;

  // 7. If |doc|'s fullscreen element is not connected.
  if (!element->isConnected()) {
    // 7.1. Append (fullscreenchange, |doc|'s fullscreen element) to
    // |doc|'s list of pending fullscreen events.
    EnqueueEvent(event_type_names::kFullscreenchange, *element, doc,
                 request_type);

    // 7.2. Unfullscreen |element|.
    Unfullscreen(*element);
  }

  // 7. Return |promise|, and run the remaining steps in parallel.
  auto promise = resolver ? resolver->Promise() : ScriptPromise<IDLUndefined>();

  // 8. If |resize| is true, resize |doc|'s viewport to its "normal" dimensions.
  if (resize) {
    if (ua_originated) {
      ContinueExitFullscreen(&doc, resolver, true /* resize */);
    } else {
      From(*top_level_doc.domWindow()).pending_exits_.push_back(resolver);
      LocalFrame& frame = *doc.GetFrame();
      frame.GetChromeClient().ExitFullscreen(frame);
    }
  } else {
    DCHECK(!ua_originated);
    // Note: We are past the "in parallel" point, and |ContinueExitFullscreen()|
    // will change script-observable state (document.fullscreenElement)
    // synchronously, so we have to continue asynchronously.
    doc.GetAgent().event_loop()->EnqueueMicrotask(
        BindOnce(ContinueExitFullscreen, WrapPersistent(&doc),
                 WrapPersistent(resolver), false /* resize */));
  }
  return promise;
}

void Fullscreen::DidExitFullscreen(Document& document) {
  Fullscreen& fullscreen = From(*document.domWindow());

  // Block automatic fullscreen temporarily, e.g. match kActivationLifespan.
  fullscreen.block_automatic_fullscreen_until_ =
      base::TimeTicks::Now() + base::Seconds(5);

  // If this is a response to an ExitFullscreen call then
  // continue exiting. Otherwise call FullyExitFullscreen.
  PendingExits exits;
  exits.swap(fullscreen.pending_exits_);
  if (exits.empty()) {
    FullyExitFullscreen(document, true /* ua_originated */);
  } else {
    for (const Member<PendingExit>& exit : exits) {
      ContinueExitFullscreen(&document, exit, true /* resize */);
    }
  }
}

void Fullscreen::ContinueExitFullscreen(
    Document* doc,
    ScriptPromiseResolver<IDLUndefined>* resolver,
    bool resize) {
  if (!doc || !doc->IsActive() || !doc->GetFrame()) {
    if (resolver) {
      ScriptState::Scope scope(resolver->GetScriptState());
      resolver->Reject(V8ThrowException::CreateTypeError(
          resolver->GetScriptState()->GetIsolate(), "Document is not active"));
    }
    return;
  }

  if (resize) {
    // See comment for step 6.
    DCHECK_EQ(nullptr, NextLocalAncestor(*doc));
  }

  // 9. If |doc|'s fullscreen element is null, then resolve |promise| with
  // undefined and terminate these steps.
  if (!FullscreenElementFrom(*doc)) {
    if (resolver) {
      ScriptState::Scope scope(resolver->GetScriptState());
      resolver->Resolve();
    }
    return;
  }

  // 10. Let |exitDocs| be the result of collecting documents to unfullscreen
  // given |doc|.
  HeapVector<Member<Document>> exit_docs = CollectDocumentsToUnfullscreen(*doc);

  // 11. Let |descendantDocs| be an ordered set consisting of |doc|'s
  // descendant browsing contexts' documents whose fullscreen element is
  // non-null, if any, in tree order.
  HeapVector<Member<Document>> descendant_docs;
  for (Frame* descendant = doc->GetFrame()->Tree().FirstChild(); descendant;
       descendant = descendant->Tree().TraverseNext(doc->GetFrame())) {
    auto* descendant_local_frame = DynamicTo<LocalFrame>(descendant);
    if (!descendant_local_frame)
      continue;
    DCHECK(descendant_local_frame->GetDocument());
    if (FullscreenElementFrom(*descendant_local_frame->GetDocument()))
      descendant_docs.push_back(descendant_local_frame->GetDocument());
  }

  // 12. For each |exitDoc| in |exitDocs|:
  for (Document* exit_doc : exit_docs) {
    Element* exit_element = FullscreenElementFrom(*exit_doc);
    DCHECK(exit_element);
    FullscreenRequestType request_type = GetRequestType(*exit_element);

    // 12.1. Append (fullscreenchange, |exitDoc|'s fullscreen element) to
    // |exitDoc|'s list of pending fullscreen events.
    EnqueueEvent(event_type_names::kFullscreenchange, *exit_element, *exit_doc,
                 request_type);

    // 12.2. If |resize| is true, unfullscreen |exitDoc|.
    // 12.3. Otherwise, unfullscreen |exitDoc|'s fullscreen element.
    if (resize)
      Unfullscreen(*exit_doc);
    else
      Unfullscreen(*exit_element);
  }

  // 13. For each |descendantDoc| in |descendantDocs|:
  for (Document* descendant_doc : descendant_docs) {
    Element* descendant_element = FullscreenElementFrom(*descendant_doc);
    DCHECK(descendant_element);
    FullscreenRequestType request_type = GetRequestType(*descendant_element);

    // 13.1. Append (fullscreenchange, |descendantDoc|'s fullscreen element) to
    // |descendantDoc|'s list of pending fullscreen events.
    EnqueueEvent(event_type_names::kFullscreenchange, *descendant_element,
                 *descendant_doc, request_type);

    // 13.2. Unfullscreen |descendantDoc|.
    Unfullscreen(*descendant_doc);
  }

  // 14. Resolve |promise| with undefined.
  if (resolver) {
    ScriptState::Scope scope(resolver->GetScriptState());
    resolver->Resolve();
  }
}

// https://fullscreen.spec.whatwg.org/#dom-document-fullscreenenabled
bool Fullscreen::FullscreenEnabled(Document& document,
                                   ReportOptions report_on_failure) {
  // The fullscreenEnabled attribute's getter must return true if the context
  // object is allowed to use the feature indicated by attribute name
  // allowfullscreen and fullscreen is supported, and false otherwise.
  return AllowedToUseFullscreen(document, report_on_failure) &&
         FullscreenIsSupported(document);
}

void Fullscreen::DidUpdateSize(Element& element) {
  // StyleAdjuster will set the size so we need to do a style recalc.
  // Normally changing size means layout so just doing a style recalc is a
  // bit surprising.
  element.SetNeedsStyleRecalc(
      kLocalStyleChange,
      StyleChangeReasonForTracing::Create(style_change_reason::kFullscreen));
}

void Fullscreen::ElementRemoved(Element& node) {
  DCHECK(node.IsInTopLayer());
  if (!HasFullscreenFlag(node))
    return;

  // 1. Let |document| be removedNode's node document.
  Document& document = node.GetDocument();

  // |Fullscreen::ElementRemoved()| is called for each removed element, so only
  // the body of the spec "removing steps" loop appears here:

  // 3.1. If |node| is its node document's fullscreen element, exit fullscreen
  // that document.
  if (IsFullscreenElement(node)) {
    ExitFullscreen(document);
  } else {
    // 3.2. Otherwise, unfullscreen |node| within its node document.
    Unfullscreen(node);
  }

  // 3.3 If document's top layer contains node, remove node from document's top
  // layer. This is done in Element::RemovedFrom.
}

bool Fullscreen::IsFullscreenFlagSetFor(const Element& element) {
  return HasFullscreenFlag(element);
}

void Fullscreen::Trace(Visitor* visitor) const {
  visitor->Trace(pending_requests_);
  visitor->Trace(pending_exits_);
  Supplement<LocalDOMWindow>::Trace(visitor);
  ExecutionContextLifecycleObserver::Trace(visitor);
}

Fullscreen::PendingRequest::PendingRequest(
    Element* element,
    FullscreenRequestType type,
    const FullscreenOptions* options,
    ScriptPromiseResolver<IDLUndefined>* resolver)
    : element_(element), type_(type), options_(options), resolver_(resolver) {}

Fullscreen::PendingRequest::~PendingRequest() = default;

void Fullscreen::PendingRequest::Trace(Visitor* visitor) const {
  visitor->Trace(element_);
  visitor->Trace(options_);
  visitor->Trace(resolver_);
}

}  // namespace blink
