// Copyright 2017 The Chromium Embedded Framework Authors. Portions copyright
// 2013 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.

// Sample implementation for the NSAccessibility protocol for interacting with
// VoiceOver and other accessibility clients.

#include "tests/cefclient/browser/osr_accessibility_node.h"

#import <AppKit/NSAccessibility.h>
#import <Cocoa/Cocoa.h>

#include "tests/cefclient/browser/osr_accessibility_helper.h"

namespace {

NSString* AxRoleToNSAxRole(const std::string& role_string) {
  if (role_string == "abbr")
    return NSAccessibilityGroupRole;
  if (role_string == "alertDialog")
    return NSAccessibilityGroupRole;
  if (role_string == "alert")
    return NSAccessibilityGroupRole;
  if (role_string == "annotation")
    return NSAccessibilityUnknownRole;
  if (role_string == "application")
    return NSAccessibilityGroupRole;
  if (role_string == "article")
    return NSAccessibilityGroupRole;
  if (role_string == "audio")
    return NSAccessibilityGroupRole;
  if (role_string == "banner")
    return NSAccessibilityGroupRole;
  if (role_string == "blockquote")
    return NSAccessibilityGroupRole;
  if (role_string == "busyIndicator")
    return NSAccessibilityBusyIndicatorRole;
  if (role_string == "button")
    return NSAccessibilityButtonRole;
  if (role_string == "buttonDropDown")
    return NSAccessibilityButtonRole;
  if (role_string == "canvas")
    return NSAccessibilityImageRole;
  if (role_string == "caption")
    return NSAccessibilityGroupRole;
  if (role_string == "checkBox")
    return NSAccessibilityCheckBoxRole;
  if (role_string == "colorWell")
    return NSAccessibilityColorWellRole;
  if (role_string == "column")
    return NSAccessibilityColumnRole;
  if (role_string == "comboBox")
    return NSAccessibilityComboBoxRole;
  if (role_string == "complementary")
    return NSAccessibilityGroupRole;
  if (role_string == "contentInfo")
    return NSAccessibilityGroupRole;
  if (role_string == "definition")
    return NSAccessibilityGroupRole;
  if (role_string == "descriptionListDetail")
    return NSAccessibilityGroupRole;
  if (role_string == "descriptionList")
    return NSAccessibilityListRole;
  if (role_string == "descriptionListTerm")
    return NSAccessibilityGroupRole;
  if (role_string == "details")
    return NSAccessibilityGroupRole;
  if (role_string == "dialog")
    return NSAccessibilityGroupRole;
  if (role_string == "directory")
    return NSAccessibilityListRole;
  if (role_string == "disclosureTriangle")
    return NSAccessibilityDisclosureTriangleRole;
  if (role_string == "div")
    return NSAccessibilityGroupRole;
  if (role_string == "document")
    return NSAccessibilityGroupRole;
  if (role_string == "embeddedObject")
    return NSAccessibilityGroupRole;
  if (role_string == "figcaption")
    return NSAccessibilityGroupRole;
  if (role_string == "figure")
    return NSAccessibilityGroupRole;
  if (role_string == "footer")
    return NSAccessibilityGroupRole;
  if (role_string == "form")
    return NSAccessibilityGroupRole;
  if (role_string == "genericContainer")
    return NSAccessibilityGroupRole;
  if (role_string == "grid")
    return NSAccessibilityGroupRole;
  if (role_string == "group")
    return NSAccessibilityGroupRole;
  if (role_string == "iframe")
    return NSAccessibilityGroupRole;
  if (role_string == "iframePresentational")
    return NSAccessibilityGroupRole;
  if (role_string == "ignored")
    return NSAccessibilityUnknownRole;
  if (role_string == "imageMapLink")
    return NSAccessibilityLinkRole;
  if (role_string == "imageMap")
    return NSAccessibilityGroupRole;
  if (role_string == "image")
    return NSAccessibilityImageRole;
  if (role_string == "labelText")
    return NSAccessibilityGroupRole;
  if (role_string == "legend")
    return NSAccessibilityGroupRole;
  if (role_string == "link")
    return NSAccessibilityLinkRole;
  if (role_string == "listBoxOption")
    return NSAccessibilityStaticTextRole;
  if (role_string == "listBox")
    return NSAccessibilityListRole;
  if (role_string == "listItem")
    return NSAccessibilityGroupRole;
  if (role_string == "list")
    return NSAccessibilityListRole;
  if (role_string == "log")
    return NSAccessibilityGroupRole;
  if (role_string == "main")
    return NSAccessibilityGroupRole;
  if (role_string == "mark")
    return NSAccessibilityGroupRole;
  if (role_string == "marquee")
    return NSAccessibilityGroupRole;
  if (role_string == "math")
    return NSAccessibilityGroupRole;
  if (role_string == "menu")
    return NSAccessibilityMenuRole;
  if (role_string == "menuBar")
    return NSAccessibilityMenuBarRole;
  if (role_string == "menuButton")
    return NSAccessibilityButtonRole;
  if (role_string == "menuItem")
    return NSAccessibilityMenuItemRole;
  if (role_string == "menuItemCheckBox")
    return NSAccessibilityMenuItemRole;
  if (role_string == "menuItemRadio")
    return NSAccessibilityMenuItemRole;
  if (role_string == "menuListOption")
    return NSAccessibilityMenuItemRole;
  if (role_string == "menuListPopup")
    return NSAccessibilityUnknownRole;
  if (role_string == "meter")
    return NSAccessibilityProgressIndicatorRole;
  if (role_string == "navigation")
    return NSAccessibilityGroupRole;
  if (role_string == "note")
    return NSAccessibilityGroupRole;
  if (role_string == "outline")
    return NSAccessibilityOutlineRole;
  if (role_string == "paragraph")
    return NSAccessibilityGroupRole;
  if (role_string == "popUpButton")
    return NSAccessibilityPopUpButtonRole;
  if (role_string == "pre")
    return NSAccessibilityGroupRole;
  if (role_string == "presentational")
    return NSAccessibilityGroupRole;
  if (role_string == "progressIndicator")
    return NSAccessibilityProgressIndicatorRole;
  if (role_string == "radioButton")
    return NSAccessibilityRadioButtonRole;
  if (role_string == "radioGroup")
    return NSAccessibilityRadioGroupRole;
  if (role_string == "region")
    return NSAccessibilityGroupRole;
  if (role_string == "row")
    return NSAccessibilityRowRole;
  if (role_string == "ruler")
    return NSAccessibilityRulerRole;
  if (role_string == "scrollBar")
    return NSAccessibilityScrollBarRole;
  if (role_string == "search")
    return NSAccessibilityGroupRole;
  if (role_string == "searchBox")
    return NSAccessibilityTextFieldRole;
  if (role_string == "slider")
    return NSAccessibilitySliderRole;
  if (role_string == "sliderThumb")
    return NSAccessibilityValueIndicatorRole;
  if (role_string == "spinButton")
    return NSAccessibilityIncrementorRole;
  if (role_string == "splitter")
    return NSAccessibilitySplitterRole;
  if (role_string == "staticText")
    return NSAccessibilityStaticTextRole;
  if (role_string == "status")
    return NSAccessibilityGroupRole;
  if (role_string == "svgRoot")
    return NSAccessibilityGroupRole;
  if (role_string == "switch")
    return NSAccessibilityCheckBoxRole;
  if (role_string == "tabGroup")
    return NSAccessibilityTabGroupRole;
  if (role_string == "tabList")
    return NSAccessibilityTabGroupRole;
  if (role_string == "tabPanel")
    return NSAccessibilityGroupRole;
  if (role_string == "tab")
    return NSAccessibilityRadioButtonRole;
  if (role_string == "tableHeaderContainer")
    return NSAccessibilityGroupRole;
  if (role_string == "table")
    return NSAccessibilityTableRole;
  if (role_string == "textField")
    return NSAccessibilityTextFieldRole;
  if (role_string == "time")
    return NSAccessibilityGroupRole;
  if (role_string == "timer")
    return NSAccessibilityGroupRole;
  if (role_string == "toggleButton")
    return NSAccessibilityCheckBoxRole;
  if (role_string == "toolbar")
    return NSAccessibilityToolbarRole;
  if (role_string == "treeGrid")
    return NSAccessibilityTableRole;
  if (role_string == "treeItem")
    return NSAccessibilityRowRole;
  if (role_string == "tree")
    return NSAccessibilityOutlineRole;
  if (role_string == "unknown")
    return NSAccessibilityUnknownRole;
  if (role_string == "tooltip")
    return NSAccessibilityGroupRole;
  if (role_string == "video")
    return NSAccessibilityGroupRole;
  if (role_string == "window")
    return NSAccessibilityWindowRole;
  return [NSString stringWithUTF8String:role_string.c_str()];
}

inline int MiddleX(const CefRect& rect) {
  return rect.x + rect.width / 2;
}

inline int MiddleY(const CefRect& rect) {
  return rect.y + rect.height / 2;
}

}  // namespace

