// 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_json_reader.h"

#include <algorithm>

#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/numerics/clamped_math.h"
#include "base/strings/string_split.h"
#include "ui/accessibility/ax_enum_util.h"

namespace {

using RoleConversions = const std::map<std::string, ax::mojom::Role>;

// The 3 lists below include all terms that are not parsed now if they are in a
// JSON file. Since this class is only used for testing, we will only encounter
// errors regarding that in the following two cases:
// - We add a new JSON file (or modify one) for testing that includes different
//   unsupported.
// - There is a new property added to AXNode that is not covered in the existing
//   JSON parsor and a test relies on it.
// In both above cases, existing tests will catch the issue and warn about the
// missing/changed property.
const base::flat_set<std::string> kUnusedAxNodeProperties = {
    "controls", "describedby", "details",    "disabled", "editable",
    "focused",  "hidden",      "hiddenRoot", "live",     "multiline",
    "readonly", "relevant",    "required",   "settable"};

const base::flat_set<std::string> kUnusedAxNodeItems = {
    "frameId", "ignoredReasons", "parentId"};

const base::flat_set<std::string> kUnusedStyles = {
    "background-image", "background-size", "clip",         "font-style",
    "margin-bottom",    "margin-left",     "margin-right", "margin-top",
    "opacity",          "padding-bottom",  "padding-left", "padding-right",
    "padding-top",      "position",        "text-align",   "text-decoration",
    "z-index"};

int GetAsInt(const base::Value& value) {
  if (value.is_int())
    return value.GetInt();
  if (value.is_string())
    return atoi(value.GetString().c_str());

  NOTREACHED() << "Unexpected: " << value;
  return 0;
}

double GetAsDouble(const base::Value& value) {
  if (value.is_double())
    return value.GetDouble();
  if (value.is_int())
    return value.GetInt();
  if (value.is_string())
    return atof(value.GetString().c_str());

  NOTREACHED() << "Unexpected: " << value;
  return 0;
}

bool GetAsBoolean(const base::Value& value) {
  if (value.is_bool())
    return value.GetBool();
  if (value.is_string()) {
    if (value.GetString() == "false")
      return false;
    if (value.GetString() == "true")
      return true;
  }

  NOTREACHED() << "Unexpected: " << value;
  return false;
}

void GetTypeAndValue(const base::Value& node,
                     std::string& type,
                     std::string& value) {
  type = node.GetDict().Find("type")->GetString();
  value = node.GetDict().Find("value")->GetString();
}

ui::AXNodeID AddNode(ui::AXTreeUpdate& tree_update,
                     const base::Value& node,
                     RoleConversions* role_conversions);

void ParseAxNodeChildIds(ui::AXNodeData& node_data,
                         const base::Value& child_ids) {
  for (const auto& item : child_ids.GetList())
    node_data.child_ids.push_back(GetAsInt(item));
}

void ParseAxNodeDescription(ui::AXNodeData& node_data,
                            const base::Value& description) {
  std::string type, value;
  GetTypeAndValue(description, type, value);
  DCHECK_EQ(type, "computedString");
  node_data.SetDescription(value);
}

void ParseAxNodeName(ui::AXNodeData& node_data, const base::Value& name) {
  std::string type, value;
  GetTypeAndValue(name, type, value);
  DCHECK_EQ(type, "computedString");
  node_data.SetName(value);
}

void ParseAxNodeProperties(ui::AXNodeData& node_data,
                           const base::Value& properties) {
  if (properties.is_list()) {
    for (const auto& item : properties.GetList())
      ParseAxNodeProperties(node_data, item);
    return;
  }

  const std::string prop_type = properties.GetDict().Find("name")->GetString();
  const base::Value* prop_value =
      properties.GetDict().Find("value")->GetDict().Find("value");

  if (prop_type == "atomic") {
    node_data.AddBoolAttribute(
        ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot,
        !GetAsBoolean(*prop_value));
  } else if (prop_type == "focusable") {
    if (GetAsBoolean(*prop_value))
      node_data.AddState(ax::mojom::State::kFocusable);
  } else if (prop_type == "expanded") {
    if (GetAsBoolean(*prop_value))
      node_data.AddState(ax::mojom::State::kExpanded);
  } else if (prop_type == "hasPopup") {
    node_data.SetHasPopup(
        ui::ParseAXEnum<ax::mojom::HasPopup>(prop_value->GetString().c_str()));
  } else if (prop_type == "invalid") {
    node_data.SetInvalidState(GetAsBoolean(*prop_value)
                                  ? ax::mojom::InvalidState::kTrue
                                  : ax::mojom::InvalidState::kFalse);
  } else if (prop_type == "level") {
    node_data.AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
                              GetAsInt(*prop_value));
  } else {
    DCHECK(base::Contains(kUnusedAxNodeProperties, prop_type)) << prop_type;
  }
}

