// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/test_ax_tree_update.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"

namespace ui {

int PlusSignCount(const std::string& s) {
  int count = 0;

  for (auto& character : s) {
    if (character == '+') {
      count++;
    }
  }

  return count;
}

bool IsSpaceOrTab(const char c) {
  return c == ' ' || c == '\t';
}

void RemoveLeadingAndTrailingWhitespace(std::string& s) {
  if (s.empty()) {
    return;
  }
  auto front_it = s.begin();
  auto back_it = s.end() - 1;

  while (IsSpaceOrTab(*front_it) || IsSpaceOrTab(*back_it)) {
    if (front_it > back_it) {
      return;
    }
    if (front_it == back_it) {
      if (IsSpaceOrTab(*front_it)) {
        s.erase(front_it);
      }
      return;
    }
    if (IsSpaceOrTab(*front_it)) {
      s.erase(front_it);

      // Iterators get invalidated when erasing.
      front_it = s.begin();
      back_it = s.end() - 1;
    }
    if (IsSpaceOrTab(*back_it)) {
      s.erase(back_it);

      // Iterators get invalidated when erasing.
      back_it = s.end() - 1;
      front_it = s.begin();
    }
  }
}

bool StringToBool(const std::string& s) {
  if (s == "true" || s == "True" || s == "1") {
    return true;
  }
  if (s == "false" || s == "False" || s == "0") {
    return false;
  }

  // TODO: Specify which node this error was found at.
  NOTREACHED() << "Invalid value passed to StringToBool: " << s;
  return false;
}

void ParseAndAddNodeProperties(
    AXNodeData& node_data,
    const std::vector<std::string>& node_line) {
  // At this point, the vector of strings we receive should be just a vector of
  // properties of the format:
  // [<property>=<value>] where value depends on the property.

  DCHECK(node_line.size() >= 1)
      << "Error in formatting of the tree structure. Possibly extra whiespace.";
  for (auto& prop : node_line) {
    std::vector<std::string> property_vector = base::SplitStringUsingSubstr(
        prop, "=", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
    DCHECK(property_vector.size() == 2)
        << "Properties should always be formed as "
           "<SupportedProperty>=<PropertyValue/s>";
    std::string property = property_vector[0];

    if (property == "name" || property == "Name") {
      // Since the structure is passed in as an unformatted string,
      // and the name has the format "<name>",
      // the string adds escape characters before the quotes.
      // Therefore when we set the name we must remove the `\"` character.

      std::string name = property_vector[1];
      DCHECK(name.front() == '\"' && name.back() == '\"');
      name.erase(name.begin());
      name.erase(--name.end());

      node_data.SetName(name);
    } else if (property == "states" || property == "state") {
      std::vector<std::string> state_values = base::SplitStringUsingSubstr(
          property_vector[1], ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
      DCHECK(state_values.size() >= 1)
          << "State values should be at least one, and they should be "
             "separated by commas.";
      for (auto& value : state_values) {
        node_data.AddState(StringToState(value));
      }

    } else if (property == "intAttribute" || property == "IntAttribute" ||
               property == "intattribute") {
      std::vector<std::string> int_attribute_vector =
          base::SplitStringUsingSubstr(property_vector[1], ",",
                                       base::KEEP_WHITESPACE,
                                       base::SPLIT_WANT_ALL);
      DCHECK(int_attribute_vector.size() == 2)
          << "Int attribute in string must always be formed: "
             "intAttribute=<intAttributeType>,<intAttributeValue>";
      int int_value = 0;
      base::StringToInt(int_attribute_vector[1], &int_value);
      DCHECK(int_value) << "Formatting error or non integer passed as node ID.";
      node_data.AddIntAttribute(StringToIntAttribute(int_attribute_vector[0]),
                                int_value);

    } else if (property == "stringAttribute" || property == "StringAttribute" ||
               property == "stringattribute") {
      std::vector<std::string> string_attribute_vector =
          base::SplitStringUsingSubstr(property_vector[1], ",",
                                       base::KEEP_WHITESPACE,
                                       base::SPLIT_WANT_ALL);
      DCHECK(string_attribute_vector.size() == 2)
          << "String attribute in string must always be formed: "
             "stringAttribute=<stringAttributeType>,<stringAttributeValue>";
      node_data.AddStringAttribute(
          StringToStringAttribute(string_attribute_vector[0]),
          string_attribute_vector[1]);
    } else if (property == "boolAttribute" || property == "BoolAttribute" ||
               property == "boolattribute") {
      std::vector<std::string> bool_attribute_vector =
          base::SplitStringUsingSubstr(property_vector[1], ",",
                                       base::KEEP_WHITESPACE,
                                       base::SPLIT_WANT_ALL);
      DCHECK(bool_attribute_vector.size() == 2)
          << "Bool attribute in string must always be formed: "
             "boolAttribute=<boolAttributeType>,<boolAttributeValue>";
      node_data.AddBoolAttribute(
          StringToBoolAttribute(bool_attribute_vector[0]),
          StringToBool(bool_attribute_vector[1]));

    } else {
      // TODO: Will extend to more properties here.
      NOTREACHED()
          << "Either an invalid property was specified, or this function does "
             "not currently support the specified property.";
    }
  }
}

AXNodeData ParseNodeInfo(std::vector<std::string>& node_line,
                                           std::set<int>& found_ids) {
  AXNodeData data;

  // Must at the very least have id and role.
  DCHECK(node_line.size() >= 2) << "Error, a node must have an id and role.";
  int int_value = 0;
  base::StringToInt(node_line[0], &int_value);
  DCHECK(int_value) << "Formatting error or non integer passed as node ID.";
  data.id = int_value;

  DCHECK(found_ids.find(data.id) == found_ids.end())
      << "Error, can't have duplicate IDs.";
  found_ids.insert(data.id);
  ax::mojom::Role role = StringToRole(node_line[1]);

  data.role = role;

  data.child_ids = {};

  if (node_line.size() >= 3) {
    node_line.erase(node_line.begin());
    node_line.erase(node_line.begin());
    ParseAndAddNodeProperties(data, node_line);
  }

  return data;
}

TestAXTreeUpdateNode::TestAXTreeUpdateNode(const TestAXTreeUpdateNode&) =
    default;

TestAXTreeUpdateNode::TestAXTreeUpdateNode(TestAXTreeUpdateNode&&) = default;

TestAXTreeUpdateNode::~TestAXTreeUpdateNode() = default;

TestAXTreeUpdateNode::TestAXTreeUpdateNode(
    ax::mojom::Role role,
    const std::vector<TestAXTreeUpdateNode>& children)
    : children(children) {
  DCHECK_NE(role, ax::mojom::Role::kUnknown);
  data.role = role;
}

TestAXTreeUpdateNode::TestAXTreeUpdateNode(
    ax::mojom::Role role,
    ax::mojom::State state,
    const std::vector<TestAXTreeUpdateNode>& children)
    : children(children) {
  DCHECK_NE(role, ax::mojom::Role::kUnknown);
  DCHECK_NE(state, ax::mojom::State::kNone);
  data.role = role;
  data.AddState(state);
}

TestAXTreeUpdateNode::TestAXTreeUpdateNode(const std::string& text) {
  data.role = ax::mojom::Role::kStaticText;
  data.SetName(text);
}

TestAXTreeUpdate::TestAXTreeUpdate(const TestAXTreeUpdateNode& root) {
  root_id = SetSubtree(root);
}

AXNodeID TestAXTreeUpdate::SetSubtree(const TestAXTreeUpdateNode& node) {
  size_t node_index = nodes.size();
  nodes.push_back(node.data);
  nodes[node_index].id = node_index + 1;
  std::vector<AXNodeID> child_ids;
  for (const auto& child : node.children) {
    child_ids.push_back(SetSubtree(child));
  }
  nodes[node_index].child_ids = child_ids;
  return nodes[node_index].id;
}

TestAXTreeUpdate::TestAXTreeUpdate(const std::string& tree_structure) {
  std::vector<AXNodeData> node_data_vector;
  std::vector<std::string> tree_structure_vector = base::SplitStringUsingSubstr(
      tree_structure, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);

  // Test authors might accidentally include whitespace when declaring the non
  // formatted string.
  RemoveLeadingAndTrailingWhitespace(*tree_structure_vector.begin());
  RemoveLeadingAndTrailingWhitespace(*--tree_structure_vector.end());

  // With how we pass in the non formatted string, the first and last elements
  // of the vector should be empty.
  DCHECK(tree_structure_vector.front().empty() &&
         tree_structure_vector.back().empty())
      << "Error in parsing the tree structure, double check the formatting.";
  tree_structure_vector.erase(tree_structure_vector.begin());
  tree_structure_vector.erase(--tree_structure_vector.end());

  node_data_vector.reserve(tree_structure_vector.size());

  RemoveLeadingAndTrailingWhitespace(tree_structure_vector[0]);

  int root_pluses = PlusSignCount(tree_structure_vector[0]);
  DCHECK_EQ(root_pluses, 2)
      << "The first line of the test needs to start with 2 '+' sign, not "
      << root_pluses;

  // We remove the plus signs from the string
  tree_structure_vector[0].erase(0, root_pluses);
  std::vector<std::string> root_line =
      base::SplitStringUsingSubstr(tree_structure_vector[0], " ",
                                   base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);

  // Set to keep track of found IDs and make sure we don't have duplicates.
  std::set<int> found_ids;

  node_data_vector.push_back(ParseNodeInfo(root_line, found_ids));

  // This maps the number of plus signs to the last index that number of plus
  // signs was found at in the `tree_structure_vector` This will be useful for
  // determining descendants.
  std::unordered_map<int, int> last_index_appearance_of_plus_count;

  // The first (zeroth) row always has two plus signs
  last_index_appearance_of_plus_count[2] = 0;
  int last_count = 2;
  int greatest_count = 2;
  for (size_t i = 1; i < tree_structure_vector.size(); i++) {
    // Test authors might have added whitespace when passing in the tree
    // structure
    RemoveLeadingAndTrailingWhitespace(tree_structure_vector[i]);

    int plus_count = PlusSignCount(tree_structure_vector[i]);
    DCHECK(plus_count % 2 == 0)
        << "Error in plus sign count, can't have an odd number of plus signs";
    DCHECK(plus_count <= greatest_count + 2)
        << "Error in plus signs count at tree structure line number " << i
        << ", it can't have more than two more plus signs than the previous "
           "element.";

    if (plus_count > greatest_count) {
      greatest_count = plus_count;
    }

    auto elem = last_index_appearance_of_plus_count.find(plus_count);
    if (elem == last_index_appearance_of_plus_count.end() ||
        (int)i > elem->second) {
      last_index_appearance_of_plus_count[plus_count] = i;
    }

    // We remove the plus signs from the string.
    tree_structure_vector[i].erase(0, plus_count);
    std::vector<std::string> node_line = base::SplitStringUsingSubstr(
        tree_structure_vector[i], " ", base::KEEP_WHITESPACE,
        base::SPLIT_WANT_ALL);

    AXNodeData curr_node = ParseNodeInfo(node_line, found_ids);
    node_data_vector.push_back(curr_node);

    // Two cases. Either This line's plus count is greater than the last line's,
    // which would make `curr_node` the direct child of the last node. Or the
    // current plus count is <= last one, which would mean the parent is higher
    // up.
    if (plus_count > last_count) {
      node_data_vector[i - 1].child_ids.push_back(curr_node.id);
    } else {
      elem = last_index_appearance_of_plus_count.find(plus_count - 2);
      DCHECK(elem != last_index_appearance_of_plus_count.end())
          << "Error in plus sign count.";
      int parent_index = elem->second;
      node_data_vector[parent_index].child_ids.push_back(curr_node.id);
    }

    last_count = plus_count;
  }

  DCHECK(node_data_vector.size() >= 1);

  tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
  tree_data.focused_tree_id = tree_data.tree_id;
  tree_data.parent_tree_id = ui::AXTreeIDUnknown();
  tree_data = tree_data;
  has_tree_data = true;
  root_id = node_data_vector[0].id;

  for (auto& node : node_data_vector) {
    nodes.push_back(node);
  }
}

}  // namespace ui