// Copyright (c) 2011 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.

#include <execinfo.h>

#import "chrome/browser/accessibility/browser_accessibility_cocoa.h"

#include "base/string16.h"
#include "base/sys_string_conversions.h"
#include "chrome/browser/renderer_host/render_widget_host_view_mac.h"
#include "grit/webkit_strings.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebRect.h"
#include "ui/base/l10n/l10n_util_mac.h"

namespace {

// Returns an autoreleased copy of the WebAccessibility's attribute.
NSString* NSStringForWebAccessibilityAttribute(
    const std::map<int32, string16>& attributes,
    WebAccessibility::Attribute attribute) {
  std::map<int32, string16>::const_iterator iter =
      attributes.find(attribute);
  NSString* returnValue = @"";
  if (iter != attributes.end()) {
    returnValue = base::SysUTF16ToNSString(iter->second);
  }
  return returnValue;
}

struct RoleEntry {
  WebAccessibility::Role value;
  NSString* string;
};

static const RoleEntry roles[] = {
  { WebAccessibility::ROLE_NONE, NSAccessibilityUnknownRole },
  { WebAccessibility::ROLE_BUTTON, NSAccessibilityButtonRole },
  { WebAccessibility::ROLE_CHECKBOX, NSAccessibilityCheckBoxRole },
  { WebAccessibility::ROLE_COLUMN, NSAccessibilityColumnRole },
  { WebAccessibility::ROLE_GRID, NSAccessibilityGridRole },
  { WebAccessibility::ROLE_GROUP, NSAccessibilityGroupRole },
  { WebAccessibility::ROLE_HEADING, @"AXHeading" },
  { WebAccessibility::ROLE_IGNORED, NSAccessibilityUnknownRole },
  { WebAccessibility::ROLE_IMAGE, NSAccessibilityImageRole },
  { WebAccessibility::ROLE_LINK, NSAccessibilityLinkRole },
  { WebAccessibility::ROLE_LIST, NSAccessibilityListRole },
  { WebAccessibility::ROLE_RADIO_BUTTON, NSAccessibilityRadioButtonRole },
  { WebAccessibility::ROLE_RADIO_GROUP, NSAccessibilityRadioGroupRole },
  { WebAccessibility::ROLE_ROW, NSAccessibilityRowRole },
  { WebAccessibility::ROLE_SCROLLAREA, NSAccessibilityScrollAreaRole },
  { WebAccessibility::ROLE_SCROLLBAR, NSAccessibilityScrollBarRole },
  { WebAccessibility::ROLE_STATIC_TEXT, NSAccessibilityStaticTextRole },
  { WebAccessibility::ROLE_TABLE, NSAccessibilityTableRole },
  { WebAccessibility::ROLE_TAB_GROUP, NSAccessibilityTabGroupRole },
  { WebAccessibility::ROLE_TEXT_FIELD, NSAccessibilityTextFieldRole },
  { WebAccessibility::ROLE_TEXTAREA, NSAccessibilityTextAreaRole },
  { WebAccessibility::ROLE_WEB_AREA, @"AXWebArea" },
  { WebAccessibility::ROLE_WEBCORE_LINK, NSAccessibilityLinkRole },
};

// GetState checks the bitmask used in webaccessibility.h to check
// if the given state was set on the accessibility object.
bool GetState(BrowserAccessibility* accessibility, int state) {
  return ((accessibility->state() >> state) & 1);
}

} // namespace

@implementation BrowserAccessibilityCocoa

- (id)initWithObject:(BrowserAccessibility*)accessibility
            delegate:(id<BrowserAccessibilityDelegateCocoa>)delegate {
  if ((self = [super init])) {
    browserAccessibility_ = accessibility;
    delegate_ = delegate;
  }
  return self;
}

// Deletes our associated BrowserAccessibilityMac.
- (void)dealloc {
  if (browserAccessibility_) {
    delete browserAccessibility_;
    browserAccessibility_ = NULL;
  }

  [super dealloc];
}

// Returns an array of BrowserAccessibilityCocoa objects, representing the
// accessibility children of this object.
- (NSArray*)children {
  if (!children_.get()) {
    children_.reset([[NSMutableArray alloc]
        initWithCapacity:browserAccessibility_->child_count()] );
    for (uint32 index = 0;
         index < browserAccessibility_->child_count();
         ++index) {
      BrowserAccessibilityCocoa* child =
          browserAccessibility_->GetChild(index)->toBrowserAccessibilityCocoa();
      if ([child isIgnored])
        [children_ addObjectsFromArray:[child children]];
      else
        [children_ addObject:child];
    }
  }
  return children_;
}

