/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "WinIMEHandler.h"

#include "IMMHandler.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_intl.h"
#include "mozilla/TextEvents.h"
#include "mozilla/WindowsVersion.h"
#include "nsWindowDefs.h"
#include "WinTextEventDispatcherListener.h"

#include "TSFTextStore.h"

#include "OSKInputPaneManager.h"
#include "OSKTabTipManager.h"
#include "OSKVRManager.h"
#include "nsLookAndFeel.h"
#include "nsWindow.h"
#include "WinUtils.h"
#include "nsIWindowsRegKey.h"
#include "WindowsUIUtils.h"

#ifdef ACCESSIBILITY
#  include "nsAccessibilityService.h"
#endif  // #ifdef ACCESSIBILITY

#include "shellapi.h"
#include "shlobj.h"
#include "powrprof.h"
#include "setupapi.h"
#include "cfgmgr32.h"

#include "FxRWindowManager.h"
#include "moz_external_vr.h"

const char* kOskEnabled = "ui.osk.enabled";
const char* kOskDetectPhysicalKeyboard = "ui.osk.detect_physical_keyboard";
const char* kOskRequireWin10 = "ui.osk.require_win10";
const char* kOskDebugReason = "ui.osk.debug.keyboardDisplayReason";

namespace mozilla {
namespace widget {

/******************************************************************************
 * IMEHandler
 ******************************************************************************/

nsWindow* IMEHandler::sFocusedWindow = nullptr;
InputContextAction::Cause IMEHandler::sLastContextActionCause =
    InputContextAction::CAUSE_UNKNOWN;
bool IMEHandler::sMaybeEditable = false;
bool IMEHandler::sForceDisableCurrentIMM_IME = false;
bool IMEHandler::sNativeCaretIsCreated = false;
bool IMEHandler::sHasNativeCaretBeenRequested = false;

bool IMEHandler::sIsInTSFMode = false;
bool IMEHandler::sIsIMMEnabled = true;
decltype(SetInputScopes)* IMEHandler::sSetInputScopes = nullptr;

static POWER_PLATFORM_ROLE sPowerPlatformRole = PlatformRoleUnspecified;
static bool sDeterminedPowerPlatformRole = false;

// static
void IMEHandler::Initialize() {
  TSFTextStore::Initialize();
  sIsInTSFMode = TSFTextStore::IsInTSFMode();
  sIsIMMEnabled =
      !sIsInTSFMode || StaticPrefs::intl_tsf_support_imm_AtStartup();
  if (!sIsInTSFMode) {
    // When full TSFTextStore is not available, try to use SetInputScopes API
    // to enable at least InputScope. Use GET_MODULE_HANDLE_EX_FLAG_PIN to
    // ensure that msctf.dll will not be unloaded.
    HMODULE module = nullptr;
    if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN, L"msctf.dll",
                           &module)) {
      sSetInputScopes = reinterpret_cast<decltype(SetInputScopes)*>(
          GetProcAddress(module, "SetInputScopes"));
    }
  }

  IMMHandler::Initialize();

  sForceDisableCurrentIMM_IME = IMMHandler::IsActiveIMEInBlockList();
}

// static
void IMEHandler::Terminate() {
  if (sIsInTSFMode) {
    TSFTextStore::Terminate();
    sIsInTSFMode = false;
  }

  IMMHandler::Terminate();
  WinTextEventDispatcherListener::Shutdown();
}

// static
void* IMEHandler::GetNativeData(nsWindow* aWindow, uint32_t aDataType) {
  if (aDataType == NS_RAW_NATIVE_IME_CONTEXT) {
    if (IsTSFAvailable()) {
      return TSFTextStore::GetThreadManager();
    }
    IMEContext context(aWindow);
    if (context.IsValid()) {
      return context.get();
    }
    // If IMC isn't associated with the window, IME is disabled on the window
    // now.  In such case, we should return default IMC instead.
    const IMEContext& defaultIMC = aWindow->DefaultIMC();
    if (defaultIMC.IsValid()) {
      return defaultIMC.get();
    }
    // If there is no default IMC, we should return the pointer to the window
    // since if we return nullptr, IMEStateManager cannot manage composition
    // with TextComposition instance.  This is possible if no IME is installed,
    // but composition may occur with dead key sequence.
    return aWindow;
  }

  void* result = TSFTextStore::GetNativeData(aDataType);
  if (!result || !(*(static_cast<void**>(result)))) {
    return nullptr;
  }
  // XXX During the TSF module test, sIsInTSFMode must be true.  After that,
  //     the value should be restored but currently, there is no way for that.
  //     When the TSF test is enabled again, we need to fix this.  Perhaps,
  //     sending a message can fix this.
  sIsInTSFMode = true;
  return result;
}

// static
bool IMEHandler::ProcessRawKeyMessage(const MSG& aMsg) {
  if (IsTSFAvailable()) {
    return TSFTextStore::ProcessRawKeyMessage(aMsg);
  }
  return false;  // noting to do in IMM mode.
}

