// Copyright 2014 The Chromium Authors. All rights reserved.
// 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"

#import <Cocoa/Cocoa.h>
#include <stddef.h>

#include "base/macros.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#include "ui/accessibility/platform/ax_platform_node_delegate.h"
#include "ui/base/l10n/l10n_util.h"
#import "ui/gfx/mac/coordinate_conversion.h"
#include "ui/strings/grit/ui_strings.h"

namespace {

using RoleMap = std::map<ui::AXRole, NSString*>;
using EventMap = std::map<ui::AXEvent, NSString*>;
using ActionList = std::vector<std::pair<ui::AXAction, NSString*>>;

RoleMap BuildRoleMap() {
  const RoleMap::value_type roles[] = {
      {ui::AX_ROLE_ABBR, NSAccessibilityGroupRole},
      {ui::AX_ROLE_ALERT, NSAccessibilityGroupRole},
      {ui::AX_ROLE_ALERT_DIALOG, NSAccessibilityGroupRole},
      {ui::AX_ROLE_ANCHOR, NSAccessibilityGroupRole},
      {ui::AX_ROLE_ANNOTATION, NSAccessibilityUnknownRole},
      {ui::AX_ROLE_APPLICATION, NSAccessibilityGroupRole},
      {ui::AX_ROLE_ARTICLE, NSAccessibilityGroupRole},
      {ui::AX_ROLE_AUDIO, NSAccessibilityGroupRole},
      {ui::AX_ROLE_BANNER, NSAccessibilityGroupRole},
      {ui::AX_ROLE_BLOCKQUOTE, NSAccessibilityGroupRole},
      {ui::AX_ROLE_BUTTON, NSAccessibilityButtonRole},
      {ui::AX_ROLE_CANVAS, NSAccessibilityImageRole},
      {ui::AX_ROLE_CAPTION, NSAccessibilityGroupRole},
      {ui::AX_ROLE_CELL, @"AXCell"},
      {ui::AX_ROLE_CHECK_BOX, NSAccessibilityCheckBoxRole},
      {ui::AX_ROLE_COLOR_WELL, NSAccessibilityColorWellRole},
      {ui::AX_ROLE_COLUMN, NSAccessibilityColumnRole},
      {ui::AX_ROLE_COLUMN_HEADER, @"AXCell"},
      {ui::AX_ROLE_COMBO_BOX_GROUPING, NSAccessibilityGroupRole},
      {ui::AX_ROLE_COMBO_BOX_MENU_BUTTON, NSAccessibilityButtonRole},
      {ui::AX_ROLE_COMPLEMENTARY, NSAccessibilityGroupRole},
      {ui::AX_ROLE_CONTENT_INFO, NSAccessibilityGroupRole},
      {ui::AX_ROLE_DATE, @"AXDateField"},
      {ui::AX_ROLE_DATE_TIME, @"AXDateField"},
      {ui::AX_ROLE_DEFINITION, NSAccessibilityGroupRole},
      {ui::AX_ROLE_DESCRIPTION_LIST_DETAIL, NSAccessibilityGroupRole},
      {ui::AX_ROLE_DESCRIPTION_LIST, NSAccessibilityListRole},
      {ui::AX_ROLE_DESCRIPTION_LIST_TERM, NSAccessibilityGroupRole},
      {ui::AX_ROLE_DIALOG, NSAccessibilityGroupRole},
      {ui::AX_ROLE_DETAILS, NSAccessibilityGroupRole},
      {ui::AX_ROLE_DIRECTORY, NSAccessibilityListRole},
      // If Mac supports AXExpandedChanged event with
      // NSAccessibilityDisclosureTriangleRole, We should update
      // AX_ROLE_DISCLOSURE_TRIANGLE mapping to
      // NSAccessibilityDisclosureTriangleRole. http://crbug.com/558324
      {ui::AX_ROLE_DISCLOSURE_TRIANGLE, NSAccessibilityButtonRole},
      {ui::AX_ROLE_DOCUMENT, NSAccessibilityGroupRole},
      {ui::AX_ROLE_EMBEDDED_OBJECT, NSAccessibilityGroupRole},
      {ui::AX_ROLE_FIGCAPTION, NSAccessibilityGroupRole},
      {ui::AX_ROLE_FIGURE, NSAccessibilityGroupRole},
      {ui::AX_ROLE_FOOTER, NSAccessibilityGroupRole},
      {ui::AX_ROLE_FORM, NSAccessibilityGroupRole},
      {ui::AX_ROLE_GENERIC_CONTAINER, NSAccessibilityGroupRole},
      // Should be NSAccessibilityGridRole but VoiceOver treating it like
      // a list as of 10.12.6, so following WebKit and using table role:
      {ui::AX_ROLE_GRID, NSAccessibilityTableRole},  // crbug.com/753925
      {ui::AX_ROLE_GROUP, NSAccessibilityGroupRole},
      {ui::AX_ROLE_HEADING, @"AXHeading"},
      {ui::AX_ROLE_IFRAME, NSAccessibilityGroupRole},
      {ui::AX_ROLE_IFRAME_PRESENTATIONAL, NSAccessibilityGroupRole},
      {ui::AX_ROLE_IGNORED, NSAccessibilityUnknownRole},
      {ui::AX_ROLE_IMAGE, NSAccessibilityImageRole},
      {ui::AX_ROLE_IMAGE_MAP, NSAccessibilityGroupRole},
      {ui::AX_ROLE_INPUT_TIME, @"AXTimeField"},
      {ui::AX_ROLE_LABEL_TEXT, NSAccessibilityGroupRole},
      {ui::AX_ROLE_LEGEND, NSAccessibilityGroupRole},
      {ui::AX_ROLE_LINE_BREAK, NSAccessibilityGroupRole},
      {ui::AX_ROLE_LINK, NSAccessibilityLinkRole},
      {ui::AX_ROLE_LIST, NSAccessibilityListRole},
      {ui::AX_ROLE_LIST_BOX, NSAccessibilityListRole},
      {ui::AX_ROLE_LIST_BOX_OPTION, NSAccessibilityStaticTextRole},
      {ui::AX_ROLE_LIST_ITEM, NSAccessibilityGroupRole},
      {ui::AX_ROLE_LIST_MARKER, @"AXListMarker"},
      {ui::AX_ROLE_LOG, NSAccessibilityGroupRole},
      {ui::AX_ROLE_MAIN, NSAccessibilityGroupRole},
      {ui::AX_ROLE_MARK, NSAccessibilityGroupRole},
      {ui::AX_ROLE_MARQUEE, NSAccessibilityGroupRole},
      {ui::AX_ROLE_MATH, NSAccessibilityGroupRole},
      {ui::AX_ROLE_MENU, NSAccessibilityMenuRole},
      {ui::AX_ROLE_MENU_BAR, NSAccessibilityMenuBarRole},
      {ui::AX_ROLE_MENU_BUTTON, NSAccessibilityButtonRole},
      {ui::AX_ROLE_MENU_ITEM, NSAccessibilityMenuItemRole},
      {ui::AX_ROLE_MENU_ITEM_CHECK_BOX, NSAccessibilityMenuItemRole},
      {ui::AX_ROLE_MENU_ITEM_RADIO, NSAccessibilityMenuItemRole},
      {ui::AX_ROLE_MENU_LIST_OPTION, NSAccessibilityMenuItemRole},
      {ui::AX_ROLE_MENU_LIST_POPUP, NSAccessibilityUnknownRole},
      {ui::AX_ROLE_METER, NSAccessibilityProgressIndicatorRole},
      {ui::AX_ROLE_NAVIGATION, NSAccessibilityGroupRole},
      {ui::AX_ROLE_NONE, NSAccessibilityGroupRole},
      {ui::AX_ROLE_NOTE, NSAccessibilityGroupRole},
      {ui::AX_ROLE_PARAGRAPH, NSAccessibilityGroupRole},
      {ui::AX_ROLE_POP_UP_BUTTON, NSAccessibilityPopUpButtonRole},
      {ui::AX_ROLE_PRE, NSAccessibilityGroupRole},
      {ui::AX_ROLE_PRESENTATIONAL, NSAccessibilityGroupRole},
      {ui::AX_ROLE_PROGRESS_INDICATOR, NSAccessibilityProgressIndicatorRole},
      {ui::AX_ROLE_RADIO_BUTTON, NSAccessibilityRadioButtonRole},
      {ui::AX_ROLE_RADIO_GROUP, NSAccessibilityRadioGroupRole},
      {ui::AX_ROLE_REGION, NSAccessibilityGroupRole},
      {ui::AX_ROLE_ROOT_WEB_AREA, @"AXWebArea"},
      {ui::AX_ROLE_ROW, NSAccessibilityRowRole},
      {ui::AX_ROLE_ROW_HEADER, @"AXCell"},
      {ui::AX_ROLE_SCROLL_BAR, NSAccessibilityScrollBarRole},
      {ui::AX_ROLE_SEARCH, NSAccessibilityGroupRole},
      {ui::AX_ROLE_SEARCH_BOX, NSAccessibilityTextFieldRole},
      {ui::AX_ROLE_SLIDER, NSAccessibilitySliderRole},
      {ui::AX_ROLE_SLIDER_THUMB, NSAccessibilityValueIndicatorRole},
      {ui::AX_ROLE_SPIN_BUTTON, NSAccessibilityIncrementorRole},
      {ui::AX_ROLE_SPLITTER, NSAccessibilitySplitterRole},
      {ui::AX_ROLE_STATIC_TEXT, NSAccessibilityStaticTextRole},
      {ui::AX_ROLE_STATUS, NSAccessibilityGroupRole},
      {ui::AX_ROLE_SVG_ROOT, NSAccessibilityGroupRole},
      {ui::AX_ROLE_SWITCH, NSAccessibilityCheckBoxRole},
      {ui::AX_ROLE_TAB, NSAccessibilityRadioButtonRole},
      {ui::AX_ROLE_TABLE, NSAccessibilityTableRole},
      {ui::AX_ROLE_TABLE_HEADER_CONTAINER, NSAccessibilityGroupRole},
      {ui::AX_ROLE_TAB_LIST, NSAccessibilityTabGroupRole},
      {ui::AX_ROLE_TAB_PANEL, NSAccessibilityGroupRole},
      {ui::AX_ROLE_TERM, NSAccessibilityGroupRole},
      {ui::AX_ROLE_TEXT_FIELD, NSAccessibilityTextFieldRole},
      {ui::AX_ROLE_TEXT_FIELD_WITH_COMBO_BOX, NSAccessibilityComboBoxRole},
      {ui::AX_ROLE_TIME, NSAccessibilityGroupRole},
      {ui::AX_ROLE_TIMER, NSAccessibilityGroupRole},
      {ui::AX_ROLE_TOGGLE_BUTTON, NSAccessibilityCheckBoxRole},
      {ui::AX_ROLE_TOOLBAR, NSAccessibilityToolbarRole},
      {ui::AX_ROLE_TOOLTIP, NSAccessibilityGroupRole},
      {ui::AX_ROLE_TREE, NSAccessibilityOutlineRole},
      {ui::AX_ROLE_TREE_GRID, NSAccessibilityTableRole},
      {ui::AX_ROLE_TREE_ITEM, NSAccessibilityRowRole},
      {ui::AX_ROLE_VIDEO, NSAccessibilityGroupRole},
      {ui::AX_ROLE_WEB_AREA, @"AXWebArea"},
      {ui::AX_ROLE_WINDOW, NSAccessibilityWindowRole},
  };

  return RoleMap(begin(roles), end(roles));
}

RoleMap BuildSubroleMap() {
  const RoleMap::value_type subroles[] = {
      {ui::AX_ROLE_ALERT, @"AXApplicationAlert"},
      {ui::AX_ROLE_ALERT_DIALOG, @"AXApplicationAlertDialog"},
      {ui::AX_ROLE_APPLICATION, @"AXLandmarkApplication"},
      {ui::AX_ROLE_ARTICLE, @"AXDocumentArticle"},
      {ui::AX_ROLE_BANNER, @"AXLandmarkBanner"},
      {ui::AX_ROLE_COMPLEMENTARY, @"AXLandmarkComplementary"},
      {ui::AX_ROLE_CONTENT_INFO, @"AXLandmarkContentInfo"},
      {ui::AX_ROLE_DEFINITION, @"AXDefinition"},
      {ui::AX_ROLE_DESCRIPTION_LIST_DETAIL, @"AXDefinition"},
      {ui::AX_ROLE_DESCRIPTION_LIST_TERM, @"AXTerm"},
      {ui::AX_ROLE_DIALOG, @"AXApplicationDialog"},
      {ui::AX_ROLE_DOCUMENT, @"AXDocument"},
      {ui::AX_ROLE_FOOTER, @"AXLandmarkContentInfo"},
      {ui::AX_ROLE_FORM, @"AXLandmarkForm"},
      {ui::AX_ROLE_LOG, @"AXApplicationLog"},
      {ui::AX_ROLE_MAIN, @"AXLandmarkMain"},
      {ui::AX_ROLE_MARQUEE, @"AXApplicationMarquee"},
      {ui::AX_ROLE_MATH, @"AXDocumentMath"},
      {ui::AX_ROLE_NAVIGATION, @"AXLandmarkNavigation"},
      {ui::AX_ROLE_NOTE, @"AXDocumentNote"},
      {ui::AX_ROLE_REGION, @"AXDocumentRegion"},
      {ui::AX_ROLE_SEARCH, @"AXLandmarkSearch"},
      {ui::AX_ROLE_SEARCH_BOX, @"AXSearchField"},
      {ui::AX_ROLE_STATUS, @"AXApplicationStatus"},
      {ui::AX_ROLE_SWITCH, @"AXSwitch"},
      {ui::AX_ROLE_TAB_PANEL, @"AXTabPanel"},
      {ui::AX_ROLE_TERM, @"AXTerm"},
      {ui::AX_ROLE_TIMER, @"AXApplicationTimer"},
      {ui::AX_ROLE_TOGGLE_BUTTON, @"AXToggleButton"},
      {ui::AX_ROLE_TOOLTIP, @"AXUserInterfaceTooltip"},
      {ui::AX_ROLE_TREE_ITEM, NSAccessibilityOutlineRowSubrole},
  };

  return RoleMap(begin(subroles), end(subroles));
}

EventMap BuildEventMap() {
  const EventMap::value_type events[] = {
      {ui::AX_EVENT_FOCUS, NSAccessibilityFocusedUIElementChangedNotification},
      {ui::AX_EVENT_TEXT_CHANGED, NSAccessibilityTitleChangedNotification},
      {ui::AX_EVENT_VALUE_CHANGED, NSAccessibilityValueChangedNotification},
      {ui::AX_EVENT_TEXT_SELECTION_CHANGED,
       NSAccessibilitySelectedTextChangedNotification},
      // TODO(patricialor): Add more events.
  };

  return EventMap(begin(events), end(events));
}

ActionList BuildActionList() {
  const ActionList::value_type entries[] = {
      // NSAccessibilityPressAction must come first in this list.
      {ui::AX_ACTION_DO_DEFAULT, NSAccessibilityPressAction},

      {ui::AX_ACTION_DECREMENT, NSAccessibilityDecrementAction},
      {ui::AX_ACTION_INCREMENT, NSAccessibilityIncrementAction},
      {ui::AX_ACTION_SHOW_CONTEXT_MENU, NSAccessibilityShowMenuAction},
  };
  return ActionList(begin(entries), end(entries));
}

const ActionList& GetActionList() {
  CR_DEFINE_STATIC_LOCAL(const ActionList, action_map, (BuildActionList()));
  return action_map;
}

void NotifyMacEvent(AXPlatformNodeCocoa* target, ui::AXEvent event_type) {
  NSAccessibilityPostNotification(
      target, [AXPlatformNodeCocoa nativeNotificationFromAXEvent:event_type]);
}

// Returns true if |action| should be added implicitly for |data|.
bool HasImplicitAction(const ui::AXNodeData& data, ui::AXAction action) {
  return action == ui::AX_ACTION_DO_DEFAULT && ui::IsRoleClickable(data.role);
}

// For roles that show a menu for the default action, ensure "show menu" also
// appears in available actions, but only if that's not already used for a
// context menu. It will be mapped back to the default action when performed.
bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) {
  return HasImplicitAction(data, ui::AX_ACTION_DO_DEFAULT) &&
         !data.HasAction(ui::AX_ACTION_SHOW_CONTEXT_MENU) &&
         data.role == ui::AX_ROLE_POP_UP_BUTTON;
}

}  // namespace