- (void)childrenChanged {
  if (![self isIgnored]) {
    children_.reset();
  } else {
    [browserAccessibility_->parent()->toBrowserAccessibilityCocoa()
       childrenChanged];
  }
}

// Returns whether or not this node should be ignored in the
// accessibility tree.
- (BOOL)isIgnored {
  return [self role] == NSAccessibilityUnknownRole;
}

// The origin of this accessibility object in the page's document.
// This is relative to webkit's top-left origin, not Cocoa's
// bottom-left origin.
- (NSPoint)origin {
  return NSMakePoint(browserAccessibility_->location().x(),
                     browserAccessibility_->location().y());
}

// Returns a string indicating the role of this object.
- (NSString*)role {
  WebAccessibility::Role value =
      static_cast<WebAccessibility::Role>( browserAccessibility_->role());

  // Roles that we only determine at runtime.
  if (value == WebAccessibility::ROLE_TEXT_FIELD &&
      GetState(browserAccessibility_, WebAccessibility::STATE_PROTECTED)) {
    return @"AXSecureTextField";
  }

  NSString* role = NSAccessibilityUnknownRole;
  const size_t numRoles = sizeof(roles) / sizeof(roles[0]);
  for (size_t i = 0; i < numRoles; ++i) {
    if (roles[i].value == value) {
      role = roles[i].string;
      break;
    }
  }

  return role;
}

// Returns a string indicating the role description of this object.
- (NSString*)roleDescription {
  // The following descriptions are specific to webkit.
  if ([[self role] isEqualToString:@"AXWebArea"])
    return l10n_util::GetNSString(IDS_AX_ROLE_WEB_AREA);

  if ([[self role] isEqualToString:@"NSAccessibilityLinkRole"])
    return l10n_util::GetNSString(IDS_AX_ROLE_LINK);

  if ([[self role] isEqualToString:@"AXHeading"])
    return l10n_util::GetNSString(IDS_AX_ROLE_HEADING);

  return NSAccessibilityRoleDescription([self role], nil);
}

// Returns the size of this object.
- (NSSize)size {
  return NSMakeSize(browserAccessibility_->location().width(),
                    browserAccessibility_->location().height());
}

