//
// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "flag_forwarder.h"

#include <cstring>

#include <map>
#include <sstream>
#include <string>
#include <unordered_set>
#include <vector>

#include <android-base/logging.h>
#include <gflags/gflags.h>
#include <libxml/parser.h>

#include "common/libs/fs/shared_buf.h"
#include "common/libs/fs/shared_fd.h"
#include "common/libs/utils/contains.h"
#include "common/libs/utils/subprocess.h"

namespace cuttlefish {

/**
 * Superclass for a flag loaded from another process.
 *
 * An instance of this class defines a flag available either in this subprocess
 * or another flag. If a flag needs to be registered in the current process, see
 * the DynamicFlag subclass. If multiple subprocesses declare a flag with the
 * same name, they all should receive that flag, but the DynamicFlag should only
 * be created zero or one times. Zero times if the parent process defines it as
 * well, one time if the parent does not define it.
 *
 * Notably, gflags itself defines some flags that are present in every binary.
 */
class SubprocessFlag {
  std::string subprocess_;
  std::string name_;
public:
  SubprocessFlag(const std::string& subprocess, const std::string& name)
      : subprocess_(subprocess), name_(name) {
  }
  virtual ~SubprocessFlag() = default;
  SubprocessFlag(const SubprocessFlag&) = delete;
  SubprocessFlag& operator=(const SubprocessFlag&) = delete;
  SubprocessFlag(SubprocessFlag&&) = delete;
  SubprocessFlag& operator=(SubprocessFlag&&) = delete;

