// Copyright 2013 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview A collection of JavaScript utilities used to simplify working
 * with ARIA (http://www.w3.org/TR/wai-aria).
 * @author clchen@google.com (Charles L. Chen)
 */


goog.provide('cvox.AriaUtil');
goog.require('cvox.AbstractEarcons');
goog.require('cvox.ChromeVox');
goog.require('cvox.NodeState');
goog.require('cvox.NodeStateUtil');


/**
 * Create the namespace
 * @constructor
 */
cvox.AriaUtil = function() {
};


/**
 * A constant indicating no role name.
 * @type {string}
 */
cvox.AriaUtil.NO_ROLE_NAME = ' ';

/**
 * A mapping from ARIA role names to their message ids.
 * Note: If you are adding a new mapping, the new message identifier needs a
 * corresponding braille message. For example, a message id 'tag_button'
 * requires another message 'tag_button_brl' within messages.js.
 * @type {Object.<string, string>}
 */
cvox.AriaUtil.WIDGET_ROLE_TO_NAME = {
  'alert' : 'aria_role_alert',
  'alertdialog' : 'aria_role_alertdialog',
  'button' : 'aria_role_button',
  'checkbox' : 'aria_role_checkbox',
  'columnheader' : 'aria_role_columnheader',
  'combobox' : 'aria_role_combobox',
  'dialog' : 'aria_role_dialog',
  'grid' : 'aria_role_grid',
  'gridcell' : 'aria_role_gridcell',
  'link' : 'aria_role_link',
  'listbox' : 'aria_role_listbox',
  'log' : 'aria_role_log',
  'marquee' : 'aria_role_marquee',
  'menu' : 'aria_role_menu',
  'menubar' : 'aria_role_menubar',
  'menuitem' : 'aria_role_menuitem',
  'menuitemcheckbox' : 'aria_role_menuitemcheckbox',
  'menuitemradio' : 'aria_role_menuitemradio',
  'option' : cvox.AriaUtil.NO_ROLE_NAME,
  'progressbar' : 'aria_role_progressbar',
  'radio' : 'aria_role_radio',
  'radiogroup' : 'aria_role_radiogroup',
  'rowheader' : 'aria_role_rowheader',
  'scrollbar' : 'aria_role_scrollbar',
  'slider' : 'aria_role_slider',
  'spinbutton' : 'aria_role_spinbutton',
  'status' : 'aria_role_status',
  'tab' : 'aria_role_tab',
  'tabpanel' : 'aria_role_tabpanel',
  'textbox' : 'aria_role_textbox',
  'timer' : 'aria_role_timer',
  'toolbar' : 'aria_role_toolbar',
  'tooltip' : 'aria_role_tooltip',
  'treeitem' : 'aria_role_treeitem'
};


/**
 * Note: If you are adding a new mapping, the new message identifier needs a
 * corresponding braille message. For example, a message id 'tag_button'
 * requires another message 'tag_button_brl' within messages.js.
 * @type {Object.<string, string>}
 */
cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME = {
  'article' : 'aria_role_article',
  'application' : 'aria_role_application',
  'banner' : 'aria_role_banner',
  'columnheader' : 'aria_role_columnheader',
  'complementary' : 'aria_role_complementary',
  'contentinfo' : 'aria_role_contentinfo',
  'definition' : 'aria_role_definition',
  'directory' : 'aria_role_directory',
  'document' : 'aria_role_document',
  'form' : 'aria_role_form',
  'group' : 'aria_role_group',
  'heading' : 'aria_role_heading',
  'img' : 'aria_role_img',
  'list' : 'aria_role_list',
  'listitem' : 'aria_role_listitem',
  'main' : 'aria_role_main',
  'math' : 'aria_role_math',
  'navigation' : 'aria_role_navigation',
  'note' : 'aria_role_note',
  'region' : 'aria_role_region',
  'rowheader' : 'aria_role_rowheader',
  'search' : 'aria_role_search',
  'separator' : 'aria_role_separator'
};


/**
 * @type {Array.<Object>}
 */
cvox.AriaUtil.ATTRIBUTE_VALUE_TO_STATUS = [
  { name: 'aria-autocomplete', values:
      {'inline' : 'aria_autocomplete_inline',
       'list' : 'aria_autocomplete_list',
       'both' : 'aria_autocomplete_both'} },
  { name: 'aria-checked', values:
      {'true' : 'aria_checked_true',
       'false' : 'aria_checked_false',
       'mixed' : 'aria_checked_mixed'} },
  { name: 'aria-disabled', values:
      {'true' : 'aria_disabled_true'} },
  { name: 'aria-expanded', values:
      {'true' : 'aria_expanded_true',
       'false' : 'aria_expanded_false'} },
  { name: 'aria-invalid', values:
      {'true' : 'aria_invalid_true',
       'grammar' : 'aria_invalid_grammar',
       'spelling' : 'aria_invalid_spelling'} },
  { name: 'aria-multiline', values:
      {'true' : 'aria_multiline_true'} },
  { name: 'aria-multiselectable', values:
      {'true' : 'aria_multiselectable_true'} },
  { name: 'aria-pressed', values:
      {'true' : 'aria_pressed_true',
       'false' : 'aria_pressed_false',
       'mixed' : 'aria_pressed_mixed'} },
  { name: 'aria-readonly', values:
      {'true' : 'aria_readonly_true'} },
  { name: 'aria-required', values:
      {'true' : 'aria_required_true'} },
  { name: 'aria-selected', values:
      {'true' : 'aria_selected_true',
       'false' : 'aria_selected_false'} }
];


/**
 * Checks if a node should be treated as a hidden node because of its ARIA
 * markup.
 *
 * @param {Node} targetNode The node to check.
 * @return {boolean} True if the targetNode should be treated as hidden.
 */
cvox.AriaUtil.isHiddenRecursive = function(targetNode) {
  if (cvox.AriaUtil.isHidden(targetNode)) {
    return true;
  }
  var parent = targetNode.parentElement;
  while (parent) {
    if ((parent.getAttribute('aria-hidden') == 'true') &&
        (parent.getAttribute('chromevoxignoreariahidden') != 'true')) {
      return true;
    }
    parent = parent.parentElement;
  }
  return false;
};


/**
 * Checks if a node should be treated as a hidden node because of its ARIA
 * markup. Does not check parents, so if you need to know if this is a
 * descendant of a hidden node, call isHiddenRecursive.
 *
 * @param {Node} targetNode The node to check.
 * @return {boolean} True if the targetNode should be treated as hidden.
 */
cvox.AriaUtil.isHidden = function(targetNode) {
  if (!targetNode) {
    return true;
  }
  if (targetNode.getAttribute) {
    if ((targetNode.getAttribute('aria-hidden') == 'true') &&
        (targetNode.getAttribute('chromevoxignoreariahidden') != 'true')) {
      return true;
    }
  }
  return false;
};


/**
 * Checks if a node should be treated as a visible node because of its ARIA
 * markup, regardless of whatever other styling/attributes it may have.
 * It is possible to force a node to be visible by setting aria-hidden to
 * false.
 *
 * @param {Node} targetNode The node to check.
 * @return {boolean} True if the targetNode should be treated as visible.
 */
cvox.AriaUtil.isForcedVisibleRecursive = function(targetNode) {
  var node = targetNode;
  while (node) {
    if (node.getAttribute) {
      // Stop and return the result based on the closest node that has
      // aria-hidden set.
      if (node.hasAttribute('aria-hidden') &&
          (node.getAttribute('chromevoxignoreariahidden') != 'true')) {
        return node.getAttribute('aria-hidden') == 'false';
      }
    }
    node = node.parentElement;
  }
  return false;
};


/**
 * Checks if a node should be treated as a leaf node because of its ARIA
 * markup. Does not check recursively, and does not check isControlWidget.
 * Note that elements with aria-label are treated as leaf elements. See:
 * http://www.w3.org/TR/wai-aria/roles#textalternativecomputation
 *
 * @param {Element} targetElement The node to check.
 * @return {boolean} True if the targetNode should be treated as a leaf node.
 */
cvox.AriaUtil.isLeafElement = function(targetElement) {
  var role = targetElement.getAttribute('role');
  var hasArialLabel = targetElement.hasAttribute('aria-label') &&
      (targetElement.getAttribute('aria-label').length > 0);
  return (role == 'img' || role == 'progressbar' || hasArialLabel);
};


/**
 * Determines whether or not a node is or is the descendant of a node
 * with a particular role.
 *
 * @param {Node} node The node to be checked.
 * @param {string} roleName The role to check for.
 * @return {boolean} True if the node or one of its ancestor has the specified
 * role.
 */
cvox.AriaUtil.isDescendantOfRole = function(node, roleName) {
  while (node) {
    if (roleName && node && (node.getAttribute('role') == roleName)) {
      return true;
    }
    node = node.parentNode;
  }
  return false;
};


/**
 * Helper function to return the role name message identifier for a role.
 * @param {string} role The role.
 * @return {?string} The role name message identifier.
 * @private
 */
cvox.AriaUtil.getRoleNameMsgForRole_ = function(role) {
  var msgId = cvox.AriaUtil.WIDGET_ROLE_TO_NAME[role];
  if (!msgId) {
    return null;
  }
  if (msgId == cvox.AriaUtil.NO_ROLE_NAME) {
    // TODO(dtseng): This isn't the way to insert silence; beware!
    return ' ';
  }
  return msgId;
};

/**
 * Returns true is the node is any kind of button.
 *
 * @param {Node} node The node to check.
 * @return {boolean} True if the node is a button.
 */
cvox.AriaUtil.isButton = function(node) {
  var role = cvox.AriaUtil.getRoleAttribute(node);
  if (role == 'button') {
    return true;
  }
  if (node.tagName == 'BUTTON') {
    return true;
  }
  if (node.tagName == 'INPUT') {
    return (node.type == 'submit' ||
            node.type == 'reset' ||
            node.type == 'button');
  }
  return false;
};

/**
 * Returns a role message identifier for a node.
 * For a localized string, see cvox.AriaUtil.getRoleName.
 * @param {Node} targetNode The node to get the role name for.
 * @return {string} The role name message identifier      for the targetNode.
 */
cvox.AriaUtil.getRoleNameMsg = function(targetNode) {
  var roleName;
  if (targetNode && targetNode.getAttribute) {
    var role = cvox.AriaUtil.getRoleAttribute(targetNode);

    // Special case for pop-up buttons.
    if (targetNode.getAttribute('aria-haspopup') == 'true' &&
        cvox.AriaUtil.isButton(targetNode)) {
      return 'aria_role_popup_button';
    }

    if (role) {
      roleName = cvox.AriaUtil.getRoleNameMsgForRole_(role);
      if (!roleName) {
        roleName = cvox.AriaUtil.STRUCTURE_ROLE_TO_NAME[role];
      }
    }

    // To a user, a menu item within a menu bar is called a "menu";
    // any other menu item is called a "menu item".
    //
    // TODO(deboer): This block feels like a hack. dmazzoni suggests
    // using css-like syntax for names.  Investigate further if
    // we need more of these hacks.
    if (role == 'menuitem') {
      var container = targetNode.parentElement;
      while (container) {
        if (container.getAttribute &&
            (cvox.AriaUtil.getRoleAttribute(container) == 'menu' ||
             cvox.AriaUtil.getRoleAttribute(container) == 'menubar')) {
          break;
        }
        container = container.parentElement;
      }
      if (container && cvox.AriaUtil.getRoleAttribute(container) == 'menubar') {
        roleName = cvox.AriaUtil.getRoleNameMsgForRole_('menu');
      }  // else roleName is already 'Menu item', no need to change it.
    }
  }
  if (!roleName) {
    roleName = '';
  }
  return roleName;
};

/**
 * Returns a string to be presented to the user that identifies what the
 * targetNode's role is.
 *
 * @param {Node} targetNode The node to get the role name for.
 * @return {string} The role name for the targetNode.
 */
cvox.AriaUtil.getRoleName = function(targetNode) {
  var roleMsg = cvox.AriaUtil.getRoleNameMsg(targetNode);
  var roleName = cvox.ChromeVox.msgs.getMsg(roleMsg);
  var role = cvox.AriaUtil.getRoleAttribute(targetNode);
  if ((role == 'heading') && (targetNode.hasAttribute('aria-level'))) {
    roleName += ' ' + targetNode.getAttribute('aria-level');
  }
  return roleName ? roleName : '';
};

/**
 * Returns a string that gives information about the state of the targetNode.
 *
 * @param {Node} targetNode The node to get the state information for.
 * @param {boolean} primary Whether this is the primary node we're
 *     interested in, where we might want extra information - as
 *     opposed to an ancestor, where we might be more brief.
 * @return {cvox.NodeState} The status information about the node.
 */
cvox.AriaUtil.getStateMsgs = function(targetNode, primary) {
  var state = [];
  if (!targetNode || !targetNode.getAttribute) {
    return state;
  }

  for (var i = 0, attr; attr = cvox.AriaUtil.ATTRIBUTE_VALUE_TO_STATUS[i];
      i++) {
    var value = targetNode.getAttribute(attr.name);
    var msg_id = attr.values[value];
    if (msg_id) {
      state.push([msg_id]);
    }
  }
  if (targetNode.getAttribute('role') == 'grid') {
      return cvox.AriaUtil.getGridState_(targetNode, targetNode);
  }

  var role = cvox.AriaUtil.getRoleAttribute(targetNode);
  if (targetNode.getAttribute('aria-haspopup') == 'true') {
    if (role == 'menuitem') {
      state.push(['has_submenu']);
    } else if (cvox.AriaUtil.isButton(targetNode)) {
      // Do nothing - the role name will be 'pop-up button'.
    } else {
      state.push(['has_popup']);
    }
  }

  var valueText = targetNode.getAttribute('aria-valuetext');
  if (valueText) {
    // If there is a valueText, that always wins.
    state.push([valueText]);
    return state;
  }

  var valueNow = targetNode.getAttribute('aria-valuenow');
  var valueMin = targetNode.getAttribute('aria-valuemin');
  var valueMax = targetNode.getAttribute('aria-valuemax');

  // Scrollbar and progressbar should speak the percentage.
  // http://www.w3.org/TR/wai-aria/roles#scrollbar
  // http://www.w3.org/TR/wai-aria/roles#progressbar
  if ((valueNow != null) && (valueMin != null) && (valueMax != null)) {
    if ((role == 'scrollbar') || (role == 'progressbar')) {
      var percent = Math.round((valueNow / (valueMax - valueMin)) * 100);
      state.push(['state_percent', percent]);
      return state;
    }
  }

  // Return as many of the value attributes as possible.
  if (valueNow != null) {
    state.push(['aria_value_now', valueNow]);
  }
  if (valueMin != null) {
    state.push(['aria_value_min', valueMin]);
  }
  if (valueMax != null) {
    state.push(['aria_value_max', valueMax]);
  }

  // If this is a composite control or an item within a composite control,
  // get the index and count of the current descendant or active
  // descendant.
  var parentControl = targetNode;
  var currentDescendant = null;

  if (cvox.AriaUtil.isCompositeControl(parentControl) && primary) {
    currentDescendant = cvox.AriaUtil.getActiveDescendant(parentControl);
  } else {
    role = cvox.AriaUtil.getRoleAttribute(targetNode);
    if (role == 'option' ||
        role == 'menuitem' ||
        role == 'menuitemcheckbox' ||
        role == 'menuitemradio' ||
        role == 'radio' ||
        role == 'tab' ||
        role == 'treeitem') {
      currentDescendant = targetNode;
      parentControl = targetNode.parentElement;
      while (parentControl &&
             !cvox.AriaUtil.isCompositeControl(parentControl)) {
        parentControl = parentControl.parentElement;
        if (parentControl &&
            cvox.AriaUtil.getRoleAttribute(parentControl) == 'treeitem') {
          break;
        }
      }
    }
  }

  if (parentControl &&
      (cvox.AriaUtil.isCompositeControl(parentControl) ||
          cvox.AriaUtil.getRoleAttribute(parentControl) == 'treeitem') &&
      currentDescendant) {
    var parentRole = cvox.AriaUtil.getRoleAttribute(parentControl);
    var descendantRoleList;
    switch (parentRole) {
      case 'combobox':
      case 'listbox':
        descendantRoleList = ['option'];
        break;
      case 'menu':
        descendantRoleList = ['menuitem',
                             'menuitemcheck',
                             'menuitemradio'];
        break;
      case 'radiogroup':
        descendantRoleList = ['radio'];
        break;
      case 'tablist':
        descendantRoleList = ['tab'];
        break;
      case 'tree':
      case 'treegrid':
      case 'treeitem':
        descendantRoleList = ['treeitem'];
        break;
    }

    if (descendantRoleList) {
      var listLength;
      var currentIndex;

      var ariaLength =
          parseInt(currentDescendant.getAttribute('aria-setsize'), 10);
      if (!isNaN(ariaLength)) {
        listLength = ariaLength;
      }
      var ariaIndex =
          parseInt(currentDescendant.getAttribute('aria-posinset'), 10);
      if (!isNaN(ariaIndex)) {
        currentIndex = ariaIndex;
      }

      if (listLength == undefined || currentIndex == undefined) {
        var descendants = cvox.AriaUtil.getNextLevel(parentControl,
            descendantRoleList);
        if (listLength == undefined) {
          listLength = descendants.length;
        }
        if (currentIndex == undefined) {
          for (var j = 0; j < descendants.length; j++) {
            if (descendants[j] == currentDescendant) {
              currentIndex = j + 1;
            }
          }
        }
      }
      if (currentIndex && listLength) {
        state.push(['list_position', currentIndex, listLength]);
      }
    }
  }
  return state;
};


/**
 * Returns a string that gives information about the state of the grid node.
 *
 * @param {Node} targetNode The node to get the state information for.
 * @param {Node} parentControl The parent composite control.
 * @return {cvox.NodeState} The status information about the node.
 * @private
 */
cvox.AriaUtil.getGridState_ = function(targetNode, parentControl) {
  var activeDescendant = cvox.AriaUtil.getActiveDescendant(parentControl);

  if (activeDescendant) {
    var descendantSelector = '*[role~="row"]';
    var rows = parentControl.querySelectorAll(descendantSelector);
    var currentIndex = null;
    for (var j = 0; j < rows.length; j++) {
      var gridcells = rows[j].querySelectorAll('*[role~="gridcell"]');
      for (var k = 0; k < gridcells.length; k++) {
        if (gridcells[k] == activeDescendant) {
          return /** @type {cvox.NodeState} */ (
                  [['aria_role_gridcell_pos', j + 1, k + 1]]);
        }
      }
    }
  }
  return [];
};


/**
 * Returns the id of a node's active descendant
 * @param {Node} targetNode The node.
 * @return {?string} The id of the active descendant.
 * @private
 */
cvox.AriaUtil.getActiveDescendantId_ = function(targetNode) {
  if (!targetNode.getAttribute) {
    return null;
  }

  var activeId = targetNode.getAttribute('aria-activedescendant');
  if (!activeId) {
    return null;
  }
  return activeId;
};


/**
 * Returns the list of elements that are one aria-level below.
 *
 * @param {Node} parentControl The node whose descendants should be analyzed.
 * @param {Array.<string>} role The role(s) of descendant we are looking for.
 * @return {Array.<Node>} The array of matching nodes.
 */
cvox.AriaUtil.getNextLevel = function(parentControl, role) {
  var result = [];
  var children = parentControl.childNodes;
  var length = children.length;
  for (var i = 0; i < children.length; i++) {
    if (cvox.AriaUtil.isHidden(children[i]) ||
        !cvox.DomUtil.isVisible(children[i])) {
      continue;
    }
    var nextLevel = cvox.AriaUtil.getNextLevelItems(children[i], role);
    if (nextLevel.length > 0) {
      result = result.concat(nextLevel);
    }
  }
  return result;
};


/**
 * Recursively finds the first node(s) that match the role.
 *
 * @param {Element} current The node to start looking at.
 * @param {Array.<string>} role The role(s) to match.
 * @return {Array.<Element>} The array of matching nodes.
 */
cvox.AriaUtil.getNextLevelItems = function(current, role) {
  if (current.nodeType != 1) { // If reached a node that is not an element.
    return [];
  }
  if (role.indexOf(cvox.AriaUtil.getRoleAttribute(current)) != -1) {
    return [current];
  } else {
    var children = current.childNodes;
    var length = children.length;
    if (length == 0) {
      return [];
    } else {
      var resultArray = [];
      for (var i = 0; i < length; i++) {
        var result = cvox.AriaUtil.getNextLevelItems(children[i], role);
        if (result.length > 0) {
          resultArray = resultArray.concat(result);
        }
      }
      return resultArray;
    }
  }
};


/**
 * If the node is an object with an active descendant, returns the
 * descendant node.
 *
 * This function will fully resolve an active descendant chain. If a circular
 * chain is detected, it will return null.
 *
 * @param {Node} targetNode The node to get descendant information for.
 * @return {Node} The descendant node or null if no node exists.
 */
cvox.AriaUtil.getActiveDescendant = function(targetNode) {
  var seenIds = {};
  var node = targetNode;

  while (node) {
    var activeId = cvox.AriaUtil.getActiveDescendantId_(node);
    if (!activeId) {
      break;
    }
    if (activeId in seenIds) {
      // A circlar activeDescendant is an error, so return null.
      return null;
    }
    seenIds[activeId] = true;
    node = document.getElementById(activeId);
  }

  if (node == targetNode) {
    return null;
  }
  return node;
};


/**
 * Given a node, returns true if it's an ARIA control widget. Control widgets
 * are treated as leaf nodes.
 *
 * @param {Node} targetNode The node to be checked.
 * @return {boolean} Whether the targetNode is an ARIA control widget.
 */
cvox.AriaUtil.isControlWidget = function(targetNode) {
  if (targetNode && targetNode.getAttribute) {
    var role = cvox.AriaUtil.getRoleAttribute(targetNode);
    switch (role) {
      case 'button':
      case 'checkbox':
      case 'combobox':
      case 'listbox':
      case 'menu':
      case 'menuitemcheckbox':
      case 'menuitemradio':
      case 'radio':
      case 'slider':
      case 'progressbar':
      case 'scrollbar':
      case 'spinbutton':
      case 'tab':
      case 'tablist':
      case 'textbox':
        return true;
    }
  }
  return false;
};


/**
 * Given a node, returns true if it's an ARIA composite control.
 *
 * @param {Node} targetNode The node to be checked.
 * @return {boolean} Whether the targetNode is an ARIA composite control.
 */
cvox.AriaUtil.isCompositeControl = function(targetNode) {
  if (targetNode && targetNode.getAttribute) {
    var role = cvox.AriaUtil.getRoleAttribute(targetNode);
    switch (role) {
      case 'combobox':
      case 'grid':
      case 'listbox':
      case 'menu':
      case 'menubar':
      case 'radiogroup':
      case 'tablist':
      case 'tree':
      case 'treegrid':
        return true;
    }
  }
  return false;
};


/**
 * Given a node, returns its 'aria-live' value if it's a live region, or
 * null otherwise.
 *
 * @param {Node} node The node to be checked.
 * @return {?string} The live region value, like 'polite' or
 *     'assertive', or null if 'off' or none.
 */
cvox.AriaUtil.getAriaLive = function(node) {
  if (!node.hasAttribute)
    return null;
  var value = node.getAttribute('aria-live');
  if (value == 'off') {
    return null;
  } else if (value) {
    return value;
  }
  var role = cvox.AriaUtil.getRoleAttribute(node);
  switch (role) {
    case 'alert':
      return 'assertive';
    case 'log':
    case 'status':
      return 'polite';
    default:
      return null;
  }
};


/**
 * Given a node, returns its 'aria-atomic' value.
 *
 * @param {Node} node The node to be checked.
 * @return {boolean} The aria-atomic live region value, either true or false.
 */
cvox.AriaUtil.getAriaAtomic = function(node) {
  if (!node.hasAttribute)
    return false;
  var value = node.getAttribute('aria-atomic');
  if (value) {
    return (value === 'true');
  }
  var role = cvox.AriaUtil.getRoleAttribute(node);
  if (role == 'alert') {
    return true;
  }
  return false;
};


/**
 * Given a node, returns its 'aria-busy' value.
 *
 * @param {Node} node The node to be checked.
 * @return {boolean} The aria-busy live region value, either true or false.
 */
cvox.AriaUtil.getAriaBusy = function(node) {
  if (!node.hasAttribute)
    return false;
  var value = node.getAttribute('aria-busy');
  if (value) {
    return (value === 'true');
  }
  return false;
};


/**
 * Given a node, checks its aria-relevant attribute (with proper inheritance)
 * and determines whether the given change (additions, removals, text, all)
 * is relevant and should be announced.
 *
 * @param {Node} node The node to be checked.
 * @param {string} change The name of the change to check - one of
 *     'additions', 'removals', 'text', 'all'.
 * @return {boolean} True if that change is relevant to that node as part of
 *     a live region.
 */
cvox.AriaUtil.getAriaRelevant = function(node, change) {
  if (!node.hasAttribute)
    return false;
  var value;
  if (node.hasAttribute('aria-relevant')) {
    value = node.getAttribute('aria-relevant');
  } else {
    value = 'additions text';
  }
  if (value == 'all') {
    value = 'additions removals text';
  }

  var tokens = value.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '').split(' ');

  if (change == 'all') {
    return (tokens.indexOf('additions') >= 0 &&
            tokens.indexOf('text') >= 0 &&
            tokens.indexOf('removals') >= 0);
  } else {
    return (tokens.indexOf(change) >= 0);
  }
};


