/*
 * Copyright (c) 2016, 2025, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "shellcore/utils_help.h"

#include <array>
#include <cassert>
#include <cctype>
#include <functional>
#include <map>
#include <regex>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "mysqlshdk/libs/textui/textui.h"
#include "mysqlshdk/libs/utils/logger.h"
#include "utils/utils_general.h"
#include "utils/utils_string.h"

#define HELP_OPTIONAL_STR "Optional"
#define HELP_IGNORE_STR "IGNORE"

#define MEMBER_TOPICS true
#define NO_MEMBER_TOPICS false

// Topic Splitter Constants
#define HELP_API_SPLITTER "."
#define HELP_CMD_SPLITTER " "
#define HELP_TOPIC_SPLITTER "/"

// Formatting constants
#define NO_PADDING 0
#define SECTION_PADDING 6
#define ITEM_DESC_PADDING 6
#define HEADER_CONTENT_SEPARATOR "\n"

#define HELP_TITLE_DESCRIPTION "DESCRIPTION"
#define HELP_TITLE_SYNTAX "SYNTAX"

// Tag to indicate a specific text line must NOT be formatted
#define HELP_NO_FORMAT "#NOFORMAT:"

namespace textui = mysqlshdk::textui;

namespace shcore {
namespace {
/**
 * This regular expression is to parse the help data for the options to be used
 * in the CLI help. Formatting of option help comes in the following format:
 *
 * @li [<b>]<option>[/b>]: [<type>|<allowed_values>[(default[:] <default>)] - ]
 *<description>
 *
 * This regular expression identifies the different tokens in order to:
 * - Remove documented default from CLI help (it is taken from API metadata)
 * - Produce a CLI formatted description as:
 *   [<allowed_values>.] <description>.[ Default: <default>.]
 **/
const std::regex k_cli_option_help_regexp(
    "^@li\\s(<b>)?([a-z|A-Z|_][a-z|A-Z|_|0-9]+)(<\\/"
    "b>)?(:\\s|\\s-\\s)((([a-z|A-Z|\\s]+)(\\s\\([default|required]*(.*)\\))?"
    "\\s-\\s)?(.*))$");

std::map<std::string, std::string> parse_cli_option_data(
    const std::vector<std::string> &data) {
  std::map<std::string, std::string> options;

  for (const auto &entry : data) {
    std::smatch results;
    if (std::regex_match(entry, results, k_cli_option_help_regexp)) {
      if (options.contains(results[2])) {
        // option was already documented, skip it
        continue;
      }

      // Result 6 indicates type description was found
      if (results[6].matched) {
        std::string full_details;
        auto type_tokens = shcore::str_split(results[7].str(), " ");
        // Type description found, should be ignored.
        if (type_tokens.size() == 1 ||
            shcore::str_caseeq(type_tokens[0], "array") ||
            shcore::str_caseeq(type_tokens[0], "list")) {
          // NO-OP (Let here for clarity)
        } else {
          // Allowed values found, they are the first part of the description.
          full_details = results[7];
          full_details.append(". ");
        }

        if (results[8].matched && results[8] == "(required)") {
          full_details.append("Required. ");
        }

        // Appends the description.
        std::string details = results[10];
        if (!details.empty()) details[0] = std::toupper(details[0]);

        // Appends the default/required data.
        full_details.append(details);
        if (results[9].matched) {
          full_details.append(" Default: ");
          std::string def_val = results[9];
          if (def_val[0] == ':') def_val = def_val.substr(1);
          def_val = shcore::str_strip(def_val);
          full_details.append(def_val + ".");
        }
        options[results[2]] = full_details;
      } else {
        // Appends the description.
        std::string details = results[5];
        if (!details.empty()) details[0] = std::toupper(details[0]);
        options[results[2]] = details;
      }
    }
  }
  return options;
}

std::string get_cli_param_type(shcore::Value_type type) {
  switch (type) {
    case shcore::Value_type::Bool:
      return "bool";
    case shcore::Value_type::Float:
      return "float";
    case shcore::Value_type::Integer:
      return "int";
    case shcore::Value_type::UInteger:
      return "uint";
    case shcore::Value_type::String:
      return "str";
    case shcore::Value_type::Array:
      return "list";
    default:
      return "";
  }
}

std::string format_cli_option(const std::shared_ptr<Parameter> &option,
                              std::string_view option_name) {
  std::string option_detail;
  if (!option->short_name.empty())
    option_detail = "-" + option->short_name + ", ";

  option_detail.append(option_name);

  if (option->type() == shcore::Array) {
    auto validator = option->validator<List_validator>();
    if (validator->element_type() == shcore::Undefined) {
      option_detail += "[:<type>]=<value>";
    } else {
      option_detail += "=<" + get_cli_param_type(validator->element_type()) +
                       " " + get_cli_param_type(option->type()) + ">";
    }
  } else if (option->type() == shcore::Map) {
    option_detail += "=<key>[:<type>]=<value>";
  } else if (option->type() == shcore::Undefined) {
    option_detail += "[:<type>]=<value>";
  } else {
    option_detail += "=<" + get_cli_param_type(option->type()) + ">";
  }

  return option_detail;
}

std::string format_cli_option_name(std::string_view name) {
  assert(!name.empty());

  std::string option = "--";

  if ('-' == name.front()) {
    assert(name.size() > 2 && '-' == name[1]);
    name.remove_prefix(2);
  }

  option += to_camel_case(name);

  return option;
}

std::string strip_param_type(const std::string &input) {
  std::string ret_val{input};

  auto pos = input.find(" - ");
  if (pos != std::string::npos) {
    auto type = input.substr(0, pos);

    std::string allowed_types =
        "DictionaryBoolIntegerUIntegerFloatStringObjectArrayMapFunctionUndefine"
        "dNull";

    if (allowed_types.find(type) != std::string::npos) {
      ret_val = ret_val.substr(pos + 3);
      ret_val[0] = std::toupper(ret_val[0]);
    }
  }
  return ret_val;
}
}  // namespace
using Mode_mask = shcore::IShell_core::Mode_mask;

bool Help_topic_compare::operator()(const Help_topic *const &lhs,
                                    const Help_topic *const &rhs) const {
  if (!lhs) {
    return true;
  } else if (!rhs) {
    return false;
  } else {
    int ret_val = str_casecmp(lhs->m_name.c_str(), rhs->m_name.c_str());

    // If case insensitive are equal, does case sensitive comparison
    return ret_val == 0 ? (lhs->m_name < rhs->m_name) : ret_val < 0;
  }
}

bool Help_topic_id_compare::operator()(const Help_topic *const &lhs,
                                       const Help_topic *const &rhs) const {
  if (!lhs) {
    return true;
  } else if (!rhs) {
    return false;
  } else {
    int ret_val = str_casecmp(lhs->m_id.c_str(), rhs->m_id.c_str());

    // If case insensitive are equal, does case sensitive comparison
    return ret_val == 0 ? (lhs->m_id < rhs->m_id) : ret_val < 0;
  }
}

bool icomp::operator()(const std::string &lhs, const std::string &rhs) const {
  return str_casecmp(lhs.c_str(), rhs.c_str()) < 0;
}

bool Help_topic::is_api() const {
  return m_type == Topic_type::MODULE || m_type == Topic_type::CLASS ||
         m_type == Topic_type::CONSTANTS || m_type == Topic_type::OBJECT ||
         m_type == Topic_type::GLOBAL_OBJECT ||
         m_type == Topic_type::FUNCTION || m_type == Topic_type::PROPERTY;
}

bool Help_topic::is_api_object() const {
  return m_type == Topic_type::MODULE || m_type == Topic_type::CLASS ||
         m_type == Topic_type::CONSTANTS || m_type == Topic_type::OBJECT ||
         m_type == Topic_type::GLOBAL_OBJECT;
}

bool Help_topic::is_member() const {
  return m_type == Topic_type::FUNCTION || m_type == Topic_type::PROPERTY ||
         m_type == Topic_type::OBJECT;
}

bool Help_topic::is_enabled(IShell_core::Mode mode) const {
  return Help_registry::get()->is_enabled(this, mode);
}

std::string Help_topic::get_name(IShell_core::Mode mode) const {
  // Member topics may have different names based on the scripting
  // mode
  if (is_member()) {
    auto names = shcore::str_split(m_name, "|");
    if (mode == IShell_core::Mode::Python) {
      if (names.size() == 2)
        return names[1];
      else
        return shcore::from_camel_case(m_name);
    } else {
      return names[0];
    }
  }

  return m_name;
}

std::string Help_topic::get_base_name() const {
  // Member topics may have different names based on the scripting
  // mode
  if (is_member()) {
    auto names = shcore::str_split(m_name, "|");
    return names[0];
  }

  return m_name;
}

const Help_topic *Help_topic::get_category() const {
  const Help_topic *category = this;
  while (category->m_parent != nullptr &&
         category->m_parent->m_name != Help_registry::HELP_ROOT) {
    category = category->m_parent;
  }

  return category;
}

void Help_topic::get_id_tokens(IShell_core::Mode mode,
                               std::vector<std::string> *tokens,
                               Topic_id_mode id_mode) const {
  assert(tokens);

  if (m_parent != nullptr) {
    // With id_mode = FULL, every single parent is considered
    if (id_mode == Topic_id_mode::FULL) {
      m_parent->get_id_tokens(mode, tokens, id_mode);
    } else {
      // When not in FULL mode, ids for API topics only consider the API
      // Hierarchy, meaning both ROOT and Category parents are ignored
      if (is_api()) {
        if (m_parent->is_api()) {
          m_parent->get_id_tokens(mode, tokens, id_mode);
        }
      } else {
        // When not in FULL mode, non API topics automatically exclude ROOT
        // But categories will be included if mode_id != EXCLUDE_CATEGORIES
        if (m_parent->m_name != Help_registry::HELP_ROOT &&
            id_mode != Topic_id_mode::EXCLUDE_CATEGORIES) {
          m_parent->get_id_tokens(mode, tokens, id_mode);
        }
      }
    }
  }

  tokens->push_back(get_name(mode));
}

