/* 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 "GlobalKeyListener.h"

#include <utility>

#include "ErrorList.h"
#include "EventTarget.h"
#include "mozilla/EventListenerManager.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/KeyEventHandler.h"
#include "mozilla/NativeKeyBindingsType.h"
#include "mozilla/Preferences.h"
#include "mozilla/ShortcutKeys.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/EventBinding.h"
#include "mozilla/dom/KeyboardEvent.h"
#include "mozilla/widget/IMEData.h"
#include "nsAtom.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsFocusManager.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsIDocShell.h"
#include "nsIWidget.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"

namespace mozilla {

using namespace mozilla::layers;

GlobalKeyListener::GlobalKeyListener(dom::EventTarget* aTarget)
    : mTarget(aTarget), mHandler(nullptr) {}

NS_IMPL_ISUPPORTS(GlobalKeyListener, nsIDOMEventListener)

static void BuildHandlerChain(nsIContent* aContent, KeyEventHandler** aResult) {
  *aResult = nullptr;

  // Since we chain each handler onto the next handler,
  // we'll enumerate them here in reverse so that when we
  // walk the chain they'll come out in the original order
  for (nsIContent* key = aContent->GetLastChild(); key;
       key = key->GetPreviousSibling()) {
    if (!key->NodeInfo()->Equals(nsGkAtoms::key, kNameSpaceID_XUL)) {
      continue;
    }

    dom::Element* keyElement = key->AsElement();
    // Check whether the key element has empty value at key/char attribute.
    // Such element is used by localizers for alternative shortcut key
    // definition on the locale. See bug 426501.
    nsAutoString valKey, valCharCode, valKeyCode;
    // Hopefully at least one of the attributes is set:
    keyElement->GetAttr(nsGkAtoms::key, valKey) ||
        keyElement->GetAttr(nsGkAtoms::charcode, valCharCode) ||
        keyElement->GetAttr(nsGkAtoms::keycode, valKeyCode);
    // If not, ignore this key element.
    if (valKey.IsEmpty() && valCharCode.IsEmpty() && valKeyCode.IsEmpty()) {
      continue;
    }

    // reserved="pref" is the default for <key> elements.
    ReservedKey reserved = ReservedKey_Unset;
    if (keyElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::reserved,
                                nsGkAtoms::_true, eCaseMatters)) {
      reserved = ReservedKey_True;
    } else if (keyElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::reserved,
                                       nsGkAtoms::_false, eCaseMatters)) {
      reserved = ReservedKey_False;
    }

    KeyEventHandler* handler = new KeyEventHandler(keyElement, reserved);

    handler->SetNextHandler(*aResult);
    *aResult = handler;
  }
}

void GlobalKeyListener::WalkHandlers(dom::KeyboardEvent* aKeyEvent) {
  if (aKeyEvent->DefaultPrevented()) {
    return;
  }

  // Don't process the event if it was not dispatched from a trusted source
  if (!aKeyEvent->IsTrusted()) {
    return;
  }

  EnsureHandlers();

  // skip keysets that are disabled
  if (IsDisabled()) {
    return;
  }

  WalkHandlersInternal(Purpose::ExecuteCommand, aKeyEvent);
}

void GlobalKeyListener::InstallKeyboardEventListenersTo(
    EventListenerManager* aEventListenerManager) {
  // For marking each keyboard event as if it's reserved by chrome,
  // GlobalKeyListeners need to listen each keyboard events before
  // web contents.
  aEventListenerManager->AddEventListenerByType(this, u"keydown"_ns,
                                                TrustedEventsAtCapture());
  aEventListenerManager->AddEventListenerByType(this, u"keyup"_ns,
                                                TrustedEventsAtCapture());
  aEventListenerManager->AddEventListenerByType(this, u"keypress"_ns,
                                                TrustedEventsAtCapture());

  // For reducing the IPC cost, preventing to dispatch reserved keyboard
  // events into the content process.
  aEventListenerManager->AddEventListenerByType(
      this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
  aEventListenerManager->AddEventListenerByType(
      this, u"keyup"_ns, TrustedEventsAtSystemGroupCapture());
  aEventListenerManager->AddEventListenerByType(
      this, u"keypress"_ns, TrustedEventsAtSystemGroupCapture());

  // Handle keyboard events in bubbling phase of the system event group.
  aEventListenerManager->AddEventListenerByType(
      this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
  aEventListenerManager->AddEventListenerByType(
      this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
  aEventListenerManager->AddEventListenerByType(
      this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
  // mozaccesskeynotfound event is fired when modifiers of keypress event
  // matches with modifier of content access key but it's not consumed by
  // remote content.
  aEventListenerManager->AddEventListenerByType(
      this, u"mozaccesskeynotfound"_ns, TrustedEventsAtSystemGroupBubble());
}

void GlobalKeyListener::RemoveKeyboardEventListenersFrom(
    EventListenerManager* aEventListenerManager) {
  aEventListenerManager->RemoveEventListenerByType(this, u"keydown"_ns,
                                                   TrustedEventsAtCapture());
  aEventListenerManager->RemoveEventListenerByType(this, u"keyup"_ns,
                                                   TrustedEventsAtCapture());
  aEventListenerManager->RemoveEventListenerByType(this, u"keypress"_ns,
                                                   TrustedEventsAtCapture());

  aEventListenerManager->RemoveEventListenerByType(
      this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture());
  aEventListenerManager->RemoveEventListenerByType(
      this, u"keyup"_ns, TrustedEventsAtSystemGroupCapture());
  aEventListenerManager->RemoveEventListenerByType(
      this, u"keypress"_ns, TrustedEventsAtSystemGroupCapture());

  aEventListenerManager->RemoveEventListenerByType(
      this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble());
  aEventListenerManager->RemoveEventListenerByType(
      this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble());
  aEventListenerManager->RemoveEventListenerByType(
      this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble());
  aEventListenerManager->RemoveEventListenerByType(
      this, u"mozaccesskeynotfound"_ns, TrustedEventsAtSystemGroupBubble());
}

NS_IMETHODIMP
GlobalKeyListener::HandleEvent(dom::Event* aEvent) {
  RefPtr<dom::KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent();
  NS_ENSURE_TRUE(keyEvent, NS_ERROR_INVALID_ARG);

  if (aEvent->EventPhase() == dom::Event_Binding::CAPTURING_PHASE) {
    if (aEvent->WidgetEventPtr()->mFlags.mInSystemGroup) {
      HandleEventOnCaptureInSystemEventGroup(keyEvent);
    } else {
      HandleEventOnCaptureInDefaultEventGroup(keyEvent);
    }
    return NS_OK;
  }

  // If this event was handled by APZ then don't do the default action, and
  // preventDefault to prevent any other listeners from handling the event.
  if (aEvent->WidgetEventPtr()->mFlags.mHandledByAPZ) {
    aEvent->PreventDefault();
    return NS_OK;
  }

  WalkHandlers(keyEvent);
  return NS_OK;
}

void GlobalKeyListener::HandleEventOnCaptureInDefaultEventGroup(
    dom::KeyboardEvent* aEvent) {
  WidgetKeyboardEvent* widgetKeyboardEvent =
      aEvent->WidgetEventPtr()->AsKeyboardEvent();

  if (widgetKeyboardEvent->IsReservedByChrome()) {
    return;
  }

  if (HasHandlerForEvent(aEvent).mReservedHandlerForChromeFound) {
    widgetKeyboardEvent->MarkAsReservedByChrome();
  }
}

void GlobalKeyListener::HandleEventOnCaptureInSystemEventGroup(
    dom::KeyboardEvent* aEvent) {
  WidgetKeyboardEvent* widgetEvent =
      aEvent->WidgetEventPtr()->AsKeyboardEvent();

  // If the event won't be sent to remote process, this listener needs to do
  // nothing.  Note that even if mOnlySystemGroupDispatchInContent is true,
  // we need to send the event to remote process and check reply event
  // before matching it with registered shortcut keys because event listeners
  // in the system event group may want to handle the event before registered
  // shortcut key handlers.
  if (!widgetEvent->WillBeSentToRemoteProcess()) {
    return;
  }

  WalkHandlersResult result = HasHandlerForEvent(aEvent);
  if (!result.mMeaningfulHandlerFound) {
    return;
  }

  // If this event wasn't marked as IsCrossProcessForwardingStopped,
  // yet, it means it wasn't processed by content. We'll not call any
  // of the handlers at this moment, and will wait the reply event.
  // So, stop immediate propagation in this event first, then, mark it as
  // waiting reply from remote process.  Finally, when this process receives
  // a reply from the remote process, it should be dispatched into this
  // DOM tree again.
  widgetEvent->StopImmediatePropagation();
  widgetEvent->MarkAsWaitingReplyFromRemoteProcess();
}

//
// WalkHandlersInternal and WalkHandlersAndExecute
//
// Given a particular DOM event and a pointer to the first handler in the list,
// scan through the list to find something to handle the event. If aPurpose =
// Purpose::ExecuteHandler, the handler will be executed; otherwise just return
// an answer telling if a handler for that event was found.
//
GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersInternal(
    Purpose aPurpose, dom::KeyboardEvent* aKeyEvent) {
  WidgetKeyboardEvent* nativeKeyboardEvent =
      aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
  MOZ_ASSERT(nativeKeyboardEvent);

  AutoShortcutKeyCandidateArray shortcutKeys;
  nativeKeyboardEvent->GetShortcutKeyCandidates(shortcutKeys);

  if (shortcutKeys.IsEmpty()) {
    return WalkHandlersAndExecute(aPurpose, aKeyEvent, 0,
                                  IgnoreModifierState());
  }

  bool foundDisabledHandler = false;
  for (const ShortcutKeyCandidate& key : shortcutKeys) {
    const bool skipIfEarlierHandlerDisabled =
        key.mSkipIfEarlierHandlerDisabled ==
        ShortcutKeyCandidate::SkipIfEarlierHandlerDisabled::Yes;
    if (foundDisabledHandler && skipIfEarlierHandlerDisabled) {
      continue;
    }
    IgnoreModifierState ignoreModifierState;
    ignoreModifierState.mShift =
        key.mShiftState == ShortcutKeyCandidate::ShiftState::Ignorable;
    WalkHandlersResult result = WalkHandlersAndExecute(
        aPurpose, aKeyEvent, key.mCharCode, ignoreModifierState);
    if (result.mMeaningfulHandlerFound) {
      return result;
    }
    // Note that if the candidate should not match if an earlier handler is
    // disabled, the char code of the candidate is a char which may be
    // introduced with different shift state. In this case, we do NOT find a
    // disabled handler which **exactly** matches with the keyboard event.
    // This avoids to override a higher priority handler with a disabled lower
    // priority handler.
    if (!skipIfEarlierHandlerDisabled && !foundDisabledHandler) {
      foundDisabledHandler = result.mDisabledHandlerFound;
    }
  }
  return {};
}

GlobalKeyListener::WalkHandlersResult GlobalKeyListener::WalkHandlersAndExecute(
    Purpose aPurpose, dom::KeyboardEvent* aKeyEvent, uint32_t aCharCode,
    const IgnoreModifierState& aIgnoreModifierState) {
  WidgetKeyboardEvent* widgetKeyboardEvent =
      aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
  if (NS_WARN_IF(!widgetKeyboardEvent)) {
    return {};
  }

  nsAtom* eventType =
      ShortcutKeys::ConvertEventToDOMEventType(widgetKeyboardEvent);

  // Try all of the handlers until we find one that matches the event.
  bool foundDisabledHandler = false;
  for (KeyEventHandler* handler = mHandler; handler;
       handler = handler->GetNextHandler()) {
    bool stopped = aKeyEvent->IsDispatchStopped();
    if (stopped) {
      // The event is finished, don't execute any more handlers
      return {};
    }

    if (aPurpose == Purpose::ExecuteCommand) {
      if (!handler->EventTypeEquals(eventType)) {
        continue;
      }
    } else {
      if (handler->EventTypeEquals(nsGkAtoms::keypress)) {
        // If the handler is a keypress event handler, we also need to check
        // if coming keydown event is a preceding event of reserved key
        // combination because if default action of a keydown event is
        // prevented, following keypress event won't be fired.  However, if
        // following keypress event is reserved, we shouldn't allow web
        // contents to prevent the default of the preceding keydown event.
        if (eventType != nsGkAtoms::keydown &&
            eventType != nsGkAtoms::keypress) {
          continue;
        }
      } else if (!handler->EventTypeEquals(eventType)) {
        // Otherwise, eventType should exactly be matched.
        continue;
      }
    }

    // Check if the keyboard event *may* execute the handler.
    if (!handler->KeyEventMatched(aKeyEvent, aCharCode, aIgnoreModifierState)) {
      continue;  // try the next one
    }

    // Before executing this handler, check that it's not disabled,
    // and that it has something to do (oncommand of the <key> or its
    // <command> is non-empty).
    if (!CanHandle(handler, aPurpose == Purpose::ExecuteCommand)) {
      foundDisabledHandler = true;
      continue;
    }

    if (aPurpose == Purpose::LookForCommand) {
      if (handler->EventTypeEquals(eventType)) {
        WalkHandlersResult result;
        result.mMeaningfulHandlerFound = true;
        result.mReservedHandlerForChromeFound =
            IsReservedKey(widgetKeyboardEvent, handler);
        return result;
      }

      // If the command is reserved and the event is keydown, check also if
      // the handler is for keypress because if following keypress event is
      // reserved, we shouldn't dispatch the event into web contents.
      if (eventType == nsGkAtoms::keydown &&
          handler->EventTypeEquals(nsGkAtoms::keypress)) {
        if (IsReservedKey(widgetKeyboardEvent, handler)) {
          WalkHandlersResult result;
          result.mMeaningfulHandlerFound = true;
          result.mReservedHandlerForChromeFound = true;
          return result;
        }
      }
      // Otherwise, we've not found a handler for the event yet.
      continue;
    }

    nsCOMPtr<dom::EventTarget> target = GetHandlerTarget(handler);

    // XXX Do we execute only one handler even if the handler neither stops
    //     propagation nor prevents default of the event?
    nsresult rv = handler->ExecuteHandler(target, aKeyEvent);
    if (NS_SUCCEEDED(rv)) {
      WalkHandlersResult result;
      result.mMeaningfulHandlerFound = true;
      result.mReservedHandlerForChromeFound =
          IsReservedKey(widgetKeyboardEvent, handler);
      result.mDisabledHandlerFound = (rv == NS_SUCCESS_DOM_NO_OPERATION);
      return result;
    }
  }

#ifdef XP_WIN
  // Windows native applications ignore Windows-Logo key state when checking
  // shortcut keys even if the key is pressed.  Therefore, if there is no
  // shortcut key which exactly matches current modifier state, we should
  // retry to look for a shortcut key without the Windows-Logo key press.
  if (!aIgnoreModifierState.mMeta && widgetKeyboardEvent->IsMeta()) {
    IgnoreModifierState ignoreModifierState(aIgnoreModifierState);
    ignoreModifierState.mMeta = true;
    return WalkHandlersAndExecute(aPurpose, aKeyEvent, aCharCode,
                                  ignoreModifierState);
  }
#endif

  WalkHandlersResult result;
  result.mDisabledHandlerFound = foundDisabledHandler;
  return result;
}

// Checks if aReservedValue is ReservedKey_True and target/doc has keyboard lock
// enabled
static bool KeyboardLockEnabledAndIsReservedKey(ReservedKey aReservedValue,
                                                dom::EventTarget* aTarget) {
  if (aReservedValue == ReservedKey_True) {
    nsINode* node = nsINode::FromEventTarget(aTarget);
    RefPtr<dom::Document> doc = node->AsDocument();
    return doc && doc->HasFullscreenKeyboardLockEnabled();
  }
  return false;
}

bool GlobalKeyListener::IsReservedKey(WidgetKeyboardEvent* aKeyEvent,
                                      KeyEventHandler* aHandler) {
  // If the event is a reply event, it means that we've already sent the event
  // to the remote process.
  if (aKeyEvent->IsHandledInRemoteProcess()) {
    return false;
  }
  ReservedKey reserved = aHandler->GetIsReserved();
  // reserved="true" means that the key is always reserved. reserved="false"
  // means that the key is never reserved. Otherwise, we check site-specific
  // permissions.
  if (reserved == ReservedKey_False) {
    return false;
  }

  // When fullscreen keyboard lock is enabled, reserved shortcuts must be
  // forwarded to content so that web pages can handle them; except the
  // fullscreen-exit shortcut (View:FullScreen), which must always remain
  // reserved so users can always exit fullscreen.
  if (KeyboardLockEnabledAndIsReservedKey(reserved, mTarget)) {
    nsCOMPtr<dom::Element> handlerElement = aHandler->GetHandlerElement();
    nsAutoString command;
    return handlerElement &&
           handlerElement->GetAttr(nsGkAtoms::command, command) &&
           command.EqualsLiteral("View:FullScreen");
  }

  if (reserved != ReservedKey_True &&
      !nsContentUtils::ShouldBlockReservedKeys(aKeyEvent)) {
    return false;
  }

  // Okay, the key handler is reserved, but if the key combination is mapped to
  // an edit command or a selection navigation command, we should not treat it
  // as reserved since user wants to do the mapped thing(s) in editor.
  if (MOZ_UNLIKELY(!aKeyEvent->IsTrusted() || !aKeyEvent->mWidget)) {
    return true;
  }
  widget::InputContext inputContext = aKeyEvent->mWidget->GetInputContext();
  if (!inputContext.mIMEState.IsEditable()) {
    return true;
  }
  return MOZ_UNLIKELY(!aKeyEvent->IsEditCommandsInitialized(
             inputContext.GetNativeKeyBindingsType())) ||
         aKeyEvent
             ->EditCommandsConstRef(inputContext.GetNativeKeyBindingsType())
             .IsEmpty();
}

GlobalKeyListener::WalkHandlersResult GlobalKeyListener::HasHandlerForEvent(
    dom::KeyboardEvent* aEvent) {
  WidgetKeyboardEvent* widgetKeyboardEvent =
      aEvent->WidgetEventPtr()->AsKeyboardEvent();
  if (NS_WARN_IF(!widgetKeyboardEvent) || !widgetKeyboardEvent->IsTrusted()) {
    return {};
  }

  EnsureHandlers();

  if (IsDisabled()) {
    return {};
  }

  return WalkHandlersInternal(Purpose::LookForCommand, aEvent);
}

//
// AttachGlobalKeyHandler
//
// Creates a new key handler and prepares to listen to key events on the given
// event receiver (either a document or an content node). If the receiver is
// content, then extra work needs to be done to hook it up to the document (XXX
// WHY??)
//
void XULKeySetGlobalKeyListener::AttachKeyHandler(
    dom::Element* aElementTarget) {
  // Only attach if we're really in a document
  nsCOMPtr<dom::Document> doc = aElementTarget->GetUncomposedDoc();
  if (!doc) {
    return;
  }

  EventListenerManager* manager = doc->GetOrCreateListenerManager();
  if (!manager) {
    return;
  }

  // the listener already exists, so skip this
  if (aElementTarget->GetProperty(nsGkAtoms::listener)) {
    return;
  }

  // Create the key handler
  RefPtr<XULKeySetGlobalKeyListener> handler =
      new XULKeySetGlobalKeyListener(aElementTarget, doc);

  handler->InstallKeyboardEventListenersTo(manager);

  aElementTarget->SetProperty(nsGkAtoms::listener, handler.forget().take(),
                              nsPropertyTable::SupportsDtorFunc, true);
}

//
// DetachGlobalKeyHandler
//
// Removes a key handler added by AttachKeyHandler.
//
void XULKeySetGlobalKeyListener::DetachKeyHandler(
    dom::Element* aElementTarget) {
  // Only attach if we're really in a document
  nsCOMPtr<dom::Document> doc = aElementTarget->GetUncomposedDoc();
  if (!doc) {
    return;
  }

  EventListenerManager* manager = doc->GetOrCreateListenerManager();
  if (!manager) {
    return;
  }

  nsIDOMEventListener* handler = static_cast<nsIDOMEventListener*>(
      aElementTarget->GetProperty(nsGkAtoms::listener));
  if (!handler) {
    return;
  }

  static_cast<XULKeySetGlobalKeyListener*>(handler)
      ->RemoveKeyboardEventListenersFrom(manager);
  aElementTarget->RemoveProperty(nsGkAtoms::listener);
}

XULKeySetGlobalKeyListener::XULKeySetGlobalKeyListener(
    dom::Element* aElement, dom::EventTarget* aTarget)
    : GlobalKeyListener(aTarget) {
  mWeakPtrForElement = do_GetWeakReference(aElement);
}

dom::Element* XULKeySetGlobalKeyListener::GetElement(bool* aIsDisabled) const {
  RefPtr<dom::Element> element = do_QueryReferent(mWeakPtrForElement);
  if (element && aIsDisabled) {
    *aIsDisabled = element->GetBoolAttr(nsGkAtoms::disabled);
  }
  return element.get();
}

XULKeySetGlobalKeyListener::~XULKeySetGlobalKeyListener() {
  if (mWeakPtrForElement) {
    delete mHandler;
  }
}

void XULKeySetGlobalKeyListener::EnsureHandlers() {
  if (mHandler) {
    return;
  }

  dom::Element* element = GetElement();
  if (!element) {
    return;
  }

  BuildHandlerChain(element, &mHandler);
}

bool XULKeySetGlobalKeyListener::IsDisabled() const {
  bool isDisabled;
  dom::Element* element = GetElement(&isDisabled);
  return element && isDisabled;
}

bool XULKeySetGlobalKeyListener::GetElementForHandler(
    KeyEventHandler* aHandler, dom::Element** aElementForHandler) const {
  MOZ_ASSERT(aElementForHandler);
  *aElementForHandler = nullptr;

  RefPtr<dom::Element> keyElement = aHandler->GetHandlerElement();
  if (!keyElement) {
    // This should only be the case where the <key> element that generated the
    // handler has been destroyed. Not sure why we return true here...
    return true;
  }

  nsCOMPtr<dom::Element> chromeHandlerElement = GetElement();
  if (!chromeHandlerElement) {
    NS_WARNING_ASSERTION(keyElement->IsInUncomposedDoc(), "uncomposed");
    keyElement.swap(*aElementForHandler);
    return true;
  }

  // We are in a XUL doc.  Obtain our command attribute.
  nsAutoString command;
  keyElement->GetAttr(nsGkAtoms::command, command);
  if (command.IsEmpty()) {
    // There is no command element associated with the key element.
    NS_WARNING_ASSERTION(keyElement->IsInUncomposedDoc(), "uncomposed");
    keyElement.swap(*aElementForHandler);
    return true;
  }

  // XXX Shouldn't we check this earlier?
  dom::Document* doc = keyElement->GetUncomposedDoc();
  if (NS_WARN_IF(!doc)) {
    return false;
  }

  nsCOMPtr<dom::Element> commandElement = doc->GetElementById(command);
  if (!commandElement) {
    NS_ERROR(
        "A XUL <key> is observing a command that doesn't exist. "
        "Unable to execute key binding!");
    return false;
  }

  commandElement.swap(*aElementForHandler);
  return true;
}

bool XULKeySetGlobalKeyListener::IsExecutableElement(
    dom::Element* aElement) const {
  if (!aElement) {
    return false;
  }

  if (aElement->GetBoolAttr(nsGkAtoms::disabled)) {
    return false;
  }

  // Internal keys are defined as <key> elements so that the menu label
  // and disabled state can be updated properly, but the command is executed
  // by some other means. This will typically be because the key is defined
  // as a shortcut defined in ShortcutKeyDefinitions.cpp instead, or on Mac,
  // some special system defined keys.
  return !aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::internal,
                                nsGkAtoms::_true, eCaseMatters);
}

already_AddRefed<dom::EventTarget> XULKeySetGlobalKeyListener::GetHandlerTarget(
    KeyEventHandler* aHandler) {
  nsCOMPtr<dom::Element> commandElement;
  if (!GetElementForHandler(aHandler, getter_AddRefs(commandElement))) {
    return nullptr;
  }

  return commandElement.forget();
}

bool XULKeySetGlobalKeyListener::CanHandle(KeyEventHandler* aHandler,
                                           bool aWillExecute) const {
  // If the <key> element itself is disabled, ignore it.
  if (aHandler->KeyElementIsDisabled()) {
    return false;
  }

  nsCOMPtr<dom::Element> commandElement;
  if (!GetElementForHandler(aHandler, getter_AddRefs(commandElement))) {
    return false;
  }

  // The only case where commandElement can be null here is where the <key>
  // element for the handler is already destroyed. I'm not sure why we continue
  // in this case.
  if (!commandElement) {
    return true;
  }

  // If we're not actually going to execute here bypass the execution check.
  return !aWillExecute || IsExecutableElement(commandElement);
}

/* static */
layers::KeyboardMap RootWindowGlobalKeyListener::CollectKeyboardShortcuts() {
  KeyEventHandler* handlers = ShortcutKeys::GetHandlers(HandlerType::eBrowser);

  // Convert the handlers into keyboard shortcuts, using an AutoTArray with
  // the maximum amount of shortcuts used on any platform to minimize
  // allocations
  AutoTArray<KeyboardShortcut, 48> shortcuts;

  // Append keyboard shortcuts for hardcoded actions like tab
  KeyboardShortcut::AppendHardcodedShortcuts(shortcuts);

  for (KeyEventHandler* handler = handlers; handler;
       handler = handler->GetNextHandler()) {
    KeyboardShortcut shortcut;
    if (handler->TryConvertToKeyboardShortcut(&shortcut)) {
      shortcuts.AppendElement(shortcut);
    }
  }

  return layers::KeyboardMap(std::move(shortcuts));
}

