// Copyright 2015 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 "content/browser/accessibility/one_shot_accessibility_tree_search.h"

#include <stdint.h>

#include "base/i18n/case_conversion.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/accessibility/browser_accessibility.h"
#include "content/browser/accessibility/browser_accessibility_manager.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"

namespace content {

// Given a node, populate a vector with all of the strings from that node's
// attributes that might be relevant for a text search.
void GetNodeStrings(BrowserAccessibility* node,
                    std::vector<base::string16>* strings) {
  if (node->HasStringAttribute(ax::mojom::StringAttribute::kName))
    strings->push_back(
        node->GetString16Attribute(ax::mojom::StringAttribute::kName));
  if (node->HasStringAttribute(ax::mojom::StringAttribute::kDescription))
    strings->push_back(
        node->GetString16Attribute(ax::mojom::StringAttribute::kDescription));
  if (node->HasStringAttribute(ax::mojom::StringAttribute::kValue))
    strings->push_back(
        node->GetString16Attribute(ax::mojom::StringAttribute::kValue));
  if (node->HasStringAttribute(ax::mojom::StringAttribute::kPlaceholder))
    strings->push_back(
        node->GetString16Attribute(ax::mojom::StringAttribute::kPlaceholder));
}

OneShotAccessibilityTreeSearch::OneShotAccessibilityTreeSearch(
    BrowserAccessibility* scope)
    : tree_(scope->manager()),
      scope_node_(scope),
      start_node_(scope),
      direction_(OneShotAccessibilityTreeSearch::FORWARDS),
      result_limit_(UNLIMITED_RESULTS),
      immediate_descendants_only_(false),
      can_wrap_to_last_element_(false),
      visible_only_(false),
      did_search_(false) {}

OneShotAccessibilityTreeSearch::~OneShotAccessibilityTreeSearch() {
}

void OneShotAccessibilityTreeSearch::SetStartNode(
    BrowserAccessibility* start_node) {
  DCHECK(!did_search_);
  CHECK(start_node);

  if (!scope_node_->PlatformGetParent() ||
      start_node->IsDescendantOf(scope_node_->PlatformGetParent())) {
    start_node_ = start_node;
  }
}

void OneShotAccessibilityTreeSearch::SetDirection(Direction direction) {
  DCHECK(!did_search_);
  direction_ = direction;
}

void OneShotAccessibilityTreeSearch::SetResultLimit(int result_limit) {
  DCHECK(!did_search_);
  result_limit_ = result_limit;
}

void OneShotAccessibilityTreeSearch::SetImmediateDescendantsOnly(
    bool immediate_descendants_only) {
  DCHECK(!did_search_);
  immediate_descendants_only_ = immediate_descendants_only;
}

void OneShotAccessibilityTreeSearch::SetCanWrapToLastElement(
    bool can_wrap_to_last_element) {
  DCHECK(!did_search_);
  can_wrap_to_last_element_ = can_wrap_to_last_element;
}

void OneShotAccessibilityTreeSearch::SetVisibleOnly(bool visible_only) {
  DCHECK(!did_search_);
  visible_only_ = visible_only;
}

void OneShotAccessibilityTreeSearch::SetSearchText(const std::string& text) {
  DCHECK(!did_search_);
  search_text_ = text;
}

void OneShotAccessibilityTreeSearch::AddPredicate(
    AccessibilityMatchPredicate predicate) {
  DCHECK(!did_search_);
  predicates_.push_back(predicate);
}

size_t OneShotAccessibilityTreeSearch::CountMatches() {
  if (!did_search_)
    Search();

  return matches_.size();
}

BrowserAccessibility* OneShotAccessibilityTreeSearch::GetMatchAtIndex(
    size_t index) {
  if (!did_search_)
    Search();

  CHECK(index < matches_.size());
  return matches_[index];
}

void OneShotAccessibilityTreeSearch::Search() {
  if (immediate_descendants_only_) {
    SearchByIteratingOverChildren();
  } else {
    SearchByWalkingTree();
  }
  did_search_ = true;
}

void OneShotAccessibilityTreeSearch::SearchByIteratingOverChildren() {
  // Iterate over the children of scope_node_.
  // If start_node_ is specified, iterate over the first child past that
  // node.

  uint32_t count = scope_node_->PlatformChildCount();
  if (count == 0)
    return;

  // We only care about immediate children of scope_node_, so walk up
  // start_node_ until we get to an immediate child. If it isn't a child,
  // we ignore start_node_.
  while (start_node_ && start_node_->PlatformGetParent() != scope_node_)
    start_node_ = start_node_->PlatformGetParent();

  uint32_t index = (direction_ == FORWARDS ? 0 : count - 1);
  if (start_node_) {
    index = start_node_->GetIndexInParent();
    if (direction_ == FORWARDS)
      index++;
    else
      index--;
  }

  while (index < count &&
         (result_limit_ == UNLIMITED_RESULTS ||
          static_cast<int>(matches_.size()) < result_limit_)) {
    BrowserAccessibility* node = scope_node_->PlatformGetChild(index);
    if (Matches(node))
      matches_.push_back(node);

    if (direction_ == FORWARDS)
      index++;
    else
      index--;
  }
}

void OneShotAccessibilityTreeSearch::SearchByWalkingTree() {
  BrowserAccessibility* node = nullptr;
  node = start_node_;
  if (node != scope_node_ || result_limit_ == 1) {
    if (direction_ == FORWARDS)
      node = tree_->NextInTreeOrder(start_node_);
    else
      node = tree_->PreviousInTreeOrder(start_node_, can_wrap_to_last_element_);
  }

  BrowserAccessibility* stop_node = scope_node_->PlatformGetParent();
  while (node &&
         node != stop_node &&
         (result_limit_ == UNLIMITED_RESULTS ||
          static_cast<int>(matches_.size()) < result_limit_)) {
    if (Matches(node))
      matches_.push_back(node);

    if (direction_ == FORWARDS)
      node = tree_->NextInTreeOrder(node);
    else
      node = tree_->PreviousInTreeOrder(node, can_wrap_to_last_element_);
  }
}

bool OneShotAccessibilityTreeSearch::Matches(BrowserAccessibility* node) {
  for (size_t i = 0; i < predicates_.size(); ++i) {
    if (!predicates_[i](start_node_, node))
      return false;
  }

  if (visible_only_) {
    if (node->HasState(ax::mojom::State::kInvisible) || node->IsOffscreen()) {
      return false;
    }
  }

  if (!search_text_.empty()) {
    base::string16 search_text_lower =
      base::i18n::ToLower(base::UTF8ToUTF16(search_text_));
    std::vector<base::string16> node_strings;
    GetNodeStrings(node, &node_strings);
    bool found_text_match = false;
    for (size_t i = 0; i < node_strings.size(); ++i) {
      base::string16 node_string_lower =
        base::i18n::ToLower(node_strings[i]);
      if (node_string_lower.find(search_text_lower) !=
          base::string16::npos) {
        found_text_match = true;
        break;
      }
    }
    if (!found_text_match)
      return false;
  }

  return true;
}

//
// Predicates
//

bool AccessibilityArticlePredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetRole() == ax::mojom::Role::kArticle;
}

