// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/accessibility/browser_accessibility_state_impl_auralinux.h"

#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

#include "base/callback_list.h"
#include "base/debug/crash_logging.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/thread_pool.h"
#include "content/browser/accessibility/browser_accessibility_state_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "ui/gfx/animation/animation.h"
#include "ui/linux/linux_ui.h"

namespace content {

namespace {

// Custom deleter function for DIR* that calls closedir
void CloseDir(DIR* dir) {
  if (dir) {
    closedir(dir);
  }
}

}  // namespace

namespace internal {

bool IsOrcaProcess(std::string_view cmdline_all, std::string_view comm) {
  std::string cmdline;
  std::stringstream ss{std::string(cmdline_all)};
  while (std::getline(ss, cmdline, '\0')) {
    if (cmdline == "orca") {
      return true;  // Orca was found
    }
  }

  // When python-setproctitle is unavailable, Orca uses ctypes to call prctl()
  // with PR_SET_NAME (defined as 15 in <sys/prctl.h>). PR_SET_NAME updates
  // /proc/<pid>/comm, but not /proc/<pid>/cmdline.
  return comm == "orca";
}

}  // namespace internal

namespace {

// Returns true if Orca is active.
bool DiscoverOrca() {
  // NOTE: this method is run from another thread to reduce jank, since
  // there's no guarantee these system calls will return quickly.
  std::unique_ptr<DIR, decltype(&CloseDir)> proc_dir(opendir("/proc"),
                                                     &CloseDir);
  if (proc_dir == nullptr) {
    LOG(ERROR) << "Error opening /proc directory.";
    return false;
  }

  bool is_orca_active = false;
  raw_ptr<dirent> entry;
  while (!is_orca_active && (entry = readdir(proc_dir.get())) != nullptr) {
    if (isdigit(entry->d_name[0])) {
      std::string pid_str(entry->d_name);

      struct stat stat_buf;
      std::string stat_path = "/proc/" + pid_str;
      if (stat(stat_path.c_str(), &stat_buf) == 0) {
        if (stat_buf.st_uid == getuid()) {
          std::string cmdline_all;
          std::ifstream cmdline_file("/proc/" + pid_str + "/cmdline");
          if (cmdline_file.is_open()) {
            std::stringstream buffer;
            buffer << cmdline_file.rdbuf();
            cmdline_all = buffer.str();
          } else {
            DVLOG(1) << "Error opening cmdline for pid: " << pid_str;
          }

          std::string comm;
          std::ifstream comm_file("/proc/" + pid_str + "/comm");
          if (comm_file.is_open()) {
            std::getline(comm_file, comm);
          } else {
            DVLOG(1) << "Error opening comm for pid: " << pid_str;
          }

          is_orca_active = internal::IsOrcaProcess(cmdline_all, comm);
        }
      } else {
        DVLOG(1) << "Error with stat for pid: " << pid_str;
      }
    }
  }

  return is_orca_active;
}

}  // namespace

class BrowserAccessibilityStateImplAuralinux
    : public BrowserAccessibilityStateImpl {
 public:
  BrowserAccessibilityStateImplAuralinux();
  ~BrowserAccessibilityStateImplAuralinux() override;

  // BrowserAccessibilityStateImpl:
  void RefreshAssistiveTech() override;
  void RefreshAssistiveTechIfNecessary(ui::AXMode new_mode) override;

 private:
  void OnDiscoveredOrca(bool is_orca_active);
  void OnAnimationsEnabledChanged();

  // A ScopedAccessibilityMode that holds AXMode::kScreenReader when
  // an active screen reader has been detected.
  std::unique_ptr<ScopedAccessibilityMode> screen_reader_mode_;

  // The presence of an AssistiveTech is currently being recomputed.
  // Will be updated via DiscoverOrca().
  bool awaiting_known_assistive_tech_computation_ = false;

  base::CallbackListSubscription animations_enabled_subscription_;
};

BrowserAccessibilityStateImplAuralinux::
    BrowserAccessibilityStateImplAuralinux() {
  animations_enabled_subscription_ =
      ui::LinuxUi::RegisterAnimationsEnabledCallback(base::BindRepeating(
          &BrowserAccessibilityStateImplAuralinux::OnAnimationsEnabledChanged,
          base::Unretained(this)));
}

BrowserAccessibilityStateImplAuralinux::
    ~BrowserAccessibilityStateImplAuralinux() = default;

void BrowserAccessibilityStateImplAuralinux::OnAnimationsEnabledChanged() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  gfx::Animation::UpdatePrefersReducedMotion();
  NotifyWebContentsPreferencesChanged();
}