@interface AXPlatformNodeCocoa ()
// Helper function for string attributes that don't require extra processing.
- (NSString*)getStringAttribute:(ui::AXStringAttribute)attribute;
// Returns AXValue, or nil if AXValue isn't an NSString.
- (NSString*)getAXValueAsString;
@end

@implementation AXPlatformNodeCocoa {
  ui::AXPlatformNodeBase* node_;  // Weak. Retains us.
}

@synthesize node = node_;

+ (NSString*)nativeRoleFromAXRole:(ui::AXRole)role {
  CR_DEFINE_STATIC_LOCAL(const RoleMap, role_map, (BuildRoleMap()));
  RoleMap::const_iterator it = role_map.find(role);
  return it != role_map.end() ? it->second : NSAccessibilityUnknownRole;
}

+ (NSString*)nativeSubroleFromAXRole:(ui::AXRole)role {
  CR_DEFINE_STATIC_LOCAL(const RoleMap, subrole_map, (BuildSubroleMap()));
  RoleMap::const_iterator it = subrole_map.find(role);
  return it != subrole_map.end() ? it->second : nil;
}

+ (NSString*)nativeNotificationFromAXEvent:(ui::AXEvent)event {
  CR_DEFINE_STATIC_LOCAL(const EventMap, event_map, (BuildEventMap()));
  EventMap::const_iterator it = event_map.find(event);
  return it != event_map.end() ? it->second : nil;
}

