/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 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/. */

#include "mozilla/layers/FocusTarget.h"
#include "mozilla/dom/BrowserBridgeChild.h"  // for BrowserBridgeChild
#include "mozilla/dom/EventTarget.h"         // for EventTarget
#include "mozilla/dom/RemoteBrowser.h"       // For RemoteBrowser
#include "mozilla/EventDispatcher.h"         // for EventDispatcher
#include "mozilla/PresShell.h"               // For PresShell
#include "mozilla/StaticPrefs_apz.h"
#include "nsIContentInlines.h"  // for nsINode::IsEditable()
#include "nsLayoutUtils.h"      // for nsLayoutUtils

static mozilla::LazyLogModule sApzFtgLog("apz.focustarget");
#define FT_LOG(...) MOZ_LOG(sApzFtgLog, LogLevel::Debug, (__VA_ARGS__))

using namespace mozilla::dom;
using namespace mozilla::layout;

namespace mozilla {
namespace layers {

static PresShell* GetRetargetEventPresShell(PresShell* aRootPresShell) {
  MOZ_ASSERT(aRootPresShell);

  // Use the last focused window in this PresShell and its
  // associated PresShell
  nsCOMPtr<nsPIDOMWindowOuter> window =
      aRootPresShell->GetFocusedDOMWindowInOurWindow();
  if (!window) {
    return nullptr;
  }

  RefPtr<Document> retargetEventDoc = window->GetExtantDoc();
  if (!retargetEventDoc) {
    return nullptr;
  }

  return retargetEventDoc->GetPresShell();
}

// _BOUNDARY because Dispatch() with `targets` must not handle the event.
MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForKeyEvents(
    nsIContent* aContent) {
  if (!aContent) {
    return false;
  }

  WidgetEvent event(true, eVoidEvent);
  nsTArray<EventTarget*> targets;
  nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr,
                                          nullptr, nullptr, &targets);
  NS_ENSURE_SUCCESS(rv, false);
  for (size_t i = 0; i < targets.Length(); i++) {
    if (targets[i]->HasNonSystemGroupListenersForUntrustedKeyEvents()) {
      return true;
    }
  }
  return false;
}

// _BOUNDARY because Dispatch() with `targets` must not handle the event.
MOZ_CAN_RUN_SCRIPT_BOUNDARY static bool HasListenersForNonPassiveKeyEvents(
    nsIContent* aContent) {
  if (!aContent) {
    return false;
  }

  WidgetEvent event(true, eVoidEvent);
  nsTArray<EventTarget*> targets;
  nsresult rv = EventDispatcher::Dispatch(aContent, nullptr, &event, nullptr,
                                          nullptr, nullptr, &targets);
  NS_ENSURE_SUCCESS(rv, false);
  for (size_t i = 0; i < targets.Length(); i++) {
    if (targets[i]
            ->HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents()) {
      return true;
    }
  }
  return false;
}

static bool IsEditableNode(nsINode* aNode) {
  return aNode && aNode->IsEditable();
}

FocusTarget::FocusTarget()
    : mSequenceNumber(0),
      mFocusHasKeyEventListeners(false),
      mData(AsVariant(NoFocusTarget())) {}

FocusTarget::FocusTarget(PresShell* aRootPresShell,
                         uint64_t aFocusSequenceNumber)
    : mSequenceNumber(aFocusSequenceNumber),
      mFocusHasKeyEventListeners(false),
      mData(AsVariant(NoFocusTarget())) {
  MOZ_ASSERT(aRootPresShell);
  MOZ_ASSERT(NS_IsMainThread());

  // Key events can be retargeted to a child PresShell when there is an iframe
  RefPtr<PresShell> presShell = GetRetargetEventPresShell(aRootPresShell);

  if (!presShell) {
    FT_LOG("Creating nil target with seq=%" PRIu64
           " (can't find retargeted presshell)\n",
           aFocusSequenceNumber);

    return;
  }

  RefPtr<Document> document = presShell->GetDocument();
  if (!document) {
    FT_LOG("Creating nil target with seq=%" PRIu64 " (no document)\n",
           aFocusSequenceNumber);

    return;
  }

  // Find the focused content and use it to determine whether there are key
  // event listeners or whether key events will be targeted at a different
  // process through a remote browser.
  nsCOMPtr<nsIContent> focusedContent =
      presShell->GetFocusedContentInOurWindow();
  nsCOMPtr<nsIContent> keyEventTarget = focusedContent;

  // If there is no focused element then event dispatch goes to the body of
  // the page if it exists or the root element.
  if (!keyEventTarget) {
    keyEventTarget = document->GetUnfocusedKeyEventTarget();
  }

  // Check if there are key event listeners that could prevent default or change
  // the focus or selection of the page.
  if (StaticPrefs::apz_keyboard_passive_listeners()) {
    mFocusHasKeyEventListeners =
        HasListenersForNonPassiveKeyEvents(keyEventTarget.get());
  } else {
    mFocusHasKeyEventListeners = HasListenersForKeyEvents(keyEventTarget.get());
  }

  // Check if the key event target is content editable or if the document
  // is in design mode.
  if (IsEditableNode(keyEventTarget) || IsEditableNode(document)) {
    FT_LOG("Creating nil target with seq=%" PRIu64
           ", kl=%d (disabling for editable node)\n",
           aFocusSequenceNumber, static_cast<int>(mFocusHasKeyEventListeners));

    return;
  }

  // Check if the key event target is a remote browser
  if (RemoteBrowser* remoteBrowser = RemoteBrowser::GetFrom(keyEventTarget)) {
    LayersId layersId = remoteBrowser->GetLayersId();

    // The globally focused element for scrolling is in a remote layer tree
    if (layersId.IsValid()) {
      FT_LOG("Creating reflayer target with seq=%" PRIu64 ", kl=%d, lt=%" PRIu64
             "\n",
             aFocusSequenceNumber, mFocusHasKeyEventListeners, layersId.mId);

      mData = AsVariant<LayersId>(std::move(layersId));
      return;
    }

    FT_LOG("Creating nil target with seq=%" PRIu64
           ", kl=%d (remote browser missing layers id)\n",
           aFocusSequenceNumber, mFocusHasKeyEventListeners);

    return;
  }

  // The content to scroll is either the focused element or the focus node of
  // the selection. It's difficult to determine if an element is an interactive
  // element requiring async keyboard scrolling to be disabled. So we only
  // allow async key scrolling based on the selection, which doesn't have
  // this problem and is more common.
  if (focusedContent) {
    FT_LOG("Creating nil target with seq=%" PRIu64
           ", kl=%d (disabling for focusing an element)\n",
           aFocusSequenceNumber, mFocusHasKeyEventListeners);

    return;
  }

  nsCOMPtr<nsIContent> selectedContent =
      presShell->GetSelectedContentForScrolling();

  // Gather the scroll container frames that would be scrolled in each direction
  // for this scroll target
  ScrollContainerFrame* horizontal =
      presShell->GetScrollContainerFrameToScrollForContent(
          selectedContent.get(), HorizontalScrollDirection);
  ScrollContainerFrame* vertical =
      presShell->GetScrollContainerFrameToScrollForContent(
          selectedContent.get(), VerticalScrollDirection);

  // We might have the globally focused element for scrolling. Gather a ViewID
  // for the horizontal and vertical scroll targets of this element.
  ScrollTargets target;
  target.mHorizontal = nsLayoutUtils::FindIDForScrollContainerFrame(horizontal);
  target.mVertical = nsLayoutUtils::FindIDForScrollContainerFrame(vertical);
  mData = AsVariant(target);

  FT_LOG("Creating scroll target with seq=%" PRIu64 ", kl=%d, h=%" PRIu64
         ", v=%" PRIu64 "\n",
         aFocusSequenceNumber, mFocusHasKeyEventListeners, target.mHorizontal,
         target.mVertical);
}

bool FocusTarget::operator==(const FocusTarget& aRhs) const {
  return mSequenceNumber == aRhs.mSequenceNumber &&
         mFocusHasKeyEventListeners == aRhs.mFocusHasKeyEventListeners &&
         mData == aRhs.mData;
}

const char* FocusTarget::Type() const {
  if (mData.is<LayersId>()) {
    return "LayersId";
  }
  if (mData.is<ScrollTargets>()) {
    return "ScrollTargets";
  }
  if (mData.is<NoFocusTarget>()) {
    return "NoFocusTarget";
  }
  return "<unknown>";
}

}  // namespace layers
}  // namespace mozilla
