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

#import "ui/accessibility/platform/ax_platform_node_mac.h"

#include "base/apple/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/accessibility/platform/ax_platform_node_cocoa.h"
#include "ui/accessibility/platform/ax_private_webkit_constants_mac.h"

namespace {

using RoleMap = std::map<ax::mojom::Role, NSString*>;
using EventMap = std::map<ax::mojom::Event, NSString*>;
using ActionList = std::vector<std::pair<ax::mojom::Action, NSString*>>;

void PostAnnouncementNotification(NSString* announcement,
                                  NSWindow* window,
                                  bool is_polite) {
  NSAccessibilityPriorityLevel priority =
      is_polite ? NSAccessibilityPriorityMedium : NSAccessibilityPriorityHigh;
  NSDictionary* notification_info = @{
    NSAccessibilityAnnouncementKey : announcement,
    NSAccessibilityPriorityKey : @(priority)
  };
  // On Mojave, announcements from an inactive window aren't spoken.
  NSAccessibilityPostNotificationWithUserInfo(
      window, NSAccessibilityAnnouncementRequestedNotification,
      notification_info);
}
void NotifyMacEvent(AXPlatformNodeCocoa* target, ax::mojom::Event event_type) {
  if (![target AXWindow]) {
    // A child tree is not attached to the window. Return early, otherwise
    // AppKit will hang trying to reach the root, resulting in a bug where
    // VoiceOver keeps repeating "[appname] is not responding".
    return;
  }
  NSString* notification =
      [AXPlatformNodeCocoa nativeNotificationFromAXEvent:event_type];
  if (notification)
    NSAccessibilityPostNotification(target, notification);
}

}  // namespace

namespace ui {

// static
AXPlatformNode::Pointer AXPlatformNode::Create(
    AXPlatformNodeDelegate& delegate) {
  AXPlatformNode* node = new AXPlatformNodeMac();
  node->Init(delegate);
  return Pointer(node);
}

// static
AXPlatformNode* AXPlatformNode::FromNativeViewAccessible(
    gfx::NativeViewAccessible accessible) {
  AXPlatformNodeCocoa* node_cocoa =
      base::apple::ObjCCast<AXPlatformNodeCocoa>(accessible.Get());
  if (node_cocoa) {
    return [node_cocoa node];
  }
  return nullptr;
}

struct AXPlatformNodeMac::ObjCStorage {
  AXPlatformNodeCocoa* __strong native_node;
};

AXPlatformNodeMac::AXPlatformNodeMac()
    : objc_storage_(std::make_unique<ObjCStorage>()) {}
AXPlatformNodeMac::~AXPlatformNodeMac() = default;

void AXPlatformNodeMac::Destroy() {
  if (objc_storage_->native_node) {
    [objc_storage_->native_node detachAndNotifyDestroyed:YES];
    // Also, clear the pointer to make accidental use-after-free impossible.
    objc_storage_->native_node = nil;
  }
  AXPlatformNodeBase::Destroy();
}

// On Mac, the checked state is mapped to AXValue.
bool AXPlatformNodeMac::IsPlatformCheckable() const {
  if (GetRole() == ax::mojom::Role::kTab) {
    // On Mac, tabs are exposed as radio buttons, and are treated as checkable.
    // Also, the internal State::kSelected is be mapped to checked via AXValue.
    return true;
  }

  return AXPlatformNodeBase::IsPlatformCheckable();
}

AXPlatformNodeCocoa* AXPlatformNodeMac::GetNativeWrapper() const {
  return objc_storage_->native_node;
}

AXPlatformNodeCocoa* AXPlatformNodeMac::ReleaseNativeWrapper() {
  AXPlatformNodeCocoa* native_node = objc_storage_->native_node;
  objc_storage_->native_node = nil;
  return native_node;
}

void AXPlatformNodeMac::SetNativeWrapper(AXPlatformNodeCocoa* native_node) {
  objc_storage_->native_node = native_node;
}

gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() {
  if (!objc_storage_->native_node) {
    objc_storage_->native_node =
        [[AXPlatformNodeCocoa alloc] initWithNode:this];
  }
  return gfx::NativeViewAccessible(objc_storage_->native_node);
}

void AXPlatformNodeMac::NotifyAccessibilityEvent(ax::mojom::Event event_type) {
  AXPlatformNodeBase::NotifyAccessibilityEvent(event_type);
  GetNativeViewAccessible();
  // Handle special cases.

  // Alerts and live regions go through the announcement API instead of the
  // regular NSAccessibility notification system.
  if (event_type == ax::mojom::Event::kAlert ||
      event_type == ax::mojom::Event::kLiveRegionChanged) {
    if (AXAnnouncementSpec* announcement =
            [objc_storage_->native_node announcementForEvent:event_type]) {
      [objc_storage_->native_node scheduleLiveRegionAnnouncement:announcement];
    }
    return;
  }
  if (event_type == ax::mojom::Event::kSelection &&
      (!HasBoolAttribute(ax::mojom::BoolAttribute::kSelected) ||
       GetBoolAttribute(ax::mojom::BoolAttribute::kSelected))) {
    ax::mojom::Role role = GetRole();
    if (IsMenuItem(role)) {
      // On Mac, map some selection notifications to focus events.
      NotifyMacEvent(objc_storage_->native_node, ax::mojom::Event::kFocus);
      return;
    }

    if (IsListItem(role)) {
      const AXPlatformNodeBase* container = GetSelectionContainer();
      if (container && container->GetRole() == ax::mojom::Role::kListBox &&
          !container->HasState(ax::mojom::State::kMultiselectable) &&
          GetDelegate()->GetFocus() == GetNativeViewAccessible()) {
        // On Mac, map some selection notifications to focus events.
        NotifyMacEvent(objc_storage_->native_node, ax::mojom::Event::kFocus);
        return;
      }
    }
  }

  if (event_type == ax::mojom::Event::kExpandedChanged) {
    if (![objc_storage_->native_node AXWindow]) {
      return;
    }
    NSString* notification =
        [AXPlatformNodeCocoa
            nativeNotificationForExpandedChangedWithRole:GetRole()
                                             isExpanded:GetData().HasState(
                                                 ax::mojom::State::kExpanded)];
    NSAccessibilityPostNotification(objc_storage_->native_node, notification);
    return;
  }

  // Otherwise, use mappings between ax::mojom::Event and NSAccessibility
  // notifications from the EventMap above.
  NotifyMacEvent(objc_storage_->native_node, event_type);
}

void AXPlatformNodeMac::AnnounceTextAs(const std::u16string& text,
                                       AnnouncementType announcement_type) {
  PostAnnouncementNotification(base::SysUTF16ToNSString(text),
                               [objc_storage_->native_node AXWindow],
                               announcement_type == AnnouncementType::kPolite);
}

bool IsNameExposedInAXValueForRole(ax::mojom::Role role) {
  switch (role) {
    case ax::mojom::Role::kListBoxOption:
    case ax::mojom::Role::kListMarker:
    case ax::mojom::Role::kMenuListOption:
    case ax::mojom::Role::kStaticText:
    case ax::mojom::Role::kTitleBar:
      return true;
    default:
      return false;
  }
}

void AXPlatformNodeMac::AddAttributeToList(const char* name,
                                           const char* value,
                                           PlatformAttributeList* attributes) {
  NOTREACHED();
}

}  // namespace ui