std::string Help_topic::get_id(IShell_core::Mode mode,
                               Topic_id_mode id_mode) const {
  std::string splitter = is_api() ? HELP_API_SPLITTER : HELP_TOPIC_SPLITTER;
  std::string last_splitter = is_command() ? HELP_CMD_SPLITTER : splitter;

  std::vector<std::string> id_tokens;
  get_id_tokens(mode, &id_tokens, id_mode);

  std::string this_name = id_tokens.back();
  id_tokens.pop_back();

  std::string ret_val = shcore::str_join(id_tokens, splitter);

  if (!ret_val.empty()) ret_val += last_splitter;

  ret_val += this_name;

  return ret_val;
}

std::string Help_topic::get_keyword(const std::string &keyword) const {
  if (auto kw = m_keywords.find(keyword); kw != m_keywords.end())
    return kw->second;
  if (m_parent) return m_parent->get_keyword(keyword);

  throw std::invalid_argument("Keyword " + keyword + " not defined");
}

const char Help_registry::HELP_ROOT[] = "Contents";
const char Help_registry::HELP_COMMANDS[] = "Shell Commands";
const char Help_registry::HELP_SQL[] = "SQL Syntax";

Topic_mask Help_registry::get_parent_topic_types() {
  static const Topic_mask kParentTopicTypes{Topic_mask(Topic_type::OBJECT)
                                                .set(Topic_type::GLOBAL_OBJECT)
                                                .set(Topic_type::CONSTANTS)
                                                .set(Topic_type::CATEGORY)
                                                .set(Topic_type::TOPIC)
                                                .set(Topic_type::SQL)
                                                .set(Topic_type::COMMAND)
                                                .set(Topic_type::MODULE)
                                                .set(Topic_type::CLASS)};

  return kParentTopicTypes;
}

Help_registry::Help_registry(bool threaded)
    : m_threaded(threaded), m_global_removed(false) {
  m_c_tor = true;
  shcore::Scoped_callback leave([this]() { m_c_tor = false; });

  if (!threaded) {
    // The Contents category is registered right away since it is the root
    // Of the help system
    add_help_topic(HELP_ROOT, Topic_type::CATEGORY, "CONTENTS", "",
                   Mode_mask::all());

    // The SQL category is also registered right away, it doesn't have content
    // at the client side but will be used as parent for SQL topics
    add_help_topic(HELP_SQL, Topic_type::SQL, "SQL_CONTENTS", HELP_ROOT,
                   Mode_mask::all());
  } else {
    auto lock = get()->ensure_lock_threaded();
    get()->m_threaded_help.push_back(this);
  }
}

Help_registry::~Help_registry() {
  // The cleanup needs to be done only and only from the thread scope so we get
  // rid of dangling pointers.
  if (m_threaded) {
    if (!m_global_removed) {
      for (auto &it : m_topics) {
        remove_topic(&it, Keyword_location::GLOBAL_CTX);
      }

      auto lock = get()->ensure_lock_threaded();
      get()->m_threaded_help.erase(
          std::remove_if(get()->m_threaded_help.begin(),
                         get()->m_threaded_help.end(),
                         [this](auto &it) { return it == this; }),
          get()->m_threaded_help.end());
    }
  } else {  // we need to go through the m_threaded_help and do a cleanup cause
            // it seems that sometimes global_ctx is removed before a local_ctx
    auto lock = ensure_lock_threaded();
    for (const auto &it : m_threaded_help) {
      it->m_global_removed = true;
      for (auto &sit : it->m_topics) {
        remove_topic(&sit);
      }
    }
  }
}

Help_registry *Help_registry::get() {
  static Help_registry instance;
  return &instance;
}

void Help_registry::add_split_help(const std::string &prefix,
                                   const std::string &data, bool auto_brief,
                                   bool nosuffix, bool is_shell_command) {
  std::map<std::string, int> current_index;

  auto token = [&prefix, &current_index](const std::string &suffix) {
    int index = current_index[suffix];
    current_index[suffix] = index + 1;

    if (index == 0)
      return shcore::str_format("%s%s%s", prefix.c_str(),
                                !suffix.empty() ? "_" : "", suffix.c_str());
    else
      return shcore::str_format("%s%s%s%i", prefix.c_str(),
                                !suffix.empty() ? "_" : "", suffix.c_str(),
                                index);
  };

  // Split the help text into multiple entries of the same prefix topic and
  // then register them one by one.

  std::vector<std::string> lines = shcore::str_split(data, "\n");

  // make sure the last line is an empty line
  if ("" != lines.back()) {
    lines.emplace_back("");
  }

  auto line_iter = lines.cbegin();
  const auto lines_end = lines.cend();

  auto get_line = [&lines_end, &line_iter](bool *eos, bool strip) {
    if (line_iter == lines_end) {
      *eos = true;
      return std::string();
    } else {
      if (strip)
        return shcore::str_rstrip(*line_iter++);
      else
        return *line_iter++;
    }
  };

  auto unget_line = [&lines, &line_iter]() {
    if (line_iter > lines.cbegin()) --line_iter;
  };

  auto get_para = [&get_line, &unget_line](bool *eos) {
    std::string line;
    std::string para;
    bool rstrip = true;

    line = get_line(eos, rstrip);
    while (!*eos && line.empty()) line = get_line(eos, rstrip);

    para = line;
    // now keep adding lines to the paragraph until one of:
    // empty line, @ at bol, $ at bol
    if (shcore::str_beginswith(para, "@code")) {
      while (!*eos && line != "@endcode") {
        line = get_line(eos, rstrip);

        if (line != "@endcode") para.append("\n");

        para.append(line);
      }
    } else {
      line = get_line(eos, rstrip);
      while (!*eos && !line.empty()) {
        if (line[0] == '@' || line[0] == '$' ||
            (line.size() > 1 && line[0] == '-' && line[1] == '#'))
          break;
        para.append(" ").append(line);
        line = get_line(eos, rstrip);
      }
      unget_line();
    }

    return para;
  };

  bool eos = false;
  std::string para;
  if (auto_brief) add_help(token("BRIEF"), get_para(&eos));

  // params, return and deprecation warning
  para = get_para(&eos);
  while (!eos && !para.empty()) {
    if (shcore::str_beginswith(para, "@param")) {
      add_help(token("PARAM"), para);
    } else if (is_shell_command && shcore::str_beginswith(para, "@syntax")) {
      add_help(token("SYNTAX"), para.substr(para.find_first_of(" \t") + 1));
    } else if (shcore::str_beginswith(para, "@return")) {
      add_help(token("RETURNS"), para);
    } else if (shcore::str_beginswith(para, "@attention") &&
               para.find("will be removed") != std::string::npos) {
      add_help(token("DEPRECATED"), para);
      break;
    } else {
      break;
    }
    para = get_para(&eos);
  }

  // main body
  while (!eos && !para.empty()) {
    if (shcore::str_beginswith(para, "@throw") ||
        shcore::str_beginswith(para, "@example")) {
      break;
    }
    if (nosuffix)
      add_help(token(""), para);
    else
      add_help(token("DETAIL"), para);
    para = get_para(&eos);
  }

  // everything after the 1st @throw and before an @example tag assumed to be
  // more throws
  while (!eos && !para.empty()) {
    if (is_shell_command && shcore::str_beginswith(para, "@example")) {
      break;
    }
    if (shcore::str_beginswith(para, "@throw"))
      add_help(token("THROWS"), para.substr(para.find_first_of(" \t") + 1));
    else
      add_help(token("THROWS"), para);
    para = get_para(&eos);
  }

  if (is_shell_command) {
    int example_index = 0;
    std::string eindex_str = "";
    while (!eos && !para.empty()) {
      if (shcore::str_beginswith(para, "@example")) {
        // Updates the example index
        if (example_index > 0) eindex_str = std::to_string(example_index);

        example_index++;
        add_help(token(shcore::str_format("EXAMPLE%s", eindex_str.c_str())),
                 para.substr(para.find_first_of(" \t") + 1));
      } else
        add_help(
            token(shcore::str_format("EXAMPLE%s_DESC", eindex_str.c_str())),
            para);

      para = get_para(&eos);
    }
  }
}

void Help_registry::add_help(const std::string &token, std::string data,
                             Keyword_location loc) {
  auto lock = ensure_lock();
  if (loc == Keyword_location::LOCAL_CTX) {
    get_thread_context_help()->m_help_data[token] = std::move(data);
  } else if (loc == Keyword_location::GLOBAL_CTX) {
    m_help_data[token] = std::move(data);
  } else {
    get_thread_context_help()->m_help_data[token] = data;
    m_help_data[token] = std::move(data);
  }
}

void Help_registry::add_help(std::string_view prefix, std::string_view tag,
                             std::string data, Keyword_location loc) {
  auto full_prefix = shcore::str_format(
      "%.*s_%.*s", static_cast<int>(prefix.size()), prefix.data(),
      static_cast<int>(tag.size()), tag.data());

  add_help(full_prefix, std::move(data), loc);
}

void Help_registry::add_help(const std::string &prefix, const std::string &tag,
                             const std::vector<std::string> &data,
                             Keyword_location loc) {
  size_t sequence = 0;
  add_help(prefix, tag, &sequence, data, loc);
}

void Help_registry::add_help(const std::string &prefix, const std::string &tag,
                             size_t *sequence,
                             const std::vector<std::string> &data,
                             Keyword_location loc) {
  assert(sequence);

  std::string index = (*sequence) ? std::to_string(*sequence) : "";

  for (auto &entry : data) {
    auto full_prefix = prefix + "_" + tag + index;
    add_help(full_prefix, entry, loc);
    (*sequence)++;
    index = std::to_string(*sequence);
  }
}

void Help_registry::add_help(const std::string &prefix,
                             const std::vector<Example> &data,
                             Keyword_location loc) {
  size_t sequence = 0;
  add_help(prefix, &sequence, data, loc);
}

void Help_registry::add_help(const std::string &prefix, size_t *sequence,
                             const std::vector<Example> &data,
                             Keyword_location loc) {
  assert(sequence);

  auto index = (*sequence) ? std::to_string(*sequence) : "";

  for (const auto &entry : data) {
    const auto full_prefix = prefix + "_EXAMPLE" + index;
    add_help(full_prefix, entry.code, loc);
    add_help(full_prefix + "_DESC", entry.description, loc);

    (*sequence)++;
    index = std::to_string(*sequence);
  }
}

