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

#include "components/ui_devtools/dom_agent.h"

#include <algorithm>
#include <memory>
#include <string>
#include <utility>

#include "base/containers/adapters.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "components/ui_devtools/devtools_server.h"
#include "components/ui_devtools/root_element.h"
#include "components/ui_devtools/ui_devtools_features.h"
#include "components/ui_devtools/ui_element.h"
#include "ui/base/interaction/element_identifier.h"

namespace {

std::string CreateIdentifier() {
  static int last_used_identifier;
  return base::NumberToString(++last_used_identifier);
}

}  // namespace

namespace ui_devtools {

using ui_devtools::protocol::Array;
using ui_devtools::protocol::DOM::Node;
using ui_devtools::protocol::Response;

namespace {

bool FindMatchInStylesProperty(const std::string& query,
                               ui_devtools::UIElement* node) {
  for (UIElement::ClassProperties& class_properties :
       node->GetCustomPropertiesForMatchedStyle()) {
    for (UIElement::UIProperty& ui_property : class_properties.properties_) {
      protocol::String dataName = ui_property.name_;
      protocol::String dataValue = ui_property.value_;
      dataName = base::ToLowerASCII(dataName);
      dataValue = base::ToLowerASCII(dataValue);
      protocol::String style_property_match = dataName + ": " + dataValue + ";";
      if (style_property_match.find(query) != protocol::String::npos) {
        return true;
      }
    }
  }
  return false;
}

bool FindMatchByID(const std::string& query, ui_devtools::UIElement* node) {
  auto identifier = ui::ElementIdentifier::FromName(query.c_str());
  if (!identifier)
    return false;
  return node->FindMatchByElementID(identifier);
}

bool FindMatchInDomProperties(const std::string& query,
                              const std::string& tag_name_query,
                              const std::string& attribute_query,
                              bool exact_attribute_match,
                              ui_devtools::UIElement* node) {
  protocol::String node_name = node->GetTypeName();
  node_name = base::ToLowerASCII(node_name);
  if (node_name.find(query) != protocol::String::npos ||
      node_name == tag_name_query) {
    return true;
  }
  for (std::string& data : node->GetAttributes()) {
    data = base::ToLowerASCII(data);
    if (data.find(query) != protocol::String::npos) {
      return true;
    }
    if (data.find(attribute_query) != protocol::String::npos &&
        (!exact_attribute_match || data.length() == attribute_query.length())) {
      return true;
    }
  }
  return false;
}

// Returns a string of spaces for indentation based on the depth level.
std::string IndentString(int indent) {
  return std::string(indent * 2, ' ');
}

// Recursively serializes a UIElement and its children into a simplified
// markup-like string (e.g. <Widget>, <View> hierarchy) with attributes.
std::string SerializeUIElementToMarkup(const ui_devtools::UIElement* element,
                                       int indent = 0) {
  if (!element) {
    return {};
  }

  std::string markup;

  // Start tag with element type, e.g. <View
  markup += IndentString(indent) + "<" + element->GetTypeName();

  // UIElement::GetAttributes() always returns a flat vector of
  // name/value pairs: [name1, value1, name2, value2, ...].
  // Iterate in steps of 2 to serialize each attribute as name="value".
  auto attrs = element->GetAttributes();
  for (size_t i = 0; i + 1 < attrs.size(); i += 2) {
    markup += " " + attrs[i] + "=\"" + attrs[i + 1] + "\"";
  }

  const auto& children = element->children();
  if (children.empty()) {
    // If there are no children, output explicit closing tag
    // instead of self-closing syntax so that copied markup matches
    // what UI DevTools shows in the Elements panel.
    // Example: <View name="RootView"></View>
    markup += "></" + element->GetTypeName() + ">\n";
    return markup;
  }

  // Close the start tag and add a newline before children.
  markup += ">\n";

  // Recursively serialize child elements, increasing indentation.
  for (const auto& child : children) {
    markup += SerializeUIElementToMarkup(child, indent + 1);
  }

  // Append closing tag with proper indentation.
  markup += IndentString(indent) + "</" + element->GetTypeName() + ">\n";

  return markup;
}

}  // namespace

struct DOMAgent::Query {
  enum QueryType { Normal, Style, ID };
  Query(protocol::String query,
        protocol::String tag_name_query,
        protocol::String attribute_query,
        bool exact_attribute_match,
        QueryType query_type)
      : query_(query),
        tag_name_query_(tag_name_query),
        attribute_query_(attribute_query),
        exact_attribute_match_(exact_attribute_match),
        query_type_(query_type) {}
  protocol::String query_;
  protocol::String tag_name_query_;
  protocol::String attribute_query_;
  bool exact_attribute_match_;
  QueryType query_type_;
};

DOMAgent::DOMAgent() = default;

DOMAgent::~DOMAgent() {
  Reset();
}

Response DOMAgent::disable() {
  Reset();
  return Response::Success();
}

Response DOMAgent::getDocument(std::unique_ptr<Node>* out_root) {
  element_root_->ResetNodeId();
  *out_root = BuildInitialTree();
  is_document_created_ = true;
  return Response::Success();
}

Response DOMAgent::pushNodesByBackendIdsToFrontend(
    std::unique_ptr<protocol::Array<int>> backend_node_ids,
    std::unique_ptr<protocol::Array<int>>* result) {
  *result = std::move(backend_node_ids);
  return Response::Success();
}

void DOMAgent::OnUIElementAdded(UIElement* parent, UIElement* child) {
  // When parent is null, only need to update |node_id_to_ui_element_|.
  if (!parent) {
    node_id_to_ui_element_[child->node_id()] = child;
    return;
  }

  DCHECK(node_id_to_ui_element_.count(parent->node_id()));

  auto* current_parent = parent;
  while (current_parent) {
    if (current_parent->is_updating()) {
      // One of the parents is updating, so no need to update here.
      return;
    }
    current_parent = current_parent->parent();
  }

  child->set_is_updating(true);

  const auto& children = parent->children();
  auto iter = std::ranges::find(children, child);
  int prev_node_id =
      (iter == children.begin()) ? 0 : (*std::prev(iter))->node_id();
  frontend()->childNodeInserted(parent->node_id(), prev_node_id,
                                BuildTreeForUIElement(child));

  child->set_is_updating(false);
  for (auto& observer : observers_)
    observer.OnElementAdded(child);
}

void DOMAgent::OnUIElementReordered(UIElement* parent, UIElement* child) {
  DCHECK(node_id_to_ui_element_.count(parent->node_id()));

  const auto& children = parent->children();
  auto iter = std::ranges::find(children, child);
  CHECK(iter != children.end());
  int prev_node_id =
      (iter == children.begin()) ? 0 : (*std::prev(iter))->node_id();
  RemoveDomNode(child, false);
  frontend()->childNodeInserted(parent->node_id(), prev_node_id,
                                BuildDomNodeFromUIElement(child));
}

void DOMAgent::OnUIElementRemoved(UIElement* ui_element) {
  if (node_id_to_ui_element_.count(ui_element->node_id())) {
    // Check if |ui_element| exists in DOM before asking the frontend to remove
    // it. It is possible that this node is already removed along with its
    // ancestor. This happens for example when a bubble closes.
    // The DOM tree is first requested to be removed in
    // |WidgetElement::OnWidgetDestroyed()| then in
    // |ViewElement::OnChildViewRemoved()|.
    RemoveDomNode(ui_element, true);
  }
}

void DOMAgent::OnUIElementBoundsChanged(UIElement* ui_element) {
  for (auto& observer : observers_)
    observer.OnElementBoundsChanged(ui_element);
}

void DOMAgent::AddObserver(DOMAgentObserver* observer) {
  observers_.AddObserver(observer);
}

void DOMAgent::RemoveObserver(DOMAgentObserver* observer) {
  observers_.RemoveObserver(observer);
}

UIElement* DOMAgent::GetElementFromNodeId(int node_id) const {
  auto it = node_id_to_ui_element_.find(node_id);
  if (it != node_id_to_ui_element_.end())
    return it->second;
  return nullptr;
}

int DOMAgent::GetParentIdOfNodeId(int node_id) const {
  DCHECK(node_id_to_ui_element_.count(node_id));
  const UIElement* element = node_id_to_ui_element_.at(node_id);
  if (element->parent() && element->parent() != element_root_.get())
    return element->parent()->node_id();
  return 0;
}

std::unique_ptr<Node> DOMAgent::BuildNode(
    const std::string& name,
    std::unique_ptr<std::vector<std::string>> attributes,
    std::unique_ptr<Array<Node>> children,
    int node_ids) {
  constexpr int kDomElementNodeType = 1;
  std::unique_ptr<Node> node = Node::create()
                                   .setNodeId(node_ids)
                                   .setBackendNodeId(node_ids)
                                   .setNodeName(name)
                                   .setNodeType(kDomElementNodeType)
                                   .setAttributes(std::move(attributes))
                                   .build();
  node->setChildNodeCount(static_cast<int>(children->size()));
  node->setChildren(std::move(children));
  return node;
}

std::unique_ptr<Node> DOMAgent::BuildDomNodeFromUIElement(UIElement* root) {
  auto children = std::make_unique<protocol::Array<Node>>();
  for (ui_devtools::UIElement* it : root->children()) {
    children->emplace_back(BuildDomNodeFromUIElement(it));
  }

  return BuildNode(
      root->GetTypeName(),
      std::make_unique<std::vector<std::string>>(root->GetAttributes()),
      std::move(children), root->node_id());
}

std::unique_ptr<Node> DOMAgent::BuildInitialTree() {
  auto children = std::make_unique<protocol::Array<Node>>();

  element_root_ = std::make_unique<RootElement>(this);
  element_root_->set_is_updating(true);

  for (auto* child : CreateChildrenForRoot()) {
    children->emplace_back(BuildTreeForUIElement(child));
    element_root_->AddChild(child);
  }
  std::unique_ptr<Node> root_node =
      BuildNode("root", nullptr, std::move(children), element_root_->node_id());
  element_root_->set_is_updating(false);
  return root_node;
}

void DOMAgent::OnElementBoundsChanged(UIElement* ui_element) {
  for (auto& observer : observers_)
    observer.OnElementBoundsChanged(ui_element);
}

void DOMAgent::RemoveDomNode(UIElement* ui_element, bool update_node_id_map) {
  for (ui_devtools::UIElement* child_element : ui_element->children()) {
    RemoveDomNode(child_element, update_node_id_map);
  }
  frontend()->childNodeRemoved(ui_element->parent()->node_id(),
                               ui_element->node_id());
  if (update_node_id_map) {
    node_id_to_ui_element_.erase(ui_element->node_id());
  }
}

void DOMAgent::Reset() {
  element_root_.reset();
  node_id_to_ui_element_.clear();
  observers_.Clear();
  is_document_created_ = false;
  search_results_.clear();
}

DOMAgent::Query DOMAgent::PreprocessQuery(protocol::String query) {
  constexpr char kSearchIDKeyword[] = "id:";
  bool exact_attribute_match = false;
  if (query.find(kSearchIDKeyword) == 0) {
    // remove whitespaces (if they exist) after the 'id:' keyword
    base::TrimWhitespaceASCII(query.substr(strlen(kSearchIDKeyword)),
                              base::TrimPositions::TRIM_ALL, &query);
    return Query(query, query, query, exact_attribute_match,
                 Query::QueryType::ID);
  }

  // If it's not query by id, transform query to lower case.
  query = base::ToLowerASCII(query);

  constexpr char kSearchStylesPanelKeyword[] = "style:";
  // Temporary Solution: search on styles panel if the keyword 'style:'
  // exists in the beginning of the query.
  if (query.find(kSearchStylesPanelKeyword) == 0) {
    // remove whitespaces (if they exist) after the 'style:' keyword
    base::TrimWhitespaceASCII(query.substr(strlen(kSearchStylesPanelKeyword)),
                              base::TrimPositions::TRIM_ALL, &query);
    return Query(query, query, query, exact_attribute_match,
                 Query::QueryType::Style);
  }

  // Preprocessing for normal dom search.
  protocol::String tag_name_query = query;
  protocol::String attribute_query = query;
  unsigned query_length = query.length();
  bool start_tag_found = !query.find('<');
  bool end_tag_found = query.rfind('>') + 1 == query_length;
  bool start_quote_found = !query.find('"');
  bool end_quote_found = query.rfind('"') + 1 == query_length;
  exact_attribute_match = start_quote_found && end_quote_found;

  if (start_tag_found)
    tag_name_query = tag_name_query.substr(1, tag_name_query.length() - 1);
  if (end_tag_found)
    tag_name_query = tag_name_query.substr(0, tag_name_query.length() - 1);
  if (start_quote_found)
    attribute_query = attribute_query.substr(1, attribute_query.length() - 1);
  if (end_quote_found)
    attribute_query = attribute_query.substr(0, attribute_query.length() - 1);
  return Query(query, tag_name_query, attribute_query, exact_attribute_match,
               Query::QueryType::Normal);
}

void DOMAgent::SearchDomTree(const DOMAgent::Query& query_data,
                             std::vector<int>* result_collector) {
  std::vector<UIElement*> stack;
  // Root node from element_root() is not a real node from the DOM tree.
  // The children of the root node are the 'actual' roots of the DOM tree.
  std::vector<raw_ptr<UIElement, VectorExperimental>> root_list =
      element_root()->children();
  DCHECK(root_list.size());
  // Children are accessed from bottom to top. So iterate backwards.
  for (ui_devtools::UIElement* root : base::Reversed(root_list)) {
    stack.push_back(root);
  }

  // Manual plain text search. DFS traversal.
  while (!stack.empty()) {
    UIElement* node = stack.back();
    stack.pop_back();
    std::vector<raw_ptr<UIElement, VectorExperimental>> children_array =
        node->children();
    // Children are accessed from bottom to top. So iterate backwards.
    for (ui_devtools::UIElement* child : base::Reversed(children_array)) {
      stack.push_back(child);
    }
    bool found_match = false;
    if (query_data.query_type_ == Query::QueryType::Style)
      found_match = FindMatchInStylesProperty(query_data.query_, node);
    else if (query_data.query_type_ == Query::QueryType::ID)
      found_match = FindMatchByID(query_data.query_, node);
    else
      found_match = FindMatchInDomProperties(
          query_data.query_, query_data.tag_name_query_,
          query_data.attribute_query_, query_data.exact_attribute_match_, node);
    if (found_match)
      result_collector->push_back(node->node_id());
  }
}

// Code is based on InspectorDOMAgent::performSearch() from
// src/third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
Response DOMAgent::performSearch(
    const protocol::String& whitespace_trimmed_query,
    std::optional<bool> optional_include_user_agent_shadow_dom,
    protocol::String* search_id,
    int* result_count) {
  Query query_data = PreprocessQuery(whitespace_trimmed_query);
  if (!query_data.query_.length())
    return Response::Success();

  std::vector<int> result_collector;
  SearchDomTree(query_data, &result_collector);

  *search_id = CreateIdentifier();
  *result_count = result_collector.size();
  search_results_.insert(
      std::make_pair(*search_id, std::move(result_collector)));
  return Response::Success();
}

Response DOMAgent::getSearchResults(
    const protocol::String& search_id,
    int from_index,
    int to_index,
    std::unique_ptr<protocol::Array<int>>* node_ids) {
  SearchResults::iterator it = search_results_.find(search_id);
  if (it == search_results_.end())
    return Response::ServerError("No search session with given id found");

  int size = it->second.size();
  if (from_index < 0 || to_index > size || from_index >= to_index)
    return Response::ServerError("Invalid search result range");

  *node_ids = std::make_unique<protocol::Array<int>>();
  for (int i = from_index; i < to_index; ++i)
    (*node_ids)->emplace_back((it->second)[i]);
  return Response::Success();
}

Response DOMAgent::discardSearchResults(const protocol::String& search_id) {
  search_results_.erase(search_id);
  return Response::Success();
}

protocol::Response DOMAgent::dispatchMouseEvent(
    int node_id,
    std::unique_ptr<protocol::DOM::MouseEvent> event) {
  if (!base::FeatureList::IsEnabled(
          ui_devtools::kUIDebugToolsEnableSyntheticEvents))
    return Response::ServerError("Dispatch mouse events is not enabled.");
  auto it = node_id_to_ui_element_.find(node_id);
  if (it == node_id_to_ui_element_.end()) {
    return Response::ServerError("Element not found on node id");
  }
  if (!it->second->DispatchMouseEvent(event.get())) {
    return Response::ServerError("Failed to dispatch mouse event for node id");
  }
  return Response::Success();
}

protocol::Response DOMAgent::dispatchKeyEvent(
    int node_id,
    std::unique_ptr<protocol::DOM::KeyEvent> event) {
  if (!base::FeatureList::IsEnabled(
          ui_devtools::kUIDebugToolsEnableSyntheticEvents))
    return Response::ServerError("Dispatch key events is not enabled.");
  auto it = node_id_to_ui_element_.find(node_id);
  if (it == node_id_to_ui_element_.end()) {
    return Response::ServerError("Element not found on node id");
  }
  if (!it->second->DispatchKeyEvent(event.get())) {
    return Response::ServerError("Failed to dispatch key event for node id");
  }
  return Response::Success();
}

protocol::Response DOMAgent::getNodeBoundsInScreen(
    int node_id,
    std::unique_ptr<protocol::DOM::Rect>* bounds_in_screen) {
  auto it = node_id_to_ui_element_.find(node_id);
  if (it == node_id_to_ui_element_.end()) {
    return Response::ServerError("Element not found on node id");
  }

  UIElement* ui_element = it->second;
  gfx::Rect bounds = ui_element->GetNodeBoundsInScreen();

  *bounds_in_screen = protocol::DOM::Rect::create()
                          .setX(bounds.x())
                          .setY(bounds.y())
                          .setWidth(bounds.width())
                          .setHeight(bounds.height())
                          .build();

  return Response::Success();
}

protocol::Response DOMAgent::getDeviceScaleFactor(int node_id,
                                                  double* device_scale_factor) {
  auto it = node_id_to_ui_element_.find(node_id);
  if (it == node_id_to_ui_element_.end()) {
    return Response::ServerError("Element not found on node id");
  }

  UIElement* ui_element = it->second;
  if (ui_element->type() != UIElementType::WINDOW) {
    return Response::ServerError(
        "Node ID does not correspond to a window element");
  }

  *device_scale_factor = ui_element->GetDeviceScaleFactor();
  return Response::Success();
}

protocol::Response DOMAgent::getOuterHTML(int node_id,
                                          protocol::String* outer_html) {
  auto* element = GetElementFromNodeId(node_id);
  if (!element) {
    return Response::ServerError("Element not found on node id");
  }
  *outer_html = SerializeUIElementToMarkup(element);
  return Response::Success();
}

}  // namespace ui_devtools