// static
bool IMEHandler::ProcessMessage(nsWindow* aWindow, UINT aMessage,
                                WPARAM& aWParam, LPARAM& aLParam,
                                MSGResult& aResult) {
  // If we're putting native caret over our caret, Windows dispatches
  // EVENT_OBJECT_LOCATIONCHANGE event on other applications which hook
  // the event with ::SetWinEventHook() and handles WM_GETOBJECT for
  // OBJID_CARET (this is request of caret from such applications) instead
  // of us.  If a11y module is active, it observes every our caret change
  // and put native caret over it automatically.  However, if other
  // applications require only caret information, activating a11y module is
  // overwork and such applications may requires carets only in editors.
  // Therefore, if it'd be possible, IMEHandler should put native caret over
  // our caret, but there is a problem.  Some versions of ATOK (Japanese TIP)
  // refer native caret and if there is, the behavior is worse than the
  // behavior without native caret.  Therefore, we shouldn't put native caret
  // as far as possible.
  if (!sHasNativeCaretBeenRequested && aMessage == WM_GETOBJECT &&
      static_cast<LONG>(aLParam) == OBJID_CARET) {
    // So, when we receive first WM_GETOBJECT for OBJID_CARET, let's start to
    // create native caret for such applications.
    sHasNativeCaretBeenRequested = true;
    // If an editable element has focus, we can put native caret now.
    // XXX Should we avoid doing this if there is composition?
    MaybeCreateNativeCaret(aWindow);
  }

  if (IsTSFAvailable()) {
    TSFTextStore::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult);
    if (aResult.mConsumed) {
      return true;
    }
    // If we don't support IMM in TSF mode, we don't use IMMHandler.
    if (!sIsIMMEnabled) {
      return false;
    }
    // IME isn't implemented with IMM, IMMHandler shouldn't handle any
    // messages.
    if (!IsIMMActive()) {
      return false;
    }
  }

  bool keepGoing =
      IMMHandler::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult);

  // If user changes active IME to an IME which is listed in our block list,
  // we should disassociate IMC from the window for preventing the IME to work
  // and crash.
  if (aMessage == WM_INPUTLANGCHANGE) {
    bool disableIME = IMMHandler::IsActiveIMEInBlockList();
    if (disableIME != sForceDisableCurrentIMM_IME) {
      bool enable =
          !disableIME && WinUtils::IsIMEEnabled(aWindow->InputContextRef());
      AssociateIMEContext(aWindow, enable);
      sForceDisableCurrentIMM_IME = disableIME;
    }
  }

  return keepGoing;
}

// static
bool IMEHandler::IsA11yHandlingNativeCaret() {
#ifndef ACCESSIBILITY
  return false;
#else   // #ifndef ACCESSIBILITY
  // Let's assume that when there is the service, it handles native caret.
  return GetAccService() != nullptr;
#endif  // #ifndef ACCESSIBILITY #else
}

// static
bool IMEHandler::IsIMMActive() { return TSFTextStore::IsIMM_IMEActive(); }

// static
bool IMEHandler::IsComposing() {
  if (IsTSFAvailable()) {
    return TSFTextStore::IsComposing() || IMMHandler::IsComposing();
  }

  return IMMHandler::IsComposing();
}

// static
bool IMEHandler::IsComposingOn(nsWindow* aWindow) {
  if (IsTSFAvailable()) {
    return TSFTextStore::IsComposingOn(aWindow) ||
           IMMHandler::IsComposingOn(aWindow);
  }

  return IMMHandler::IsComposingOn(aWindow);
}