// Returns the accessibility value for the given attribute.  If the value isn't
// supported this will return nil.
- (id)accessibilityAttributeValue:(NSString*)attribute {
  if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) {
    return [self role];
  }
  if ([attribute isEqualToString:NSAccessibilityDescriptionAttribute]) {
    return NSStringForWebAccessibilityAttribute(
        browserAccessibility_->attributes(),
        WebAccessibility::ATTR_DESCRIPTION);
  }
  if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) {
    return [NSValue valueWithPoint:[delegate_ accessibilityPointInScreen:self]];
  }
  if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) {
    return [NSValue valueWithSize:[self size]];
  }
  if ([attribute isEqualToString:NSAccessibilityTopLevelUIElementAttribute] ||
      [attribute isEqualToString:NSAccessibilityWindowAttribute]) {
    return [delegate_ window];
  }
  if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) {
    return [self children];
  }
  if ([attribute isEqualToString:NSAccessibilityParentAttribute]) {
    // A nil parent means we're the root.
    if (browserAccessibility_->parent()) {
      return NSAccessibilityUnignoredAncestor(
          browserAccessibility_->parent()->toBrowserAccessibilityCocoa());
    } else {
      // Hook back up to RenderWidgetHostViewCocoa.
      return browserAccessibility_->manager()->GetParentView();
    }
  }
  if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) {
    return base::SysUTF16ToNSString(browserAccessibility_->name());
  }
  if ([attribute isEqualToString:NSAccessibilityHelpAttribute]) {
    return NSStringForWebAccessibilityAttribute(
        browserAccessibility_->attributes(),
        WebAccessibility::ATTR_HELP);
  }
  if ([attribute isEqualToString:NSAccessibilityValueAttribute]) {
    // WebCore uses an attachmentView to get the below behavior.
    // We do not have any native views backing this object, so need
    // to approximate Cocoa ax behavior best as we can.
    if ([self role] == @"AXHeading") {
      NSString* headingLevel =
          NSStringForWebAccessibilityAttribute(
              browserAccessibility_->attributes(),
              WebAccessibility::ATTR_HTML_TAG);
      if ([headingLevel length] >= 2) {
        return [NSNumber numberWithInt:
            [[headingLevel substringFromIndex:1] intValue]];
      }
    } else if ([self role] == NSAccessibilityButtonRole) {
      // AXValue does not make sense for pure buttons.
      return @"";
    } else if ([self role] == NSAccessibilityCheckBoxRole ||
               [self role] == NSAccessibilityRadioButtonRole) {
      return [NSNumber numberWithInt:GetState(
          browserAccessibility_, WebAccessibility::STATE_CHECKED) ? 1 : 0];
    } else {
      return base::SysUTF16ToNSString(browserAccessibility_->value());
    }
  }
  if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute]) {
    return [self roleDescription];
  }
  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
    NSNumber* ret = [NSNumber numberWithBool:
        GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSED)];
    return ret;
  }
  if ([attribute isEqualToString:NSAccessibilityEnabledAttribute]) {
    return [NSNumber numberWithBool:
        !GetState(browserAccessibility_, WebAccessibility::STATE_UNAVAILABLE)];
  }
  if ([attribute isEqualToString:@"AXVisited"]) {
    return [NSNumber numberWithBool:
        GetState(browserAccessibility_, WebAccessibility::STATE_TRAVERSED)];
  }

  // AXWebArea attributes.
  if ([attribute isEqualToString:@"AXLoaded"])
    return [NSNumber numberWithBool:YES];
  if ([attribute isEqualToString:@"AXURL"]) {
    WebAccessibility::Attribute urlAttribute =
        [[self role] isEqualToString:@"AXWebArea"] ?
            WebAccessibility::ATTR_DOC_URL :
            WebAccessibility::ATTR_URL;
    return NSStringForWebAccessibilityAttribute(
        browserAccessibility_->attributes(),
        urlAttribute);
  }

  // Text related attributes.
  if ([attribute isEqualToString:
      NSAccessibilityNumberOfCharactersAttribute]) {
    return [NSNumber numberWithInt:browserAccessibility_->value().length()];
  }
  if ([attribute isEqualToString:
      NSAccessibilityVisibleCharacterRangeAttribute]) {
    return [NSValue valueWithRange:
        NSMakeRange(0, browserAccessibility_->value().length())];
  }

  int selStart, selEnd;
  if (browserAccessibility_->
          GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_START, &selStart) &&
      browserAccessibility_->
          GetAttributeAsInt(WebAccessibility::ATTR_TEXT_SEL_END, &selEnd)) {
    if (selStart > selEnd)
      std::swap(selStart, selEnd);
    int selLength = selEnd - selStart;
    if ([attribute isEqualToString:
        NSAccessibilityInsertionPointLineNumberAttribute]) {
      return [NSNumber numberWithInt:0];
    }
    if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) {
      return base::SysUTF16ToNSString(browserAccessibility_->value().substr(
          selStart, selLength));
    }
    if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) {
      return [NSValue valueWithRange:NSMakeRange(selStart, selLength)];
    }
  }
  return nil;
}

// Returns an array of action names that this object will respond to.
- (NSArray*)accessibilityActionNames {
  NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease];

  // General actions.
  [ret addObject:NSAccessibilityShowMenuAction];

  // TODO(dtseng): this should only get set when there's a default action.
  if ([self role] != NSAccessibilityStaticTextRole &&
      [self role] != NSAccessibilityTextAreaRole &&
      [self role] != NSAccessibilityTextFieldRole) {
    [ret addObject:NSAccessibilityPressAction];
  }

  return ret;
}

// Returns a sub-array of values for the given attribute value, starting at
// index, with up to maxCount items.  If the given index is out of bounds,
// or there are no values for the given attribute, it will return nil.
// This method is used for querying subsets of values, without having to
// return a large set of data, such as elements with a large number of
// children.
- (NSArray*)accessibilityArrayAttributeValues:(NSString*)attribute
                                        index:(NSUInteger)index
                                     maxCount:(NSUInteger)maxCount {
  NSArray* fullArray = [self accessibilityAttributeValue:attribute];
  if (!fullArray)
    return nil;
  NSUInteger arrayCount = [fullArray count];
  if (index >= arrayCount)
    return nil;
  NSRange subRange;
  if ((index + maxCount) > arrayCount) {
    subRange = NSMakeRange(index, arrayCount - index);
  } else {
    subRange = NSMakeRange(index, maxCount);
  }
  return [fullArray subarrayWithRange:subRange];
}

// Returns the count of the specified accessibility array attribute.
- (NSUInteger)accessibilityArrayAttributeCount:(NSString*)attribute {
  NSArray* fullArray = [self accessibilityAttributeValue:attribute];
  return [fullArray count];
}