- (instancetype)initWithNode:(ui::AXPlatformNodeBase*)node {
  if ((self = [super init])) {
    node_ = node;
  }
  return self;
}

- (void)detach {
  if (!node_)
    return;
  NSAccessibilityPostNotification(
      self, NSAccessibilityUIElementDestroyedNotification);
  node_ = nil;
}

- (NSRect)boundsInScreen {
  if (!node_)
    return NSZeroRect;
  return gfx::ScreenRectToNSRect(node_->GetBoundsInScreen());
}

- (NSString*)getStringAttribute:(ui::AXStringAttribute)attribute {
  std::string attributeValue;
  if (node_->GetStringAttribute(attribute, &attributeValue))
    return base::SysUTF8ToNSString(attributeValue);
  return nil;
}

- (NSString*)getAXValueAsString {
  id value = [self AXValue];
  return [value isKindOfClass:[NSString class]] ? value : nil;
}

// NSAccessibility informal protocol implementation.

- (BOOL)accessibilityIsIgnored {
  if (!node_)
    return YES;

  return [[self AXRole] isEqualToString:NSAccessibilityUnknownRole] ||
         node_->GetData().HasState(ui::AX_STATE_INVISIBLE);
}

- (id)accessibilityHitTest:(NSPoint)point {
  for (AXPlatformNodeCocoa* child in [self AXChildren]) {
    if (![child accessibilityIsIgnored] &&
        NSPointInRect(point, child.boundsInScreen)) {
      return [child accessibilityHitTest:point];
    }
  }
  return NSAccessibilityUnignoredAncestor(self);
}