// static
nsresult IMEHandler::NotifyIME(nsWindow* aWindow,
                               const IMENotification& aIMENotification) {
  if (IsTSFAvailable()) {
    switch (aIMENotification.mMessage) {
      case NOTIFY_IME_OF_SELECTION_CHANGE: {
        nsresult rv = TSFTextStore::OnSelectionChange(aIMENotification);
        // If IMM IME is active, we need to notify IMMHandler of updating
        // composition change.  It will adjust candidate window position or
        // composition window position.
        bool isIMMActive = IsIMMActive();
        if (isIMMActive) {
          IMMHandler::OnUpdateComposition(aWindow);
        }
        IMMHandler::OnSelectionChange(aWindow, aIMENotification, isIMMActive);
        return rv;
      }
      case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
        // If IMM IME is active, we need to notify IMMHandler of updating
        // composition change.  It will adjust candidate window position or
        // composition window position.
        if (IsIMMActive()) {
          IMMHandler::OnUpdateComposition(aWindow);
        } else {
          TSFTextStore::OnUpdateComposition();
        }
        return NS_OK;
      case NOTIFY_IME_OF_TEXT_CHANGE:
        return TSFTextStore::OnTextChange(aIMENotification);
      case NOTIFY_IME_OF_FOCUS: {
        sFocusedWindow = aWindow;
        IMMHandler::OnFocusChange(true, aWindow);
        nsresult rv = TSFTextStore::OnFocusChange(true, aWindow,
                                                  aWindow->GetInputContext());
        MaybeCreateNativeCaret(aWindow);
        IMEHandler::MaybeShowOnScreenKeyboard(aWindow,
                                              aWindow->GetInputContext());
        return rv;
      }
      case NOTIFY_IME_OF_BLUR:
        sFocusedWindow = nullptr;
        IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
        IMMHandler::OnFocusChange(false, aWindow);
        return TSFTextStore::OnFocusChange(false, aWindow,
                                           aWindow->GetInputContext());
      case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
        // If IMM IME is active, we should send a mouse button event via IMM.
        if (IsIMMActive()) {
          return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification);
        }
        return TSFTextStore::OnMouseButtonEvent(aIMENotification);
      case REQUEST_TO_COMMIT_COMPOSITION:
        if (TSFTextStore::IsComposingOn(aWindow)) {
          TSFTextStore::CommitComposition(false);
        } else if (IsIMMActive()) {
          IMMHandler::CommitComposition(aWindow);
        }
        return NS_OK;
      case REQUEST_TO_CANCEL_COMPOSITION:
        if (TSFTextStore::IsComposingOn(aWindow)) {
          TSFTextStore::CommitComposition(true);
        } else if (IsIMMActive()) {
          IMMHandler::CancelComposition(aWindow);
        }
        return NS_OK;
      case NOTIFY_IME_OF_POSITION_CHANGE:
        return TSFTextStore::OnLayoutChange();
      default:
        return NS_ERROR_NOT_IMPLEMENTED;
    }
  }

  switch (aIMENotification.mMessage) {
    case REQUEST_TO_COMMIT_COMPOSITION:
      IMMHandler::CommitComposition(aWindow);
      return NS_OK;
    case REQUEST_TO_CANCEL_COMPOSITION:
      IMMHandler::CancelComposition(aWindow);
      return NS_OK;
    case NOTIFY_IME_OF_POSITION_CHANGE:
    case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
      IMMHandler::OnUpdateComposition(aWindow);
      return NS_OK;
    case NOTIFY_IME_OF_SELECTION_CHANGE:
      IMMHandler::OnSelectionChange(aWindow, aIMENotification, true);
      // IMMHandler::OnSelectionChange() cannot work without its singleton
      // instance.  Therefore, IMEHandler needs to create native caret instead
      // if it's necessary.
      MaybeCreateNativeCaret(aWindow);
      return NS_OK;
    case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
      return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification);
    case NOTIFY_IME_OF_FOCUS:
      sFocusedWindow = aWindow;
      IMMHandler::OnFocusChange(true, aWindow);
      IMEHandler::MaybeShowOnScreenKeyboard(aWindow,
                                            aWindow->GetInputContext());
      MaybeCreateNativeCaret(aWindow);
      return NS_OK;
    case NOTIFY_IME_OF_BLUR:
      sFocusedWindow = nullptr;
      IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
      IMMHandler::OnFocusChange(false, aWindow);
      // If a plugin gets focus while TSF has focus, we need to notify TSF of
      // the blur.
      if (TSFTextStore::ThinksHavingFocus()) {
        return TSFTextStore::OnFocusChange(false, aWindow,
                                           aWindow->GetInputContext());
      }
      return NS_OK;
    default:
      return NS_ERROR_NOT_IMPLEMENTED;
  }
}

// static
IMENotificationRequests IMEHandler::GetIMENotificationRequests() {
  if (IsTSFAvailable()) {
    if (!sIsIMMEnabled) {
      return TSFTextStore::GetIMENotificationRequests();
    }
    // Even if TSF is available, the active IME may be an IMM-IME.
    // Unfortunately, changing the result of GetIMENotificationRequests() while
    // an editor has focus isn't supported by IMEContentObserver nor
    // ContentCacheInParent.  Therefore, we need to request whole notifications
    // which are necessary either IMMHandler or TSFTextStore.
    return IMMHandler::GetIMENotificationRequests() |
           TSFTextStore::GetIMENotificationRequests();
  }

  return IMMHandler::GetIMENotificationRequests();
}

// static
TextEventDispatcherListener*
IMEHandler::GetNativeTextEventDispatcherListener() {
  return WinTextEventDispatcherListener::GetInstance();
}

// static
bool IMEHandler::GetOpenState(nsWindow* aWindow) {
  if (IsTSFAvailable() && !IsIMMActive()) {
    return TSFTextStore::GetIMEOpenState();
  }

  IMEContext context(aWindow);
  return context.GetOpenState();
}

// static
void IMEHandler::OnDestroyWindow(nsWindow* aWindow) {
  // When focus is in remote process, but the window is being destroyed, we
  // need to clean up TSFTextStore here since NOTIFY_IME_OF_BLUR won't reach
  // here because BrowserParent already lost the reference to the nsWindow when
  // it receives from the remote process.
  if (sFocusedWindow == aWindow) {
    MOZ_ASSERT(aWindow->GetInputContext().IsOriginContentProcess(),
               "input context of focused widget should've been set by a remote "
               "process "
               "if IME focus isn't cleared before destroying the widget");
    NotifyIME(aWindow, IMENotification(NOTIFY_IME_OF_BLUR));
  }

  // We need to do nothing here for TSF. Just restore the default context
  // if it's been disassociated.
  if (!sIsInTSFMode) {
    // MSDN says we need to set IS_DEFAULT to avoid memory leak when we use
    // SetInputScopes API. Use an empty string to do this.
    SetInputScopeForIMM32(aWindow, u""_ns, u""_ns, false);
  }
  AssociateIMEContext(aWindow, true);
}

// static
bool IMEHandler::NeedsToAssociateIMC() { return !sForceDisableCurrentIMM_IME; }