bool AccessibilityButtonPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  switch (node->GetRole()) {
    case ax::mojom::Role::kButton:
    case ax::mojom::Role::kMenuButton:
    case ax::mojom::Role::kPopUpButton:
    case ax::mojom::Role::kSwitch:
    case ax::mojom::Role::kToggleButton:
      return true;
    default:
      return false;
  }
}

bool AccessibilityBlockquotePredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetRole() == ax::mojom::Role::kBlockquote;
}

bool AccessibilityCheckboxPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (node->GetRole() == ax::mojom::Role::kCheckBox ||
          node->GetRole() == ax::mojom::Role::kMenuItemCheckBox);
}

bool AccessibilityComboboxPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (node->GetRole() == ax::mojom::Role::kComboBoxGrouping ||
          node->GetRole() == ax::mojom::Role::kComboBoxMenuButton ||
          node->GetRole() == ax::mojom::Role::kTextFieldWithComboBox ||
          node->GetRole() == ax::mojom::Role::kPopUpButton);
}

bool AccessibilityControlPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  if (ui::IsControl(node->GetRole()))
    return true;
  if (node->HasState(ax::mojom::State::kFocusable) &&
      node->GetRole() != ax::mojom::Role::kIframe &&
      node->GetRole() != ax::mojom::Role::kIframePresentational &&
      !ui::IsLink(node->GetRole()) &&
      node->GetRole() != ax::mojom::Role::kWebArea &&
      node->GetRole() != ax::mojom::Role::kRootWebArea) {
    return true;
  }
  return false;
}