/**
 * Given a node, return all live regions that are either rooted at this
 * node or contain this node.
 *
 * @param {Node} node The node to be checked.
 * @return {Array.<Element>} All live regions affected by this node changing.
 */
cvox.AriaUtil.getLiveRegions = function(node) {
  var result = [];
  if (node.querySelectorAll) {
    var nodes = node.querySelectorAll(
        '[role="alert"], [role="log"],  [role="marquee"], ' +
        '[role="status"], [role="timer"],  [aria-live]');
    if (nodes) {
      for (var i = 0; i < nodes.length; i++) {
        result.push(nodes[i]);
      }
    }
  }

  while (node) {
    if (cvox.AriaUtil.getAriaLive(node)) {
      result.push(node);
      return result;
    }
    node = node.parentElement;
  }

  return result;
};


/**
 * Checks to see whether or not a node is an ARIA landmark.
 *
 * @param {Node} node The node to be checked.
 * @return {boolean} Whether or not the node is an ARIA landmark.
 */
cvox.AriaUtil.isLandmark = function(node) {
    if (!node || !node.getAttribute) {
      return false;
    }
    var role = cvox.AriaUtil.getRoleAttribute(node);
    switch (role) {
      case 'application':
      case 'banner':
      case 'complementary':
      case 'contentinfo':
      case 'form':
      case 'main':
      case 'navigation':
      case 'search':
        return true;
    }
    return false;
};