// static
void IMEHandler::SetInputContext(nsWindow* aWindow, InputContext& aInputContext,
                                 const InputContextAction& aAction) {
  sLastContextActionCause = aAction.mCause;
  // FYI: If there is no composition, this call will do nothing.
  NotifyIME(aWindow, IMENotification(REQUEST_TO_COMMIT_COMPOSITION));

  if (aInputContext.mHTMLInputMode.EqualsLiteral("none")) {
    IMEHandler::MaybeDismissOnScreenKeyboard(aWindow, Sync::Yes);
  } else if (aAction.UserMightRequestOpenVKB()) {
    IMEHandler::MaybeShowOnScreenKeyboard(aWindow, aInputContext);
  }

  bool enable = WinUtils::IsIMEEnabled(aInputContext);
  bool adjustOpenState = (enable && aInputContext.mIMEState.mOpen !=
                                        IMEState::DONT_CHANGE_OPEN_STATE);
  bool open =
      (adjustOpenState && aInputContext.mIMEState.mOpen == IMEState::OPEN);

  // Note that even while a plugin has focus, we need to notify TSF of that.
  if (sIsInTSFMode) {
    TSFTextStore::SetInputContext(aWindow, aInputContext, aAction);
    if (IsTSFAvailable()) {
      if (sIsIMMEnabled) {
        // Associate IMC with aWindow only when it's necessary.
        AssociateIMEContext(aWindow, enable && NeedsToAssociateIMC());
      }
      if (adjustOpenState) {
        TSFTextStore::SetIMEOpenState(open);
      }
      return;
    }
  } else {
    // Set at least InputScope even when TextStore is not available.
    SetInputScopeForIMM32(aWindow, aInputContext.mHTMLInputType,
                          aInputContext.mHTMLInputMode,
                          aInputContext.mInPrivateBrowsing);
  }

  AssociateIMEContext(aWindow, enable);

  IMEContext context(aWindow);
  if (adjustOpenState) {
    context.SetOpenState(open);
  }
}

// static
void IMEHandler::AssociateIMEContext(nsWindow* aWindowBase, bool aEnable) {
  IMEContext context(aWindowBase);
  if (aEnable) {
    context.AssociateDefaultContext();
    return;
  }
  // Don't disassociate the context after the window is destroyed.
  if (aWindowBase->Destroyed()) {
    return;
  }
  context.Disassociate();
}

// static
void IMEHandler::InitInputContext(nsWindow* aWindow,
                                  InputContext& aInputContext) {
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->GetWindowHandle(),
             "IMEHandler::SetInputContext() requires non-nullptr HWND");

  static bool sInitialized = false;
  if (!sInitialized) {
    sInitialized = true;
    // Some TIPs like QQ Input (Simplified Chinese) may need normal window
    // (i.e., windows except message window) when initializing themselves.
    // Therefore, we need to initialize TSF/IMM modules after first normal
    // window is created.  InitInputContext() should be called immediately
    // after creating each normal window, so, here is a good place to
    // initialize these modules.
    Initialize();
  }

  // For a11y, the default enabled state should be 'enabled'.
  aInputContext.mIMEState.mEnabled = IMEEnabled::Enabled;

  if (sIsInTSFMode) {
    TSFTextStore::SetInputContext(
        aWindow, aInputContext,
        InputContextAction(InputContextAction::CAUSE_UNKNOWN,
                           InputContextAction::WIDGET_CREATED));
    // IME context isn't necessary in pure TSF mode.
    if (!sIsIMMEnabled) {
      AssociateIMEContext(aWindow, false);
    }
    return;
  }

#ifdef DEBUG
  // NOTE: IMC may be null if IMM module isn't installed.
  IMEContext context(aWindow);
  MOZ_ASSERT(context.IsValid() || !CurrentKeyboardLayoutHasIME());
#endif  // #ifdef DEBUG
}

#ifdef DEBUG
// static
bool IMEHandler::CurrentKeyboardLayoutHasIME() {
  if (sIsInTSFMode) {
    return TSFTextStore::CurrentKeyboardLayoutHasIME();
  }

  return IMMHandler::IsIMEAvailable();
}
#endif  // #ifdef DEBUG

// static
void IMEHandler::OnKeyboardLayoutChanged() {
  // Be aware, this method won't be called until TSFStaticSink starts to
  // observe active TIP change.  If you need to be notified of this, you
  // need to create TSFStaticSink::Observe() or something and call it
  // TSFStaticSink::EnsureInitActiveTIPKeyboard() forcibly.

  if (!sIsIMMEnabled || !IsTSFAvailable()) {
    return;
  }
}

// static
void IMEHandler::SetInputScopeForIMM32(nsWindow* aWindow,
                                       const nsAString& aHTMLInputType,
                                       const nsAString& aHTMLInputMode,
                                       bool aInPrivateBrowsing) {
  if (sIsInTSFMode || !sSetInputScopes || aWindow->Destroyed()) {
    return;
  }
  AutoTArray<InputScope, 3> scopes;

  // IME may refer only first input scope, but we will append inputmode's
  // input scopes since IME may refer it like Chrome.
  AppendInputScopeFromType(aHTMLInputType, scopes);
  AppendInputScopeFromInputMode(aHTMLInputMode, scopes);

  if (aInPrivateBrowsing) {
    scopes.AppendElement(IS_PRIVATE);
  }

  if (scopes.IsEmpty()) {
    // At least, 1 item is necessary.
    scopes.AppendElement(IS_DEFAULT);
  }

  sSetInputScopes(aWindow->GetWindowHandle(), scopes.Elements(),
                  scopes.Length(), nullptr, 0, nullptr, nullptr);
}

