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

#include "chrome/browser/ash/arc/accessibility/arc_accessibility_helper_bridge.h"

#include <utility>

#include "ash/public/cpp/window_properties.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/ash/accessibility/magnification_manager.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs_factory.h"
#include "chrome/browser/ash/arc/accessibility/geometry_util.h"
#include "chrome/browser/ash/arc/input_method_manager/arc_input_method_manager_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/accessibility_private.h"
#include "chromeos/ash/experiences/arc/arc_browser_context_keyed_service_factory_base.h"
#include "chromeos/ash/experiences/arc/message_center/arc_notification_surface.h"
#include "chromeos/ash/experiences/arc/session/arc_bridge_service.h"
#include "chromeos/ash/experiences/arc/session/arc_service_manager.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "components/exo/wm_helper.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/event_router.h"
#include "services/accessibility/android/accessibility_info_data_wrapper.h"
#include "services/accessibility/android/android_accessibility_util.h"
#include "services/accessibility/android/public/mojom/accessibility_helper.mojom-shared.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/views/controls/native/native_view_host.h"

namespace arc {
namespace {

using ::ash::AccessibilityManager;
using ::ash::AccessibilityNotificationType;
using ::ash::MagnificationManager;

// ClassName for toast from ARC++ R onwards.
constexpr char kToastEventSourceArcR[] = "android.widget.Toast";
// TODO(sarakato): Remove this once ARC++ P has been deprecated.
constexpr char kToastEventSourceArcP[] = "android.widget.Toast$TN";

bool ShouldAnnounceEvent(
    ax::android::mojom::AccessibilityEventData* event_data) {
  if (event_data->event_type ==
      ax::android::mojom::AccessibilityEventType::ANNOUNCEMENT) {
    return true;
  } else if (event_data->event_type ==
             ax::android::mojom::AccessibilityEventType::
                 NOTIFICATION_STATE_CHANGED) {
    // Only announce the event from toast.
    if (!event_data->string_properties)
      return false;

    auto it = event_data->string_properties->find(
        ax::android::mojom::AccessibilityEventStringProperty::CLASS_NAME);
    if (it == event_data->string_properties->end())
      return false;

    return (it->second == kToastEventSourceArcP) ||
           (it->second == kToastEventSourceArcR);
  }
  return false;
}

void DispatchFocusChange(
    ax::android::mojom::AccessibilityNodeInfoData* node_data,
    Profile* profile) {
  AccessibilityManager* accessibility_manager = AccessibilityManager::Get();
  if (!node_data || !accessibility_manager ||
      accessibility_manager->profile() != profile)
    return;

  DCHECK(exo::WMHelper::HasInstance());
  aura::Window* active_window = exo::WMHelper::GetInstance()->GetActiveWindow();
  if (!active_window)
    return;

  // Convert bounds from Android pixels to Chrome DIP, and adjust coordinate to
  // Chrome's screen coordinate.
  gfx::Rect bounds_in_screen = gfx::ScaleToEnclosingRect(
      node_data->bounds_in_screen,
      1.0f / exo::WMHelper::GetInstance()->GetDeviceScaleFactorForWindow(
                 active_window));
  bounds_in_screen.Offset(0, GetChromeWindowHeightOffsetInDip(active_window));

  const display::Display display =
      display::Screen::Get()->GetDisplayNearestView(active_window);
  bounds_in_screen.Offset(display.bounds().x(), display.bounds().y());

  accessibility_manager->OnViewFocusedInArc(bounds_in_screen);
}

// Singleton factory for ArcAccessibilityHelperBridge.
class ArcAccessibilityHelperBridgeFactory
    : public internal::ArcBrowserContextKeyedServiceFactoryBase<
          ArcAccessibilityHelperBridge,
          ArcAccessibilityHelperBridgeFactory> {
 public:
  // Factory name used by ArcBrowserContextKeyedServiceFactoryBase.
  static constexpr const char* kName = "ArcAccessibilityHelperBridgeFactory";

  static ArcAccessibilityHelperBridgeFactory* GetInstance() {
    static base::NoDestructor<ArcAccessibilityHelperBridgeFactory> instance;
    return instance.get();
  }

 protected:
  bool ServiceIsCreatedWithBrowserContext() const override { return true; }

 private:
  friend base::NoDestructor<ArcAccessibilityHelperBridgeFactory>;

  ArcAccessibilityHelperBridgeFactory() {
    // ArcAccessibilityHelperBridge needs to track task creation and
    // destruction in the container, which are notified to ArcAppListPrefs
    // via Mojo.
    DependsOn(ArcAppListPrefsFactory::GetInstance());

    // ArcAccessibilityHelperBridge needs to track visibility change of Android
    // keyboard to delete its accessibility tree when it becomes hidden.
    DependsOn(ArcInputMethodManagerService::GetFactory());
  }
  ~ArcAccessibilityHelperBridgeFactory() override = default;
};

}  // namespace

// static
void ArcAccessibilityHelperBridge::CreateFactory() {
  ArcAccessibilityHelperBridgeFactory::GetInstance();
}

// static
ArcAccessibilityHelperBridge*
ArcAccessibilityHelperBridge::GetForBrowserContext(
    content::BrowserContext* context) {
  return ArcAccessibilityHelperBridgeFactory::GetForBrowserContext(context);
}

ArcAccessibilityHelperBridge::ArcAccessibilityHelperBridge(
    content::BrowserContext* browser_context,
    ArcBridgeService* arc_bridge_service)
    : profile_(Profile::FromBrowserContext(browser_context)),
      arc_bridge_service_(arc_bridge_service),
      accessibility_helper_instance_(arc_bridge_service_),
      tree_tracker_(this,
                    profile_,
                    accessibility_helper_instance_,
                    arc_bridge_service_) {
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(
      Profile::FromBrowserContext(browser_context)->GetPrefs());

  arc_bridge_service_->accessibility_helper()->SetHost(this);
  arc_bridge_service_->accessibility_helper()->AddObserver(this);

  automation_event_router_observer_.Observe(
      extensions::AutomationEventRouter::GetInstance());
}

ArcAccessibilityHelperBridge::~ArcAccessibilityHelperBridge() = default;

void ArcAccessibilityHelperBridge::SetNativeChromeVoxArcSupport(
    bool enabled,
    SetNativeChromeVoxCallback callback) {
  tree_tracker_.SetNativeChromeVoxArcSupport(enabled, std::move(callback));
}

bool ArcAccessibilityHelperBridge::EnableTree(const ui::AXTreeID& tree_id) {
  return tree_tracker_.EnableTree(tree_id);
}

void ArcAccessibilityHelperBridge::Shutdown() {
  // We do not unregister ourselves from WMHelper as an ActivationObserver
  // because it is always null at this point during teardown.

  arc_bridge_service_->accessibility_helper()->RemoveObserver(this);
  arc_bridge_service_->accessibility_helper()->SetHost(nullptr);

  tree_tracker_.Shutdown();
}

void ArcAccessibilityHelperBridge::OnConnectionReady() {
  UpdateEnabledFeature();

  AccessibilityManager* accessibility_manager = AccessibilityManager::Get();
  if (accessibility_manager) {
    accessibility_status_subscription_ =
        accessibility_manager->RegisterCallback(base::BindRepeating(
            &ArcAccessibilityHelperBridge::OnAccessibilityStatusChanged,
            base::Unretained(this)));
    accessibility_helper_instance_.SetExploreByTouchEnabled(
        accessibility_manager->IsSpokenFeedbackEnabled());
  }
}

void ArcAccessibilityHelperBridge::OnAccessibilityEvent(
    ax::android::mojom::AccessibilityEventDataPtr event_data) {
  filter_type_ = GetFilterType();
  switch (filter_type_) {
    case ax::android::mojom::AccessibilityFilterType::ALL:
      HandleFilterTypeAllEvent(std::move(event_data));
      break;
    case ax::android::mojom::AccessibilityFilterType::FOCUS:
      HandleFilterTypeFocusEvent(std::move(event_data));
      break;
    case ax::android::mojom::AccessibilityFilterType::OFF:
      break;
    case ax::android::mojom::AccessibilityFilterType::INVALID_ENUM_VALUE:
      NOTREACHED();
  }
}

void ArcAccessibilityHelperBridge::OnNotificationStateChanged(
    const std::string& notification_key,
    ax::android::mojom::AccessibilityNotificationStateType state) {
  tree_tracker_.OnNotificationStateChanged(std::move(notification_key),
                                           std::move(state));
}

void ArcAccessibilityHelperBridge::OnToggleNativeChromeVoxArcSupport(
    bool enabled) {
  tree_tracker_.OnToggleNativeChromeVoxArcSupport(enabled);
}

void ArcAccessibilityHelperBridge::OnAction(
    const ui::AXActionData& data) const {
  DCHECK(data.target_node_id);

  ax::android::AXTreeSourceAndroid* tree_source =
      tree_tracker_.GetFromTreeId(data.target_tree_id);
  if (!tree_source)
    return;

  std::optional<int32_t> window_id = tree_source->window_id();
  if (!window_id)
    return;

  const std::optional<ax::android::mojom::AccessibilityActionType> action =
      ax::android::ConvertToAndroidAction(data.action);
  if (!action.has_value())
    return;

  ax::android::mojom::AccessibilityActionDataPtr action_data =
      ax::android::mojom::AccessibilityActionData::New();

  action_data->node_id = data.target_node_id;
  action_data->window_id = window_id.value();
  action_data->action_type = action.value();
  PopulateActionParameters(data, *action_data);

  if (action ==
      ax::android::mojom::AccessibilityActionType::GET_TEXT_LOCATION) {
    action_data->start_index = data.start_index;
    action_data->end_index = data.end_index;
    if (!accessibility_helper_instance_.RefreshWithExtraData(
            std::move(action_data),
            base::BindOnce(
                &ArcAccessibilityHelperBridge::OnGetTextLocationDataResult,
                base::Unretained(this), data))) {
      OnActionResult(data, false);
    }
    return;
  }

  if (!accessibility_helper_instance_.PerformAction(
          std::move(action_data),
          base::BindOnce(&ArcAccessibilityHelperBridge::OnActionResult,
                         base::Unretained(this), data))) {
    // TODO(b/146809329): This case should probably destroy all trees.
    OnActionResult(data, false);
  }
}

void ArcAccessibilityHelperBridge::PopulateActionParameters(
    const ui::AXActionData& chrome_data,
    ax::android::mojom::AccessibilityActionData& action_data) const {
  switch (action_data.action_type) {
    case ax::android::mojom::AccessibilityActionType::SCROLL_TO_POSITION: {
      base::flat_map<ax::android::mojom::ActionIntArgumentType, int32_t> args;
      const auto [row, column] = chrome_data.row_column;
      args[ax::android::mojom::ActionIntArgumentType::ROW_INT] = row;
      args[ax::android::mojom::ActionIntArgumentType::COLUMN_INT] = column;
      action_data.int_parameters = args;
      break;
    }
    case ax::android::mojom::AccessibilityActionType::CUSTOM_ACTION:
      action_data.custom_action_id = chrome_data.custom_action_id;
      break;
    case ax::android::mojom::AccessibilityActionType::NEXT_HTML_ELEMENT:
    case ax::android::mojom::AccessibilityActionType::PREVIOUS_HTML_ELEMENT:
    case ax::android::mojom::AccessibilityActionType::FOCUS:
    case ax::android::mojom::AccessibilityActionType::CLEAR_FOCUS:
    case ax::android::mojom::AccessibilityActionType::SELECT:
    case ax::android::mojom::AccessibilityActionType::CLEAR_SELECTION:
    case ax::android::mojom::AccessibilityActionType::CLICK:
    case ax::android::mojom::AccessibilityActionType::LONG_CLICK:
    case ax::android::mojom::AccessibilityActionType::ACCESSIBILITY_FOCUS:
    case ax::android::mojom::AccessibilityActionType::CLEAR_ACCESSIBILITY_FOCUS:
    case ax::android::mojom::AccessibilityActionType::
        NEXT_AT_MOVEMENT_GRANULARITY:
    case ax::android::mojom::AccessibilityActionType::
        PREVIOUS_AT_MOVEMENT_GRANULARITY:
    case ax::android::mojom::AccessibilityActionType::SCROLL_FORWARD:
    case ax::android::mojom::AccessibilityActionType::SCROLL_BACKWARD:
    case ax::android::mojom::AccessibilityActionType::COPY:
    case ax::android::mojom::AccessibilityActionType::PASTE:
    case ax::android::mojom::AccessibilityActionType::CUT:
    case ax::android::mojom::AccessibilityActionType::SET_SELECTION:
    case ax::android::mojom::AccessibilityActionType::EXPAND:
    case ax::android::mojom::AccessibilityActionType::COLLAPSE:
    case ax::android::mojom::AccessibilityActionType::DISMISS:
    case ax::android::mojom::AccessibilityActionType::SET_TEXT:
    case ax::android::mojom::AccessibilityActionType::CONTEXT_CLICK:
    case ax::android::mojom::AccessibilityActionType::SCROLL_DOWN:
    case ax::android::mojom::AccessibilityActionType::SCROLL_LEFT:
    case ax::android::mojom::AccessibilityActionType::SCROLL_RIGHT:
    case ax::android::mojom::AccessibilityActionType::SCROLL_UP:
    case ax::android::mojom::AccessibilityActionType::SET_PROGRESS:
    case ax::android::mojom::AccessibilityActionType::SHOW_ON_SCREEN:
    case ax::android::mojom::AccessibilityActionType::GET_TEXT_LOCATION:
    case ax::android::mojom::AccessibilityActionType::SHOW_TOOLTIP:
    case ax::android::mojom::AccessibilityActionType::HIDE_TOOLTIP:
      break;
    case ax::android::mojom::AccessibilityActionType::INVALID_ENUM_VALUE:
      NOTREACHED();
  }
}

bool ArcAccessibilityHelperBridge::UseFullFocusMode() const {
  return use_full_focus_mode_;
}

void ArcAccessibilityHelperBridge::OnNotificationSurfaceAdded(
    ash::ArcNotificationSurface* surface) {
  tree_tracker_.OnNotificationSurfaceAdded(surface);
}

void ArcAccessibilityHelperBridge::AllAutomationExtensionsGone() {
  // Extension features are directly monitored, so no work needed here.
}

void ArcAccessibilityHelperBridge::ExtensionListenerAdded() {
  tree_tracker_.InvalidateTrees();
}

extensions::EventRouter* ArcAccessibilityHelperBridge::GetEventRouter() const {
  return extensions::EventRouter::Get(profile_);
}

ax::android::mojom::AccessibilityFilterType
ArcAccessibilityHelperBridge::GetFilterType() {
  AccessibilityManager* accessibility_manager = AccessibilityManager::Get();
  const MagnificationManager* magnification_manager =
      MagnificationManager::Get();

  if (!accessibility_manager || !magnification_manager)
    return ax::android::mojom::AccessibilityFilterType::OFF;

  // TODO(yawano): Support the case where primary user is in background.
  if (accessibility_manager->profile() != profile_)
    return ax::android::mojom::AccessibilityFilterType::OFF;

  if (accessibility_manager->IsSelectToSpeakEnabled() ||
      accessibility_manager->IsSwitchAccessEnabled() ||
      accessibility_manager->IsSpokenFeedbackEnabled() ||
      magnification_manager->IsMagnifierEnabled() ||
      magnification_manager->IsDockedMagnifierEnabled()) {
    return ax::android::mojom::AccessibilityFilterType::ALL;
  }

  if (accessibility_manager->IsFocusHighlightEnabled()) {
    return ax::android::mojom::AccessibilityFilterType::FOCUS;
  }

  return ax::android::mojom::AccessibilityFilterType::OFF;
}

void ArcAccessibilityHelperBridge::OnActionResult(const ui::AXActionData& data,
                                                  bool result) const {
  ax::android::AXTreeSourceAndroid* tree_source =
      tree_tracker_.GetFromTreeId(data.target_tree_id);

  if (!tree_source)
    return;

  tree_source->NotifyActionResult(data, result);
}

void ArcAccessibilityHelperBridge::OnGetTextLocationDataResult(
    const ui::AXActionData& data,
    const std::optional<gfx::Rect>& result_rect) const {
  ax::android::AXTreeSourceAndroid* tree_source =
      tree_tracker_.GetFromTreeId(data.target_tree_id);

  if (!tree_source)
    return;

  tree_source->NotifyGetTextLocationDataResult(
      data,
      OnGetTextLocationDataResultInternal(data.target_tree_id, result_rect));
}

std::optional<gfx::Rect>
ArcAccessibilityHelperBridge::OnGetTextLocationDataResultInternal(
    const ui::AXTreeID& ax_tree_id,
    const std::optional<gfx::Rect>& result_rect) const {
  if (!result_rect)
    return std::nullopt;

  ax::android::AXTreeSourceAndroid* tree_source =
      tree_tracker_.GetFromTreeId(ax_tree_id);
  if (!tree_source)
    return std::nullopt;

  aura::Window* window = tree_source->window();
  if (!window)
    return std::nullopt;

  const gfx::RectF& rect_f =
      ScaleAndroidPxToChromePx(result_rect.value(), window);

  return gfx::ToEnclosingRect(rect_f);
}

void ArcAccessibilityHelperBridge::OnAccessibilityStatusChanged(
    const ash::AccessibilityStatusEventDetails& event_details) {
  if (event_details.notification_type !=
          AccessibilityNotificationType::kToggleFocusHighlight &&
      event_details.notification_type !=
          AccessibilityNotificationType::kToggleSelectToSpeak &&
      event_details.notification_type !=
          AccessibilityNotificationType::kToggleSpokenFeedback &&
      event_details.notification_type !=
          AccessibilityNotificationType::kToggleSwitchAccess &&
      event_details.notification_type !=
          AccessibilityNotificationType::kToggleDockedMagnifier &&
      event_details.notification_type !=
          AccessibilityNotificationType::kToggleScreenMagnifier) {
    return;
  }

  UpdateEnabledFeature();

  if (event_details.notification_type ==
      AccessibilityNotificationType::kToggleSpokenFeedback) {
    accessibility_helper_instance_.SetExploreByTouchEnabled(
        event_details.enabled);
  }
}

void ArcAccessibilityHelperBridge::UpdateEnabledFeature() {
  filter_type_ = GetFilterType();

  // Let Android know the filter type change.
  accessibility_helper_instance_.SetFilter(filter_type_);

  const AccessibilityManager* accessibility_manager =
      AccessibilityManager::Get();
  if (accessibility_manager) {
    is_focus_event_enabled_ = accessibility_manager->IsFocusHighlightEnabled();

    use_full_focus_mode_ = accessibility_manager->IsSwitchAccessEnabled() ||
                           accessibility_manager->IsSpokenFeedbackEnabled();
  }

  tree_tracker_.OnEnabledFeatureChanged(filter_type_);
}

void ArcAccessibilityHelperBridge::HandleFilterTypeFocusEvent(
    ax::android::mojom::AccessibilityEventDataPtr event_data) {
  if (event_data.get()->node_data.size() == 1 &&
      event_data->event_type ==
          ax::android::mojom::AccessibilityEventType::VIEW_FOCUSED) {
    DispatchFocusChange(event_data.get()->node_data[0].get(), profile_);
  }
}

void ArcAccessibilityHelperBridge::HandleFilterTypeAllEvent(
    ax::android::mojom::AccessibilityEventDataPtr event_data) {
  if (ShouldAnnounceEvent(event_data.get())) {
    DispatchEventTextAnnouncement(event_data.get());
    return;
  }

  if (event_data->node_data.empty())
    return;

  ax::android::AXTreeSourceAndroid* tree_source =
      tree_tracker_.OnAccessibilityEvent(event_data.get());
  if (!tree_source)
    return;

  tree_source->NotifyAccessibilityEvent(event_data.get());

  bool is_notification_event = event_data->notification_key.has_value();
  if (is_notification_event && event_data->event_type ==
                                   ax::android::mojom::AccessibilityEventType::
                                       VIEW_TEXT_SELECTION_CHANGED) {
    // If text selection changed event is dispatched from Android, it
    // means that user is trying to type a text in Android notification.
    // Dispatch text selection changed event to notification content view
    // as the view can take necessary actions, e.g. activate itself, etc.
    auto* surface_manager = ash::ArcNotificationSurfaceManager::Get();
    if (surface_manager) {
      ash::ArcNotificationSurface* surface =
          surface_manager->GetArcSurface(event_data->notification_key.value());
      if (surface && surface->IsAttached()) {
        surface->GetAttachedHost()->NotifyAccessibilityEventDeprecated(
            ax::mojom::Event::kTextSelectionChanged, true);
      }
    }
  }

  if (is_focus_event_enabled_ &&
      event_data->event_type ==
          ax::android::mojom::AccessibilityEventType::VIEW_FOCUSED) {
    for (size_t i = 0; i < event_data->node_data.size(); ++i) {
      if (event_data->node_data[i]->id == event_data->source_id) {
        DispatchFocusChange(event_data->node_data[i].get(), profile_);
        break;
      }
    }
  }
}

void ArcAccessibilityHelperBridge::DispatchEventTextAnnouncement(
    ax::android::mojom::AccessibilityEventData* event_data) const {
  if (!event_data->event_text.has_value())
    return;

  auto event_args(
      extensions::api::accessibility_private::OnAnnounceForAccessibility::
          Create(*(event_data->event_text)));
  std::unique_ptr<extensions::Event> event(new extensions::Event(
      extensions::events::ACCESSIBILITY_PRIVATE_ON_ANNOUNCE_FOR_ACCESSIBILITY,
      extensions::api::accessibility_private::OnAnnounceForAccessibility::
          kEventName,
      std::move(event_args)));
  GetEventRouter()->BroadcastEvent(std::move(event));
}

// static
void ArcAccessibilityHelperBridge::EnsureFactoryBuilt() {
  ArcAccessibilityHelperBridgeFactory::GetInstance();
}

}  // namespace arc