std::vector<Help_topic *> Help_registry::add_help_topic(
    const std::string &name, Topic_type type, const std::string &tag,
    const std::string &parent_id, Mode_mask mode, Keyword_location loc,
    bool exact_id_match) {
  auto parent =
      get_topic(parent_id, true, get_parent_topic_types(), exact_id_match);

  if (loc == Keyword_location::LOCAL_CTX) {
    return {get_thread_context_help()->create_help_topic(
        name, type, tag, parent_id, parent, mode)};
  } else if (loc == Keyword_location::GLOBAL_CTX) {
    return {create_help_topic(name, type, tag, parent_id, parent, mode)};
  } else {
    std::vector<Help_topic *> ret;
    ret.push_back(get_thread_context_help()->create_help_topic(
        name, type, tag, parent_id, parent, mode));
    ret.push_back(create_help_topic(name, type, tag, parent_id, parent, mode));
    return ret;
  }
}

Help_topic *Help_registry::create_help_topic(const std::string &name,
                                             Topic_type type,
                                             const std::string &tag,
                                             const std::string &parent_id,
                                             Help_topic *parent, Mode_mask mode,
                                             bool do_register) {
  auto lock = ensure_lock();
  Help_topic *new_topic = &(*m_topics.insert(
      m_topics.end(), {name, name, type, tag, nullptr, {}, true, {}}));
  m_topics_by_type[type].push_back(new_topic);

  std::string splitter;

  if (!parent_id.empty()) {
    if (parent) {
      new_topic->m_parent = parent;
      parent->m_childs.insert(new_topic);
    } else {
      // Adds the topic to the orphans so it is properly registered when the
      // parent chain is made available
      if (m_orphans.find(parent_id) == m_orphans.end())
        m_orphans[parent_id] = {};

      m_orphans[parent_id].push_back(new_topic);

      do_register = false;
    }
  }

  if (do_register) {
    register_topic(new_topic, true, mode);
  }

  if (new_topic->is_api_object()) {
    create_help_topic("help", Topic_type::FUNCTION, "help", new_topic->m_id,
                      new_topic, mode, do_register);
  }

  return new_topic;
}

void Help_registry::remove_topic(Help_topic *topic, Keyword_location loc) {
  if (loc == Keyword_location::LOCAL_CTX) {
    get_thread_context_help()->remove_topic(topic);
  } else if (loc == Keyword_location::GLOBAL_CTX) {
    // The explicit call to the global ctx is needed because we're calling this
    // from the d-tor of the thread context.
    // it won't be possible to do this without it.
    get()->remove_topic(topic);
  } else {
    get_thread_context_help()->remove_topic(topic);
    get()->remove_topic(topic);
  }
}

void Help_registry::remove_topic(Help_topic *topic) {
  auto lock = ensure_lock();
  for (auto it = m_cs_keywords.begin(); it != m_cs_keywords.end();) {
    it->second.erase(topic);
    if (it->second.empty()) {
      it = m_cs_keywords.erase(it);
    } else {
      it = std::next(it);
    }
  }

  for (auto it = m_keywords.begin(); it != m_keywords.end();) {
    it->second.erase(topic);
    if (it->second.empty()) {
      it = m_keywords.erase(it);
    } else {
      it = std::next(it);
    }
  }

  if (topic->m_parent != nullptr) {
    topic->m_parent->m_childs.erase(topic);
    topic->m_parent = nullptr;
  }

  auto it = std::find_if(m_topics.begin(), m_topics.end(),
                         [topic](const Help_topic &t) { return &t == topic; });

  if (it != m_topics.end()) {
    m_topics.erase(it);
  }
}

bool Help_registry::is_enabled(const Keyword_registry &registry,
                               const Help_topic *topic,
                               IShell_core::Mode mode) const {
  bool ret_val = false;
  if (topic->is_enabled()) {
    auto it = registry.find(topic->get_id(mode));
    if (it != registry.end()) {
      auto mask_it = it->second.find(const_cast<Help_topic *>(topic));
      if (mask_it != it->second.end()) {
        ret_val = mask_it->second.is_set(mode);
      }
    }
  }
  return ret_val;
}

std::unique_lock<std::recursive_mutex> Help_registry::ensure_lock() {
  std::unique_lock<std::recursive_mutex> lock(m_mutex);
  return lock;
}

std::unique_lock<std::recursive_mutex> Help_registry::ensure_lock_threaded() {
  std::unique_lock<std::recursive_mutex> lock(m_th_help);
  return lock;
}

void Help_registry::add_help_class(
    const std::string &name, const std::string &parent,
    const std::string &upper_class, IShell_core::Mode_mask mode,
    std::map<std::string, std::string> keywords) {
  auto lock = ensure_lock();
  Help_topic *topic =
      add_help_topic(name, Topic_type::CLASS, name, parent, mode).back();

  topic->m_keywords = std::move(keywords);

  // A parent class has been specified
  if (!upper_class.empty()) {
    Help_topic *ptopic = get_topic(upper_class, true);

    // If the parent class corresponding topic already exists
    // adds the relation, otherwise adds the child to the corresponding
    // orphan list
    if (ptopic) {
      inherit_members(ptopic, topic);
    } else {
      m_class_childs[upper_class].push_back(topic);
    }
  }
}

void Help_registry::inherit_members(Help_topic *parent, Help_topic *child) {
  // Creates the parent relationship
  m_class_parents[child] = parent;

  // Propagates inherited members to the child
  auto lock = ensure_lock();
  for (auto member : parent->m_childs) {
    inherit_member(child, member);
  }
}

void Help_registry::inherit_member(Help_topic *parent, Help_topic *member) {
  // If the member already exists on the target parent then is not added again
  // std::string new_id = parent->m_id + HELP_API_SPLITTER + member->m_name;
  // if (m_keywords.find(new_id) == m_keywords.end()) {
  if (member->m_name != "help") {
    Mode_mask mode(IShell_core::Mode::JavaScript);
    mode.set(IShell_core::Mode::Python);
    add_help_topic(member->m_name, member->m_type, member->m_help_tag,
                   parent->m_id, mode);

    // If the parent is not orphan the inheritance continues
    // Otherwise it will be triggered when this object is constructed
    if (m_orphans.find(parent->m_name) == m_orphans.end()) {
      std::string upper_class = parent->m_name;
      if (m_class_childs.find(upper_class) != m_class_childs.end()) {
        for (auto child : m_class_childs[upper_class]) {
          inherit_member(child, member);
        }
      }
    }
  }
}

void Help_registry::register_topic(Help_topic *topic, bool new_topic,
                                   Mode_mask mode) {
  auto lock = ensure_lock();
  if (topic->m_parent) {
    if (topic->is_api()) {
      if (topic->m_parent->is_api()) {
        topic->m_id = topic->m_parent->m_id + HELP_API_SPLITTER + topic->m_name;
      }
    } else {
      if (topic->is_command())
        topic->m_id = topic->m_parent->m_id + HELP_CMD_SPLITTER + topic->m_name;
      else
        topic->m_id =
            topic->m_parent->m_id + HELP_TOPIC_SPLITTER + topic->m_name;
    }
  }

  register_keywords(topic, mode);

  // The help function is 'injected' into every object, we need to do the same
  if (topic->is_api_object()) {
    // for it's help data
    std::string help_tag = shcore::str_upper(topic->m_id) + ".HELP";
    std::string type;

    switch (topic->m_type) {
      case Topic_type::MODULE:
        type = "module";
        break;

      case Topic_type::CLASS:
      case Topic_type::CONSTANTS:
        type = "class";
        break;

      case Topic_type::OBJECT:
      case Topic_type::GLOBAL_OBJECT:
        type = "object";
        break;

      default:
        throw std::logic_error("Unexpected topic type");
    }

    add_help(help_tag + "_BRIEF",
             "Provides help about this " + type + " and it's members");

    add_help(help_tag + "_PARAM",
             "@param member Optional If specified, provides detailed "
             "information on the given member.");
  }

  // If there are orphans associated to the new topic, they are properly
  // registered
  if (!topic->is_function() && !topic->is_property() &&
      m_orphans.find(topic->m_help_tag) != m_orphans.end()) {
    auto orphans = m_orphans[topic->m_help_tag];
    m_orphans.erase(topic->m_help_tag);

    for (auto orphan : orphans) {
      orphan->m_parent = topic;
      topic->m_childs.insert(orphan);
      register_topic(orphan, false, mode);
    }
  }

  if (topic->is_api()) {
    // A new object inherits all it's content to the subclasses if any
    if (topic->is_api_object()) {
      if (m_class_childs.find(topic->m_name) != m_class_childs.end()) {
        for (auto subclass : m_class_childs[topic->m_name]) {
          inherit_members(topic, subclass);
        }
      }
    } else if (new_topic) {
      // A new member is added to subclasses if any
      if (m_class_childs.find(topic->m_parent->m_name) !=
          m_class_childs.end()) {
        for (auto subclass : m_class_childs[topic->m_parent->m_name]) {
          inherit_member(subclass, topic);
        }
      }
    }
  }

  // If the topic has childs they need to be registered as well,
  // this may be the case of initially orphan objects
  for (const auto child : topic->m_childs) {
    register_topic(child, new_topic, mode);
  }
}

/**
 * Associates the token to all the keywords that will identify it.
 *
 * The keywords are created by splitting the topic id and adding a new keyword
 * with from each token and until the end.
 *
 * i.e.
 *
 * For an ID as: mysqlx.Session.getSchema
 *
 * The following keywords will be added:
 * - getSchema
 * - Session.getSchema
 * - mysqlx.Session.getSchema
 *
 * Each keyword is valid depending on the active shell mode, for example on API
 * topics, the ones above are valid only for JavaScript. In addition the
 * following would be registered for Python:
 * - get_schema
 * - Session.get_schema
 * - mysqlx.Session.get_schema
 *
 * Non API topics register keywords available on any mode.
 */
void Help_registry::register_keywords(Help_topic *topic, Mode_mask mode) {
  std::vector<IShell_core::Mode> modes = {IShell_core::Mode::JavaScript,
                                          IShell_core::Mode::Python,
                                          IShell_core::Mode::SQL};

  std::string splitter =
      topic->is_api() ? HELP_API_SPLITTER : HELP_TOPIC_SPLITTER;
  std::string last_splitter =
      topic->is_command() ? HELP_CMD_SPLITTER : splitter;

  for (auto shell_mode : modes) {
    if (mode.is_set(shell_mode)) {
      std::string topic_name = topic->get_name(shell_mode);
      std::vector<std::string> id_tokens;

      if (topic->m_parent)
        topic->m_parent->get_id_tokens(shell_mode, &id_tokens);

      while (!id_tokens.empty()) {
        std::string keyword = shcore::str_join(id_tokens, splitter);
        keyword += last_splitter + topic_name;
        register_keyword(keyword, shell_mode, topic);
        id_tokens.erase(id_tokens.begin());
      }

      register_keyword(topic_name, shell_mode, topic);

      if (!topic->is_api() &&
          !shcore::str_caseeq(topic_name, topic->m_help_tag)) {
        register_keyword(topic->m_help_tag, shell_mode, topic);
      }
    }
  }
}