// static
void IMEHandler::AppendInputScopeFromInputMode(const nsAString& aHTMLInputMode,
                                               nsTArray<InputScope>& aScopes) {
  if (aHTMLInputMode.EqualsLiteral("mozAwesomebar")) {
    // Even if Awesomebar has focus, user may not input URL directly.
    // However, on-screen keyboard for URL should be shown because it has
    // some useful additional keys like ".com" and they are not hindrances
    // even when inputting non-URL text, e.g., words to search something in
    // the web.  On the other hand, a lot of Microsoft's IMEs and Google
    // Japanese Input make their open state "closed" automatically if we
    // notify them of URL as the input scope.  However, this is very annoying
    // for the users when they try to input some words to search the web or
    // bookmark/history items.  Therefore, if they are active, we need to
    // notify them of the default input scope for avoiding this issue.
    // FYI: We cannot check active TIP without TSF.  Therefore, if it's
    //      not in TSF mode, this will check only if active IMM-IME is Google
    //      Japanese Input.  Google Japanese Input is a TIP of TSF basically.
    //      However, if the OS is Win7 or it's installed on Win7 but has not
    //      been updated yet even after the OS is upgraded to Win8 or later,
    //      it's installed as IMM-IME.
    if (TSFTextStore::ShouldSetInputScopeOfURLBarToDefault()) {
      return;
    }
    // Don't append IS_SEARCH here for showing on-screen keyboard for URL.
    if (!aScopes.Contains(IS_URL)) {
      aScopes.AppendElement(IS_URL);
    }
    return;
  }

  // https://html.spec.whatwg.org/dev/interaction.html#attr-inputmode
  if (aHTMLInputMode.EqualsLiteral("url")) {
    if (!aScopes.Contains(IS_SEARCH)) {
      aScopes.AppendElement(IS_URL);
    }
    return;
  }
  if (aHTMLInputMode.EqualsLiteral("email")) {
    if (!aScopes.Contains(IS_EMAIL_SMTPEMAILADDRESS)) {
      aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS);
    }
    return;
  }
  if (aHTMLInputMode.EqualsLiteral("tel")) {
    if (!aScopes.Contains(IS_TELEPHONE_FULLTELEPHONENUMBER)) {
      aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER);
    }
    if (!aScopes.Contains(IS_TELEPHONE_LOCALNUMBER)) {
      aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER);
    }
    return;
  }
  if (aHTMLInputMode.EqualsLiteral("numeric")) {
    if (!aScopes.Contains(IS_DIGITS)) {
      aScopes.AppendElement(IS_DIGITS);
    }
    return;
  }
  if (aHTMLInputMode.EqualsLiteral("decimal")) {
    if (!aScopes.Contains(IS_NUMBER)) {
      aScopes.AppendElement(IS_NUMBER);
    }
    return;
  }
  if (aHTMLInputMode.EqualsLiteral("search")) {
    if (NeedsSearchInputScope() && !aScopes.Contains(IS_SEARCH)) {
      aScopes.AppendElement(IS_SEARCH);
    }
    return;
  }
}

// static
void IMEHandler::AppendInputScopeFromType(const nsAString& aHTMLInputType,
                                          nsTArray<InputScope>& aScopes) {
  // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html
  if (aHTMLInputType.EqualsLiteral("url")) {
    aScopes.AppendElement(IS_URL);
    return;
  }
  if (aHTMLInputType.EqualsLiteral("search")) {
    if (NeedsSearchInputScope()) {
      aScopes.AppendElement(IS_SEARCH);
    }
    return;
  }
  if (aHTMLInputType.EqualsLiteral("email")) {
    aScopes.AppendElement(IS_EMAIL_SMTPEMAILADDRESS);
    return;
  }
  if (aHTMLInputType.EqualsLiteral("password")) {
    aScopes.AppendElement(IS_PASSWORD);
    return;
  }
  if (aHTMLInputType.EqualsLiteral("datetime") ||
      aHTMLInputType.EqualsLiteral("datetime-local")) {
    aScopes.AppendElement(IS_DATE_FULLDATE);
    aScopes.AppendElement(IS_TIME_FULLTIME);
    return;
  }
  if (aHTMLInputType.EqualsLiteral("date") ||
      aHTMLInputType.EqualsLiteral("month") ||
      aHTMLInputType.EqualsLiteral("week")) {
    aScopes.AppendElement(IS_DATE_FULLDATE);
    return;
  }
  if (aHTMLInputType.EqualsLiteral("time")) {
    aScopes.AppendElement(IS_TIME_FULLTIME);
    return;
  }
  if (aHTMLInputType.EqualsLiteral("tel")) {
    aScopes.AppendElement(IS_TELEPHONE_FULLTELEPHONENUMBER);
    aScopes.AppendElement(IS_TELEPHONE_LOCALNUMBER);
    return;
  }
  if (aHTMLInputType.EqualsLiteral("number")) {
    aScopes.AppendElement(IS_NUMBER);
    return;
  }
}