// OsrAXNodeObject is sample implementation for the NSAccessibility protocol
// for interacting with VoiceOver and other accessibility clients.
@interface OsrAXNodeObject : NSObject {
  // OsrAXNode* proxy object
  client::OsrAXNode* node_;
  CefNativeAccessible* parent_;
}

- (id)init:(client::OsrAXNode*)node;
+ (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node;
@end

@implementation OsrAXNodeObject
- (id)init:(client::OsrAXNode*)node {
  node_ = node;
  parent_ = node_->GetParentAccessibleObject();
  if (!parent_) {
    parent_ = node_->GetWindowHandle();
  }
  return self;
}

+ (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node {
  // We manage the release ourself
  return [[OsrAXNodeObject alloc] init:node];
}

- (BOOL)isEqual:(id)object {
  if ([object isKindOfClass:[OsrAXNodeObject self]]) {
    OsrAXNodeObject* other = object;
    return (node_ == other->node_);
  } else {
    return NO;
  }
}

// Utility methods to map AX information received from renderer
// to platform properties
- (NSString*)axRole {
  // Get the Role from CefAccessibilityHelper and Map to NSRole
  return AxRoleToNSAxRole(node_->AxRole());
}

- (NSString*)axDescription {
  std::string desc = node_->AxDescription();
  return [NSString stringWithUTF8String:desc.c_str()];
}

- (NSString*)axName {
  std::string desc = node_->AxName();
  return [NSString stringWithUTF8String:desc.c_str()];
}

- (NSString*)axValue {
  std::string desc = node_->AxValue();
  return [NSString stringWithUTF8String:desc.c_str()];
}

- (void)doMouseClick:(cef_mouse_button_type_t)type {
  CefRefPtr<CefBrowser> browser = node_->GetBrowser();
  if (browser) {
    CefMouseEvent mouse_event;
    const CefRect& rect = node_->AxLocation();
    mouse_event.x = MiddleX(rect);
    mouse_event.y = MiddleY(rect);

    mouse_event.modifiers = 0;
    browser->GetHost()->SendMouseClickEvent(mouse_event, type, false, 1);
    browser->GetHost()->SendMouseClickEvent(mouse_event, type, true, 1);
  }
}

- (NSMutableArray*)getKids {
  int numChilds = node_->GetChildCount();
  if (numChilds > 0) {
    NSMutableArray* kids = [NSMutableArray arrayWithCapacity:numChilds];
    for (int index = 0; index < numChilds; index++) {
      client::OsrAXNode* child = node_->ChildAtIndex(index);
      [kids addObject:child ? CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(
                                  child->GetNativeAccessibleObject(node_))
                            : nil];
    }
    return kids;
  }
  return nil;
}

- (NSPoint)position {
  CefRect cef_rect = node_->AxLocation();
  NSPoint origin = NSMakePoint(cef_rect.x, cef_rect.y);
  NSSize size = NSMakeSize(cef_rect.width, cef_rect.height);

  NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(node_->GetWindowHandle());
  origin.y = NSHeight([view bounds]) - origin.y;
  NSPoint originInWindow = [view convertPoint:origin toView:nil];

  NSRect point_rect = NSMakeRect(originInWindow.x, originInWindow.y, 0, 0);
  NSPoint originInScreen =
      [[view window] convertRectToScreen:point_rect].origin;

  originInScreen.y = originInScreen.y - size.height;
  return originInScreen;
}

- (NSSize)size {
  CefRect cef_rect = node_->AxLocation();
  NSRect rect =
      NSMakeRect(cef_rect.x, cef_rect.y, cef_rect.width, cef_rect.height);
  NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(node_->GetWindowHandle());
  rect = [[view window] convertRectToScreen:rect];
  return rect.size;
}

//
// accessibility protocol
//

// attributes

- (BOOL)accessibilityIsIgnored {
  return NO;
}

- (NSArray*)accessibilityAttributeNames {
  static NSArray* attributes = nil;
  if (attributes == nil) {
    attributes = [[NSArray alloc]
        initWithObjects:NSAccessibilityRoleAttribute,
                        NSAccessibilityRoleDescriptionAttribute,
                        NSAccessibilityChildrenAttribute,
                        NSAccessibilityValueAttribute,
                        NSAccessibilityTitleAttribute,
                        NSAccessibilityDescriptionAttribute,
                        NSAccessibilityFocusedAttribute,
                        NSAccessibilityParentAttribute,
                        NSAccessibilityWindowAttribute,
                        NSAccessibilityTopLevelUIElementAttribute,
                        NSAccessibilityPositionAttribute,
                        NSAccessibilitySizeAttribute, nil];
  }
  return attributes;
}

- (id)accessibilityAttributeValue:(NSString*)attribute {
  NSObject* typed_parent = CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(parent_);
  if (!node_)
    return nil;
  if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) {
    return [self axRole];
  } else if ([attribute
                 isEqualToString:NSAccessibilityRoleDescriptionAttribute]) {
    return NSAccessibilityRoleDescription([self axRole], nil);
  } else if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
    // Just check if the app thinks we're focused.
    id focusedElement = [NSApp
        accessibilityAttributeValue:NSAccessibilityFocusedUIElementAttribute];
    return [NSNumber numberWithBool:[focusedElement isEqual:self]];
  } else if ([attribute isEqualToString:NSAccessibilityParentAttribute]) {
    return NSAccessibilityUnignoredAncestor(typed_parent);
  } else if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) {
    return NSAccessibilityUnignoredChildren([self getKids]);
  } else if ([attribute isEqualToString:NSAccessibilityWindowAttribute]) {
    // We're in the same window as our parent.
    return [typed_parent
        accessibilityAttributeValue:NSAccessibilityWindowAttribute];
  } else if ([attribute
                 isEqualToString:NSAccessibilityTopLevelUIElementAttribute]) {
    // We're in the same top level element as our parent.
    return [typed_parent
        accessibilityAttributeValue:NSAccessibilityTopLevelUIElementAttribute];
  } else if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) {
    return [NSValue valueWithPoint:[self position]];
  } else if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) {
    return [NSValue valueWithSize:[self size]];
  } else if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) {
    return [self axDescription];
  } else if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
    return [self axValue];
  } else if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) {
    return [self axName];
  }
  return nil;
}