void Help_registry::register_keyword(const std::string &keyword,
                                     IShell_core::Mode_mask mode,
                                     Help_topic *topic, bool case_sensitive) {
  if (mode.is_set(IShell_core::Mode::Python))
    register_keyword(keyword, IShell_core::Mode::Python, topic, case_sensitive);
  if (mode.is_set(IShell_core::Mode::JavaScript))
    register_keyword(keyword, IShell_core::Mode::JavaScript, topic,
                     case_sensitive);
  if (mode.is_set(IShell_core::Mode::SQL))
    register_keyword(keyword, IShell_core::Mode::SQL, topic, case_sensitive);
}

Help_registry *Help_registry::get_thread_context_help() const {
  thread_local static Help_registry instance_thread(true);
  return &instance_thread;
}

void Help_registry::register_keyword(const std::string &keyword,
                                     IShell_core::Mode mode, Help_topic *topic,
                                     bool case_sensitive) {
  auto lock = ensure_lock();
  if (!case_sensitive) {
    // The keywords topics will be created here if it doesn't exist.
    auto &topics = m_keywords[keyword];

    if (topics.find(topic) == topics.end())
      topics[topic] = Mode_mask(mode);
    else
      topics[topic].set(mode);
  } else {
    // The cs_keywords topics will be created here if it doesn't exist.
    auto &topics = m_cs_keywords[keyword];

    if (topics.find(topic) == topics.end())
      topics[topic] = Mode_mask(mode);
    else
      topics[topic].set(mode);
  }
}

std::string Help_registry::get_token(const std::string &token) const {
  std::string ret_val;

  try {
    ret_val = m_help_data.at(token);
  } catch (const std::out_of_range &) {
    try {
      ret_val = get_thread_context_help()->m_help_data.at(token);
    } catch (const std::out_of_range &) {
      // noop
    }
  }

  return ret_val;
}

bool find_wildcard(const std::string &pattern, const std::string &wildcard) {
  bool found = false;
  size_t pos = 0;

  while (!found && pos != std::string::npos) {
    pos = pattern.find(wildcard, pos);

    if (pos != std::string::npos) {
      found = (pos == 0 || pattern[pos - 1] != '\\');

      if (!found) pos++;
    }
  }

  return found;
}

bool is_pattern(const std::string &pattern) {
  return find_wildcard(pattern, "*") || find_wildcard(pattern, "?");
}

template <class Iterable>
std::set<const Help_topic *> get_topics(const Iterable &topic_map,
                                        const std::string &pattern_,
                                        IShell_core::Mode_mask mode) {
  std::set<const Help_topic *> ret_val;

  std::string pattern = pattern_;

  // strip (), in case it's a search for a function()
  if (auto p = pattern.find('('); p != std::string::npos) {
    pattern = pattern.substr(0, p);
  }

  if (is_pattern(pattern)) {
    // First we look in the case sensitive registry
    for (auto &entry : topic_map) {
      // Verifies if the registered keyword matches the given pattern
      if (match_glob(pattern, entry.first)) {
        // Verifies the associated topics to see which are enabled for
        // the active mode
        for (auto &topic : entry.second) {
          if (topic.second.matches_any(mode)) {
            ret_val.insert(topic.first);
          }
        }
      }
    }
  } else if (topic_map.find(pattern) != topic_map.end()) {
    auto topics = topic_map.at(pattern);
    for (auto &topic : topics) {
      if (topic.second.matches_any(mode)) {
        ret_val.insert(topic.first);
      }
    }
  }

  return ret_val;
}

std::vector<const Help_topic *> Help_registry::search_topics(
    const std::string &pattern, IShell_core::Mode_mask mode,
    bool case_sensitive) {
  // First searches on the case sensitive topics
  std::set<const Help_topic *> found_topics =
      get_topics(m_cs_keywords, pattern, mode);

  // If not restricted to case sensitive, searches in the case insensitive
  if (found_topics.empty() && !case_sensitive) {
    found_topics = get_topics(m_keywords, pattern, mode);
  }

  // We need to also look for the pattern in the thread context help,
  {
    std::set<const Help_topic *> thread_result =
        get_topics(get_thread_context_help()->m_cs_keywords, pattern, mode);
    found_topics.insert(thread_result.begin(), thread_result.end());
  }

  if (found_topics.empty() && !case_sensitive) {
    std::set<const Help_topic *> thread_result =
        get_topics(get_thread_context_help()->m_keywords, pattern, mode);
    found_topics.insert(thread_result.begin(), thread_result.end());
  }

  std::vector<const Help_topic *> ret_val;

  ret_val.insert(ret_val.end(), found_topics.begin(), found_topics.end());

  return ret_val;
}

std::vector<const Help_topic *> Help_registry::search_topics(
    const std::string &pattern, IShell_core::Mode mode) {
  return search_topics(pattern, IShell_core::Mode_mask(mode), false);
}

std::vector<std::string> Help_registry::search_topic_names(
    const std::string &pattern, IShell_core::Mode mode) {
  std::vector<std::string> matches;

  for (auto &entry : m_cs_keywords) {
    if (shcore::str_beginswith(entry.first, pattern)) {
      for (auto &topic : entry.second) {
        if (topic.second.matches_any(IShell_core::Mode_mask(mode))) {
          matches.push_back(entry.first);
        }
      }
    }
  }
  for (auto &entry : m_keywords) {
    if (shcore::str_beginswith(entry.first, pattern)) {
      for (auto &topic : entry.second) {
        if (topic.second.matches_any(IShell_core::Mode_mask(mode))) {
          matches.push_back(entry.first);
        }
      }
    }
  }

  return matches;
}

Help_topic *Help_registry::get_topic(const std::string &id,
                                     bool allow_unexisting,
                                     const Topic_mask &type,
                                     bool exact_id_match) const {
  auto topics = get_topic(this, id, true, type, exact_id_match);
  if (topics == nullptr && !m_c_tor) {
    topics = get_topic(get_thread_context_help(), id, allow_unexisting, type,
                       exact_id_match);
  }

  if (!allow_unexisting && topics == nullptr) {
    throw std::logic_error("Unable to find topic '" + id + "'");
  }

  return topics;
}

Help_topic *Help_registry::get_topic(const Help_registry *registry,
                                     const std::string &id,
                                     bool allow_unexisting,
                                     const Topic_mask &type,
                                     bool exact_id_match) const {
  // This contains all the topics associated to the keyword, despite their type
  auto it = registry->m_keywords.find(id);
  decltype(it->second) *topics = nullptr;

  // Type matches contains the list filtered by the matching types
  decltype(it->second) type_matches;
  if (it != registry->m_keywords.end()) {
    for (const auto &topic : it->second) {
      if (type.is_set(topic.first->m_type)) {
        type_matches[topic.first] = topic.second;
      }
    }
    // We are really interested ONLY in the type matches
    topics = &type_matches;
  }

  decltype(it->second) exact_matches;
  // If there are more than 1 topic, let's search for exact id matches only
  if (topics && (topics->size() > 2 || exact_id_match)) {
    for (const auto &topic : *topics) {
      if (topic.first->get_id() == id) {
        exact_matches[topic.first] = topic.second;
        break;
      }
    }

    // If exact_id_match was requested we update the list to the exact matches
    // whether there was one or not. Otoh, if not requested but we found then we
    // update topics too
    if (exact_id_match || !exact_matches.empty()) {
      topics = &exact_matches;
    }
  }

  if (topics && !topics->empty()) {
    if (topics->size() > 1) {
      throw std::logic_error("Non unique topic found as '" + id + "'");
    } else {
      return topics->begin()->first;
    }
  } else if (!allow_unexisting) {
    throw std::logic_error("Unable to find topic '" + id + "'");
  }

  return nullptr;
}

Help_topic *Help_registry::get_class_parent(Help_topic *topic) const {
  if (m_class_parents.find(topic) != m_class_parents.end())
    return m_class_parents.at(topic);

  if (get_thread_context_help()->m_class_parents.find(topic) !=
      get_thread_context_help()->m_class_parents.end())
    return get_thread_context_help()->m_class_parents.at(topic);
  return nullptr;
}

bool Help_registry::is_enabled(const Help_topic *topic,
                               IShell_core::Mode mode) const {
  auto ret_val = is_enabled(m_keywords, topic, mode);
  if (ret_val == false) {
    ret_val = is_enabled(get_thread_context_help()->m_keywords, topic, mode);
  }

  if (ret_val == false) {
    log_error("Help_registry::is_enabled: using an unregistered topic '%s'.",
              topic->m_id.c_str());
  }
  return ret_val;
}

Help_class_register::Help_class_register(
    const std::string &child, const std::string &parent,
    const std::string &upper_class, Help_mode mode,
    const std::map<std::string, std::string> &keywords) {
  IShell_core::Mode_mask mask;
  using Mode = IShell_core::Mode;
  if (mode == Help_mode::SCRIPTING) {
    mask = IShell_core::Mode_mask(Mode::JavaScript).set(Mode::Python);
  } else if (mode == Help_mode::JAVASCRIPT) {
    mask = IShell_core::Mode_mask(Mode::JavaScript);
  } else if (mode == Help_mode::PYTHON) {
    mask = IShell_core::Mode_mask(Mode::Python);
  } else {
    throw std::logic_error("Invalid mode for a scripting class.");
  }

  Help_registry::get()->add_help_class(child, parent, upper_class, mask,
                                       keywords);
}

Help_manager::Help_manager() {
  m_option_vals["brief"] = Help_option::Brief;
  m_option_vals["detail"] = Help_option::Detail;
  m_option_vals["categories"] = Help_option::Categories;
  m_option_vals["modules"] = Help_option::Modules;
  m_option_vals["objects"] = Help_option::Objects;
  m_option_vals["classes"] = Help_option::Classes;
  m_option_vals["functions"] = Help_option::Functions;
  m_option_vals["properties"] = Help_option::Properties;
  m_option_vals["childs"] = Help_option::Childs;
  m_option_vals["closing"] = Help_option::Closing;
  m_option_vals["example"] = Help_option::Example;

  m_registry = Help_registry::get();
}