// static
bool IMEHandler::NeedsSearchInputScope() {
  return !StaticPrefs::intl_tsf_hack_atok_search_input_scope_disabled() ||
         !TSFTextStore::IsATOKActive();
}

// static
bool IMEHandler::IsOnScreenKeyboardSupported() {
#ifdef NIGHTLY_BUILD
  if (FxRWindowManager::GetInstance()->IsFxRWindow(sFocusedWindow)) {
    return true;
  }
#endif  // NIGHTLY_BUILD
  if (!IsWin8OrLater() || !Preferences::GetBool(kOskEnabled, true) ||
      !IMEHandler::NeedOnScreenKeyboard()) {
    return false;
  }

  // On Windows 10 we require tablet mode, unless the user has set the relevant
  // Windows setting to enable the on-screen keyboard in desktop mode.
  // We might be disabled specifically on Win8(.1), so we check that afterwards.
  if (IsWin10OrLater() && !IsWin11OrLater()) {
    if (!IsInTabletMode() && !AutoInvokeOnScreenKeyboardInDesktopMode()) {
      return false;
    }
  } else if (Preferences::GetBool(kOskRequireWin10, true)) {
    return false;
  }

  return true;
}

// static
void IMEHandler::MaybeShowOnScreenKeyboard(nsWindow* aWindow,
                                           const InputContext& aInputContext) {
  if (aInputContext.mHTMLInputMode.EqualsLiteral("none")) {
    return;
  }

  if (!IsOnScreenKeyboardSupported()) {
    return;
  }

  IMEHandler::ShowOnScreenKeyboard(aWindow);
}

// static
void IMEHandler::MaybeDismissOnScreenKeyboard(nsWindow* aWindow, Sync aSync) {
#ifdef NIGHTLY_BUILD
  if (FxRWindowManager::GetInstance()->IsFxRWindow(aWindow)) {
    OSKVRManager::DismissOnScreenKeyboard();
  }
#endif  // NIGHTLY_BUILD
  if (!IsWin8OrLater()) {
    return;
  }

  if (aSync == Sync::Yes) {
    DismissOnScreenKeyboard(aWindow);
    return;
  }

  RefPtr<nsWindow> window(aWindow);
  NS_DispatchToCurrentThreadQueue(
      NS_NewRunnableFunction("IMEHandler::MaybeDismissOnScreenKeyboard",
                             [window]() {
                               if (window->Destroyed()) {
                                 return;
                               }
                               if (!sFocusedWindow) {
                                 DismissOnScreenKeyboard(window);
                               }
                             }),
      EventQueuePriority::Idle);
}

// static
bool IMEHandler::WStringStartsWithCaseInsensitive(const std::wstring& aHaystack,
                                                  const std::wstring& aNeedle) {
  std::wstring lowerCaseHaystack(aHaystack);
  std::wstring lowerCaseNeedle(aNeedle);
  std::transform(lowerCaseHaystack.begin(), lowerCaseHaystack.end(),
                 lowerCaseHaystack.begin(), ::tolower);
  std::transform(lowerCaseNeedle.begin(), lowerCaseNeedle.end(),
                 lowerCaseNeedle.begin(), ::tolower);
  return wcsstr(lowerCaseHaystack.c_str(), lowerCaseNeedle.c_str()) ==
         lowerCaseHaystack.c_str();
}

// Returns false if a physical keyboard is detected on Windows 8 and up,
// or there is some other reason why an onscreen keyboard is not necessary.
// Returns true if no keyboard is found and this device looks like it needs
// an on-screen keyboard for text input.
// static
bool IMEHandler::NeedOnScreenKeyboard() {
  // This function is only supported for Windows 8 and up.
  if (!IsWin8OrLater()) {
    Preferences::SetString(kOskDebugReason, L"IKPOS: Requires Win8+.");
    return false;
  }

  if (!Preferences::GetBool(kOskDetectPhysicalKeyboard, true)) {
    Preferences::SetString(kOskDebugReason, L"IKPOS: Detection disabled.");
    return true;
  }

  // If the last focus cause was not user-initiated (ie a result of code
  // setting focus to an element) then don't auto-show a keyboard. This
  // avoids cases where the keyboard would pop up "just" because e.g. a
  // web page chooses to focus a search field on the page, even when that
  // really isn't what the user is trying to do at that moment.
  if (!InputContextAction::IsHandlingUserInput(sLastContextActionCause)) {
    return false;
  }

  // This function should be only invoked for machines with touch screens.
  if ((::GetSystemMetrics(SM_DIGITIZER) & NID_INTEGRATED_TOUCH) !=
      NID_INTEGRATED_TOUCH) {
    Preferences::SetString(kOskDebugReason, L"IKPOS: Touch screen not found.");
    return false;
  }

  // If the device is docked, the user is treating the device as a PC.
  if (::GetSystemMetrics(SM_SYSTEMDOCKED) != 0) {
    Preferences::SetString(kOskDebugReason, L"IKPOS: System docked.");
    return false;
  }

  // To determine whether a keyboard is present on the device, we do the
  // following:-
  // 1. If the platform role is that of a mobile or slate device, check the
  //    system metric SM_CONVERTIBLESLATEMODE to see if it is being used
  //    in slate mode. If it is, also check that the last input was a touch.
  //    If all of this is true, then we should show the on-screen keyboard.

  // 2. If step 1 didn't determine we should show the keyboard, we check if
  //    this device has keyboards attached to it.

  // Check if the device is being used as a laptop or a tablet. This can be
  // checked by first checking the role of the device and then the
  // corresponding system metric (SM_CONVERTIBLESLATEMODE). If it is being
  // used as a tablet then we want the OSK to show up.
  if (!sDeterminedPowerPlatformRole) {
    sDeterminedPowerPlatformRole = true;
    sPowerPlatformRole = WinUtils::GetPowerPlatformRole();
  }

  // If this a mobile or slate (tablet) device, check if it is in slate mode.
  // If the last input was touch, ignore whether or not a keyboard is present.
  if ((sPowerPlatformRole == PlatformRoleMobile ||
       sPowerPlatformRole == PlatformRoleSlate) &&
      ::GetSystemMetrics(SM_CONVERTIBLESLATEMODE) == 0 &&
      sLastContextActionCause == InputContextAction::CAUSE_TOUCH) {
    Preferences::SetString(
        kOskDebugReason,
        L"IKPOS: Mobile/Slate Platform role, in slate mode with touch event.");
    return true;
  }

  return !IMEHandler::IsKeyboardPresentOnSlate();
}