  const std::string& Subprocess() const { return subprocess_; }
  const std::string& Name() const { return name_; }
};

/*
 * A dynamic gflags flag. Creating an instance of this class is equivalent to
 * registering a flag with DEFINE_<type>. Instances of this class should not
 * be deleted while flags are still in use (likely through the end of main).
 *
 * This is implemented as a wrapper around gflags::FlagRegisterer. This class
 * serves a dual purpose of holding the memory for gflags::FlagRegisterer as
 * that normally expects memory to be held statically. The other reason is to
 * subclass class SubprocessFlag to fit into the flag-forwarding scheme.
 */
template<typename T>
class DynamicFlag : public SubprocessFlag {
  std::string help_;
  std::string filename_;
  T current_storage_;
  T defvalue_storage_;
  gflags::FlagRegisterer registerer_;
public:
  DynamicFlag(const std::string& subprocess, const std::string& name,
              const std::string& help, const std::string& filename,
              const T& current, const T& defvalue)
      : SubprocessFlag(subprocess, name), help_(help), filename_(filename),
        current_storage_(current), defvalue_storage_(defvalue),
        registerer_(Name().c_str(), help_.c_str(), filename_.c_str(),
                    &current_storage_, &defvalue_storage_) {
  }
};

namespace {

/**
 * Returns a mapping between flag name and "gflags type" as strings for flags
 * defined in the binary.
 */
std::map<std::string, std::string> CurrentFlagsToTypes() {
  std::map<std::string, std::string> name_to_type;
  std::vector<gflags::CommandLineFlagInfo> self_flags;
  gflags::GetAllFlags(&self_flags);
  for (auto& flag : self_flags) {
    name_to_type[flag.name] = flag.type;
  }
  return name_to_type;
}

/**
 * Returns a pointer to the child of `node` with name `name`.
 *
 * For example, invoking `xmlChildWithName(<foo><bar>abc</bar></foo>, "foo")`
 * will return <bar>abc</bar>.
 */
xmlNodePtr xmlChildWithName(xmlNodePtr node, const std::string& name) {
  for (xmlNodePtr child = node->children; child != nullptr; child = child->next) {
    if (child->type != XML_ELEMENT_NODE) {
      continue;
    }
    if (std::strcmp((const char*) child->name, name.c_str()) == 0) {
      return child;
    }
  }
  LOG(WARNING) << "no child with name " << name;
  return nullptr;
}

/**
 * Returns a string with the content of an xml node.
 *
 * For example, calling `xmlContent(<bar>abc</bar>)` will return "abc".
 */
std::string xmlContent(xmlNodePtr node) {
  if (node == nullptr || node->children == NULL
      || node->children->type != xmlElementType::XML_TEXT_NODE) {
    return "";
  }
  return std::string((char*) node->children->content);
}

template<typename T>
T FromString(const std::string& str) {
  std::stringstream stream(str);
  T output;
  stream >> output;
  return output;
}

/**
 * Creates a dynamic flag
 */
std::unique_ptr<SubprocessFlag> MakeDynamicFlag(
    const std::string& subprocess,
    const gflags::CommandLineFlagInfo& flag_info) {
  std::unique_ptr<SubprocessFlag> ptr;
  if (flag_info.type == "bool") {
    ptr.reset(new DynamicFlag<bool>(subprocess, flag_info.name,
                                    flag_info.description,
                                    flag_info.filename,
                                    FromString<bool>(flag_info.default_value),
                                    FromString<bool>(flag_info.current_value)));
  } else if (flag_info.type == "int32") {
    ptr.reset(new DynamicFlag<int32_t>(subprocess, flag_info.name,
                                       flag_info.description,
                                       flag_info.filename,
                                       FromString<int32_t>(flag_info.default_value),
                                       FromString<int32_t>(flag_info.current_value)));
  } else if (flag_info.type == "uint32") {
    ptr.reset(new DynamicFlag<uint32_t>(subprocess, flag_info.name,
                                        flag_info.description,
                                        flag_info.filename,
                                        FromString<uint32_t>(flag_info.default_value),
                                        FromString<uint32_t>(flag_info.current_value)));
  } else if (flag_info.type == "int64") {
    ptr.reset(new DynamicFlag<int64_t>(subprocess, flag_info.name,
                                       flag_info.description,
                                       flag_info.filename,
                                       FromString<int64_t>(flag_info.default_value),
                                       FromString<int64_t>(flag_info.current_value)));
  } else if (flag_info.type == "uint64") {
    ptr.reset(new DynamicFlag<uint64_t>(subprocess, flag_info.name,
                                        flag_info.description,
                                        flag_info.filename,
                                        FromString<uint64_t>(flag_info.default_value),
                                        FromString<uint64_t>(flag_info.current_value)));
  } else if (flag_info.type == "double") {
    ptr.reset(new DynamicFlag<double>(subprocess, flag_info.name,
                                      flag_info.description,
                                      flag_info.filename,
                                      FromString<double>(flag_info.default_value),
                                      FromString<double>(flag_info.current_value)));
  } else if (flag_info.type == "string") {
    ptr.reset(new DynamicFlag<std::string>(subprocess, flag_info.name,
                                           flag_info.description,
                                           flag_info.filename,
                                           flag_info.default_value,
                                           flag_info.current_value));
  } else {
    LOG(FATAL) << "Unknown type \"" << flag_info.type << "\" for flag " << flag_info.name;
  }
  return ptr;
}

std::vector<gflags::CommandLineFlagInfo> FlagsForSubprocess(std::string helpxml_output) {
  auto xml_begin = helpxml_output.find("<?xml");
  CHECK(xml_begin != std::string::npos)
      << "No xml found in '" << helpxml_output << "'";
  // Hack to try to filter out log messages that come before the xml
  helpxml_output = helpxml_output.substr(xml_begin);

  xmlDocPtr doc = xmlReadMemory(helpxml_output.c_str(), helpxml_output.size(),
                                NULL, NULL, 0);
  if (doc == NULL) {
    LOG(FATAL) << "Could not parse xml of subprocess `--helpxml`";
  }
  xmlNodePtr root_element = xmlDocGetRootElement(doc);
  std::vector<gflags::CommandLineFlagInfo> flags;
  for (xmlNodePtr flag = root_element->children; flag != nullptr; flag = flag->next) {
    if (std::strcmp((const char*) flag->name, "flag") != 0) {
      continue;
    }
    gflags::CommandLineFlagInfo flag_info;
    flag_info.name = xmlContent(xmlChildWithName(flag, "name"));
    flag_info.type = xmlContent(xmlChildWithName(flag, "type"));
    flag_info.filename = xmlContent(xmlChildWithName(flag, "file"));
    flag_info.description = xmlContent(xmlChildWithName(flag, "meaning"));
    flag_info.current_value = xmlContent(xmlChildWithName(flag, "current"));
    flag_info.default_value = xmlContent(xmlChildWithName(flag, "default"));
    flags.emplace_back(std::move(flag_info));
  }
  xmlFree(doc);
  xmlCleanupParser();
  return flags;
}

} // namespace

FlagForwarder::FlagForwarder(std::set<std::string> subprocesses,
                             const std::vector<std::vector<std::string>>& args)
    : subprocesses_(std::move(subprocesses)) {
  std::map<std::string, std::string> flag_to_type = CurrentFlagsToTypes();

  int subprocess_index = 0;
  for (const auto& subprocess : subprocesses_) {
    Command cmd(subprocess);
    cmd.AddParameter("--helpxml");

    if (subprocess_index < args.size()) {
      for (auto arg : args[subprocess_index]) {
        cmd.AddParameter(arg);
      }
    }
    subprocess_index++;

    std::string helpxml_input, helpxml_output, helpxml_error;
    SubprocessOptions options;
    options.Verbose(false);
    int helpxml_ret =
        RunWithManagedStdio(std::move(cmd), &helpxml_input, &helpxml_output,
                            &helpxml_error, std::move(options));
    if (helpxml_ret != 1) {
      LOG(FATAL) << subprocess << " --helpxml returned unexpected response "
                 << helpxml_ret << ". Stderr was " << helpxml_error;
      return;
    }

    auto subprocess_flags = FlagsForSubprocess(helpxml_output);
    for (const auto& flag : subprocess_flags) {
      if (flag_to_type.count(flag.name)) {
        if (flag_to_type[flag.name] == flag.type) {
          flags_.emplace(std::make_unique<SubprocessFlag>(subprocess, flag.name));
        } else {
          LOG(FATAL) << flag.name << "defined as " << flag_to_type[flag.name]
                     << " and " << flag.type;
          return;
        }
      } else {
        flag_to_type[flag.name] = flag.type;
        flags_.emplace(MakeDynamicFlag(subprocess, flag));
      }
    }
  }
}

// Destructor must be defined in an implementation file.
// https://stackoverflow.com/questions/6012157
FlagForwarder::~FlagForwarder() = default;

void FlagForwarder::UpdateFlagDefaults() const {

  for (const auto& subprocess : subprocesses_) {
    Command cmd(subprocess);
    std::vector<std::string> invocation = {subprocess};
    for (const auto& flag : ArgvForSubprocess(subprocess)) {
      cmd.AddParameter(flag);
    }
    // Disable flags that could cause the subprocess to exit before helpxml.
    // See gflags_reporting.cc.
    cmd.AddParameter("--nohelp");
    cmd.AddParameter("--nohelpfull");
    cmd.AddParameter("--nohelpshort");
    cmd.AddParameter("--helpon=");
    cmd.AddParameter("--helpmatch=");
    cmd.AddParameter("--nohelppackage=");
    cmd.AddParameter("--noversion");
    // Ensure this is set on by putting it at the end.
    cmd.AddParameter("--helpxml");
    std::string helpxml_input, helpxml_output, helpxml_error;
    auto options = SubprocessOptions().Verbose(false);
    int helpxml_ret =
        RunWithManagedStdio(std::move(cmd), &helpxml_input, &helpxml_output,
                            &helpxml_error, std::move(options));
    if (helpxml_ret != 1) {
      LOG(FATAL) << subprocess << " --helpxml returned unexpected response "
                 << helpxml_ret << ". Stderr was " << helpxml_error;
      return;
    }

    auto subprocess_flags = FlagsForSubprocess(helpxml_output);
    for (const auto& flag : subprocess_flags) {
      gflags::SetCommandLineOptionWithMode(
          flag.name.c_str(),
          flag.default_value.c_str(),
          gflags::FlagSettingMode::SET_FLAGS_DEFAULT);
    }
  }
}

// Hash table for repeatable flags (able to have repeated flag inputs)
static std::unordered_set<std::string> kRepeatableFlags = {
    "custom_action_config", "custom_actions", "display", "touchpad"};

std::vector<std::string> FlagForwarder::ArgvForSubprocess(
    const std::string& subprocess, const std::vector<std::string>& args) const {
  std::vector<std::string> subprocess_argv;
  std::map<std::string, std::vector<std::string>> name_to_value;

  if (!args.empty()) {
    for (int index = 0; index < args.size(); index++) {
      std::string_view argument = args[index];
      if (!android::base::ConsumePrefix(&argument, "-")) {
        continue;
      }
      android::base::ConsumePrefix(&argument, "-");
      std::size_t qual_pos = argument.find('=');
      if (qual_pos == std::string::npos) {
        // to handle error cases: --flag value and -flag value
        // but it only apply to repeatable flag case
        if (Contains(kRepeatableFlags, argument)) {
          // matched
          LOG(FATAL) << subprocess
                     << " has wrong flag input: " << args[index];
        }
        continue;
      }
      const std::string name(argument.substr(0, qual_pos));
      const std::string value(
          argument.substr(qual_pos + 1, argument.length() - qual_pos - 1));

      if (Contains(kRepeatableFlags, name)) {
        // matched
        if (!Contains(name_to_value, name)) {
          // this flag is new
          std::vector<std::string> values;
          name_to_value[name] = values;
        }
        name_to_value[name].push_back(value);
      }
    }
  }

  for (const auto& flag : flags_) {
    if (flag->Subprocess() == subprocess) {
      if (Contains(kRepeatableFlags, flag->Name()) &&
          Contains(name_to_value, flag->Name())) {
        // this is a repeatable flag with input values
        for (const auto& value : name_to_value[flag->Name()]) {
          subprocess_argv.push_back("--" + flag->Name() + "=" + value);
        }
      } else {
        // normal case
        gflags::CommandLineFlagInfo flag_info =
            gflags::GetCommandLineFlagInfoOrDie(flag->Name().c_str());
        if (!flag_info.is_default) {
          subprocess_argv.push_back("--" + flag->Name() + "=" +
                                    flag_info.current_value);
        }
      }
    }
  }
  return subprocess_argv;
}

}  // namespace cuttlefish