std::vector<const Help_topic *> Help_manager::search_topics(
    const std::string &pattern) {
  return Help_registry::get()->search_topics(pattern, m_mode);
}

std::vector<std::string> Help_manager::resolve_help_text(
    const Help_topic &object, const std::string &suffix, bool legacy) {
  std::vector<std::string> help_text;

  auto object_ptr = &object;

  if (legacy) {
    while (object_ptr && help_text.empty()) {
      help_text = get_help_text(object_ptr->m_name + "_" + suffix);
      object_ptr =
          m_registry->get_class_parent(const_cast<Help_topic *>(object_ptr));
    }
  } else {
    // In new format, searches the very specific help registered using the fully
    // qualified object id
    help_text = get_help_text(object_ptr->m_id + "_" + suffix);

    // If not found, falls back to the legacy help resolution
    if (help_text.empty()) {
      return resolve_help_text(*object.m_parent,
                               object.get_base_name() + "_" + suffix, true);
    }
  }

  return help_text;
}

std::vector<std::string> Help_manager::get_help_text(const std::string &token) {
  int index = 0;

  std::string text = m_registry->get_token(token);

  std::vector<std::string> lines;

  while (!text.empty()) {
    if (shcore::str_beginswith(text.c_str(), "${") &&
        shcore::str_endswith(text.c_str(), "}")) {
      auto tmp = get_help_text(text.substr(2, text.size() - 3));
      lines.insert(lines.end(), tmp.begin(), tmp.end());
    } else if (shcore::str_beginswith(text.c_str(), "%{") &&
               shcore::str_endswith(text.c_str(), "}")) {
      std::string target = text.substr(2, text.size() - 3);
      std::string options;
      size_t pos = target.find(":");

      if (pos != std::string::npos) {
        options = target.substr(pos + 1);
        target = target.substr(0, pos);
      }

      lines.push_back(HELP_NO_FORMAT +
                      get_help(target, Topic_mask::any(), options));
    } else {
      lines.push_back(text);
    }

    text = m_registry->get_token(token + std::to_string(++index));
  }

  return lines;
}

std::vector<std::pair<std::string, std::string>>
Help_manager::parse_function_parameters(
    const std::vector<std::string> &parameters, std::string *signature) {
  std::vector<std::pair<std::string, std::string>> pdata;

  if (!parameters.empty()) {
    std::vector<std::string>
        fpnames;  // Parameter names as they will look in the signature
    std::vector<std::string>
        pdescs;  // Parameter descriptions as they are defined

    for (const auto &paramdef : parameters) {
      // 7 is the length of: "\param " or "@param "
      size_t start_index = 7;
      auto pname = paramdef.substr(
          start_index, paramdef.find(" ", start_index) - start_index);

      start_index += pname.size() + 1;
      std::string desc;
      std::string first_word;

      if (paramdef.size() > start_index) {
        desc = paramdef.substr(start_index);
        first_word = desc.substr(0, desc.find(" "));
      }

      // Updates parameter names to reflect the optional attribute on the
      // signature Removed the options word from the description
      if (shcore::str_caseeq(first_word, HELP_OPTIONAL_STR)) {
        if (fpnames.empty()) {
          fpnames.push_back("[" + pname + "]");  // First creates: [pname]
        } else {
          fpnames[fpnames.size() - 1].append("[");
          fpnames.push_back(pname + "]");  // Rest creates: pname[, pname]
        }
        desc = desc.substr(first_word.size() + 1);  // Deletes the optional word
        desc[0] = std::toupper(desc[0]);
      } else {
        fpnames.push_back(pname);
      }

      pdata.push_back({pname, desc});
    }

    // Continues the syntax
    if (signature) *signature = "(" + shcore::str_join(fpnames, ", ") + ")";
  } else {
    if (signature) *signature = "()";
  }

  return pdata;
}

void Help_manager::add_simple_function_help(const Help_topic &function,
                                            std::vector<std::string> *sections,
                                            const Help_options &options,
                                            cli::Shell_cli_mapper *cli) {
  const auto name = function.get_base_name();
  const auto parent = function.m_parent;

  std::vector<std::pair<std::string, std::string>> pdata;

  if (options.is_set(Help_option::Syntax)) {
    // Gets the function signature
    const auto signature = get_signature(function, cli);

    {
      // Starts the syntax
      std::string syntax =
          textui::bold(HELP_TITLE_SYNTAX) + HEADER_CONTENT_SEPARATOR;
      std::string fsyntax;

      if (cli) {
        fsyntax = shcore::str_join(cli->get_object_chain(), " ") + " " +
                  textui::bold(shcore::from_camel_case_to_dashes(name));
      } else {
        const auto display_name = function.get_name(m_mode);

        if (parent->is_api()) {
          if (parent->is_class()) {
            fsyntax += "<" + parent->m_name + ">." + textui::bold(display_name);
          } else {
            std::string parent_name = function.m_parent->get_id(
                m_mode, Topic_id_mode::EXCLUDE_CATEGORIES);
            fsyntax +=
                parent_name + HELP_API_SPLITTER + textui::bold(display_name);
          }
        } else {
          fsyntax += textui::bold(display_name);
        }
      }

      std::vector<std::string> signatures;

      // Ellipsis indicates the function is overloaded
      if (signature == "(...)") {
        signatures = resolve_help_text(function, "SIGNATURE");

        for (auto &item : signatures) {
          item = fsyntax + item;
        }
      } else {
        signatures.emplace_back(fsyntax + signature);
      }

      syntax += textui::format_markup_text(signatures, MAX_HELP_WIDTH,
                                           SECTION_PADDING, false);
      sections->emplace_back(std::move(syntax));
    }

    if (signature != "()" || (cli && !signature.empty())) {
      const auto params = resolve_help_text(function, "PARAM");

      pdata = parse_function_parameters(params);

      if (!pdata.empty()) {
        // Describes the parameters
        std::string where = textui::bold("WHERE") + HEADER_CONTENT_SEPARATOR;
        std::vector<std::string> where_items;

        size_t index;
        for (index = 0; index < params.size(); index++) {
          // for CLI only anonymous parameters get explained on the WHERE
          // section, the rest (the options) will be explained on the OPTIONS
          // section
          if (!cli || !cli->has_argument_options(pdata[index].first) ||
              cli->metadata()->param_codes[index] == 'C') {
            // Padding includes two spaces at the beggining, colon and space at
            // the end like:"  name: "<description>.
            size_t desc_padding = SECTION_PADDING;
            desc_padding += pdata[index].first.size() + 2;
            std::vector<std::string> data = {pdata[index].second};

            std::string desc = format_help_text(function, &data, MAX_HELP_WIDTH,
                                                desc_padding, true);

            if (!desc.empty()) {
              desc.replace(SECTION_PADDING, pdata[index].first.size() + 1,
                           pdata[index].first + ":");

              where_items.emplace_back(std::move(desc));
            }
          }
        }

        if (!where_items.empty()) {
          where += shcore::str_join(where_items, "\n");
          sections->emplace_back(std::move(where));
        }
      }
    }

    if (auto returns = resolve_help_text(function, "RETURNS");
        !returns.empty()) {
      std::string ret = textui::bold("RETURNS") + HEADER_CONTENT_SEPARATOR;
      // Removes the @returns tag
      returns[0] = returns[0].substr(returns[0].find_first_of(" \t") + 1);
      ret += format_help_text(function, &returns, MAX_HELP_WIDTH,
                              SECTION_PADDING, true);
      sections->emplace_back(std::move(ret));
    }
  }

  if (cli) {
    if (options.is_set(Help_option::Detail)) {
      add_cli_options_section("DETAIL", function, pdata, sections,
                              SECTION_PADDING, cli);
    }
  } else {
    if (options.is_set(Help_option::Detail)) {
      // Description
      add_member_section(HELP_TITLE_DESCRIPTION, "DETAIL", function, sections,
                         SECTION_PADDING);

      // Exceptions
      add_member_section("EXCEPTIONS", "THROWS", function, sections,
                         SECTION_PADDING);
    }

    if (options.is_set(Help_option::Example)) {
      // Examples are registered in sequential order, identifies the proper
      // prefix to be used in the example lookup, uses the fully qualified ID
      // first and falls back to the old format then
      std::string x_tag = parent->m_id + "." + name + "_EXAMPLE";

      if (m_registry->get_token(x_tag).empty())
        x_tag = parent->m_name + "_" + name + "_EXAMPLE";

      add_examples_section(function, x_tag, sections, SECTION_PADDING);
    }
  }
}

std::string Help_manager::get_signature(const Help_topic &function,
                                        cli::Shell_cli_mapper *cli) {
  std::string signature;
  std::vector<std::string> signatures;

  if (cli) {
    for (size_t index = 0; index < cli->metadata()->param_codes.size();
         index++) {
      std::string param_signature;

      const auto &param = cli->metadata()->signature[index];

      if (cli->metadata()->param_codes[index] == 'C' ||
          cli->metadata()->param_codes[index] == 'D' ||
          !cli->has_argument_options(param->name)) {
        param_signature = "<" + param->name + ">";
      } else if (cli->metadata()->param_codes[index] == 'A') {
        param_signature = "--" + param->name;
        auto validator = param->validator<List_validator>();
        if (validator->element_type() == shcore::Value_type::Undefined) {
          param_signature.append("[:<type>]=<value>...");
        } else {
          param_signature.append("=<" + get_cli_param_type(param->type()) +
                                 " list>...");
        }
      } else {
        param_signature = "--" + param->name;
        if (param->type() == shcore::Value_type::Undefined) {
          param_signature.append("[:<type>]=<value>");
        } else {
          param_signature.append("=<" + get_cli_param_type(param->type()) +
                                 ">");
        }
      }

      if (param->flag == shcore::Param_flag::Optional) {
        signature.append(" [" + param_signature + "]");
      } else {
        signature.append(" " + param_signature + "");
      }
    }
  } else {
    signatures = resolve_help_text(function, "SIGNATURE");

    if (signatures.empty()) {
      std::vector<std::string> params;
      // No signatures, we create it using the defined parameters
      params = resolve_help_text(function, "PARAM");

      parse_function_parameters(params, &signature);
    } else if (signatures.size() > 1) {
      signature = "(...)";
    } else {
      signature = signatures[0];
    }
  }

  return signature;
}