// Uses the Setup APIs to enumerate the attached keyboards and returns true
// if the keyboard count is 1 or more. While this will work in most cases
// it won't work if there are devices which expose keyboard interfaces which
// are attached to the machine.
// Based on IsKeyboardPresentOnSlate() in Chromium's base/win/win_util.cc.
// static
bool IMEHandler::IsKeyboardPresentOnSlate() {
  const GUID KEYBOARD_CLASS_GUID = {
      0x4D36E96B,
      0xE325,
      0x11CE,
      {0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18}};

  // Query for all the keyboard devices.
  HDEVINFO device_info = ::SetupDiGetClassDevs(&KEYBOARD_CLASS_GUID, nullptr,
                                               nullptr, DIGCF_PRESENT);
  if (device_info == INVALID_HANDLE_VALUE) {
    Preferences::SetString(kOskDebugReason, L"IKPOS: No keyboard info.");
    return false;
  }

  // Enumerate all keyboards and look for ACPI\PNP and HID\VID devices. If
  // the count is more than 1 we assume that a keyboard is present. This is
  // under the assumption that there will always be one keyboard device.
  for (DWORD i = 0;; ++i) {
    SP_DEVINFO_DATA device_info_data = {0};
    device_info_data.cbSize = sizeof(device_info_data);
    if (!::SetupDiEnumDeviceInfo(device_info, i, &device_info_data)) {
      break;
    }

    // Get the device ID.
    wchar_t device_id[MAX_DEVICE_ID_LEN];
    CONFIGRET status = ::CM_Get_Device_ID(device_info_data.DevInst, device_id,
                                          MAX_DEVICE_ID_LEN, 0);
    if (status == CR_SUCCESS) {
      static const std::wstring BT_HID_DEVICE = L"HID\\{00001124";
      static const std::wstring BT_HOGP_DEVICE = L"HID\\{00001812";
      // To reduce the scope of the hack we only look for ACPI and HID\\VID
      // prefixes in the keyboard device ids.
      if (IMEHandler::WStringStartsWithCaseInsensitive(device_id, L"ACPI") ||
          IMEHandler::WStringStartsWithCaseInsensitive(device_id,
                                                       L"HID\\VID") ||
          IMEHandler::WStringStartsWithCaseInsensitive(device_id,
                                                       BT_HID_DEVICE) ||
          IMEHandler::WStringStartsWithCaseInsensitive(device_id,
                                                       BT_HOGP_DEVICE)) {
        // The heuristic we are using is to check the count of keyboards and
        // return true if the API's report one or more keyboards. Please note
        // that this will break for non keyboard devices which expose a
        // keyboard PDO.
        Preferences::SetString(kOskDebugReason,
                               L"IKPOS: Keyboard presence confirmed.");
        return true;
      }
    }
  }
  Preferences::SetString(kOskDebugReason,
                         L"IKPOS: Lack of keyboard confirmed.");
  return false;
}

// static
bool IMEHandler::IsInTabletMode() {
  bool isInTabletMode = WindowsUIUtils::GetInTabletMode();
  if (isInTabletMode) {
    Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=true.");
  } else {
    Preferences::SetString(kOskDebugReason, L"IITM: GetInTabletMode=false.");
  }
  return isInTabletMode;
}

static bool ReadEnableDesktopModeAutoInvoke(uint32_t aRoot,
                                            nsIWindowsRegKey* aRegKey,
                                            uint32_t& aValue) {
  nsresult rv;
  rv = aRegKey->Open(aRoot, u"SOFTWARE\\Microsoft\\TabletTip\\1.7"_ns,
                     nsIWindowsRegKey::ACCESS_QUERY_VALUE);
  if (NS_FAILED(rv)) {
    Preferences::SetString(kOskDebugReason,
                           L"AIOSKIDM: failed opening regkey.");
    return false;
  }
  // EnableDesktopModeAutoInvoke is an opt-in option from the Windows
  // Settings to "Automatically show the touch keyboard in windowed apps
  // when there's no keyboard attached to your device." If the user has
  // opted-in to this behavior, the tablet-mode requirement is skipped.
  rv = aRegKey->ReadIntValue(u"EnableDesktopModeAutoInvoke"_ns, &aValue);
  if (NS_FAILED(rv)) {
    Preferences::SetString(kOskDebugReason,
                           L"AIOSKIDM: failed reading value of regkey.");
    return false;
  }
  return true;
}