bool AccessibilityFocusablePredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  bool focusable = node->HasState(ax::mojom::State::kFocusable);
  if (node->GetRole() == ax::mojom::Role::kIframe ||
      node->GetRole() == ax::mojom::Role::kIframePresentational ||
      node->GetRole() == ax::mojom::Role::kWebArea ||
      node->GetRole() == ax::mojom::Role::kRootWebArea) {
    focusable = false;
  }
  return focusable;
}

bool AccessibilityGraphicPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return ui::IsImage(node->GetRole());
}

bool AccessibilityHeadingPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return ui::IsHeading(node->GetRole());
}

bool AccessibilityH1Predicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (ui::IsHeading(node->GetRole()) &&
          node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
              1);
}

bool AccessibilityH2Predicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (ui::IsHeading(node->GetRole()) &&
          node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
              2);
}

bool AccessibilityH3Predicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (ui::IsHeading(node->GetRole()) &&
          node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
              3);
}

bool AccessibilityH4Predicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (ui::IsHeading(node->GetRole()) &&
          node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
              4);
}

bool AccessibilityH5Predicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (ui::IsHeading(node->GetRole()) &&
          node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
              5);
}

bool AccessibilityH6Predicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (ui::IsHeading(node->GetRole()) &&
          node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
              6);
}

bool AccessibilityHeadingSameLevelPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (
      node->GetRole() == ax::mojom::Role::kHeading &&
      start->GetRole() == ax::mojom::Role::kHeading &&
      (node->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel) ==
       start->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel)));
}

bool AccessibilityFramePredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  if (node->IsWebAreaForPresentationalIframe())
    return false;
  if (!node->PlatformGetParent())
    return false;
  return (node->GetRole() == ax::mojom::Role::kWebArea ||
          node->GetRole() == ax::mojom::Role::kRootWebArea);
}

bool AccessibilityLandmarkPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  switch (node->GetRole()) {
    case ax::mojom::Role::kApplication:
    case ax::mojom::Role::kArticle:
    case ax::mojom::Role::kBanner:
    case ax::mojom::Role::kComplementary:
    case ax::mojom::Role::kContentInfo:
    case ax::mojom::Role::kMain:
    case ax::mojom::Role::kNavigation:
    case ax::mojom::Role::kSearch:
    case ax::mojom::Role::kRegion:
      return true;
    default:
      return false;
  }
}

bool AccessibilityLinkPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return ui::IsLink(node->GetRole());
}

bool AccessibilityListPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return ui::IsList(node->GetRole());
}

bool AccessibilityListItemPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return ui::IsListItem(node->GetRole());
}

bool AccessibilityLiveRegionPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->HasStringAttribute(ax::mojom::StringAttribute::kLiveStatus);
}

bool AccessibilityMainPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (node->GetRole() == ax::mojom::Role::kMain);
}

bool AccessibilityMediaPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  const std::string& tag =
      node->GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag);
  return tag == "audio" || tag == "video";
}

bool AccessibilityRadioButtonPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (node->GetRole() == ax::mojom::Role::kRadioButton ||
          node->GetRole() == ax::mojom::Role::kMenuItemRadio);
}

bool AccessibilityRadioGroupPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetRole() == ax::mojom::Role::kRadioGroup;
}

bool AccessibilityTablePredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return ui::IsTableLike(node->GetRole());
}

bool AccessibilityTextfieldPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (node->IsPlainTextField() || node->IsRichTextField());
}

bool AccessibilityTextStyleBoldPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetData().HasTextStyle(ax::mojom::TextStyle::kBold);
}

bool AccessibilityTextStyleItalicPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetData().HasTextStyle(ax::mojom::TextStyle::kItalic);
}

bool AccessibilityTextStyleUnderlinePredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetData().HasTextStyle(ax::mojom::TextStyle::kUnderline);
}

bool AccessibilityTreePredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return (node->IsPlainTextField() || node->IsRichTextField());
}

bool AccessibilityUnvisitedLinkPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetRole() == ax::mojom::Role::kLink &&
         !node->HasState(ax::mojom::State::kVisited);
}

bool AccessibilityVisitedLinkPredicate(
    BrowserAccessibility* start, BrowserAccessibility* node) {
  return node->GetRole() == ax::mojom::Role::kLink &&
         node->HasState(ax::mojom::State::kVisited);
}

}  // namespace content
