// Copyright 2016 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/css_agent.h"

#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "components/ui_devtools/agent_util.h"
#include "components/ui_devtools/ui_element.h"

namespace ui_devtools {

namespace CSS = protocol::CSS;
using protocol::Array;
using protocol::Response;

namespace {

const char kHeight[] = "height";
const char kWidth[] = "width";
const char kX[] = "x";
const char kY[] = "y";
const char kVisibility[] = "visibility";

std::unique_ptr<CSS::SourceRange> BuildDefaultPropertySourceRange() {
  // These tell the frontend where in the stylesheet a certain style
  // is located. Since we don't have stylesheets, this is all 0.
  // We need this because CSS fields are not editable unless
  // the range is provided.
  return CSS::SourceRange::create()
      .setStartLine(0)
      .setEndLine(0)
      .setStartColumn(0)
      .setEndColumn(0)
      .build();
}

std::unique_ptr<CSS::SourceRange> BuildDefaultSelectorSourceRange() {
  // This is a different source range from BuildDefaultPropertySourceRange()
  // used for the Selectors, so the frontend correctly handles property edits.
  return CSS::SourceRange::create()
      .setStartLine(1)
      .setEndLine(0)
      .setStartColumn(0)
      .setEndColumn(0)
      .build();
}

std::unique_ptr<Array<int>> BuildDefaultMatchingSelectors() {
  auto matching_selectors = std::make_unique<Array<int>>();

  // Add index 0 to matching selectors array, so frontend uses the class name
  // from the selectors array as the header for the properties section
  matching_selectors->emplace_back(0);
  return matching_selectors;
}

std::unique_ptr<CSS::CSSProperty> BuildCSSProperty(const std::string& name,
                                                   const std::string& value) {
  return CSS::CSSProperty::create()
      .setRange(BuildDefaultPropertySourceRange())
      .setName(name)
      .setValue(value)
      .build();
}

std::unique_ptr<Array<CSS::CSSProperty>> BuildCSSProperties(
    const std::vector<UIElement::UIProperty>& properties_vector) {
  auto css_properties = std::make_unique<Array<CSS::CSSProperty>>();
  for (const auto& property : properties_vector) {
    css_properties->emplace_back(
        BuildCSSProperty(property.name_, property.value_));
  }
  return css_properties;
}

std::unique_ptr<CSS::CSSStyle> BuildCSSStyle(
    std::string stylesheet_uid,
    const std::vector<UIElement::UIProperty>& properties) {
  return CSS::CSSStyle::create()
      .setRange(BuildDefaultPropertySourceRange())
      .setCssProperties(BuildCSSProperties(properties))
      .setShorthandEntries(std::make_unique<Array<CSS::ShorthandEntry>>())
      .setStyleSheetId(stylesheet_uid)
      .build();
}

std::unique_ptr<Array<CSS::Value>> BuildSelectors(const std::string& name) {
  auto selectors = std::make_unique<Array<CSS::Value>>();
  selectors->emplace_back(CSS::Value::create()
                              .setText(name)
                              .setRange(BuildDefaultSelectorSourceRange())
                              .build());
  return selectors;
}

std::unique_ptr<CSS::SelectorList> BuildSelectorList(const std::string& name) {
  return CSS::SelectorList::create().setSelectors(BuildSelectors(name)).build();
}

std::unique_ptr<CSS::CSSRule> BuildCSSRule(
    std::string stylesheet_uid,
    const UIElement::ClassProperties& class_properties) {
  return CSS::CSSRule::create()
      .setStyleSheetId(stylesheet_uid)
      .setSelectorList(BuildSelectorList(class_properties.class_name_))
      .setStyle(BuildCSSStyle(stylesheet_uid, class_properties.properties_))
      .build();
}

std::vector<UIElement::ClassProperties> GetClassPropertiesWithBounds(
    UIElement* ui_element) {
  std::vector<UIElement::ClassProperties> properties_vector =
      ui_element->GetCustomPropertiesForMatchedStyle();

  // If GetCustomPropertiesForMatchedStyle not overridden to return custom
  // properties, populate vector with bounds properties.
  if (properties_vector.empty()) {
    gfx::Rect bounds;
    ui_element->GetBounds(&bounds);
    std::vector<UIElement::UIProperty> bound_properties;
    bound_properties.emplace_back(kX, base::NumberToString(bounds.x()));
    bound_properties.emplace_back(kY, base::NumberToString(bounds.y()));
    bound_properties.emplace_back(kWidth, base::NumberToString(bounds.width()));
    bound_properties.emplace_back(kHeight,
                                  base::NumberToString(bounds.height()));
    if (ui_element->type() != VIEW) {
      bool visible;
      ui_element->GetVisible(&visible);
      bound_properties.emplace_back(kVisibility, base::ToString(visible));
    }
    properties_vector.emplace_back(ui_element->GetTypeName(), bound_properties);
  }

  // Set base stylesheet ID to the last index in the vector, so when bounds
  // properties are modified, CSSAgent can update and return the right
  // properties section
  ui_element->SetBaseStylesheetId(properties_vector.size() - 1);
  return properties_vector;
}

std::string BuildStylesheetUId(int node_id, int stylesheet_id) {
  return base::NumberToString(node_id) + "_" +
         base::NumberToString(stylesheet_id);
}

Response NodeNotFoundError(int node_id) {
  return Response::ServerError("Node with id=" + base::NumberToString(node_id) +
                               " not found");
}

Response ParseProperties(const std::string& style_text,
                         gfx::Rect* bounds,
                         bool* visible) {
  std::vector<std::string> tokens = base::SplitString(
      style_text, ":;", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

  if (tokens.size() < 2 || tokens.size() % 2 != 0)
    return Response::ServerError("Need both a property name and value.");

  for (size_t i = 0; i < tokens.size() - 1; i += 2) {
    const std::string& property = tokens.at(i);
    int value;
    if (!base::StringToInt(tokens.at(i + 1), &value)) {
      return Response::ServerError("Unable to parse value for property=" +
                                   property);
    }

    if (property == kHeight)
      bounds->set_height(std::max(0, value));
    else if (property == kWidth)
      bounds->set_width(std::max(0, value));
    else if (property == kX)
      bounds->set_x(value);
    else if (property == kY)
      bounds->set_y(value);
    else if (property == kVisibility)
      *visible = std::max(0, value) == 1;
    else
      return Response::ServerError("Unsupported property=" + property);
  }
  return Response::Success();
}

std::unique_ptr<CSS::CSSStyleSheetHeader> BuildObjectForStyleSheetInfo(
    std::string stylesheet_uid,
    std::string url_path,
    int line) {
  std::unique_ptr<CSS::CSSStyleSheetHeader> result =
      CSS::CSSStyleSheetHeader::create()
          .setStyleSheetId(stylesheet_uid)
          .setSourceURL(kChromiumCodeSearchSrcURL + url_path +
                        "?l=" + base::NumberToString(line))
          .setStartLine(line)
          .setStartColumn(0)
          .build();
  return result;
}

}  // namespace

CSSAgent::CSSAgent(DOMAgent* dom_agent) : dom_agent_(dom_agent) {
  DCHECK(dom_agent_);
}

CSSAgent::~CSSAgent() {
  disable();
}

Response CSSAgent::enable() {
  dom_agent_->AddObserver(this);
  return Response::Success();
}

Response CSSAgent::disable() {
  dom_agent_->RemoveObserver(this);
  return Response::Success();
}

Response CSSAgent::getMatchedStylesForNode(
    int node_id,
    std::unique_ptr<Array<CSS::RuleMatch>>* matched_css_rules) {
  UIElement* ui_element = dom_agent_->GetElementFromNodeId(node_id);
  if (!ui_element)
    return NodeNotFoundError(node_id);
  *matched_css_rules = BuildMatchedStyles(ui_element);
  return Response::Success();
}

Response CSSAgent::getStyleSheetText(const protocol::String& style_sheet_id,
                                     protocol::String* result) {
  int node_id;
  int stylesheet_id;
  std::vector<std::string> ids = base::SplitString(
      style_sheet_id, "_", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  if (ids.size() < 2 || !base::StringToInt(ids[0], &node_id) ||
      !base::StringToInt(ids[1], &stylesheet_id))
    return Response::ServerError("Invalid stylesheet id");

  UIElement* ui_element = dom_agent_->GetElementFromNodeId(node_id);
  if (!ui_element)
    return Response::ServerError("Node id not found");

  auto sources = ui_element->GetSources();
  if (static_cast<int>(sources.size()) <= stylesheet_id || stylesheet_id < 0) {
    return Response::ServerError("Stylesheet id not found");
  }

  if (GetSourceCode(sources[stylesheet_id].path_, result))
    return Response::Success();
  return Response::ServerError("Could not read source file");
}

Response CSSAgent::setStyleTexts(
    std::unique_ptr<Array<CSS::StyleDeclarationEdit>> edits,
    std::unique_ptr<Array<CSS::CSSStyle>>* result) {
  auto updated_styles = std::make_unique<Array<CSS::CSSStyle>>();
  for (const auto& edit : *edits) {
    int node_id;
    int stylesheet_id;

    std::vector<std::string> ids =
        base::SplitString(edit->getStyleSheetId(), "_", base::TRIM_WHITESPACE,
                          base::SPLIT_WANT_NONEMPTY);
    if (ids.size() < 2 || !base::StringToInt(ids[0], &node_id) ||
        !base::StringToInt(ids[1], &stylesheet_id))
      return Response::ServerError("Invalid stylesheet id");

    UIElement* ui_element = dom_agent_->GetElementFromNodeId(node_id);

    if (!ui_element)
      return Response::ServerError("Node id not found");
    if (stylesheet_id < 0 ||
        !ui_element->SetPropertiesFromString(static_cast<size_t>(stylesheet_id),
                                             edit->getText())) {
      gfx::Rect updated_bounds;
      bool visible = false;
      if (!GetPropertiesForUIElement(ui_element, &updated_bounds, &visible))
        return NodeNotFoundError(node_id);

      Response response(
          ParseProperties(edit->getText(), &updated_bounds, &visible));
      if (!response.IsSuccess())
        return response;

      if (!SetPropertiesForUIElement(ui_element, updated_bounds, visible))
        return NodeNotFoundError(node_id);
    }

    updated_styles->emplace_back(BuildCSSStyle(
        edit->getStyleSheetId(), GetClassPropertiesWithBounds(ui_element)
                                     .at(stylesheet_id)
                                     .properties_));
  }
  *result = std::move(updated_styles);
  return Response::Success();
}

void CSSAgent::OnElementBoundsChanged(UIElement* ui_element) {
  InvalidateStyleSheet(ui_element);
}

void CSSAgent::InvalidateStyleSheet(UIElement* ui_element) {
  // The stylesheetId for each node is equivalent to a string of its
  // node_id + "_" + index of CSS::RuleMatch in vector.
  frontend()->styleSheetChanged(BuildStylesheetUId(
      ui_element->node_id(), ui_element->GetBaseStylesheetId()));
}

bool CSSAgent::GetPropertiesForUIElement(UIElement* ui_element,
                                         gfx::Rect* bounds,
                                         bool* visible) {
  if (ui_element) {
    ui_element->GetBounds(bounds);
    if (ui_element->type() != VIEW)
      ui_element->GetVisible(visible);
    return true;
  }
  return false;
}

bool CSSAgent::SetPropertiesForUIElement(UIElement* ui_element,
                                         const gfx::Rect& bounds,
                                         bool visible) {
  if (ui_element) {
    ui_element->SetBounds(bounds);
    ui_element->SetVisible(visible);
    return true;
  }
  return false;
}

std::unique_ptr<Array<CSS::RuleMatch>> CSSAgent::BuildMatchedStyles(
    UIElement* ui_element) {
  auto result = std::make_unique<Array<CSS::RuleMatch>>();
  std::vector<UIElement::ClassProperties> properties_vector =
      GetClassPropertiesWithBounds(ui_element);

  for (size_t i = 0; i < properties_vector.size(); i++) {
    result->emplace_back(
        CSS::RuleMatch::create()
            .setRule(BuildCSSRule(BuildStylesheetUId(ui_element->node_id(), i),
                                  properties_vector[i]))
            .setMatchingSelectors(BuildDefaultMatchingSelectors())
            .build());
  }
  if (!ui_element->header_sent()) {
    InitStylesheetHeaders(ui_element);
  }
  return result;
}

void CSSAgent::InitStylesheetHeaders(UIElement* ui_element) {
  std::vector<UIElement::Source> sources = ui_element->GetSources();
  for (size_t i = 0; i < sources.size(); i++) {
    frontend()->styleSheetAdded(BuildObjectForStyleSheetInfo(
        BuildStylesheetUId(ui_element->node_id(), i), sources[i].path_,
        sources[i].line_));
  }
  ui_element->set_header_sent();
}

}  // namespace ui_devtools