- (BOOL)accessibilityNotifiesWhenDestroyed {
  return YES;
}

- (id)accessibilityFocusedUIElement {
  return node_ ? node_->GetDelegate()->GetFocus() : nil;
}

- (NSArray*)accessibilityActionNames {
  if (!node_)
    return @[];

  base::scoped_nsobject<NSMutableArray> axActions(
      [[NSMutableArray alloc] init]);

  const ui::AXNodeData& data = node_->GetData();
  const ActionList& action_list = GetActionList();

  // VoiceOver expects the "press" action to be first. Note that some roles
  // should be given a press action implicitly.
  DCHECK([action_list[0].second isEqualToString:NSAccessibilityPressAction]);
  for (const auto item : action_list) {
    if (data.HasAction(item.first) || HasImplicitAction(data, item.first))
      [axActions addObject:item.second];
  }

  if (AlsoUseShowMenuActionForDefaultAction(data))
    [axActions addObject:NSAccessibilityShowMenuAction];

  return axActions.autorelease();
}

- (void)accessibilityPerformAction:(NSString*)action {
  // Actions are performed asynchronously, so it's always possible for an object
  // to change its mind after previously reporting an action as available.
  if (![[self accessibilityActionNames] containsObject:action])
    return;

  ui::AXActionData data;
  if ([action isEqualToString:NSAccessibilityShowMenuAction] &&
      AlsoUseShowMenuActionForDefaultAction(node_->GetData())) {
    data.action = ui::AX_ACTION_DO_DEFAULT;
  } else {
    for (const ActionList::value_type& entry : GetActionList()) {
      if ([action isEqualToString:entry.second]) {
        data.action = entry.first;
        break;
      }
    }
  }

  // Note ui::AX_ACTIONs which are just overwriting an accessibility attribute
  // are already implemented in -accessibilitySetValue:forAttribute:, so ignore
  // those here.

  if (data.action != ui::AX_ACTION_NONE)
    node_->GetDelegate()->AccessibilityPerformAction(data);
}