/**
 * Chain function processing for help generation.
 *
 * Format is simple:
 *
 * fname1.fname2.fname3.fname4
 *
 * Syntax for optional functions is between brackets, i.e.:
 *
 * fname1.[fname2]
 *
 * Syntax for dependent functions uses '->' instead of '.', i.e.:
 *
 * limit->[offset]
 *
 * NOTE: Current chains are simple so nothing more elaborated is required.
 */
void Help_manager::add_chained_function_help(
    const Help_topic &main_function, std::vector<std::string> *sections) {
  Help_topic *parent = main_function.m_parent;

  // Gets the chain definition already formatted for the active language
  auto chain_definition = get_help_text(
      parent->m_name + "_" + main_function.get_base_name() + "_CHAINED");
  format_help_text(main_function, &chain_definition, MAX_HELP_WIDTH, 0, false);

  // Gets the main chain definition elements
  auto functions = shcore::split_string(chain_definition[0], HELP_API_SPLITTER);

  // First element is the chain definition class
  std::string target_class = functions[0];
  functions.erase(functions.begin());

  std::map<std::string, std::string> signatures;

  std::vector<std::string> full_syntax;
  std::vector<const Help_topic *> function_topics;

  auto resolve = [this, &signatures, &function_topics](const std::string &id) {
    // Searches the topic corresponding to the given id
    auto topics = m_registry->search_topics(id, m_mode);
    assert(topics.size() == 1);
    const Help_topic *function = *topics.begin();
    function_topics.push_back(function);

    // Now retrieves the display signature
    signatures[function->get_name(m_mode)] = get_signature(*function);
  };

  for (auto mfunction : functions) {
    bool optional = false;
    bool child_optional = false;

    if (mfunction[0] == '[') {
      optional = true;
      mfunction = mfunction.substr(1, mfunction.length() - 2);
    }

    // Verifies dependent functions
    auto dependents = shcore::split_string(mfunction, "->");

    // Gets topic and display signature
    resolve(target_class + HELP_API_SPLITTER + dependents[0]);

    mfunction = dependents[0] + signatures[dependents[0]];

    // Removes the parent function
    dependents.erase(dependents.begin());

    // There's a function that is valid only on the context of a parent function
    // This section will format the child function
    if (!dependents.empty()) {
      std::string dependent = dependents[0];

      if (dependent[0] == '[') {
        child_optional = true;
        dependent = dependent.substr(1, dependent.length() - 2);
      }

      // Gets topic and display signature
      resolve(target_class + HELP_API_SPLITTER + dependent);

      dependent += signatures[dependent];

      if (child_optional) {
        mfunction += "[." + dependent + "]";
      } else {
        mfunction += "." + dependent;
      }
    }

    if (optional)
      full_syntax.push_back("[." + mfunction + "]");
    else
      full_syntax.push_back("." + mfunction);
  }

  // Adds the full syntax section
  std::string syntax;
  size_t padding = SECTION_PADDING + parent->m_name.length();
  syntax =
      textui::format_markup_text(full_syntax, MAX_HELP_WIDTH, padding, false);
  syntax.replace(SECTION_PADDING, parent->m_name.length(), parent->m_name);
  sections->push_back(textui::bold("SYNTAX") + HEADER_CONTENT_SEPARATOR +
                      syntax);

  // The main description is taken from the parent class
  std::string tag =
      parent->m_name + "_" + function_topics[0]->m_name + "_DETAIL";
  add_section(*parent, HELP_TITLE_DESCRIPTION, tag, sections, SECTION_PADDING);

  // Padding for the chained function descriptions
  padding = SECTION_PADDING + ITEM_DESC_PADDING;

  // function_topics.erase(function_topics.begin());

  // Now adds a light version of the rest of the chained functions
  while (!function_topics.empty()) {
    const Help_topic &function_topic = *function_topics[0];
    std::string name = function_topic.get_base_name();
    std::string dname = function_topic.get_name(m_mode);
    std::string signature = signatures[dname];
    std::string space(SECTION_PADDING, ' ');
    std::vector<std::string> section;

    std::string title = space + textui::bold(dname + signature);

    if (signature == "(...)") {
      section.push_back("This function has the following overloads:");
      for (auto &real_signature :
           get_help_text(target_class + "_" + name + "_SIGNATURE")) {
        section.push_back("@li " + dname + real_signature);
      }

      add_section_data(function_topic, title, &section, sections, padding);

      // Since title has been added, we clean it up for the section below
      title = "";
    }

    auto details = m_registry->get_token(target_class + "_" + name + "_DETAIL");

    if (details.empty())
      add_section(function_topic, title, target_class + "_" + name + "_BRIEF",
                  sections, padding);
    else
      add_section(function_topic, title, target_class + "_" + name + "_DETAIL",
                  sections, padding);

    auto single_title = space + "Example";
    auto multi_title = space + "Examples";
    add_examples_section(function_topic, target_class + "_" + name + "_EXAMPLE",
                         sections, padding, single_title, multi_title);

    function_topics.erase(function_topics.begin());
  }
}

std::string Help_manager::format_list_description(
    const Help_topic &topic, const std::string &name,
    std::vector<std::string> *help_text, size_t name_max_len, size_t lpadding,
    const std::string &alias, size_t alias_max_len) {
  std::string desc;

  // Adds the extra espace before the descriptions begin
  // and the three spaces for the " - " before the property names
  size_t dpadding = lpadding + name_max_len + 4;

  // Adds alias max length + a space
  if (alias_max_len) dpadding += alias_max_len + 1;

  if (help_text && !help_text->empty()) {
    desc = format_help_text(topic, help_text, MAX_HELP_WIDTH, dpadding, true);

    if (!alias.empty())
      desc.replace(dpadding - alias_max_len - 1, alias.size(), alias);

    desc.replace(lpadding, name.size() + 3, " - " + name);
  } else {
    if (!alias.empty()) {
      desc = std::string(name_max_len, ' ');
      desc.replace(lpadding, name.size() + 3, " - " + name);
      desc += " " + alias;
    } else {
      desc = " - " + name;
    }
  }

  return desc;
}

size_t Help_manager::get_max_topic_length(
    const std::vector<const Help_topic *> &topics, bool members,
    bool alias) const {
  size_t ret_val = 0;

  if (members) {
    // On members uses the corresponding name for the style
    for (auto topic : topics) {
      size_t new_len = topic->get_name(m_mode).length();
      if (new_len > ret_val) ret_val = new_len;
    }
  } else {
    // On topics it may use either the name or the alias
    for (auto topic : topics) {
      size_t new_len;
      if (alias)
        new_len = m_registry->get_token(topic->m_help_tag + "_ALIAS").length();
      else
        new_len = topic->m_name.length();

      if (new_len > ret_val) ret_val = new_len;
    }
  }

  return ret_val;
}

std::vector<std::string> Help_manager::get_topic_brief(
    const Help_topic *topic) {
  auto help_text = get_help_text(topic->m_help_tag + "_BRIEF");

  // Deprecation notices are added as part of the description on list format
  auto deprecated = get_help_text(topic->m_help_tag + "_DEPRECATED");
  for (const auto &text : deprecated) {
    help_text.push_back(text);
  }
  return help_text;
}

std::string Help_manager::format_topic_list(
    const std::vector<const Help_topic *> &topics, size_t lpadding,
    bool alias) {
  std::vector<std::string> formatted;

  // Gets the required padding for the descriptions
  size_t name_max_len = get_max_topic_length(topics, false);
  size_t alias_max_len = 0;

  if (alias) alias_max_len = get_max_topic_length(topics, false, true);

  for (auto topic : topics) {
    std::string alias_str;
    if (alias) alias_str = m_registry->get_token(topic->m_help_tag + "_ALIAS");

    auto help_text = get_topic_brief(topic);

    formatted.push_back(
        format_list_description(*topic, topic->m_name, &help_text, name_max_len,
                                lpadding, alias_str, alias_max_len));
  }

  return shcore::str_join(formatted, "\n");
}

std::vector<std::string> Help_manager::get_member_brief(
    const Help_topic *member) {
  auto help_text = resolve_help_text(*member, "BRIEF");

  // Deprecation notices are added as part of the description on list format
  auto deprecated = resolve_help_text(*member, "DEPRECATED");
  for (const auto &text : deprecated) {
    help_text.push_back(text);
  }

  return help_text;
}

std::string Help_manager::format_member_list(
    const std::vector<const Help_topic *> &topics, size_t lpadding,
    bool do_signatures,
    std::function<std::string(const Help_topic *)> format_name_cb) {
  std::vector<std::string> descriptions;
  for (auto member : topics) {
    std::string description(format_name_cb ? 0 : SECTION_PADDING, ' ');
    description +=
        format_name_cb ? format_name_cb(member) : member->get_name(m_mode);

    // If it is a function we need to retrieve the signature
    if (do_signatures && member->is_function()) {
      std::string name = member->get_base_name();
      auto chain_definition = resolve_help_text(*member, "CHAINED");

      if (chain_definition.empty()) {
        description += get_signature(*member);
      } else {
        // When the member is a chained function, the signature needs to be
        // function at the operation handler
        auto tokens = shcore::str_split(chain_definition[0], HELP_API_SPLITTER);
        auto op_name = tokens[0] + HELP_API_SPLITTER + name;
        auto operations = m_registry->search_topics(op_name, m_mode);
        auto operation = *operations.begin();
        assert(operations.size() == 1);
        description += get_signature(*operation);
      }
    }

    description += "\n";

    // Members could now be objects, briefs need to be retrieved
    // accordingly
    std::vector<std::string> help_text;
    if (member->is_object() || member->is_class())
      help_text = get_topic_brief(member);
    else
      help_text = get_member_brief(member);

    if (!help_text.empty()) {
      description += format_help_text(*member, &help_text, MAX_HELP_WIDTH,
                                      lpadding + ITEM_DESC_PADDING, true);
    }

    descriptions.push_back(description);
  }

  return shcore::str_join(descriptions, "\n\n");
}

