// Copyright 2019 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/platform/inspect/ax_property_node.h"

#include <optional>

#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "ui/accessibility/platform/inspect/ax_inspect.h"

namespace ui {

// static
AXPropertyNode AXPropertyNode::From(
    const std::string& property,
    const std::vector<std::string>& line_indexes) {
  std::string::const_iterator lvalue_end(property.end()),
      rvalue_begin(property.end());

  // Find rvalue start if any. For example, in a statement
  // textarea.AXSelectedTextMarkerRange = textarea_range, textarea_range will be
  // rvalue. Do not confuse it with variable assignment ':=', for example,
  // textarea_range:= textarea.AXTextMarkerRangeForUIElement(textarea) which is
  // stored as a labelled lvalue.
  size_t index = property.find_last_of('=');
  if (index > 0 && index != std::string::npos) {
    if (property[index - 1] != ':') {
      lvalue_end = property.begin() + index - 1;
      index = property.find_first_not_of("= ", index);
      rvalue_begin = index == std::string::npos ? property.end()
                                                : property.begin() + index;
    }
  }

  // lvalue
  AXPropertyNode lvalue_root;
  Parse(&lvalue_root, property.begin(), lvalue_end);
  if (lvalue_root.arguments.empty()) {  // Empty AXPropertyNode.
    return lvalue_root;
  }

  AXPropertyNode* lvalue = &lvalue_root.arguments[0];
  lvalue->original_property = std::string(property.begin(), lvalue_end);
  lvalue->line_indexes = line_indexes;

  // rvalue if any
  if (lvalue_end != property.end()) {
    AXPropertyNode rvalue_root;
    Parse(&rvalue_root, rvalue_begin, property.end());

    // Connect rvalue to the latest lvalue in a chain.
    AXPropertyNode* last_in_chain = lvalue;
    for (; last_in_chain->next; last_in_chain = last_in_chain->next.get())
      ;

    // Use {std::make_unique_for_overwrite} once we allow C++20.
    last_in_chain->rvalue = std::unique_ptr<AXPropertyNode>(new AXPropertyNode);
    *(last_in_chain->rvalue) = std::move(rvalue_root.arguments[0]);
  }

  return std::move(*lvalue);
}

// static
AXPropertyNode AXPropertyNode::From(const AXPropertyFilter& filter) {
  // Expel an optional trailing wildcard which is used in tree formatter output
  // filtering.
  std::string property =
      filter.property_str.substr(0, filter.property_str.find_last_of('*'));

  // Line indexes filter: filter_str expected format is
  // :line_num_1, ... :line_num_N, a comma separated list of line indexes
  // the property should be queried for. For example, ":1,:5,:7" indicates that
  // the property should called for objects placed on 1, 5 and 7 lines only.
  return From(property, base::SplitString(
                            filter.filter_str, std::string(1, ','),
                            base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY));
}

AXPropertyNode::AXPropertyNode() = default;
AXPropertyNode::AXPropertyNode(AXPropertyNode&& o) = default;
AXPropertyNode::~AXPropertyNode() = default;
AXPropertyNode& AXPropertyNode::operator=(AXPropertyNode&& o) = default;

AXPropertyNode::operator bool() const {
  return !name_or_value.empty();
}

bool AXPropertyNode::IsMatching(const std::string& pattern) const {
  // Looking for exact property match. Expel a trailing whildcard from
  // the property filter to handle filters like AXRole*.
  return name_or_value.compare(0, name_or_value.find_last_of('*'), pattern) ==
         0;
}

bool AXPropertyNode::IsArray() const {
  return name_or_value == "[]";
}

bool AXPropertyNode::IsDict() const {
  return name_or_value == "{}";
}

std::optional<int> AXPropertyNode::AsInt() const {
  int value = 0;
  if (!base::StringToInt(name_or_value, &value)) {
    return std::nullopt;
  }
  return value;
}

std::string AXPropertyNode::AsString() const {
  if (name_or_value.size() > 1 && name_or_value[0] == '\'' &&
      name_or_value[name_or_value.size() - 1] == '\'') {
    return name_or_value.substr(1, name_or_value.size() - 2);
  }
  return name_or_value;
}

const AXPropertyNode* AXPropertyNode::FindKey(const char* refkey) const {
  for (const auto& param : arguments) {
    if (param.key == refkey) {
      return &param;
    }
  }
  return nullptr;
}

std::optional<std::string> AXPropertyNode::FindStringKey(
    const char* refkey) const {
  for (const auto& param : arguments) {
    if (param.key == refkey) {
      return param.name_or_value;
    }
  }
  return std::nullopt;
}

std::optional<int> AXPropertyNode::FindIntKey(const char* refkey) const {
  for (const auto& param : arguments) {
    if (param.key == refkey) {
      return param.AsInt();
    }
  }
  return std::nullopt;
}

std::string AXPropertyNode::ToString() const {
  if (key.empty())
    return original_property;
  return key;
}

std::string AXPropertyNode::ToFlatString() const {
  std::string out;
  for (const auto& index : line_indexes) {
    if (!out.empty()) {
      out += ',';
    }
    out += index;
  }
  if (!out.empty()) {
    out += ';';
  }

  // Format a dictionary key or a variable.
  if (!key.empty()) {
    out += key + ": ";
  }

  out += name_or_value;
  if (arguments.size()) {
    out += '(';
    for (size_t i = 0; i < arguments.size(); i++) {
      if (i != 0) {
        out += ", ";
      }
      out += arguments[i].ToFlatString();
    }
    out += ')';
  }

  // Chains.
  if (next) {
    out += "." + next->ToFlatString();
  }

  // Rvalue.
  if (rvalue) {
    out += "=" + rvalue->ToFlatString();
  }

  return out;
}

std::string AXPropertyNode::ToTreeString(const std::string& indent) const {
  std::string out = indent;
  if (!key.empty()) {  // Key.
    out += key + ':';
  }
  out += name_or_value;    // Name or value.
  if (arguments.size()) {  // Arguments.
    out += "(\n";
    for (size_t i = 0; i < arguments.size(); i++) {
      if (i != 0) {
        out += ",\n";
      }
      out += arguments[i].ToTreeString(indent + "  ");
    }
    out += '\n' + indent + ')';
  }
  if (next) {  // Chains.
    out += ".\n" + next->ToTreeString(indent);
  }
  if (rvalue) {  // Rvalue.
    out += "=\n" + rvalue->ToTreeString(indent);
  }
  return out;
}

// private
AXPropertyNode::AXPropertyNode(AXPropertyNode::iterator key_begin,
                               AXPropertyNode::iterator key_end,
                               const std::string& name_or_value)
    : key(key_begin, key_end), name_or_value(name_or_value) {}
AXPropertyNode::AXPropertyNode(AXPropertyNode::iterator begin,
                               AXPropertyNode::iterator end)
    : name_or_value(begin, end) {}
AXPropertyNode::AXPropertyNode(AXPropertyNode::iterator key_begin,
                               AXPropertyNode::iterator key_end,
                               AXPropertyNode::iterator value_begin,
                               AXPropertyNode::iterator value_end)
    : key(key_begin, key_end), name_or_value(value_begin, value_end) {
}

// private static
AXPropertyNode::iterator AXPropertyNode::Parse(AXPropertyNode* node,
                                               AXPropertyNode::iterator begin,
                                               AXPropertyNode::iterator end) {
  auto iter = begin;
  auto key_begin = end, key_end = end, maybe_key_end = end;
  ParseState state = kArgument;
  while (iter != end) {
    // Subnode begins: create a new node, record its name and parse its
    // arguments.
    if (*iter == '(') {
      AXPropertyNode* child_node =
          node->ConnectTo(state & kChain, key_begin, key_end, begin, iter);

      key_begin = key_end = end;
      begin = iter = Parse(child_node, ++iter, end);
      continue;
    }

    // Subnode begins: a special case for arrays, which have [arg1, ..., argN]
    // form.
    if (*iter == '[') {
      // If a node ends by an array operator[], then chain them, for example,
      // AXChildren[0].
      if (begin != iter) {
        node->ConnectTo(state & kChain, key_begin, key_end, begin, iter);
        key_begin = key_end = end;
        state = kChain;
      }
      AXPropertyNode* child_node =
          node->ConnectTo(state & kChain, key_begin, key_end, "[]");
      key_begin = key_end = end;
      begin = iter = Parse(child_node, ++iter, end);
      continue;
    }

    // Subnode begins: a special case for dictionaries of {key1: value1, ...,
    // key2: value2} form.
    if (*iter == '{') {
      AXPropertyNode* child_node =
          node->AppendToArguments(key_begin, key_end, "{}");
      key_begin = key_end = end;
      begin = iter = Parse(child_node, ++iter, end);
      continue;
    }

    // Subnode ends.
    if (*iter == ')' || *iter == ']' || *iter == '}') {
      if (begin != iter) {
        node->ConnectTo(state & kChain, key_begin, key_end, begin, iter);
        key_begin = key_end = end;
      }
      state = kChain;
      return ++iter;
    }

    // Possible dictionary key or variable assignment end.
    if (*iter == ':') {
      maybe_key_end = iter++;
      continue;
    }

    // Possible variable assignment, move next.
    if (*iter == '=') {
      iter++;
      continue;
    }

    // Dictionary key or variable. If so, get a key and adjust a new node start.
    if (*iter == ' ') {
      if (maybe_key_end != end) {
        key_begin = begin;
        key_end = maybe_key_end;
        maybe_key_end = end;
      }
      begin = ++iter;
      continue;
    }

    // Not a dictionary key or a variable.
    maybe_key_end = end;

    // Call chains.
    if (*iter == '.') {
      if (begin != iter) {
        node->ConnectTo(state & kChain, key_begin, key_end, begin, iter);
        key_begin = key_end = end;
      }
      begin = ++iter;
      state = kChain;
      continue;
    }

    // Subsequent literal case.
    if (*iter == ',') {
      if (begin != iter) {
        node->ConnectTo(state & kChain, key_begin, key_end, begin, iter);
        key_begin = key_end = end;
      }
      state = kArgument;
      begin = ++iter;
      continue;
    }

    iter++;
  }

  // Single scalar param case.
  if (begin != iter) {
    node->ConnectTo(state & kChain, begin, iter);
  }
  return iter;
}

}  // namespace ui