- (NSArray*)accessibilityAttributeNames {
  if (!node_)
    return @[];

  // These attributes are required on all accessibility objects.
  NSArray* const kAllRoleAttributes = @[
    NSAccessibilityChildrenAttribute,
    NSAccessibilityParentAttribute,
    NSAccessibilityPositionAttribute,
    NSAccessibilityRoleAttribute,
    NSAccessibilitySizeAttribute,
    NSAccessibilitySubroleAttribute,

    // Title is required for most elements. Cocoa asks for the value even if it
    // is omitted here, but won't present it to accessibility APIs without this.
    NSAccessibilityTitleAttribute,

    // Attributes which are not required, but are general to all roles.
    NSAccessibilityRoleDescriptionAttribute,
    NSAccessibilityEnabledAttribute,
    NSAccessibilityFocusedAttribute,
    NSAccessibilityHelpAttribute,
    NSAccessibilityTopLevelUIElementAttribute,
    NSAccessibilityWindowAttribute,
  ];

  // Attributes required for user-editable controls.
  NSArray* const kValueAttributes = @[ NSAccessibilityValueAttribute ];

  // Attributes required for unprotected textfields and labels.
  NSArray* const kUnprotectedTextAttributes = @[
    NSAccessibilityInsertionPointLineNumberAttribute,
    NSAccessibilityNumberOfCharactersAttribute,
    NSAccessibilitySelectedTextAttribute,
    NSAccessibilitySelectedTextRangeAttribute,
    NSAccessibilityVisibleCharacterRangeAttribute,
  ];

  // Required for all text, including protected textfields.
  NSString* const kTextAttributes = NSAccessibilityPlaceholderValueAttribute;

  base::scoped_nsobject<NSMutableArray> axAttributes(
      [[NSMutableArray alloc] init]);

  [axAttributes addObjectsFromArray:kAllRoleAttributes];

  switch (node_->GetData().role) {
    case ui::AX_ROLE_TEXT_FIELD:
    case ui::AX_ROLE_TEXT_FIELD_WITH_COMBO_BOX:
    case ui::AX_ROLE_STATIC_TEXT:
      [axAttributes addObject:kTextAttributes];
      if (!node_->GetData().HasState(ui::AX_STATE_PROTECTED))
        [axAttributes addObjectsFromArray:kUnprotectedTextAttributes];
    // Fallthrough.
    case ui::AX_ROLE_CHECK_BOX:
    case ui::AX_ROLE_COMBO_BOX_MENU_BUTTON:
    case ui::AX_ROLE_MENU_ITEM_CHECK_BOX:
    case ui::AX_ROLE_MENU_ITEM_RADIO:
    case ui::AX_ROLE_RADIO_BUTTON:
    case ui::AX_ROLE_SEARCH_BOX:
    case ui::AX_ROLE_SLIDER:
    case ui::AX_ROLE_SLIDER_THUMB:
    case ui::AX_ROLE_TOGGLE_BUTTON:
      [axAttributes addObjectsFromArray:kValueAttributes];
      break;
    // TODO(tapted): Add additional attributes based on role.
    default:
      break;
  }
  return axAttributes.autorelease();
}