/**
 * Checks to see whether or not a node is an ARIA grid.
 *
 * @param {Node} node The node to be checked.
 * @return {boolean} Whether or not the node is an ARIA grid.
 */
cvox.AriaUtil.isGrid = function(node) {
    if (!node || !node.getAttribute) {
      return false;
    }
    var role = cvox.AriaUtil.getRoleAttribute(node);
    switch (role) {
      case 'grid':
      case 'treegrid':
        return true;
    }
    return false;
};


/**
 * Returns the id of an earcon to play along with the description for a node.
 *
 * @param {Node} node The node to get the earcon for.
 * @return {number?} The earcon id, or null if none applies.
 */
cvox.AriaUtil.getEarcon = function(node) {
  if (!node || !node.getAttribute) {
    return null;
  }
  var role = cvox.AriaUtil.getRoleAttribute(node);
  switch (role) {
    case 'button':
      return cvox.AbstractEarcons.BUTTON;
    case 'checkbox':
    case 'radio':
    case 'menuitemcheckbox':
    case 'menuitemradio':
      var checked = node.getAttribute('aria-checked');
      if (checked == 'true') {
        return cvox.AbstractEarcons.CHECK_ON;
      } else {
        return cvox.AbstractEarcons.CHECK_OFF;
      }
    case 'combobox':
    case 'listbox':
      return cvox.AbstractEarcons.LISTBOX;
    case 'textbox':
      return cvox.AbstractEarcons.EDITABLE_TEXT;
    case 'listitem':
      return cvox.AbstractEarcons.BULLET;
    case 'link':
      return cvox.AbstractEarcons.LINK;
  }

  return null;
};


/**
 * Returns the role of the node.
 *
 * This is equivalent to targetNode.getAttribute('role')
 * except it also takes into account cases where ChromeVox
 * itself has changed the role (ie, adding role="application"
 * to BODY elements for better screen reader compatibility.
 *
 * @param {Node} targetNode The node to get the role for.
 * @return {string} role of the targetNode.
 */
cvox.AriaUtil.getRoleAttribute = function(targetNode) {
  if (!targetNode.getAttribute) {
    return '';
  }
  var role = targetNode.getAttribute('role');
  if (targetNode.hasAttribute('chromevoxoriginalrole')) {
    role = targetNode.getAttribute('chromevoxoriginalrole');
  }
  return role;
};


/**
 * Checks to see whether or not a node is an ARIA math node.
 *
 * @param {Node} node The node to be checked.
 * @return {boolean} Whether or not the node is an ARIA math node.
 */
cvox.AriaUtil.isMath = function(node) {
  if (!node || !node.getAttribute) {
    return false;
  }
  var role = cvox.AriaUtil.getRoleAttribute(node);
  return role == 'math';
};