void Help_manager::add_childs_section(
    const Help_topic &parent, const std::vector<const Help_topic *> &childs,
    std::vector<std::string> *sections, size_t lpadding, bool members,
    const std::string &tag, const std::string &default_title, bool alias) {
  if (!childs.empty()) {
    // Child list can be configured to be ignored
    if (m_registry->get_token(tag + "_DESC") != HELP_IGNORE_STR) {
      std::string title = m_registry->get_token(tag + "_TITLE");
      if (title.empty()) title = default_title;

      size_t count = sections->size();
      add_section(parent, "", tag + "_DESC", sections, lpadding);

      std::string section_data;
      if (count == sections->size()) {
        section_data = textui::bold(title);
        section_data += HEADER_CONTENT_SEPARATOR;
      }

      if (members)
        section_data += format_member_list(childs, SECTION_PADDING);
      else
        section_data += format_topic_list(childs, NO_PADDING, alias);

      sections->push_back(section_data);

      add_section(parent, "", tag + "_CLOSING_DESC", sections, lpadding);
    }
  }
}

void Help_manager::add_name_section(const Help_topic &topic,
                                    std::vector<std::string> *sections,
                                    cli::Shell_cli_mapper *cli) {
  std::string dname;
  if (cli) {
    dname = shcore::from_camel_case_to_dashes(topic.get_base_name());
  } else {
    dname = topic.get_name(m_mode);
  }

  std::string formatted;
  std::vector<std::string> brief;

  if (topic.is_member() && !topic.is_object())
    brief = resolve_help_text(topic, "BRIEF");
  else
    brief = get_help_text(topic.m_help_tag + "_BRIEF");

  if (!brief.empty()) {
    formatted = format_help_text(topic, &brief, MAX_HELP_WIDTH,
                                 dname.size() + 3 + SECTION_PADDING, true);

    formatted[dname.size() + 1 + SECTION_PADDING] = '-';
    formatted.replace(SECTION_PADDING, dname.size(), textui::bold(dname));
  } else {
    formatted = std::string(SECTION_PADDING, ' ');
    formatted += textui::bold(dname);
  }

  sections->push_back(textui::bold("NAME") + HEADER_CONTENT_SEPARATOR +
                      formatted);
}

std::string Help_manager::format_object_help(const Help_topic &object,
                                             const Help_options &options) {
  std::string tag = object.m_help_tag;
  size_t lpadding = object.is_api() ? SECTION_PADDING : NO_PADDING;

  std::vector<std::string> sections;
  std::string description_title;

  // The name section is not for all the objects
  if (object.is_api()) {
    if (options.is_set(Help_option::Name)) add_name_section(object, &sections);

    description_title = HELP_TITLE_DESCRIPTION;
  }

  if (options.is_set(Help_option::Syntax)) {
    // This is an object instance, so it's a property, we follow
    // Properties way for formatting
    if (object.is_object() && object.m_parent &&
        (object.m_parent->is_object() || object.m_parent->is_module())) {
      std::string syntax =
          textui::bold(HELP_TITLE_SYNTAX) + HEADER_CONTENT_SEPARATOR;

      syntax += std::string(SECTION_PADDING, ' ');

      std::string parent_name =
          object.m_parent->get_id(m_mode, Topic_id_mode::EXCLUDE_CATEGORIES);

      std::string display_name = object.m_name;
      if (object.is_member()) display_name = object.get_name(m_mode);

      syntax += parent_name + HELP_API_SPLITTER + textui::bold(display_name);

      sections.push_back(syntax);
    }
  }

  if (options.is_set(Help_option::Detail)) {
    size_t count = sections.size();

    if (object.is_topic()) {
      add_section(object, "", tag, &sections, lpadding);
    } else {
      add_section(object, description_title, tag + "_DETAIL", &sections,
                  lpadding);

      if (count == sections.size())
        add_section(object, description_title, tag + "_BRIEF", &sections,
                    lpadding);
    }
  }

  // Classifies the child topics
  std::vector<const Help_topic *> cat, prop, func, mod, cls, obj, consts,
      others;
  {
    auto lock = Help_registry::get()->ensure_lock();
    for (auto child : object.m_childs) {
      if (child->is_enabled(m_mode)) {
        switch (child->m_type) {
          case Topic_type::MODULE:
            mod.push_back(child);
            break;
          case Topic_type::CLASS:
            cls.push_back(child);
            break;
          case Topic_type::GLOBAL_OBJECT:
            obj.push_back(child);
            break;
          case Topic_type::CONSTANTS:
            consts.push_back(child);
            break;
          case Topic_type::FUNCTION:
            func.push_back(child);
            break;
          case Topic_type::PROPERTY:
          case Topic_type::OBJECT:
            prop.push_back(child);
            break;
          case Topic_type::CATEGORY:
          case Topic_type::SQL:
            cat.push_back(child);
            break;
          case Topic_type::TOPIC:
          case Topic_type::COMMAND:
            others.push_back(child);
            break;
        }
      }
    }
  }

  if (options.is_set(Help_option::Constants)) {
    add_childs_section(object, consts, &sections, lpadding, NO_MEMBER_TOPICS,
                       tag + "_CONSTANTS", "CONSTANTS");
  }

  if (options.is_set(Help_option::Properties)) {
    add_childs_section(object, prop, &sections, lpadding, MEMBER_TOPICS,
                       tag + "_PROPERTIES", "PROPERTIES");
  }

  if (options.is_set(Help_option::Objects)) {
    add_childs_section(object, obj, &sections, lpadding, NO_MEMBER_TOPICS,
                       tag + "_OBJECTS", "OBJECTS");
  }

  if (options.is_set(Help_option::Functions)) {
    add_childs_section(object, func, &sections, lpadding, MEMBER_TOPICS,
                       tag + "_FUNCTIONS", "FUNCTIONS");
  }

  if (options.is_set(Help_option::Classes)) {
    add_childs_section(object, cls, &sections, lpadding, NO_MEMBER_TOPICS,
                       tag + "_CLASSES", "CLASSES");
  }

  if (options.is_set(Help_option::Modules)) {
    add_childs_section(object, mod, &sections, lpadding, NO_MEMBER_TOPICS,
                       tag + "_MODULES", "MODULES");
  }

  if (options.is_set(Help_option::Categories)) {
    add_childs_section(object, cat, &sections, lpadding, NO_MEMBER_TOPICS,
                       tag + "_CATEGORIES", "RELATED CATEGORIES");
  }

  if (options.is_set(Help_option::Childs)) {
    add_childs_section(object, others, &sections, lpadding, NO_MEMBER_TOPICS,
                       tag + "_CHILDS", "RELATED TOPICS",
                       object.m_name == shcore::Help_registry::HELP_COMMANDS);
  }

  if (options.is_set(Help_option::Closing))
    add_section(object, "", tag + "_CLOSING", &sections, lpadding);

  if (options.is_set(Help_option::Example))
    add_examples_section(object, tag + "_EXAMPLE", &sections, lpadding);

  return shcore::str_join(sections, "\n\n");
}

void Help_manager::add_section_data(const Help_topic &topic,
                                    const std::string &title,
                                    std::vector<std::string> *details,
                                    std::vector<std::string> *sections,
                                    size_t padding, bool insert_blank_lines) {
  if (!details->empty()) {
    std::string section;

    if (!title.empty())
      section += (mysqlshdk::textui::bold(title) + HEADER_CONTENT_SEPARATOR);

    section.append(format_help_text(topic, details, MAX_HELP_WIDTH, padding,
                                    insert_blank_lines));

    sections->push_back(section);
  }
}

void Help_manager::add_member_section(const std::string &title,
                                      const std::string &tag,
                                      const Help_topic &member,
                                      std::vector<std::string> *sections,
                                      size_t padding) {
  auto details = resolve_help_text(member, tag);
  add_section_data(member, title, &details, sections, padding);
}

void Help_manager::add_cli_options_section(
    const std::string &tag, const Help_topic &member,
    const std::vector<std::pair<std::string, std::string>> &pdata,
    std::vector<std::string> *sections, size_t padding,
    cli::Shell_cli_mapper *cli) {
  if (!cli->options().empty()) {
    auto option_data = parse_cli_option_data(resolve_help_text(member, tag));
    std::map<std::string_view, std::string> options_details;

    for (size_t index = 0; index < cli->metadata()->signature.size(); index++) {
      const auto &param = cli->metadata()->signature[index];
      const auto &param_code = cli->metadata()->param_codes[index];

      // Only parameters with named options
      if (cli->has_argument_options(param->name)) {
        if (param_code == 'C') {
          // NO-OP: Connection options are not being included to avoid making it
          // spammy.
        } else if (param_code == 'D') {
          auto validator = param->validator<Option_validator>();
          const auto &allowed_list = validator->allowed();
          for (const auto &allowed : allowed_list) {
            // Only options that are enabled for CLI
            if (allowed->cmd_line_enabled && !allowed->is_deprecated) {
              const auto option_name = format_cli_option_name(allowed->name);
              const auto is_ambiguous = cli->is_ambiguous_option(option_name);
              const auto &cli_option_name =
                  is_ambiguous ? format_cli_option_name(
                                     cli::Shell_cli_mapper::disambiguate_option(
                                         param->name, allowed->name))
                               : option_name;

              std::string option_detail =
                  format_cli_option(allowed, cli_option_name);

              auto param_data = option_data.find(allowed->name);
              if (param_data != option_data.end()) {
                std::vector<std::string> brief{param_data->second};

                if (is_ambiguous) {
                  auto &text = brief[0];

                  text += " NOTE: this is a disambiguation of the ";
                  text += option_name;
                  text += " option.";
                }

                option_detail += "\n";
                option_detail +=
                    format_help_text(member, &brief, MAX_HELP_WIDTH,
                                     padding + ITEM_DESC_PADDING, true);
              }
              options_details.emplace(allowed->name, std::move(option_detail));
            }
          }
        } else {
          std::string option_detail =
              format_cli_option(param, format_cli_option_name(param->name));

          for (const auto &param_data : pdata) {
            if (param_data.first == param->name) {
              if (!param_data.second.empty()) {
                std::vector<std::string> brief{param_data.second};

                // The option type is not needed on the description as it is
                // already displayed in the syntax.
                brief[0] = strip_param_type(brief[0]);
                option_detail += "\n";
                option_detail +=
                    format_help_text(member, &brief, MAX_HELP_WIDTH,
                                     padding + ITEM_DESC_PADDING, true);
              }
              break;
            }
          }
          options_details.emplace(param->name, std::move(option_detail));
        }
      }
    }

    if (!options_details.empty()) {
      std::string section = mysqlshdk::textui::bold("OPTIONS");
      section += HEADER_CONTENT_SEPARATOR;
      section += shcore::str_join(
          options_details, "\n\n",
          [](const auto &it) -> const std::string & { return it.second; });
      sections->push_back(section);
    }
  }
}