- (NSArray*)accessibilityParameterizedAttributeNames {
  if (!node_)
    return @[];

  static NSArray* const kSelectableTextAttributes = [@[
    NSAccessibilityLineForIndexParameterizedAttribute,
    NSAccessibilityRangeForLineParameterizedAttribute,
    NSAccessibilityStringForRangeParameterizedAttribute,
    NSAccessibilityRangeForPositionParameterizedAttribute,
    NSAccessibilityRangeForIndexParameterizedAttribute,
    NSAccessibilityBoundsForRangeParameterizedAttribute,
    NSAccessibilityRTFForRangeParameterizedAttribute,
    NSAccessibilityStyleRangeForIndexParameterizedAttribute,
    NSAccessibilityAttributedStringForRangeParameterizedAttribute,
  ] retain];

  switch (node_->GetData().role) {
    case ui::AX_ROLE_TEXT_FIELD:
    case ui::AX_ROLE_STATIC_TEXT:
      return kSelectableTextAttributes;
    default:
      break;
  }
  return nil;
}

- (BOOL)accessibilityIsAttributeSettable:(NSString*)attributeName {
  if (!node_)
    return NO;

  const int restriction =
      node_->GetData().GetIntAttribute(ui::AX_ATTR_RESTRICTION);
  if (restriction == ui::AX_RESTRICTION_DISABLED)
    return NO;

  // Allow certain attributes to be written via an accessibility client. A
  // writable attribute will only appear as such if the accessibility element
  // has a value set for that attribute.
  if ([attributeName
          isEqualToString:NSAccessibilitySelectedChildrenAttribute] ||
      [attributeName
          isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) {
    return NO;
  }

  if ([attributeName isEqualToString:NSAccessibilityValueAttribute]) {
    // Since tabs use the Radio Button role on Mac, the standard way to set
    // them is via the value attribute rather than the selected attribute.
    if (node_->GetData().role == ui::AX_ROLE_TAB)
      return !node_->GetData().HasState(ui::AX_STATE_SELECTED);

    return restriction != ui::AX_RESTRICTION_READ_ONLY;
  }

  // Readonly fields and selected text operations:
  // - Selecting different text via NSAccessibilitySelectedTextRangeAttribute
  //   should work but it does not - see http://crbug.com/692362 .
  // - Changing the actual text contents in the selection via
  //   NSAccessibilitySelectedTextAttribute is prevented, which is correct.
  if ([attributeName isEqualToString:NSAccessibilitySelectedTextAttribute] ||
      [attributeName isEqualToString:NSAccessibilitySelectedTextRangeAttribute])
    return restriction != ui::AX_RESTRICTION_READ_ONLY;

  if ([attributeName isEqualToString:NSAccessibilityFocusedAttribute]) {
    return node_->GetData().HasState(ui::AX_STATE_FOCUSABLE);
  }

  // TODO(patricialor): Add callbacks for updating the above attributes except
  // NSAccessibilityValueAttribute and return YES.
  return NO;
}

- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
  if (!node_)
    return;

  ui::AXActionData data;

  // Check for attributes first. Only the |data.action| should be set here - any
  // type-specific information, if needed, should be set below.
  if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
    data.action = node_->GetData().role == ui::AX_ROLE_TAB
                      ? ui::AX_ACTION_SET_SELECTION
                      : ui::AX_ACTION_SET_VALUE;
  } else if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) {
    data.action = ui::AX_ACTION_REPLACE_SELECTED_TEXT;
  } else if ([attribute
                 isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
    data.action = ui::AX_ACTION_SET_SELECTION;
  } else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
    if ([value isKindOfClass:[NSNumber class]]) {
      data.action =
          [value boolValue] ? ui::AX_ACTION_FOCUS : ui::AX_ACTION_BLUR;
    }
  }

  // Set type-specific information as necessary for actions set above.
  if ([value isKindOfClass:[NSString class]]) {
    data.value = base::SysNSStringToUTF16(value);
  } else if (data.action == ui::AX_ACTION_SET_SELECTION &&
             [value isKindOfClass:[NSValue class]]) {
    NSRange range = [value rangeValue];
    data.anchor_offset = range.location;
    data.focus_offset = NSMaxRange(range);
  }

  if (data.action != ui::AX_ACTION_NONE)
    node_->GetDelegate()->AccessibilityPerformAction(data);

  // TODO(patricialor): Plumb through all the other writable attributes as
  // specified in accessibilityIsAttributeSettable.
}