ax::mojom::Role RoleFromString(std::string role,
                               RoleConversions* role_conversions) {
  const auto& item = role_conversions->find(role);
  DCHECK(item != role_conversions->end()) << role;
  return item->second;
}

void ParseAxNodeRole(ui::AXNodeData& node_data,
                     const base::Value& role,
                     RoleConversions* role_conversions) {
  const std::string role_type = role.GetDict().Find("type")->GetString();
  std::string role_value = role.GetDict().Find("value")->GetString();

  DCHECK(role_type == "role" || role_type == "internalRole");

  node_data.role = RoleFromString(role_value, role_conversions);
}

void ParseAxNode(ui::AXNodeData& node_data,
                 const base::Value& ax_node,
                 RoleConversions* role_conversions) {
  // Store the name and set it at the end because |AXNodeData::SetName|
  // expects a valid role to have already been set prior to calling it.
  base::Value name_value;
  for (const auto item : ax_node.GetDict()) {
    if (item.first == "backendDOMNodeId") {
      node_data.AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId,
                                GetAsInt(item.second));
    } else if (item.first == "childIds") {
      ParseAxNodeChildIds(node_data, item.second);
    } else if (item.first == "description") {
      ParseAxNodeDescription(node_data, item.second);
    } else if (item.first == "ignored") {
      DCHECK(item.second.is_bool());
      if (item.second.GetBool())
        node_data.AddState(ax::mojom::State::kIgnored);
    } else if (item.first == "name") {
      name_value = item.second.Clone();
    } else if (item.first == "nodeId") {
      node_data.id = GetAsInt(item.second);
    } else if (item.first == "properties") {
      ParseAxNodeProperties(node_data, item.second);
    } else if (item.first == "role") {
      ParseAxNodeRole(node_data, item.second, role_conversions);
    } else {
      DCHECK(base::Contains(kUnusedAxNodeItems, item.first)) << item.first;
    }
  }
  if (!name_value.is_none())
    ParseAxNodeName(node_data, name_value);
}

void ParseChildren(ui::AXTreeUpdate& tree_update,
                   const base::Value& children,
                   RoleConversions* role_conversions) {
  for (const auto& child : children.GetList())
    AddNode(tree_update, child, role_conversions);
}