//
// AttachGlobalKeyHandler
//
// Creates a new key handler and prepares to listen to key events on the given
// event receiver (either a document or an content node). If the receiver is
// content, then extra work needs to be done to hook it up to the document (XXX
// WHY??)
//
void RootWindowGlobalKeyListener::AttachKeyHandler(dom::EventTarget* aTarget) {
  EventListenerManager* manager = aTarget->GetOrCreateListenerManager();
  if (!manager) {
    return;
  }

  // Create the key handler
  RefPtr<RootWindowGlobalKeyListener> handler =
      new RootWindowGlobalKeyListener(aTarget);

  // This registers handler with the manager so the manager will keep handler
  // alive past this point.
  handler->InstallKeyboardEventListenersTo(manager);
}

RootWindowGlobalKeyListener::RootWindowGlobalKeyListener(
    dom::EventTarget* aTarget)
    : GlobalKeyListener(aTarget) {}

/* static */
bool RootWindowGlobalKeyListener::IsHTMLEditorFocused() {
  nsFocusManager* fm = nsFocusManager::GetFocusManager();
  if (!fm) {
    return false;
  }

  nsCOMPtr<mozIDOMWindowProxy> focusedWindow;
  fm->GetFocusedWindow(getter_AddRefs(focusedWindow));
  if (!focusedWindow) {
    return false;
  }

  auto* piwin = nsPIDOMWindowOuter::From(focusedWindow);
  nsIDocShell* docShell = piwin->GetDocShell();
  if (!docShell) {
    return false;
  }

  HTMLEditor* htmlEditor = docShell->GetHTMLEditor();
  if (!htmlEditor) {
    return false;
  }

  if (htmlEditor->IsInDesignMode()) {
    // Don't need to perform any checks in designMode documents.
    return true;
  }

  nsINode* focusedNode = fm->GetFocusedElement();
  if (focusedNode && focusedNode->IsElement()) {
    // If there is a focused element, make sure it's in the active editing host.
    // Note that ComputeEditingHost finds the current editing host based on
    // the document's selection.  Even though the document selection is usually
    // collapsed to where the focus is, but the page may modify the selection
    // without our knowledge, in which case this check will do something useful.
    dom::Element* editingHost = htmlEditor->ComputeEditingHost();
    if (!editingHost) {
      return false;
    }
    return focusedNode->IsInclusiveDescendantOf(editingHost);
  }

  return false;
}

void RootWindowGlobalKeyListener::EnsureHandlers() {
  if (IsHTMLEditorFocused()) {
    mHandler = ShortcutKeys::GetHandlers(HandlerType::eEditor);
  } else {
    mHandler = ShortcutKeys::GetHandlers(HandlerType::eBrowser);
  }
}

}  // namespace mozilla