- (id)accessibilityAttributeValue:(NSString*)attribute {
  if (!node_)
    return nil;  // Return nil when detached. Even for AXRole.

  SEL selector = NSSelectorFromString(attribute);
  if ([self respondsToSelector:selector])
    return [self performSelector:selector];
  return nil;
}

- (id)accessibilityAttributeValue:(NSString*)attribute
                     forParameter:(id)parameter {
  if (!node_)
    return nil;

  SEL selector = NSSelectorFromString([attribute stringByAppendingString:@":"]);
  if ([self respondsToSelector:selector])
    return [self performSelector:selector withObject:parameter];
  return nil;
}

// NSAccessibility attributes. Order them according to
// NSAccessibilityConstants.h, or see https://crbug.com/678898.

- (NSString*)AXRole {
  if (!node_)
    return nil;

  return [[self class] nativeRoleFromAXRole:node_->GetData().role];
}

- (NSString*)AXRoleDescription {
  switch (node_->GetData().role) {
    case ui::AX_ROLE_TAB:
      // There is no NSAccessibilityTabRole or similar (AXRadioButton is used
      // instead). Do the same as NSTabView and put "tab" in the description.
      return [l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB_ROLE_DESCRIPTION)
          lowercaseString];
    case ui::AX_ROLE_DISCLOSURE_TRIANGLE:
      return [l10n_util::GetNSStringWithFixup(
          IDS_ACCNAME_DISCLOSURE_TRIANGLE_ROLE_DESCRIPTION) lowercaseString];
    default:
      break;
  }
  return NSAccessibilityRoleDescription([self AXRole], [self AXSubrole]);
}

- (NSString*)AXSubrole {
  ui::AXRole role = node_->GetData().role;
  switch (role) {
    case ui::AX_ROLE_TEXT_FIELD:
      if (node_->GetData().HasState(ui::AX_STATE_PROTECTED))
        return NSAccessibilitySecureTextFieldSubrole;
      break;
    default:
      break;
  }
  return [AXPlatformNodeCocoa nativeSubroleFromAXRole:role];
}

- (NSString*)AXHelp {
  // TODO(aleventhal) Key shortcuts attribute should eventually get
  // its own field. Follow what WebKit does for aria-keyshortcuts, see
  // https://bugs.webkit.org/show_bug.cgi?id=159215 (WebKit bug).
  NSString* desc = [self getStringAttribute:ui::AX_ATTR_DESCRIPTION];
  NSString* key = [self getStringAttribute:ui::AX_ATTR_KEY_SHORTCUTS];
  if (!desc.length)
    return key.length ? key : @"";
  if (!key.length)
    return desc;
  return [NSString stringWithFormat:@"%@ %@", desc, key];
}

- (id)AXValue {
  ui::AXRole role = node_->GetData().role;
  if (role == ui::AX_ROLE_TAB)
    return [self AXSelected];

  if (ui::IsNameExposedInAXValueForRole(role))
    return [self getStringAttribute:ui::AX_ATTR_NAME];

  return [self getStringAttribute:ui::AX_ATTR_VALUE];
}

- (NSNumber*)AXEnabled {
  return @(node_->GetData().GetIntAttribute(ui::AX_ATTR_RESTRICTION) !=
           ui::AX_RESTRICTION_DISABLED);
}

- (NSNumber*)AXFocused {
  if (node_->GetData().HasState(ui::AX_STATE_FOCUSABLE))
    return
        @(node_->GetDelegate()->GetFocus() == node_->GetNativeViewAccessible());
  return @NO;
}

- (id)AXParent {
  if (!node_)
    return nil;
  return NSAccessibilityUnignoredAncestor(node_->GetParent());
}

- (NSArray*)AXChildren {
  if (!node_)
    return @[];

  int count = node_->GetChildCount();
  NSMutableArray* children = [NSMutableArray arrayWithCapacity:count];
  for (int i = 0; i < count; ++i)
    [children addObject:node_->ChildAtIndex(i)];
  return NSAccessibilityUnignoredChildren(children);
}

- (id)AXWindow {
  return node_->GetDelegate()->GetTopLevelWidget();
}

- (id)AXTopLevelUIElement {
  return [self AXWindow];
}

- (NSValue*)AXPosition {
  return [NSValue valueWithPoint:self.boundsInScreen.origin];
}

- (NSValue*)AXSize {
  return [NSValue valueWithSize:self.boundsInScreen.size];
}

- (NSString*)AXTitle {
  if (ui::IsNameExposedInAXValueForRole(node_->GetData().role))
    return @"";

  return [self getStringAttribute:ui::AX_ATTR_NAME];
}

// Misc attributes.

- (NSNumber*)AXSelected {
  return @(node_->GetData().HasState(ui::AX_STATE_SELECTED));
}