// static
bool IMEHandler::AutoInvokeOnScreenKeyboardInDesktopMode() {
  nsresult rv;
  nsCOMPtr<nsIWindowsRegKey> regKey(
      do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Preferences::SetString(kOskDebugReason,
                           L"AIOSKIDM: "
                           L"nsIWindowsRegKey not available");
    return false;
  }

  uint32_t value;
  if (!ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
                                       regKey, value) &&
      !ReadEnableDesktopModeAutoInvoke(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE,
                                       regKey, value)) {
    return false;
  }
  if (!!value) {
    Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=true.");
  } else {
    Preferences::SetString(kOskDebugReason, L"AIOSKIDM: regkey value=false.");
  }
  return !!value;
}

// Based on DisplayVirtualKeyboard() in Chromium's base/win/win_util.cc.
// static
void IMEHandler::ShowOnScreenKeyboard(nsWindow* aWindow) {
#ifdef NIGHTLY_BUILD
  if (FxRWindowManager::GetInstance()->IsFxRWindow(sFocusedWindow)) {
    OSKVRManager::ShowOnScreenKeyboard();
    return;
  }
#endif  // NIGHTLY_BUILD

  if (IsWin10AnniversaryUpdateOrLater()) {
    OSKInputPaneManager::ShowOnScreenKeyboard(aWindow->GetWindowHandle());
    return;
  }

  OSKTabTipManager::ShowOnScreenKeyboard();
}

// Based on DismissVirtualKeyboard() in Chromium's base/win/win_util.cc.
// static
void IMEHandler::DismissOnScreenKeyboard(nsWindow* aWindow) {
  // Dismiss the virtual keyboard if it's open
  if (IsWin10AnniversaryUpdateOrLater()) {
    OSKInputPaneManager::DismissOnScreenKeyboard(aWindow->GetWindowHandle());
    return;
  }

  OSKTabTipManager::DismissOnScreenKeyboard();
}

bool IMEHandler::MaybeCreateNativeCaret(nsWindow* aWindow) {
  MOZ_ASSERT(aWindow);

  if (IsA11yHandlingNativeCaret()) {
    return false;
  }

  if (!sHasNativeCaretBeenRequested) {
    // If we have not received WM_GETOBJECT for OBJID_CARET, there may be new
    // application which requires our caret information.  For kicking its
    // window event proc, we should fire a window event here.
    // (If there is such application, sHasNativeCaretBeenRequested will be set
    // to true later.)
    // FYI: If we create native caret and move its position, native caret
    //      causes EVENT_OBJECT_LOCATIONCHANGE event with OBJID_CARET and
    //      OBJID_CLIENT.
    ::NotifyWinEvent(EVENT_OBJECT_LOCATIONCHANGE, aWindow->GetWindowHandle(),
                     OBJID_CARET, OBJID_CLIENT);
    return false;
  }

  MaybeDestroyNativeCaret();

  // If focused content is not text editable, we don't support caret
  // caret information without a11y module.
  if (!aWindow->GetInputContext().mIMEState.IsEditable()) {
    return false;
  }

  WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWindow);
  aWindow->InitEvent(queryCaretRectEvent);

  WidgetQueryContentEvent::Options options;
  options.mRelativeToInsertionPoint = true;
  queryCaretRectEvent.InitForQueryCaretRect(0, options);

  aWindow->DispatchWindowEvent(queryCaretRectEvent);
  if (NS_WARN_IF(queryCaretRectEvent.Failed())) {
    return false;
  }

  return CreateNativeCaret(aWindow, queryCaretRectEvent.mReply->mRect);
}

bool IMEHandler::CreateNativeCaret(nsWindow* aWindow,
                                   const LayoutDeviceIntRect& aCaretRect) {
  MOZ_ASSERT(aWindow);

  MOZ_ASSERT(!IsA11yHandlingNativeCaret());

  sNativeCaretIsCreated =
      ::CreateCaret(aWindow->GetWindowHandle(), nullptr, aCaretRect.Width(),
                    aCaretRect.Height());
  if (!sNativeCaretIsCreated) {
    return false;
  }
  nsWindow* toplevelWindow = aWindow->GetTopLevelWindow(false);
  if (NS_WARN_IF(!toplevelWindow)) {
    MaybeDestroyNativeCaret();
    return false;
  }

  LayoutDeviceIntPoint caretPosition(aCaretRect.TopLeft());
  if (toplevelWindow != aWindow) {
    caretPosition += toplevelWindow->WidgetToScreenOffset();
    caretPosition -= aWindow->WidgetToScreenOffset();
  }

  ::SetCaretPos(caretPosition.x, caretPosition.y);
  return true;
}

void IMEHandler::MaybeDestroyNativeCaret() {
  if (!sNativeCaretIsCreated) {
    return;
  }
  ::DestroyCaret();
  sNativeCaretIsCreated = false;
}

}  // namespace widget
}  // namespace mozilla