- (id)accessibilityHitTest:(NSPoint)point {
  return NSAccessibilityUnignoredAncestor(self);
}

- (NSArray*)accessibilityActionNames {
  return [NSArray arrayWithObject:NSAccessibilityPressAction];
}

- (NSString*)accessibilityActionDescription:(NSString*)action {
  return NSAccessibilityActionDescription(action);
}

- (void)accessibilityPerformAction:(NSString*)action {
  if ([action isEqualToString:NSAccessibilityPressAction]) {
    // Do Click on Default action
    [self doMouseClick:MBT_LEFT];
  } else if ([action isEqualToString:NSAccessibilityShowMenuAction]) {
    // Right click for Context Menu
    [self doMouseClick:MBT_RIGHT];
  }
}

- (id)accessibilityFocusedUIElement {
  return NSAccessibilityUnignoredAncestor(self);
}

- (BOOL)accessibilityNotifiesWhenDestroyed {
  // Indicate that BrowserAccessibilityCocoa will post a notification when it's
  // destroyed (see -detach). This allows VoiceOver to do some internal things
  // more efficiently.
  return YES;
}

@end

namespace client {

void OsrAXNode::NotifyAccessibilityEvent(std::string event_type) const {
  NSView* view = CAST_CEF_WINDOW_HANDLE_TO_NSVIEW(GetWindowHandle());
  if (event_type == "focus") {
    NSAccessibilityPostNotification(
        view, NSAccessibilityFocusedUIElementChangedNotification);
  } else if (event_type == "textChanged") {
    NSAccessibilityPostNotification(view,
                                    NSAccessibilityTitleChangedNotification);
  } else if (event_type == "valueChanged") {
    NSAccessibilityPostNotification(view,
                                    NSAccessibilityValueChangedNotification);
  } else if (event_type == "textSelectionChanged") {
    NSAccessibilityPostNotification(view,
                                    NSAccessibilityValueChangedNotification);
  }
}

void OsrAXNode::Destroy() {
  if (platform_accessibility_) {
    NSAccessibilityPostNotification(
        CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(platform_accessibility_),
        NSAccessibilityUIElementDestroyedNotification);
  }

  delete this;
}

// Create and return NSAccessibility Implementation Object for Mac
CefNativeAccessible* OsrAXNode::GetNativeAccessibleObject(
    client::OsrAXNode* parent) {
  if (!platform_accessibility_) {
    platform_accessibility_ = CAST_NSOBJECT_TO_CEF_NATIVE_ACCESSIBLE(
        [OsrAXNodeObject elementWithNode:this]);
    SetParent(parent);
  }
  return platform_accessibility_;
}

}  // namespace client
