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

#include "mozilla/WindowsVersion.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#include "mozilla/StaticPrefs_accessibility.h"
#include "nsExceptionHandler.h"
#include "nsIXULRuntime.h"
#include "nsPrintfCString.h"
#include "nsUnicharUtils.h"
#include "nsWinUtils.h"
#include "Statistics.h"
#include "AccessibleWrap.h"

#include "mozilla/Preferences.h"

#include <shlobj.h>

using namespace mozilla;
using namespace mozilla::a11y;

/**
 * String versions of consumer flags. See GetHumanReadableConsumersStr.
 */
static const wchar_t* ConsumerStringMap[CONSUMERS_ENUM_LEN + 1] = {
    L"NVDA",    L"JAWS",         L"OLDJAWS",       L"WE",       L"DOLPHIN",
    L"SEROTEK", L"COBRA",        L"ZOOMTEXT",      L"KAZAGURU", L"YOUDAO",
    L"UNKNOWN", L"UIAUTOMATION", L"VISPEROSHARED", L"\0"};

bool Compatibility::IsModuleVersionLessThan(HMODULE aModuleHandle,
                                            unsigned long long aVersion) {
  LauncherResult<ModuleVersion> version = GetModuleVersion(aModuleHandle);
  if (version.isErr()) {
    return true;
  }

  return version.unwrap() < aVersion;
}

////////////////////////////////////////////////////////////////////////////////
// Compatibility
////////////////////////////////////////////////////////////////////////////////

uint32_t Compatibility::sConsumers = Compatibility::UNKNOWN;

/**
 * This function is safe to call multiple times.
 */
/* static */
void Compatibility::InitConsumers() {
  HMODULE jawsHandle = ::GetModuleHandleW(L"jhook");
  if (jawsHandle) {
    sConsumers |=
        IsModuleVersionLessThan(jawsHandle, MAKE_FILE_VERSION(19, 0, 0, 0))
            ? OLDJAWS
            : JAWS;
  }

  if (::GetModuleHandleW(L"gwm32inc")) sConsumers |= WE;

  if (::GetModuleHandleW(L"dolwinhk")) sConsumers |= DOLPHIN;

  if (::GetModuleHandleW(L"STSA32")) sConsumers |= SEROTEK;

  if (::GetModuleHandleW(L"nvdaHelperRemote")) sConsumers |= NVDA;

  if (::GetModuleHandleW(L"OsmHooks") || ::GetModuleHandleW(L"OsmHks64"))
    sConsumers |= COBRA;

  if (::GetModuleHandleW(L"WebFinderRemote")) sConsumers |= ZOOMTEXT;

  if (::GetModuleHandleW(L"Kazahook")) sConsumers |= KAZAGURU;

  if (::GetModuleHandleW(L"TextExtractorImpl32") ||
      ::GetModuleHandleW(L"TextExtractorImpl64"))
    sConsumers |= YOUDAO;

  if (::GetModuleHandleW(L"uiautomation") ||
      ::GetModuleHandleW(L"uiautomationcore"))
    sConsumers |= UIAUTOMATION;

  if (::GetModuleHandleW(L"AccEventCache")) {
    sConsumers |= VISPEROSHARED;
  }

  // If we have a known consumer remove the unknown bit.
  if (sConsumers != Compatibility::UNKNOWN)
    sConsumers &= ~Compatibility::UNKNOWN;
}

/* static */
bool Compatibility::HasKnownNonUiaConsumer() {
  InitConsumers();
  return sConsumers & ~(Compatibility::UNKNOWN | UIAUTOMATION);
}

void Compatibility::Init() {
  // Note we collect some AT statistics/telemetry here for convenience.
  InitConsumers();

  CrashReporter::RecordAnnotationNSCString(
      CrashReporter::Annotation::AccessibilityInProcClient,
      nsPrintfCString("0x%X", sConsumers));

  // Gather telemetry
  uint32_t temp = sConsumers;
  for (int i = 0; temp; i++) {
    if (temp & 0x1) statistics::A11yConsumers(i);

    temp >>= 1;
  }

  // Turn off new tab switching for Jaws and WE.
  if (sConsumers & (JAWS | OLDJAWS | WE)) {
    // Check to see if the pref for disallowing CtrlTab is already set. If so,
    // bail out (respect the user settings). If not, set it.
    if (!Preferences::HasUserValue("browser.ctrlTab.disallowForScreenReaders"))
      Preferences::SetBool("browser.ctrlTab.disallowForScreenReaders", true);
  }
}

// static
void Compatibility::GetHumanReadableConsumersStr(nsAString& aResult) {
  bool appened = false;
  uint32_t index = 0;
  for (uint32_t consumers = sConsumers; consumers; consumers = consumers >> 1) {
    if (consumers & 0x1) {
      if (appened) {
        aResult.AppendLiteral(",");
      }
      aResult.Append(ConsumerStringMap[index]);
      appened = true;
    }
    if (++index > CONSUMERS_ENUM_LEN) {
      break;
    }
  }
}

