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

#include "chromeos-dbus-bindings/xml_interface_parser.h"

#include <utility>

#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/string_util.h>
#include <brillo/strings/string_utils.h>

using std::string;
using std::vector;

namespace chromeos_dbus_bindings {

// static
const char XmlInterfaceParser::kArgumentTag[] = "arg";
const char XmlInterfaceParser::kInterfaceTag[] = "interface";
const char XmlInterfaceParser::kMethodTag[] = "method";
const char XmlInterfaceParser::kNodeTag[] = "node";
const char XmlInterfaceParser::kSignalTag[] = "signal";
const char XmlInterfaceParser::kPropertyTag[] = "property";
const char XmlInterfaceParser::kAnnotationTag[] = "annotation";
const char XmlInterfaceParser::kDocStringTag[] = "tp:docstring";
const char XmlInterfaceParser::kNameAttribute[] = "name";
const char XmlInterfaceParser::kTypeAttribute[] = "type";
const char XmlInterfaceParser::kValueAttribute[] = "value";
const char XmlInterfaceParser::kDirectionAttribute[] = "direction";
const char XmlInterfaceParser::kAccessAttribute[] = "access";
const char XmlInterfaceParser::kArgumentDirectionIn[] = "in";
const char XmlInterfaceParser::kArgumentDirectionOut[] = "out";

const char XmlInterfaceParser::kTrue[] = "true";
const char XmlInterfaceParser::kFalse[] = "false";

const char XmlInterfaceParser::kMethodConst[] =
    "org.chromium.DBus.Method.Const";
const char XmlInterfaceParser::kMethodAsync[] =
    "org.freedesktop.DBus.GLib.Async";
const char XmlInterfaceParser::kMethodIncludeDBusMessage[] =
    "org.chromium.DBus.Method.IncludeDBusMessage";

const char XmlInterfaceParser::kMethodKind[] = "org.chromium.DBus.Method.Kind";
const char XmlInterfaceParser::kMethodKindSimple[] = "simple";
const char XmlInterfaceParser::kMethodKindNormal[] = "normal";
const char XmlInterfaceParser::kMethodKindAsync[] = "async";
const char XmlInterfaceParser::kMethodKindRaw[] = "raw";

namespace {

string GetElementPath(const vector<string>& path) {
  return brillo::string_utils::Join("/", path);
}

}  // anonymous namespace

bool XmlInterfaceParser::ParseXmlInterfaceFile(
    const std::string& contents,
    const std::vector<std::string>& ignore_interfaces) {
  auto parser = XML_ParserCreate(nullptr);
  XML_SetUserData(parser, this);
  XML_SetElementHandler(parser,
                        &XmlInterfaceParser::HandleElementStart,
                        &XmlInterfaceParser::HandleElementEnd);
  XML_SetCharacterDataHandler(parser, &XmlInterfaceParser::HandleCharData);
  const int kIsFinal = XML_TRUE;

  element_path_.clear();
  XML_Status res = XML_Parse(parser,
                             contents.c_str(),
                             contents.size(),
                             kIsFinal);
  XML_ParserFree(parser);

  if (res != XML_STATUS_OK) {
    LOG(ERROR) << "XML parse failure";
    return false;
  }

  CHECK(element_path_.empty());

  if (!ignore_interfaces.empty()) {
    // Remove interfaces whose names are in |ignore_interfaces| list.
    auto condition = [&ignore_interfaces](const Interface& itf) {
      return std::find(ignore_interfaces.begin(), ignore_interfaces.end(),
                       itf.name) != ignore_interfaces.end();
    };
    auto p = std::remove_if(interfaces_.begin(), interfaces_.end(), condition);
    interfaces_.erase(p, interfaces_.end());
  }
  return true;
}

void XmlInterfaceParser::OnOpenElement(
    const string& element_name, const XmlAttributeMap& attributes) {
  string prev_element;
  if (!element_path_.empty())
    prev_element = element_path_.back();
  element_path_.push_back(element_name);
  if (element_name == kNodeTag) {
    CHECK(prev_element.empty() || prev_element == kNodeTag)
        << "Unexpected tag " << element_name << " inside " << prev_element;
    // 'name' attribute is optional for <node> element.
    string name;
    GetElementAttribute(attributes, element_path_, kNameAttribute, &name);
    base::TrimWhitespaceASCII(name, base::TRIM_ALL, &name);
    node_names_.push_back(name);
  } else if (element_name == kInterfaceTag) {
    CHECK_EQ(kNodeTag, prev_element)
        << "Unexpected tag " << element_name << " inside " << prev_element;
    string interface_name = GetValidatedElementName(attributes, element_path_);
    Interface itf(interface_name,
                  std::vector<Interface::Method>{},
                  std::vector<Interface::Signal>{},
                  std::vector<Interface::Property>{});
    itf.path = node_names_.back();
    interfaces_.push_back(std::move(itf));
  } else if (element_name == kMethodTag) {
    CHECK_EQ(kInterfaceTag, prev_element)
        << "Unexpected tag " << element_name << " inside " << prev_element;
    interfaces_.back().methods.push_back(
        Interface::Method(GetValidatedElementName(attributes, element_path_)));
  } else if (element_name == kSignalTag) {
    CHECK_EQ(kInterfaceTag, prev_element)
        << "Unexpected tag " << element_name << " inside " << prev_element;
    interfaces_.back().signals.push_back(
        Interface::Signal(GetValidatedElementName(attributes, element_path_)));
  } else if (element_name == kPropertyTag) {
    CHECK_EQ(kInterfaceTag, prev_element)
        << "Unexpected tag " << element_name << " inside " << prev_element;
    interfaces_.back().properties.push_back(ParseProperty(attributes,
                                                          element_path_));
  } else if (element_name == kArgumentTag) {
    if (prev_element == kMethodTag) {
      AddMethodArgument(attributes);
    } else if (prev_element == kSignalTag) {
      AddSignalArgument(attributes);
    } else {
      LOG(FATAL) << "Unexpected tag " << element_name
                 << " inside " << prev_element;
    }
  } else if (element_name == kAnnotationTag) {
    string name = GetValidatedElementAttribute(attributes, element_path_,
                                               kNameAttribute);
    // Value is optional. Default to empty string if omitted.
    string value;
    GetElementAttribute(attributes, element_path_, kValueAttribute, &value);
    if (prev_element == kInterfaceTag) {
      // Parse interface annotations...
    } else if (prev_element == kMethodTag) {
      // Parse method annotations...
      Interface::Method& method = interfaces_.back().methods.back();
      if (name == kMethodConst) {
        CHECK(value == kTrue || value == kFalse);
        method.is_const = (value == kTrue);
      } else if (name == kMethodIncludeDBusMessage) {
        CHECK(value == kTrue || value == kFalse);
        method.include_dbus_message = (value == kTrue);
      } else if (name == kMethodAsync) {
        // Support GLib.Async annotation as well.
        method.kind = Interface::Method::Kind::kAsync;
      } else if (name == kMethodKind) {
        if (value == kMethodKindSimple) {
          method.kind = Interface::Method::Kind::kSimple;
        } else if (value == kMethodKindNormal) {
          method.kind = Interface::Method::Kind::kNormal;
        } else if (value == kMethodKindAsync) {
          method.kind = Interface::Method::Kind::kAsync;
        } else if (value == kMethodKindRaw) {
          method.kind = Interface::Method::Kind::kRaw;
        } else {
          LOG(FATAL) << "Invalid method kind: " << value;
        }
      }
    } else if (prev_element == kSignalTag) {
      // Parse signal annotations...
    } else if (prev_element == kPropertyTag) {
      // Parse property annotations...
    } else {
      LOG(FATAL) << "Unexpected tag " << element_name
                 << " inside " << prev_element;
    }
  } else if (element_name == kDocStringTag) {
    CHECK(!prev_element.empty() && prev_element != kNodeTag)
        << "Unexpected tag " << element_name << " inside " << prev_element;
  }
}

void XmlInterfaceParser::OnCharData(const std::string& content) {
  // Handle the text data only for <tp:docstring> element.
  if (element_path_.back() != kDocStringTag)
    return;

  CHECK_GT(element_path_.size(), 1u);
  string* doc_string_ptr = nullptr;
  string target_element = element_path_[element_path_.size() - 2];
  if (target_element == kInterfaceTag) {
    doc_string_ptr = &(interfaces_.back().doc_string);
  } else if (target_element == kMethodTag) {
    doc_string_ptr = &(interfaces_.back().methods.back().doc_string);
  } else if (target_element == kSignalTag) {
    doc_string_ptr = &(interfaces_.back().signals.back().doc_string);
  } else if (target_element == kPropertyTag) {
    doc_string_ptr = &(interfaces_.back().properties.back().doc_string);
  }

  // If <tp:docstring> is attached to elements we don't care about, do nothing.
  if (doc_string_ptr == nullptr)
    return;

  (*doc_string_ptr) += content;
}


void XmlInterfaceParser::AddMethodArgument(const XmlAttributeMap& attributes) {
  string argument_direction;
  vector<string> path = element_path_;
  path.push_back(kArgumentTag);
  bool is_direction_paramter_present = GetElementAttribute(
      attributes, path, kDirectionAttribute, &argument_direction);
  vector<Interface::Argument>* argument_list = nullptr;
  if (!is_direction_paramter_present ||
      argument_direction == kArgumentDirectionIn) {
    argument_list = &interfaces_.back().methods.back().input_arguments;
  } else if (argument_direction == kArgumentDirectionOut) {
    argument_list = &interfaces_.back().methods.back().output_arguments;
  } else {
    LOG(FATAL) << "Unknown method argument direction " << argument_direction;
  }
  argument_list->push_back(ParseArgument(attributes, element_path_));
}

void XmlInterfaceParser::AddSignalArgument(const XmlAttributeMap& attributes) {
  interfaces_.back().signals.back().arguments.push_back(
      ParseArgument(attributes, element_path_));
}

void XmlInterfaceParser::OnCloseElement(const string& element_name) {
  VLOG(1) << "Close Element " << element_name;
  CHECK(!element_path_.empty());
  CHECK_EQ(element_path_.back(), element_name);
  element_path_.pop_back();
  if (element_name == kNodeTag) {
    CHECK(!node_names_.empty());
    node_names_.pop_back();
  }
}

// static
bool XmlInterfaceParser::GetElementAttribute(
    const XmlAttributeMap& attributes,
    const vector<string>& element_path,
    const string& element_key,
    string* element_value) {
  if (attributes.find(element_key) == attributes.end()) {
    return false;
  }
  *element_value = attributes.find(element_key)->second;
  VLOG(1) << "Got " << GetElementPath(element_path) << " element with "
          << element_key << " = " << *element_value;
  return true;
}

// static
string XmlInterfaceParser::GetValidatedElementAttribute(
    const XmlAttributeMap& attributes,
    const vector<string>& element_path,
    const string& element_key) {
  string element_value;
  CHECK(GetElementAttribute(attributes,
                            element_path,
                            element_key,
                            &element_value))
      << GetElementPath(element_path) << " does not contain a " << element_key
      << " attribute";
  CHECK(!element_value.empty()) << GetElementPath(element_path) << " "
                                << element_key << " attribute is empty";
  return element_value;
}

// static
string XmlInterfaceParser::GetValidatedElementName(
    const XmlAttributeMap& attributes,
    const vector<string>& element_path) {
  return GetValidatedElementAttribute(attributes, element_path, kNameAttribute);
}

// static
Interface::Argument XmlInterfaceParser::ParseArgument(
    const XmlAttributeMap& attributes, const vector<string>& element_path) {
  vector<string> path = element_path;
  path.push_back(kArgumentTag);
  string argument_name;
  // Since the "name" field is optional, use the un-validated variant.
  GetElementAttribute(attributes, path, kNameAttribute, &argument_name);

  string argument_type = GetValidatedElementAttribute(
      attributes, path, kTypeAttribute);
  return Interface::Argument(argument_name, argument_type);
}

// static
Interface::Property XmlInterfaceParser::ParseProperty(
    const XmlAttributeMap& attributes,
    const std::vector<std::string>& element_path) {
  vector<string> path = element_path;
  path.push_back(kPropertyTag);
  string property_name = GetValidatedElementName(attributes, path);
  string property_type = GetValidatedElementAttribute(attributes, path,
                                                      kTypeAttribute);
  string property_access = GetValidatedElementAttribute(attributes, path,
                                                        kAccessAttribute);
  return Interface::Property(property_name, property_type, property_access);
}

// static
void XmlInterfaceParser::HandleElementStart(void* user_data,
                                            const XML_Char* element,
                                            const XML_Char** attr) {
  XmlAttributeMap attributes;
  if (attr != nullptr) {
    for (size_t n = 0; attr[n] != nullptr && attr[n+1] != nullptr; n += 2) {
      auto key = attr[n];
      auto value = attr[n + 1];
      attributes.insert(std::make_pair(key, value));
    }
  }
  auto parser = reinterpret_cast<XmlInterfaceParser*>(user_data);
  parser->OnOpenElement(element, attributes);
}

// static
void XmlInterfaceParser::HandleElementEnd(void* user_data,
                                          const XML_Char* element) {
  auto parser = reinterpret_cast<XmlInterfaceParser*>(user_data);
  parser->OnCloseElement(element);
}

// static
void XmlInterfaceParser::HandleCharData(void* user_data,
                                        const char *content,
                                        int length) {
  auto parser = reinterpret_cast<XmlInterfaceParser*>(user_data);
  parser->OnCharData(string(content, length));
}

}  // namespace chromeos_dbus_bindings