// Returns the list of accessibility attributes that this object supports.
- (NSArray*)accessibilityAttributeNames {
  NSMutableArray* ret = [[NSMutableArray alloc] init];

  // General attributes.
  [ret addObjectsFromArray:[NSArray arrayWithObjects:
      NSAccessibilityChildrenAttribute,
      NSAccessibilityDescriptionAttribute,
      NSAccessibilityEnabledAttribute,
      NSAccessibilityFocusedAttribute,
      NSAccessibilityHelpAttribute,
      NSAccessibilityParentAttribute,
      NSAccessibilityPositionAttribute,
      NSAccessibilityRoleAttribute,
      NSAccessibilityRoleDescriptionAttribute,
      NSAccessibilitySizeAttribute,
      NSAccessibilityTitleAttribute,
      NSAccessibilityTopLevelUIElementAttribute,
      NSAccessibilityValueAttribute,
      NSAccessibilityWindowAttribute,
      @"AXURL",
      @"AXVisited",
      nil]];

  // Specific role attributes.
  if ([self role] == @"AXWebArea") {
    [ret addObjectsFromArray:[NSArray arrayWithObjects:
        @"AXLoaded",
        nil]];
  }

  if ([self role] == NSAccessibilityTextFieldRole) {
    [ret addObjectsFromArray:[NSArray arrayWithObjects:
        NSAccessibilityInsertionPointLineNumberAttribute,
        NSAccessibilityNumberOfCharactersAttribute,
        NSAccessibilitySelectedTextAttribute,
        NSAccessibilitySelectedTextRangeAttribute,
        NSAccessibilityVisibleCharacterRangeAttribute,
        nil]];
  }
  return ret;
}

// Returns the index of the child in this objects array of children.
- (NSUInteger)accessibilityGetIndexOf:(id)child {
  NSUInteger index = 0;
  for (BrowserAccessibilityCocoa* childToCheck in [self children]) {
    if ([child isEqual:childToCheck])
      return index;
    ++index;
  }
  return NSNotFound;
}

// Returns whether or not the specified attribute can be set by the
// accessibility API via |accessibilitySetValue:forAttribute:|.
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute])
    return GetState(browserAccessibility_, WebAccessibility::STATE_FOCUSABLE);
  if ([attribute isEqualToString:NSAccessibilityValueAttribute])
    return !GetState(browserAccessibility_, WebAccessibility::STATE_READONLY);
  return NO;
}

// Returns whether or not this object should be ignored in the accessibilty
// tree.
- (BOOL)accessibilityIsIgnored {
  return [self isIgnored];
}

// Performs the given accessibilty action on the webkit accessibility object
// that backs this object.
- (void)accessibilityPerformAction:(NSString*)action {
  // TODO(feldstein): Support more actions.
  if ([action isEqualToString:NSAccessibilityPressAction]) {
    [delegate_ doDefaultAction:browserAccessibility_->renderer_id()];
  } else if ([action isEqualToString:NSAccessibilityShowMenuAction]) {
    // TODO(dtseng): implement.
  }
}

// Returns the description of the given action.
- (NSString*)accessibilityActionDescription:(NSString*)action {
  return NSAccessibilityActionDescription(action);
}

// Sets an override value for a specific accessibility attribute.
// This class does not support this.
- (BOOL)accessibilitySetOverrideValue:(id)value
                         forAttribute:(NSString*)attribute {
  return NO;
}

// Sets the value for an accessibility attribute via the accessibility API.
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
  if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) {
    NSNumber* focusedNumber = value;
    BOOL focused = [focusedNumber intValue];
    [delegate_ setAccessibilityFocus:focused
                     accessibilityId:browserAccessibility_->renderer_id()];
  }
}

// Returns the deepest accessibility child that should not be ignored.
// It is assumed that the hit test has been narrowed down to this object
// or one of its children, so this will never return nil.
- (id)accessibilityHitTest:(NSPoint)point {
  id hit = self;
  for (id child in [self children]) {
    NSPoint origin = [child origin];
    NSSize size = [child size];
    NSRect rect;
    rect.origin = origin;
    rect.size = size;
    if (NSPointInRect(point, rect)) {
      hit = child;
      id childResult = [child accessibilityHitTest:point];
      if (![childResult accessibilityIsIgnored]) {
        hit = childResult;
        break;
      }
    }
  }
  return NSAccessibilityUnignoredAncestor(hit);
}

- (BOOL)isEqual:(id)object {
  if (![object isKindOfClass:[BrowserAccessibilityCocoa class]])
    return NO;
  return ([self hash] == [object hash]);
}

- (NSUInteger)hash {
  // Potentially called during dealloc.
  if (!browserAccessibility_)
    return [super hash];
  return browserAccessibility_->renderer_id();
}

@end