// Converts "rgb(R,G,B)" or "rgba(R,G,B,A)" to one ARGB integer where R,G, and B
// are integers and A is float < 1.
uint32_t ConvertRgbaStringToArgbInt(const std::string& argb_string) {
  std::vector<std::string> values = base::SplitString(
      argb_string, ",()", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

  uint32_t a, r, g, b;

  if (values.size() == 4 && values[0] == "rgb") {
    a = 0;
  } else if (values.size() == 5 && values[0] == "rgba") {
    a = base::ClampRound(atof(values[4].c_str()) * 255);
  } else {
    NOTREACHED() << "Unexpected color value: " << argb_string;
    return -1;
  }

  r = atoi(values[1].c_str());
  g = atoi(values[2].c_str());
  b = atoi(values[3].c_str());

  return (a << 24) + (r << 16) + (g << 8) + b;
}

void ParseStyle(ui::AXNodeData& node_data, const base::Value& style) {
  const std::string& name = style.GetDict().Find("name")->GetString();
  const std::string& value = style.GetDict().Find("value")->GetString();

  if (name == "color") {
    node_data.AddIntAttribute(ax::mojom::IntAttribute::kColor,
                              ConvertRgbaStringToArgbInt(value));
  } else if (name == "direction") {
    node_data.AddIntAttribute(
        ax::mojom::IntAttribute::kTextDirection,
        static_cast<int>(
            ui::ParseAXEnum<ax::mojom::WritingDirection>(value.c_str())));
  } else if (name == "display") {
    node_data.AddStringAttribute(ax::mojom::StringAttribute::kDisplay, value);
  } else if (name == "font-size") {
    // Drop the 'px' at the end of font size.
    DCHECK(style.GetDict().Find("value")->is_string());
    node_data.AddFloatAttribute(
        ax::mojom::FloatAttribute::kFontSize,
        atof(value.substr(0, value.length() - 2).c_str()));
  } else if (name == "font-weight") {
    DCHECK(style.GetDict().Find("value")->is_string());
    node_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize,
                                atof(value.c_str()));
  } else if (name == "list-style-type") {
    node_data.AddIntAttribute(
        ax::mojom::IntAttribute::kListStyle,
        static_cast<int>(ui::ParseAXEnum<ax::mojom::ListStyle>(value.c_str())));
  } else if (name == "visibility") {
    if (value == "hidden")
      node_data.AddState(ax::mojom::State::kInvisible);
    else
      DCHECK_EQ(value, "visible");
  } else {
    DCHECK(base::Contains(kUnusedStyles, name)) << name;
  }
}

void ParseExtras(ui::AXNodeData& node_data, const base::Value& extras) {
  for (const auto extra : extras.GetDict()) {
    const base::Value::List& items = extra.second.GetList();
    if (extra.first == "bounds") {
      node_data.relative_bounds.bounds.set_x(GetAsDouble(items[0]));
      node_data.relative_bounds.bounds.set_y(GetAsDouble(items[1]));
      node_data.relative_bounds.bounds.set_width(GetAsDouble(items[2]));
      node_data.relative_bounds.bounds.set_height(GetAsDouble(items[3]));
    } else if (extra.first == "styles") {
      for (const auto& style : items)
        ParseStyle(node_data, style);
    } else {
      NOTREACHED() << "Unexpected: " << extra.first;
    }
  }
}

// Adds a node and returns its id.
ui::AXNodeID AddNode(ui::AXTreeUpdate& tree_update,
                     const base::Value& node,
                     RoleConversions* role_conversions) {
  ui::AXNodeData node_data;

  // Store the string and set it at the end because |AXNodeData::SetName|
  // expects a valid role to have already been set prior to calling it.
  std::string name_string;

  for (const auto item : node.GetDict()) {
    if (item.first == "axNode") {
      ParseAxNode(node_data, item.second, role_conversions);
    } else if (item.first == "backendDomId") {
      node_data.AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId,
                                GetAsInt(item.second));
    } else if (item.first == "children") {
      ParseChildren(tree_update, item.second, role_conversions);
    } else if (item.first == "description") {
      node_data.SetDescription(item.second.GetString());
    } else if (item.first == "extras") {
      ParseExtras(node_data, item.second);
    } else if (item.first == "interesting") {
      // Not used yet, boolean.
    } else if (item.first == "name") {
      name_string = item.second.GetString();
    } else if (item.first == "role") {
      node_data.role =
          RoleFromString(item.second.GetString(), role_conversions);
    } else {
      NOTREACHED() << "Unexpected: " << item.first;
    }
  }

  node_data.SetName(name_string);

  tree_update.nodes.push_back(node_data);

  return node_data.id;
}

}  // namespace

namespace ui {

AXTreeUpdate AXTreeUpdateFromJSON(const base::Value& json,
                                  RoleConversions* role_conversions) {
  AXTreeUpdate tree_update;

  // Input should be a list with one item, which is the root node.
  DCHECK(json.is_list() && json.GetList().size() == 1);

  tree_update.root_id =
      AddNode(tree_update, json.GetList().front(), role_conversions);

  // |AddNode| adds child nodes before parent nodes, while AXTree deserializer
  // expects parents first.
  std::reverse(tree_update.nodes.begin(), tree_update.nodes.end());

  return tree_update;
}

}  // namespace ui