- (NSString*)AXPlaceholderValue {
  return [self getStringAttribute:ui::AX_ATTR_PLACEHOLDER];
}

// Text-specific attributes.

- (NSString*)AXSelectedText {
  NSRange selectedTextRange;
  [[self AXSelectedTextRange] getValue:&selectedTextRange];
  return [[self getAXValueAsString] substringWithRange:selectedTextRange];
}

- (NSValue*)AXSelectedTextRange {
  // Selection might not be supported. Return (NSRange){0,0} in that case.
  int start = 0, end = 0;
  node_->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_START, &start);
  node_->GetIntAttribute(ui::AX_ATTR_TEXT_SEL_END, &end);

  // NSRange cannot represent the direction the text was selected in.
  return [NSValue valueWithRange:{std::min(start, end), abs(end - start)}];
}

- (NSNumber*)AXNumberOfCharacters {
  return @([[self getAXValueAsString] length]);
}

- (NSValue*)AXVisibleCharacterRange {
  return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}];
}

- (NSNumber*)AXInsertionPointLineNumber {
  // Multiline is not supported on views.
  return @0;
}

// Parameterized text-specific attributes.

- (id)AXLineForIndex:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSNumber class]]);
  // Multiline is not supported on views.
  return @0;
}

- (id)AXRangeForLine:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSNumber class]]);
  DCHECK_EQ(0, [parameter intValue]);
  return [NSValue valueWithRange:{0, [[self getAXValueAsString] length]}];
}

- (id)AXStringForRange:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSValue class]]);
  return [[self getAXValueAsString] substringWithRange:[parameter rangeValue]];
}

- (id)AXRangeForPosition:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSValue class]]);
  // TODO(tapted): Hit-test [parameter pointValue] and return an NSRange.
  NOTIMPLEMENTED();
  return nil;
}

- (id)AXRangeForIndex:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSNumber class]]);
  NOTIMPLEMENTED();
  return nil;
}

- (id)AXBoundsForRange:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSValue class]]);
  // TODO(tapted): Provide an accessor on AXPlatformNodeDelegate to obtain this
  // from ui::TextInputClient::GetCompositionCharacterBounds().
  NOTIMPLEMENTED();
  return nil;
}

- (id)AXRTFForRange:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSValue class]]);
  NOTIMPLEMENTED();
  return nil;
}

- (id)AXStyleRangeForIndex:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSNumber class]]);
  NOTIMPLEMENTED();
  return nil;
}

- (id)AXAttributedStringForRange:(id)parameter {
  DCHECK([parameter isKindOfClass:[NSValue class]]);
  base::scoped_nsobject<NSAttributedString> attributedString(
      [[NSAttributedString alloc]
          initWithString:[self AXStringForRange:parameter]]);
  // TODO(tapted): views::WordLookupClient has a way to obtain the actual
  // decorations, and BridgedContentView has a conversion function that creates
  // an NSAttributedString. Refactor things so they can be used here.
  return attributedString.autorelease();
}

@end

namespace ui {

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

// static
AXPlatformNode* AXPlatformNode::FromNativeViewAccessible(
    gfx::NativeViewAccessible accessible) {
  if ([accessible isKindOfClass:[AXPlatformNodeCocoa class]])
    return [accessible node];
  return nullptr;
}

AXPlatformNodeMac::AXPlatformNodeMac() {
}

AXPlatformNodeMac::~AXPlatformNodeMac() {
}

void AXPlatformNodeMac::Destroy() {
  if (native_node_)
    [native_node_ detach];
  AXPlatformNodeBase::Destroy();
}

gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() {
  if (!native_node_)
    native_node_.reset([[AXPlatformNodeCocoa alloc] initWithNode:this]);
  return native_node_.get();
}

void AXPlatformNodeMac::NotifyAccessibilityEvent(ui::AXEvent event_type) {
  GetNativeViewAccessible();
  // Add mappings between ui::AXEvent and NSAccessibility notifications using
  // the EventMap above. This switch contains exceptions to those mappings.
  switch (event_type) {
    case ui::AX_EVENT_TEXT_CHANGED:
      // If the view is a user-editable textfield, this should change the value.
      if (GetData().role == ui::AX_ROLE_TEXT_FIELD) {
        NotifyMacEvent(native_node_, ui::AX_EVENT_VALUE_CHANGED);
        return;
      }
      break;
    default:
      break;
  }
  NotifyMacEvent(native_node_, event_type);
}

int AXPlatformNodeMac::GetIndexInParent() {
  // TODO(dmazzoni): implement this.  http://crbug.com/396137
  return -1;
}

bool IsNameExposedInAXValueForRole(AXRole role) {
  switch (role) {
    case AX_ROLE_LIST_BOX_OPTION:
    case AX_ROLE_LIST_MARKER:
    case AX_ROLE_MENU_LIST_OPTION:
    case AX_ROLE_STATIC_TEXT:
      return true;
    default:
      return false;
  }
}

}  // namespace ui