struct SuppressionTimer {
  constexpr SuppressionTimer() = default;
  void Start() { mStart = ::GetTickCount(); }
  bool IsActive(DWORD aTickCount) const {
    return mStart && aTickCount - mStart < kSuppressTimeout;
  }
  // Last time Start() was called, as returned by ::GetTickCount().
  DWORD mStart = 0;

  static constexpr DWORD kSuppressTimeout = 1500;  // ms
};

static SuppressionTimer sClipboardSuppressionTimer;
static SuppressionTimer sSnapLayoutsSuppressionTimer;

/* static */
void Compatibility::SuppressA11yForClipboardCopy() {
  // Bug 1774285: Windows Suggested Actions (introduced in Windows 11 22H2)
  // might walk the a11y tree using UIA whenever anything is copied to the
  // clipboard. This causes an unacceptable hang, particularly when the cache
  // is disabled.
  bool doSuppress = [&] {
    switch (
        StaticPrefs::accessibility_windows_suppress_after_clipboard_copy()) {
      case 0:
        return false;
      case 1:
        return true;
      default:
        // Our workaround for Suggested Actions is needed from Windows 11 22H2
        return IsWin1122H2OrLater();
    }
  }();

  if (doSuppress) {
    sClipboardSuppressionTimer.Start();
  }
}

/* static */
void Compatibility::SuppressA11yForSnapLayouts() {
  // Bug 1883132: Snap Layouts might enable a11y to find the maximize button
  // position.
  bool doSuppress = [&] {
    switch (StaticPrefs::accessibility_windows_suppress_for_snap_layout()) {
      case 0:
        return false;
      case 1:
        return true;
      default:
        // Our workaround for Snap Layouts is needed from Windows 11 22H2
        return IsWin1122H2OrLater();
    }
  }();

  if (doSuppress) {
    sSnapLayoutsSuppressionTimer.Start();
  }
}

/* static */
SuppressionReasons Compatibility::A11ySuppressionReasons() {
  const auto now = ::GetTickCount();
  auto reasons = SuppressionReasons::None;
  if (sClipboardSuppressionTimer.IsActive(now)) {
    reasons |= SuppressionReasons::Clipboard;
  }
  if (sSnapLayoutsSuppressionTimer.IsActive(now)) {
    reasons |= SuppressionReasons::SnapLayouts;
  }
  return reasons;
}

/* static */
bool Compatibility::IsUiaEnabled() {
  // This is the only function which should call the UIA pref function directly.
  const uint32_t pref =
      StaticPrefs::accessibility_uia_enable_DoNotUseDirectly();
  if (pref == 0) {
    return false;  // Never enable.
  }
  if (pref == 1) {
    return true;  // Always enable.
  }
  // Bug 1956415: Some screen readers break when native UIA is enabled, so don't
  // enable it when they're detected.
  // Call InitConsumers() if we haven't already. It's safe to call this multiple
  // times, but it still does a small amount of work and we can easily avoid
  // unnecessary calls here.
  if (!(sConsumers & UIAUTOMATION)) {
    InitConsumers();
  }
  if (sConsumers & NVDA) {
    // Cache to avoid repeated version checks.
    static Maybe<bool> sIsNvdaVersionSupported;
    if (sIsNvdaVersionSupported.isNothing()) {
      if (HMODULE nvdaHandle = ::GetModuleHandleW(L"nvdaHelperRemote")) {
        // NVDA 2025.1 and later handles UIA correctly in Gecko. Out of an
        // abundance of caution, we check for >= 2025.2 just in case someone is
        // running an old 2025.1 alpha which doesn't include this change.
        sIsNvdaVersionSupported = Some(!IsModuleVersionLessThan(
            nvdaHandle, MAKE_FILE_VERSION(2025, 2, 0, 0)));
      } else {
        sIsNvdaVersionSupported = Some(false);
      }
    }
    return *sIsNvdaVersionSupported;
  }
  if (IsVisperoShared()) {
    if (!::GetModuleHandleW(L"firefox.exe")) {
      // JAWS and ZoomText only handle UIA correctly for firefox.exe. They
      // don't handle it correctly for any other Gecko application; e.g.
      // Thunderbird.
      return false;
    }
    // Cache to avoid repeated version checks.
    static Maybe<bool> sIsVisperoVersionSupported;
    if (sIsVisperoVersionSupported.isNothing()) {
      if (HMODULE visperoHandle = ::GetModuleHandleW(L"AccEventCache")) {
        // JAWS and ZoomText 2026 and later handle UIA correctly in Firefox.
        // They both use Vispero AccEventCache >= 6.0.
        sIsVisperoVersionSupported = Some(!IsModuleVersionLessThan(
            visperoHandle, MAKE_FILE_VERSION(6, 0, 0, 0)));
      } else {
        sIsVisperoVersionSupported = Some(false);
      }
    }
    return *sIsVisperoVersionSupported;
  }
  // Disable UIA for older versions of JAWS and ZoomText which don't have
  // AccEventCache.
  return !IsJAWS() && !(sConsumers & ZOOMTEXT);
}