void Help_manager::add_section(const Help_topic &topic,
                               const std::string &title, const std::string &tag,
                               std::vector<std::string> *sections,
                               size_t padding, bool insert_blank_lines) {
  std::vector<std::string> details;
  details = get_help_text(tag);
  add_section_data(topic, title, &details, sections, padding,
                   insert_blank_lines);
}

std::string Help_manager::format_function_help(const Help_topic &function,
                                               const Help_options &options,
                                               cli::Shell_cli_mapper *cli) {
  std::string name = function.get_base_name();
  std::vector<std::string> sections;

  if (options.is_set(Help_option::Name)) {
    // Brief Description
    add_name_section(function, &sections, cli);
  }

  auto chain_definition = resolve_help_text(function, "CHAINED");

  std::string additional_help;
  if (chain_definition.empty()) {
    add_simple_function_help(function, &sections, options, cli);
  } else {
    add_chained_function_help(function, &sections);
  }

  return shcore::str_join(sections, "\n\n");
}

std::string Help_manager::format_property_help(const Help_topic &property) {
  std::vector<std::string> sections;

  add_name_section(property, &sections);

  // Syntax
  std::string display_name = property.get_name(m_mode);
  std::string syntax =
      textui::bold(HELP_TITLE_SYNTAX) + HEADER_CONTENT_SEPARATOR;
  syntax += std::string(SECTION_PADDING, ' ');

  if (property.m_parent->is_class()) {
    syntax +=
        "<" + property.m_parent->m_name + ">." + textui::bold(display_name);
  } else {
    std::string parent_name =
        property.m_parent->get_id(m_mode, Topic_id_mode::EXCLUDE_CATEGORIES);

    syntax += parent_name + HELP_API_SPLITTER + textui::bold(display_name);
  }

  sections.push_back(syntax);

  // Details
  add_member_section(HELP_TITLE_DESCRIPTION, "DETAIL", property, &sections,
                     SECTION_PADDING);

  return shcore::str_join(sections, "\n\n");
}

std::string Help_manager::format_command_help(const Help_topic &command,
                                              const Help_options &options) {
  std::vector<std::string> sections;
  std::string tag = command.m_help_tag;

  if (options.is_set(Help_option::Name)) {
    std::string description = "<b>" + command.m_name + "</b> - " +
                              m_registry->get_token(tag + "_BRIEF");

    std::string intro = mysqlshdk::textui::bold("NAME") + "\n";

    intro += textui::format_markup_text({description}, MAX_HELP_WIDTH,
                                        SECTION_PADDING, true);

    sections.push_back(intro);
  }

  if (options.is_set(Help_option::Syntax))
    add_section(command, HELP_TITLE_SYNTAX, tag + "_SYNTAX", &sections,
                SECTION_PADDING, false);

  if (options.is_set(Help_option::Detail))
    add_section(command, HELP_TITLE_DESCRIPTION, tag + "_DETAIL", &sections,
                SECTION_PADDING);

  if (options.is_set(Help_option::Example))
    add_examples_section(command, tag + "_EXAMPLE", &sections, SECTION_PADDING);

  return shcore::str_join(sections, "\n\n");
}

void Help_manager::add_examples_section(const Help_topic &topic,
                                        const std::string &tag,
                                        std::vector<std::string> *sections,
                                        size_t padding,
                                        const std::string &single_title,
                                        const std::string &multi_title) {
  std::string current_tag(tag);
  std::string example = m_registry->get_token(current_tag);

  if (!example.empty()) {
    int index = 0;

    std::vector<std::string> examples;
    while (!example.empty()) {
      std::string formatted;
      std::vector<std::string> data = {example};
      formatted = format_help_text(topic, &data, MAX_HELP_WIDTH, padding, true);

      auto desc = get_help_text(current_tag + "_DESC");
      if (!desc.empty()) {
        formatted += "\n";
        formatted += format_help_text(topic, &desc, MAX_HELP_WIDTH,
                                      padding + ITEM_DESC_PADDING, true);
      }

      examples.push_back(formatted);
      current_tag = tag + std::to_string(++index);

      example = m_registry->get_token(current_tag);
    }

    std::string section;

    if (examples.size() == 1)
      section = mysqlshdk::textui::bold(single_title);
    else
      section = mysqlshdk::textui::bold(multi_title);
    section += HEADER_CONTENT_SEPARATOR;
    section += shcore::str_join(examples, "\n\n");
    sections->push_back(section);
  }
}

std::map<std::string, std::string> Help_manager::preprocess_help(
    const Help_topic &topic, std::vector<std::string> *text_lines) const {
  std::map<std::string, std::string> no_format_data;
  for (auto &text : *text_lines) {
    if (shcore::str_beginswith(text.c_str(), HELP_NO_FORMAT)) {
      std::string id = HELP_NO_FORMAT;
      std::string data = text.substr(id.size());
      id += "[" + std::to_string(no_format_data.size()) + "]";
      no_format_data[id] = data;
      text = id;
    } else {
      text = shcore::str_subvars(
          text,
          [this, topic](std::string_view s) {
            if (!s.empty() && s[0] == ':') {
              // sub context specific keywords
              return topic.get_keyword(std::string(s.substr(1)));
            } else {
              // sub member names by naming convention
              std::string member_name(s);

              if (m_mode == IShell_core::Mode::Python)
                member_name = shcore::from_camel_case(member_name);

              return member_name;
            }
          },
          "<<<", ">>>");
    }
  }

  return no_format_data;
}

std::string Help_manager::format_help_text(const Help_topic &topic,
                                           std::vector<std::string> *lines,
                                           size_t width, size_t left_padding,
                                           bool paragraph_per_line) const {
  std::map<std::string, std::string> no_formats;
  no_formats = preprocess_help(topic, lines);
  std::string formatted = textui::format_markup_text(
      *lines, width, left_padding, paragraph_per_line);

  for (auto item : no_formats) {
    formatted = shcore::str_replace(formatted, item.first, item.second);
  }

  return formatted;
}

std::string Help_manager::get_help(const Help_topic &topic,
                                   const Help_options &options,
                                   cli::Shell_cli_mapper *cli) {
  switch (topic.m_type) {
    case Topic_type::CATEGORY:
    case Topic_type::TOPIC:
    case Topic_type::MODULE:
    case Topic_type::CLASS:
    case Topic_type::OBJECT:
    case Topic_type::CONSTANTS:
    case Topic_type::GLOBAL_OBJECT: {
      return format_object_help(topic, options);
    }
    case Topic_type::FUNCTION: {
      return format_function_help(topic, options, cli);
    }
    case Topic_type::PROPERTY: {
      return format_property_help(topic);
    }
    case Topic_type::COMMAND:
      return format_command_help(topic, options);
    case Topic_type::SQL:
      throw std::logic_error("Unable to get help for topic...");
  }

  return "";
}

std::string Help_manager::get_help(const std::string &topic_id, Topic_mask type,
                                   const std::string &options) {
  Help_options hoptions;

  if (options.empty()) {
    hoptions = Help_options::all();
  } else {
    std::vector<std::string> tokens = shcore::str_split(options, ",");
    for (auto &token : tokens) {
      if (m_option_vals.find(token) != m_option_vals.end()) {
        hoptions.set(m_option_vals[token]);
      } else {
        throw std::logic_error("get_help: invalid option '" + token + "'");
      }
    }
  }

  return get_help(topic_id, type, hoptions);
}

std::string Help_manager::get_help(const std::string &topic_id, Topic_mask type,
                                   const Help_options &options) {
  auto topics = search_topics(topic_id);
  decltype(topics) *found_topics = &topics;

  if (!type.empty()) {
    for (size_t index = 0; index < topics.size(); index++) {
      if (!type.is_set(topics[index]->m_type)) {
        topics.erase(topics.begin() + index);
        index--;
      }
    }
  }

  // This function is for internal use, meaning the provided topic_id might be
  // exact, i.e. in extension objects.
  // At this point if we still have multiple topics, we search to see if there's
  // an exact match with the provided topic_id, if so, that one is returned
  decltype(topics) exact_matches;

  if (topics.size() > 1) {
    std::array<std::function<bool(const shcore::Help_topic *)>, 3> comparators =
        {{
            // case sensitive comparison with topic ID
            [&topic_id](const shcore::Help_topic *topic) {
              return topic->get_id() == topic_id;
            },
            // if topic is a global object or has a parent that's a global
            // object, that global object might have been registered with a
            // lower case name (i.e. a global shell object of Shell class) and
            // that became a part of its ID; topic_id uses class names, so we
            // need to compare first letter case-insensitive, and the rest
            // case-sensitive
            [&topic_id](const shcore::Help_topic *topic) {
              auto global = topic;

              while (global) {
                if (Topic_type::GLOBAL_OBJECT == global->m_type) {
                  break;
                }

                global = global->m_parent;
              }

              if (!global) {
                return false;
              }

              const auto id = std::string_view{topic->get_id()};
              const auto tid = std::string_view{topic_id};

              return shcore::str_caseeq(id.substr(0, 1), tid.substr(0, 1)) &&
                     id.substr(1) == tid.substr(1);
            },
            // if topic has a parent that's a module, its name is part of the
            // topic's ID, but topic_id may not start with it, strip it and
            // do a case sensitive comparison
            [&topic_id](const shcore::Help_topic *topic) {
              auto parent = topic->m_parent;

              while (parent) {
                if (parent->is_module()) {
                  break;
                }

                parent = parent->m_parent;
              }

              if (!parent) {
                return false;
              }

              return std::string_view{topic->get_id()}.substr(
                         parent->get_id().length() + 1) == topic_id;
            },
        }};

    for (const auto &comparator : comparators) {
      exact_matches.clear();

      for (const auto &topic : topics) {
        if (comparator(topic)) {
          exact_matches.push_back(topic);
        }
      }

      if (1 == exact_matches.size()) {
        found_topics = &exact_matches;
        break;
      }
    }
  }

  if (found_topics->empty())
    throw std::logic_error("Unable to find help for '" + topic_id + "'");
  else if (found_topics->size() > 1)
    throw std::logic_error("Multiple matches found for topic '" + topic_id +
                           "'");

  return get_help(**found_topics->begin(), options);
}

}  // namespace shcore