void BrowserAccessibilityStateImplAuralinux::RefreshAssistiveTech() {
  if (!awaiting_known_assistive_tech_computation_) {
    awaiting_known_assistive_tech_computation_ = true;
    // Using base::Unretained() instead of a weak pointer as the lifetime of
    // this is tied to BrowserMainLoop.
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
        base::BindOnce(&DiscoverOrca),
        base::BindOnce(
            &BrowserAccessibilityStateImplAuralinux::OnDiscoveredOrca,
            base::Unretained(this)));
  }
}

void BrowserAccessibilityStateImplAuralinux::RefreshAssistiveTechIfNecessary(
    ui::AXMode new_mode) {
  bool was_screen_reader_active = ax_platform().IsScreenReaderActive();
  bool has_screen_reader_mode = new_mode.has_mode(ui::AXMode::kScreenReader);
  if (was_screen_reader_active != has_screen_reader_mode) {
    OnAssistiveTechFound(has_screen_reader_mode
                             ? ui::AssistiveTech::kGenericScreenReader
                             : ui::AssistiveTech::kNone);
    return;
  }

  // An expensive check is required to determine which type of assistive tech is
  // in use. Make this check only when `kExtendedProperties` is added or removed
  // from the process-wide mode flags and no previous assistive tech has been
  // discovered (in the former case) or one had been discovered (in the latter
  // case). `kScreenReader` will be added/removed from the process-wide mode
  // flags on completion and `OnAssistiveTechFound()` will be called with the
  // results of the check.
  bool has_extended_properties =
      new_mode.has_mode(ui::AXMode::kExtendedProperties);
  if (was_screen_reader_active != has_extended_properties) {
    // Perform expensive assistive tech detection.
    RefreshAssistiveTech();
  }
}

void BrowserAccessibilityStateImplAuralinux::OnDiscoveredOrca(
    bool is_orca_active) {
  awaiting_known_assistive_tech_computation_ = false;

  if (ActiveAssistiveTech() == ui::AssistiveTech::kGenericScreenReader) {
    // A test has overridden the screen reader state manually.
    // In such cases, we don't want to alter it.
    return;
  }

  UMA_HISTOGRAM_BOOLEAN("Accessibility.Linux.Orca", is_orca_active);
  static auto* ax_orca_crash_key = base::debug::AllocateCrashKeyString(
      "ax_orca", base::debug::CrashKeySize::Size32);
  // Save the current assistive tech before toggling AXModes, so
  // that RefreshAssistiveTechIfNecessary() is a noop.
  if (is_orca_active) {
    base::debug::SetCrashKeyString(ax_orca_crash_key, "true");
    OnAssistiveTechFound(ui::AssistiveTech::kOrca);
    if (!screen_reader_mode_) {
      screen_reader_mode_ = CreateScopedModeForProcess(
          ui::kAXModeComplete | ui::AXMode::kScreenReader |
          ui::AXMode::kFromPlatform);
    }
  } else {
    base::debug::ClearCrashKeyString(ax_orca_crash_key);
    OnAssistiveTechFound(ui::AssistiveTech::kNone);
    screen_reader_mode_.reset();
  }
}

// static
std::unique_ptr<BrowserAccessibilityStateImpl>
BrowserAccessibilityStateImpl::Create() {
  return std::make_unique<BrowserAccessibilityStateImplAuralinux>();
}

}  // namespace content
