/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <mrpt/containers/ValueCommentPair.h>
#include <mrpt/containers/YamlEmitOptions.h>
#include <mrpt/containers/internal_yaml_fwrds.h>
#include <mrpt/core/bits_math.h>  // mrpt::RAD2DEG
#include <mrpt/core/demangle.h>
#include <mrpt/core/exceptions.h>
#include <mrpt/core/format.h>
#include <mrpt/typemeta/TEnumType.h>

#include <any>
#include <array>
#include <cstdint>
#include <cstdlib>
#include <iosfwd>
#include <limits>
#include <map>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <typeinfo>
#include <variant>
#include <vector>

/** \defgroup mrpt_containers_yaml YAML/JSON C++ API
 * Header: `#include <mrpt/containers/yaml.h>`.
 * Library: \ref mrpt_containers_grp
 * \ingroup mrpt_containers_grp */

namespace mrpt::containers
{
/** Powerful YAML-like container for possibly-nested blocks of parameters or
 *any arbitrary structured data contents, including documentation in the
 *form of comments attached to each node. Supports parsing from YAML or JSON
 *streams, files, or text strings.
 *
 * This class holds the root "node" in a YAML-like tree structure.
 * Each tree node can be of one of these types:
 * - Scalar values ("leaf nodes"): Can hold any type, stored as C++17 std::any.
 * - Sequence container.
 * - Map ("dictionary"): pairs of `name: value`.
 * - Null, empty nodes: yaml `~` or `null`.
 *
 * Sequences and dictionaries can hold, in turn, any of the four types above,
 * leading to arbitrarialy-complex nested structures.
 *
 * This class was designed as a lightweight, while structured, way to pass
 *arbitrarialy-complex parameter blocks but can be used to load and save
 *YAML files or as a database.
 *
 * yaml can be used to parse YAML (v1.2) or JSON streams, and to emit YAML.
 * It does not support event-based parsing.
 * The parser uses Pantelis Antoniou's awesome
 *[libfyaml](https://github.com/pantoniou/libfyaml), which
 *[passes](http://matrix.yaml.io/) the full [YAML
 *testsuite](https://github.com/yaml/yaml-test-suite).
 *
 * Known limitations:
 * - *Parsing* comments is limited to right-hand comments for *sequence* or
 *   *map* entries.
 *
 * See examples below (\ref containers_yaml_example/test.cpp):
 * \snippet containers_yaml_example/test.cpp example-yaml
 * Output:
 *  \include containers_yaml_example/console.out
 *
 * Verbose debug information on YAML document parsing is emitted if the
 * environment variable `MRPT_YAML_PARSER_VERBOSE` is set to `1`.
 *
 * \ingroup mrpt_containers_yaml
 * \note [New in MRPT 2.1.0]
 */
class yaml
{
 public:
  /** @name Types
   * @{ */

  struct node_t;
  using scalar_t = std::any;
  using sequence_t = std::vector<node_t>;
  using map_t = std::map<node_t, node_t>;

  using comments_t =
      std::array<std::optional<std::string>, static_cast<size_t>(CommentPosition::MAX)>;

  struct mark_t
  {
    /// Position from the start of the input file
    std::size_t input_pos = 0;
    int line = 0;    //!< Line position (0-based index)
    int column = 0;  //!< Column  position (0-based index)
  };

  struct node_t
  {
    /** @name Data
     *  @{ */

    /** Node data */
    std::variant<std::monostate, sequence_t, map_t, scalar_t> d;

    /** Optional comment block */
    comments_t comments;

    /** Optional flag to print collections in short form (e.g. [A,B] for
     * sequences) \note (New in MRPT 2.1.8) */
    bool printInShortFormat = false;

    /** Positioning information about the placement of the element in the
     * original input file/stream, i.e. line and column number
     * \note (New in MRPT 2.5.0)
     */
    mark_t marks;

    /** @} */

    node_t() = default;
    ~node_t() = default;

    template <
        typename T,  //
        typename = std::enable_if_t<
            !std::is_constructible_v<std::initializer_list<map_t::value_type>, T>>,  //
        typename = std::enable_if_t<
            !std::is_constructible_v<std::initializer_list<sequence_t::value_type>, T>>>
    node_t(const T& scalar)
    {
      d.emplace<scalar_t>().emplace<T>(scalar);
    }
    /** Specialization for literals */
    node_t(const char* str)
    {
      // Storing char* is not safe if it points to temporary
      // memory or a stack zone (!):
      d.emplace<scalar_t>().emplace<std::string>(str);
    }

    node_t(std::initializer_list<map_t::value_type> init) { d.emplace<map_t>(init); }
    node_t(std::initializer_list<sequence_t::value_type> init) { d.emplace<sequence_t>(init); }

    bool isNullNode() const;
    bool isScalar() const;
    bool isSequence() const;
    bool isMap() const;

    /** Returns: "null", "sequence", "map", "scalar(<TYPE>)" */
    std::string typeName() const;

    /** Use: `for (auto &kv: n.asSequence()) {...}`
     * \exception std::exception If called on a non-sequence node. */
    sequence_t& asSequence();
    const sequence_t& asSequence() const;

    /** Use: `for (auto &kv: n.asMap()) {...}`
     * \exception std::exception If called on a non-map node. */
    map_t& asMap();
    const map_t& asMap() const;

    /** \exception std::exception If called on a non-scalar node. */
    scalar_t& asScalar();
    const scalar_t& asScalar() const;

    /** Returns 1 for null or scalar nodes, the number of children for
     * sequence or map nodes. */
    size_t size() const;

    /** Returns a copy of the existing value of the given type, or tries
     * to convert it between easily-compatible types (e.g. double<->int,
     * string<->int).
     * \exception std::exception If the contained type does not  match
     * and there is no obvious conversion.
     */
    template <typename T>
    T as() const
    {
      ASSERTMSG_(
          std::holds_alternative<scalar_t>(d),
          mrpt::format(
              "Trying to use as() on a node of type `%s`, but only "
              "available for `scalar` nodes.",
              typeName().c_str()));
      return internal::implAnyAsGetter<T>(std::get<scalar_t>(d));
    }

    const std::string_view internalAsStr() const
    {
      ASSERT_(isScalar());
      if (const char* const* s = std::any_cast<const char*>(&asScalar()); s != nullptr)
      {
        return {*s};
      }
      if (const std::string* s = std::any_cast<std::string>(&asScalar()); s != nullptr)
      {
        return {*s};
      }
      if (const std::string_view* s = std::any_cast<std::string_view>(&asScalar()); s != nullptr)
      {
        return {*s};
      }
      THROW_EXCEPTION_FMT(
          "Used node_t as map key with a type non-convertible to "
          "string: "
          "'%s'",
          typeName().c_str());
    }

    bool hasComment() const
    {
      for (const auto& c : comments)
        if (c.has_value()) return true;
      return false;
    }
    bool hasComment(CommentPosition pos) const
    {
      MRPT_START
      int posIndex = static_cast<int>(pos);
      ASSERT_GE_(posIndex, 0);
      ASSERT_LT_(posIndex, static_cast<int>(CommentPosition::MAX));
      return comments[posIndex].has_value();
      MRPT_END
    }
    const std::string& comment() const
    {
      MRPT_START
      for (const auto& c : comments)
        if (c.has_value()) return c.value();
      THROW_EXCEPTION("Trying to access comment but this node has none.");
      MRPT_END
    }
    const std::string& comment(CommentPosition pos) const
    {
      MRPT_START
      int posIndex = static_cast<int>(pos);
      ASSERT_GE_(posIndex, 0);
      ASSERT_LT_(posIndex, static_cast<int>(CommentPosition::MAX));
      ASSERTMSG_(
          comments[posIndex].has_value(), "Trying to access comment but this node has none.");
      return comments[posIndex].value();
      MRPT_END
    }
  };

  /** @} */

  /** @name Constructors and initializers
   * @{ */

  yaml() = default;
  ~yaml() = default;

  /** Constructor for maps, from list of pairs of values. See examples in
   * yaml above. */
  yaml(std::initializer_list<map_t::value_type> init) : root_(init) {}

  /** Constructor for sequences, from list of values. See examples in
   * yaml above. */
  yaml(std::initializer_list<sequence_t::value_type> init) : root_(init) {}
  yaml(const yaml& v);

  yaml(const node_t& s) : root_(s) {}

  static node_t Sequence(std::initializer_list<sequence_t::value_type> init)
  {
    return node_t(init);
  }
  static node_t Sequence()
  {
    node_t n;
    n.d.emplace<sequence_t>();
    return n;
  }

  static node_t Map(std::initializer_list<map_t::value_type> init) { return node_t(init); }
  static node_t Map()
  {
    node_t n;
    n.d.emplace<map_t>();
    return n;
  }

  /** Parses a text as YAML or JSON (autodetected) and returns a document.
   * \exception std::exception Upon format errors
   */
  static yaml FromText(const std::string& yamlTextBlock);

  /** Parses a text as YAML or JSON (autodetected) and stores the contents
   * into this document.
   *
   * \exception std::exception Upon format errors
   */
  void loadFromText(const std::string& yamlTextBlock);

  /** Parses the stream as YAML or JSON (autodetected) and returns a
   * document. \exception std::exception Upon format errors
   */
  static yaml FromStream(std::istream& i);

  /** Parses a text as YAML or JSON (autodetected) and stores the contents
   * into this document.
   *
   * \exception std::exception Upon I/O or format errors
   */
  void loadFromFile(const std::string& fileName);

  /** Parses the filename as YAML or JSON (autodetected) and returns a
   * document.
   * \exception std::exception Upon I/O or format errors.
   */
  static yaml FromFile(const std::string& fileName);

  /** Parses the stream as YAML or JSON (autodetected) and stores the
   * contents into this document.
   *
   * \exception std::exception Upon format errors
   */
  void loadFromStream(std::istream& i);

  /** Builds an object copying the structure and contents from an existing
   * YAMLCPP Node. Requires user to #include yamlcpp from your calling
   * program (does NOT requires yamlcpp while compiling mrpt itself).
   *
   * \tparam YAML_NODE Must be `YAML::Node`. Made a template just to avoid
   * build-time depedencies.
   */
  template <typename YAML_NODE>
  inline static yaml FromYAMLCPP(const YAML_NODE& n);

  /** \overload (loads an existing YAMLCPP into this) */
  template <typename YAML_NODE>
  inline void loadFromYAMLCPP(const YAML_NODE& n);

  /** Creates a yaml dictionary node from an Eigen or mrpt::math matrix.
   * Example (compatible with OpenCV & ROS YAML formats):
   * \code
   * rows: 2
   * cols: 3
   * data: [11, 12, 13, 21, 22, 23]
   * \endcode
   * \sa toMatrix()
   */
  template <typename MATRIX>
  inline static yaml FromMatrix(const MATRIX& m);

  /** Fills in a matrix from a yaml dictionary node.
   * The matrix can be either an Eigen or mrpt::math matrix.
   * Example yaml node (compatible with OpenCV & ROS YAML formats):
   * \code
   * rows: 2
   * cols: 3
   * data: [11, 12, 13, 21, 22, 23]
   * \endcode
   * \sa FromMatrix()
   */
  template <typename MATRIX>
  inline void toMatrix(MATRIX& m) const;

  /** Converts a sequence yaml node into a std::vector, trying to convert
   * all nodes to the same given `Scalar` type. \note (New in MRPT 2.3.3)
   */
  template <typename Scalar>
  inline std::vector<Scalar> toStdVector() const;

  /** @} */

  /** @name Content and type checkers
   * @{ */
  /** For map nodes, checks if the given key name exists.
   *  Returns false if the node is a `null` node.
   *  Throws if the node is not a map or null.
   */
  bool has(const std::string& key) const;

  /** For map or sequence nodes, checks if the container is empty. Also
   * returns true for null(empty) nodes. */
  bool empty() const;

  /** Resets to empty (can be called on a root node or any other node to
   * clear that subtree only). */
  void clear();

  bool isNullNode() const;
  bool isScalar() const;
  bool isSequence() const;
  bool isMap() const;

  /** For scalar nodes, returns its type, or typeid(void) if an empty
   * node. \exception std::exception If called on a map or sequence. */
  const std::type_info& scalarType() const;

  /** @} */

  /** @name Range-for and conversion helpers
   * @{ */

  /** Use: `for (auto &kv: n.asSequence()) {...}`
   * \exception std::exception If called on a non-sequence node. */
  sequence_t& asSequence();
  const sequence_t& asSequence() const;

  /// Returns a copy of asSequence(), suitable for range-based loops
  const sequence_t asSequenceRange() const { return asSequence(); }

  /** Use: `for (auto &kv: n.asMap()) {...}`
   * \exception std::exception If called on a non-map node. */
  map_t& asMap();
  const map_t& asMap() const;

  /// Returns a copy of asMap(), suitable for range-based loops
  const map_t asMapRange() const { return asMap(); }

  /** \exception std::exception If called on a non-scalar node. */
  scalar_t& asScalar();
  const scalar_t& asScalar() const;

  /** Returns 1 for null or scalar nodes, the number of children for
   * sequence or map nodes. */
  size_t size() const;

  /** For a master yaml document, returns the root node; otherwise, the
   * referenced node. */
  node_t& node() { return *dereferenceProxy(); }
  /** \overload */
  const node_t& node() const { return *dereferenceProxy(); }

  /** Maps only: returns a reference to the key node of a key-value pair.
   * \exception std::exception If called on a non-map node or key does not
   * exist.
   */
  const node_t& keyNode(const std::string& keyName) const;
  node_t& keyNode(const std::string& keyName);

  /** @} */

  /** @name Print and export
   * @{ */

  /** Prints the document in YAML format to the given stream. */
  void printAsYAML(std::ostream& o, const YamlEmitOptions& eo = {}) const;

  /// \overload (prints to std::cout)
  void printAsYAML() const;

  /** Prints a tree-like representation of all nodes in the document in a
   * custom format (nor YAML neither JSON). */
  void printDebugStructure(std::ostream& o) const;

  /** @} */

  /** @name Read/write to maps (dictionaries)
   * @{ */

  /** Write access for maps */
  yaml operator[](const std::string& key);
  /// \overload
  inline yaml operator[](const char* key)
  {
    ASSERT_(key != nullptr);
    return operator[](std::string(key));
  }

  /** Read access  for maps
   * \throw std::runtime_error if key does not exist. */
  const yaml operator[](const std::string& key) const;
  /// \overload
  inline const yaml operator[](const char* key) const
  {
    ASSERT_(key != nullptr);
    return operator[](std::string(key));
  }

  /** Scalar read access for maps, with default value if key does not
   * exist.
   */
  template <typename T>
  const T getOrDefault(const std::string& key, const T& defaultValue) const
  {
    MRPT_START
    const node_t* n = dereferenceProxy();
    if (n->isNullNode()) return defaultValue;
    if (!n->isMap())
      THROW_EXCEPTION_FMT(
          "getOrDefault() is only for map nodes, invoked on a node "
          "of "
          "type: '%s'",
          n->typeName().c_str());

    const map_t& m = std::get<map_t>(n->d);
    auto it = m.find(key);
    if (m.end() == it) return defaultValue;
    try
    {
      return yaml(internal::tag_as_const_proxy_t(), it->second, "").as<T>();
    }
    catch (const std::bad_any_cast& e)
    {
      throw std::logic_error(mrpt::format(
          "getOrDefault(): Trying to access key `%s` holding type "
          "`%s` "
          "as the wrong type: `%s`",
          key.c_str(), n->typeName().c_str(), e.what()));
    }
    MRPT_END
  }
  /** @} */

  /** Write into an existing index of a sequence.
   * \throw std::out_of_range if index is out of range. */
  yaml operator()(int index);
  /** Read from an existing index of a sequence.
   * \throw std::out_of_range if index is out of range. */
  const yaml operator()(int index) const;

  /** Append a new value to a sequence.
   * \throw std::exception If this is not a sequence */
  void push_back(double v) { internalPushBack(v); }
  /// \overload
  void push_back(const std::string& v) { internalPushBack(v); }
  /// \overload
  void push_back(uint64_t v) { internalPushBack(v); }
  /// \overload
  void push_back(bool v) { internalPushBack(bool(v)); }
  /// \overload
  void push_back(const yaml& v)
  {
    sequence_t& seq = asSequence();
    seq.emplace_back(v.root_);
  }

 private:
  node_t root_;
  bool isProxy_ = false;
  bool isConstProxy_ = false;

  // Proxy members:
  const std::string proxiedMapEntryName_;
  const node_t* proxiedNode_ = nullptr;

  /** @name Internal proxy
   * @{ */

  /** Returns the pointer to the referenced node data, if a proxy, or to
   * the root node otherwise. Will never return nullptr. */
  const node_t* dereferenceProxy() const;
  node_t* dereferenceProxy();

  explicit yaml(internal::tag_as_proxy_t, node_t& val, const std::string& name) :
      isProxy_(true), isConstProxy_(false), proxiedMapEntryName_(name), proxiedNode_(&val)
  {
  }
  explicit yaml(internal::tag_as_const_proxy_t, const node_t& val, const std::string& name) :
      isProxy_(true), isConstProxy_(true), proxiedMapEntryName_(name), proxiedNode_(&val)
  {
  }
  /** @} */

 public:
  /** @name Getters / setters
   * @{ */

  /** Returns a copy of the existing value of the given type, or tries to
   * convert it between easily-compatible types (e.g. double<->int,
   * string<->int).
   * \exception std::exception If the contained type does not  match and
   * there is no obvious conversion.
   */
  template <typename T>
  T as() const
  {
    return internal::implAsGetter<T>(*this);
  }

  /** Returns a ref to the existing or new value of the given type. If
   * types do not match, the old content will be discarded and a new
   * variable created into this scalar node. \exception std::exception If
   * accessing to a non-scalar node.
   */
  template <typename T>
  T& asRef();

  /** const version of asRef(). Unlike `as<T>()`, this version will NOT
   * try to convert between types if T does not match exactly the stored
   * type, and will raise an exception instead. */
  template <typename T>
  const T& asRef() const;

  yaml& operator=(bool v);

  yaml& operator=(float v);
  yaml& operator=(double v);

  yaml& operator=(int8_t v);
  yaml& operator=(uint8_t v);
  yaml& operator=(int16_t v);
  yaml& operator=(uint16_t v);
  yaml& operator=(int32_t v);
  yaml& operator=(uint32_t v);
  yaml& operator=(int64_t v);
  yaml& operator=(uint64_t v);

  // Additional operator for "size_t", in systems/compilers where
  // size_t != all other types above
  // (e.g. OSX with clang, see
  // https://stackoverflow.com/a/11603907/1631514 )
  template <
      typename = std::enable_if<
          !std::is_same_v<std::size_t, uint64_t> && !std::is_same_v<std::size_t, int64_t> &&
          !std::is_same_v<std::size_t, uint32_t> && !std::is_same_v<std::size_t, int32_t>>  //
      >
  yaml& operator=(std::size_t v)
  {
    return operator=(static_cast<uint64_t>(v));
  }

  yaml& operator=(const std::string& v);
  inline yaml& operator=(const char* v) { return operator=(std::string(v)); }
  inline yaml& operator=(const std::string_view& v) { return operator=(std::string(v)); }
  yaml& operator=(const yaml& v);

  /** vcp (value-comment) wrapper */
  template <typename T>
  yaml& operator=(const ValueCommentPair<T>& vc)
  {
    this->comment(vc.comment, vc.position);
    operator=(vc.value);
    return *this;
  }

  /** vkcp (value-keyComment) wrapper */
  template <typename T>
  yaml& operator<<(const ValueKeyCommentPair<T>& vc)
  {
    // Init as map on first use:
    if (isNullNode()) node().d.emplace<map_t>();
    ASSERTMSG_(
        isMap(),
        "<< operator with ValueKeyCommentPair requires a map "
        "(dictionary) "
        "on the left hand.");
    operator[](vc.keyname) = vc.value;
    // keyComment:
    int posIndex = static_cast<int>(vc.position);
    ASSERT_GE_(posIndex, 0);
    ASSERT_LT_(posIndex, static_cast<int>(CommentPosition::MAX));
    auto& n = keyNode(vc.keyname);
    n.comments[posIndex].emplace(vc.comment);
    return *this;
  }

  inline operator bool() const { return as<bool>(); }

  inline operator double() const { return as<double>(); }
  inline operator float() const { return as<float>(); }

  inline operator int8_t() const { return as<int8_t>(); }
  inline operator uint8_t() const { return as<uint8_t>(); }
  inline operator int16_t() const { return as<int16_t>(); }
  inline operator uint16_t() const { return as<uint16_t>(); }
  inline operator int32_t() const { return as<int32_t>(); }
  inline operator uint32_t() const { return as<uint32_t>(); }
  inline operator int64_t() const { return as<int64_t>(); }
  inline operator uint64_t() const { return as<uint64_t>(); }

  inline operator std::string() const { return as<std::string>(); }
  /** @} */

  /** @name Leaf node comments API
   * @{ */

  /** Returns true if the proxied node has an associated comment block, at
   * any location */
  bool hasComment() const;

  /** Returns true if the proxied node has an associated comment block at
   * a particular position */
  bool hasComment(CommentPosition pos) const;

  /** Gets the comment associated to the proxied node. This version
   * returns the first comment, of all possible (top, right).
   *
   * \exception std::exception If there is no comment attached.
   * \sa hasComment()
   */
  const std::string& comment() const;

  /** Gets the comment associated to the proxied node, at the particular
   * position. See code examples in mrpt::containers::yaml.
   *
   * \exception std::exception If there is no comment attached.
   * \sa hasComment()
   */
  const std::string& comment(CommentPosition pos) const;

  /** Sets the comment attached to a given proxied node.
   * See code examples in mrpt::containers::yaml
   * \sa hasComment()
   */
  void comment(const std::string& c, CommentPosition position = CommentPosition::RIGHT);

  /** @} */

  /** @name Map key node comments API
   * @{ */

  /** Maps only: returns true if the given key node has an associated
   * comment block, at any location. \exception std::exception If called
   * on a non-map or key does not exist.
   */
  bool keyHasComment(const std::string& key) const;

  /** Maps only: Returns true if the given key has an associated comment
   * block at a particular position. \exception std::exception If called
   * on a non-map or key does not exist.
   */
  bool keyHasComment(const std::string& key, CommentPosition pos) const;

  /** Maps only: Gets the comment associated to the given key. This
   * version returns the first comment, of all possible (top, right).
   *
   * \exception std::exception If called on a non-map or key does not
   * exist. \exception std::exception If there is no comment attached. \sa
   * hasComment()
   */
  const std::string& keyComment(const std::string& key) const;

  /** Maps only: Gets the comment associated to the given key, at the
   * particular position. See code examples in mrpt::containers::yaml.
   *
   * \exception std::exception If called on a non-map or key does not
   * exist. \exception std::exception If there is no comment attached. \sa
   * hasComment()
   */
  const std::string& keyComment(const std::string& key, CommentPosition pos) const;

  /** Maps only: Sets the comment attached to a given key.
   * See code examples in mrpt::containers::yaml
   *
   * \exception std::exception If called on a non-map or key does not
   * exist. \sa hasComment()
   */
  void keyComment(
      const std::string& key,
      const std::string& c,
      CommentPosition position = CommentPosition::TOP);

  /** @} */

 private:
  template <typename T>
  friend T internal::implAsGetter(const yaml& p);
  template <typename T>
  friend T internal::implAnyAsGetter(const scalar_t& p);

  struct InternalPrintState
  {
    YamlEmitOptions eo;

    int indent = 0;
    bool needsNL = false;
    bool needsSpace = false;
    bool shortFormat = false;
  };

  // Return: true if the last printed char is a newline char
  static bool internalPrintNodeAsYAML(
      const node_t& p, std::ostream& o, const InternalPrintState& ps);

  static void internalPrintDebugStructure(const node_t& p, std::ostream& o, int indent);

  template <typename T>
  void internalPushBack(const T& v);

  static bool internalPrintAsYAML(
      const std::monostate&, std::ostream& o, const InternalPrintState& ps, const comments_t& cs);
  static bool internalPrintAsYAML(
      const sequence_t& v, std::ostream& o, const InternalPrintState& ps, const comments_t& cs);
  static bool internalPrintAsYAML(
      const map_t& v, std::ostream& o, const InternalPrintState& ps, const comments_t& cs);
  static bool internalPrintAsYAML(
      const scalar_t& v, std::ostream& o, const InternalPrintState& ps, const comments_t& cs);
  static bool internalPrintStringScalar(
      const std::string& s, std::ostream& o, const InternalPrintState& ps, const comments_t& cs);

  /** Impl of operator=() */
  template <typename T>
  yaml& implOpAssign(const T& v)
  {
    ASSERTMSG_(
        isProxy_,
        "Trying to write into a non-leaf node, `p[\"name\"]=value;` "
        "instead");
    ASSERTMSG_(!isConstProxy_, "Trying to write into read-only proxy");
    ASSERT_(proxiedNode_ != nullptr);

    scalar_t& s = const_cast<node_t*>(proxiedNode_)->d.emplace<scalar_t>();
    s.emplace<T>(v);
    return *this;
  }
};

/** Prints a scalar, a part of a yaml tree, or the entire structure,
 * in YAML-like format. This version does NOT emit neither the YAML header
 * nor the final end line.
 *
 * \sa yaml::PrintAsYAML
 */
std::ostream& operator<<(std::ostream& o, const yaml& p);

/** Macro to load a variable from a mrpt::containers::yaml (initials MCP)
 * dictionary, throwing an std::invalid_argument exception  if the value is not
 * found (REQuired).
 *
 * Usage:
 * \code
 * mrpt::containers::yaml p;
 * double K;
 *
 * MCP_LOAD_REQ(p, K);
 * \endcode
 *
 * Since MRPT 2.3.2, this also works for enums, converting to textual names of
 * values. Note that this requires enums to implement mrpt::typemeta::TEnumType.
 */
#define MCP_LOAD_REQ(Yaml__, Var__)                                                          \
  if (!Yaml__.has(#Var__))                                                                   \
    throw std::invalid_argument(                                                             \
        mrpt::format("Required parameter `%s` not an existing key in dictionary.", #Var__)); \
  if constexpr (std::is_enum_v<decltype(Var__)>)                                             \
    Var__ = mrpt::typemeta::TEnumType<std::remove_cv_t<decltype(Var__)>>::name2value(        \
        Yaml__[#Var__].as<std::string>());                                                   \
  else                                                                                       \
    Var__ = Yaml__[#Var__].as<decltype(Var__)>()

/** Macro to load a variable from a mrpt::containers::yaml (initials MCP)
 * dictionary, leaving it with its former value if not found (OPTional).
 *
 * Usage:
 * \code
 * mrpt::containers::yaml p;
 * double K;
 *
 * MCP_LOAD_OPT(p, K);
 * \endcode
 *
 * Since MRPT 2.3.2, this also works for enums, converting to textual names of
 * values. Note that this requires enums to implement mrpt::typemeta::TEnumType.
 */
#define MCP_LOAD_OPT(Yaml__, Var__)                                                     \
  if constexpr (std::is_enum_v<decltype(Var__)>)                                        \
  {                                                                                     \
    if (!Yaml__.empty() && Yaml__.has(#Var__))                                          \
      Var__ = mrpt::typemeta::TEnumType<std::remove_cv_t<decltype(Var__)>>::name2value( \
          Yaml__[#Var__].as<std::string>());                                            \
  }                                                                                     \
  else if (!Yaml__.isNullNode() && !Yaml__.empty() && Yaml__.has(#Var__))               \
  Var__ = Yaml__[#Var__].as<decltype(Var__)>()

/** Just like MCP_LOAD_REQ(), but converts the read number from degrees to
 * radians */
#define MCP_LOAD_REQ_DEG(Yaml__, Var__) \
  MCP_LOAD_REQ(Yaml__, Var__);          \
  Var__ = mrpt::DEG2RAD(Var__)

/** Just like MCP_LOAD_OPT(), but converts the read number from degrees to
 * radians */
#define MCP_LOAD_OPT_DEG(Yaml__, Var__) \
  Var__ = mrpt::RAD2DEG(Var__);         \
  MCP_LOAD_OPT(Yaml__, Var__);          \
  Var__ = mrpt::DEG2RAD(Var__)

namespace internal
{
// We need to implement this as a template for the "if constexpr()" false branch
// not to be evaluated for enums, which would lead to build errors.
template <typename T, typename YAML_T>
void impl_mcp_save(YAML_T& y, const T& var, const char* varName)
{
  using enum_t = std::remove_cv_t<T>;

  if constexpr (std::is_enum_v<enum_t>)
    y[varName] = mrpt::typemeta::TEnumType<enum_t>::value2name(var);
  else
    y[varName] = var;
}
}  // namespace internal

/** Macro to store a variable into a mrpt::containers::yaml (initials MCP)
 * dictionary, using as "key" the name of the variable.
 *
 * Usage:
 * \code
 * mrpt::containers::yaml p;
 * double K = ...;
 *
 * MCP_SAVE(p, K);
 *
 * // If you want "K" to have degree units in the parameter block, radians when
 * // loaded in memory:
 * MCP_SAVE_DEG(p,K);
 * \endcode
 *
 * Since MRPT 2.3.2, this also works for enums, converting to textual names of
 * values. Note that this requires enums to implement mrpt::typemeta::TEnumType.
 */
#define MCP_SAVE(Yaml__, Var__) mrpt::containers::internal::impl_mcp_save(Yaml__, Var__, #Var__);

#define MCP_SAVE_DEG(Yaml__, Var__) Yaml__[#Var__] = mrpt::RAD2DEG(Var__);

}  // namespace mrpt::containers

namespace mrpt::containers
{
template <typename T>
T& yaml::asRef()
{
  ASSERTMSG_(
      isProxy_,
      "Trying to read from a non-scalar. Use `p[\"name\"].asRef<T>();` "
      "instead");
  ASSERTMSG_(!isConstProxy_, "Trying to write into read-only proxy");
  scalar_t& s = this->asScalar();

  try
  {
    std::any_cast<T>(s);
  }
  catch (const std::bad_any_cast&)
  {
    s.emplace<T>();
  }
  return *std::any_cast<T>(&s);
}

template <typename T>
const T& yaml::asRef() const
{
  ASSERTMSG_(
      isProxy_,
      "Trying to read from a non-scalar. Use `p[\"name\"].asRef<T>();` "
      "instead");
  const auto& expectedType = typeid(T);
  const scalar_t& s = this->asScalar();
  const auto& storedType = s.type();

  if (storedType != expectedType)
  {
    std::stringstream ss;
    yaml::internalPrintAsYAML(s, ss, {}, {});

    THROW_EXCEPTION_FMT(
        "Trying to read parameter `%s` (value='%s') of type `%s` as if it "
        "was `%s` and no obvious conversion found .",
        proxiedMapEntryName_.c_str(), ss.str().c_str(), mrpt::demangle(storedType.name()).c_str(),
        mrpt::demangle(expectedType.name()).c_str());
  }

  return *std::any_cast<T>(&s);
}

template <typename T>
void yaml::internalPushBack(const T& v)
{
  ASSERT_(this->isSequence());
  sequence_t& seq = asSequence();
  seq.emplace_back().d.emplace<scalar_t>().emplace<T>(v);
}

template <typename YAML_NODE>
inline yaml yaml::FromYAMLCPP(const YAML_NODE& n)
{
  const auto invalidDbl = std::numeric_limits<double>::max();

  if (n.IsSequence())
  {
    yaml ret = yaml(Sequence());

    for (const auto& e : n)
    {
      if (e.IsNull())
      {
        sequence_t& seq = std::get<sequence_t>(ret.dereferenceProxy()->d);
        seq.push_back(node_t());
      }
      else if (e.IsScalar())
      {
        if (double v = e.template as<double>(invalidDbl); v != invalidDbl)
          ret.push_back(v);
        else
          ret.push_back(e.template as<std::string>());
      }
      else
      {
        // Recursive:
        ret.push_back(yaml::FromYAMLCPP(YAML_NODE(e)));
      }
    }
    return ret;
  }
  else if (n.IsMap())
  {
    yaml ret = yaml(yaml::Map());

    for (const auto& kv : n)
    {
      const auto& key = kv.first.template as<std::string>();
      const auto& val = kv.second;

      if (val.IsNull())
      {
        map_t& m = std::get<map_t>(ret.dereferenceProxy()->d);
        m[key];
      }
      else if (val.IsScalar())
      {
        if (double v = val.template as<double>(invalidDbl); v != invalidDbl)
          ret[key] = v;
        else
          ret[key] = val.template as<std::string>();
      }
      else
      {
        // Recursive:
        ret[key] = yaml::FromYAMLCPP(YAML_NODE(val));
      }
    }
    return ret;
  }
  else
  {
    THROW_EXCEPTION(
        "FromYAMLCPP only supports root YAML as sequence "
        "or map");
  }
}

template <typename YAML_NODE>
inline void yaml::loadFromYAMLCPP(const YAML_NODE& n)
{
  *this = yaml::FromYAMLCPP(n);
}

template <typename MATRIX>
inline yaml yaml::FromMatrix(const MATRIX& m)
{
  yaml r = mrpt::containers::yaml::Map();
  r["rows"] = static_cast<int64_t>(m.rows());
  r["cols"] = static_cast<int64_t>(m.cols());
  r["data"] = mrpt::containers::yaml::Sequence();
  auto data = r["data"];
  data.node().printInShortFormat = true;
  for (int iRow = 0; iRow < m.rows(); iRow++)
    for (int iCol = 0; iCol < m.cols(); iCol++) data.push_back(m(iRow, iCol));
  return r;
}

template <typename MATRIX>
inline void yaml::toMatrix(MATRIX& m) const
{
  ASSERT_(isMap());
  ASSERT_(has("rows") && has("cols") && has("data"));
  const int nRows = (*this)["rows"].as<int>();
  const int nCols = (*this)["cols"].as<int>();
  ASSERT_((nRows > 0 && nCols > 0) || (nRows == 0 && nCols == 0));

  const auto& data = (*this)["data"];
  ASSERT_(data.isSequence());
  ASSERT_EQUAL_(static_cast<int>(data.size()), nRows * nCols);

  using entry_t = std::decay_t<decltype(m(0, 0))>;

  if (m.cols() <= 0 || m.rows() <= 0)
  {
    try
    {
      m.resize(nRows, nCols);
    }
    catch (const std::exception&)
    {
    }
  }
  ASSERT_EQUAL_(m.cols(), nCols);
  ASSERT_EQUAL_(m.rows(), nRows);

  for (int r = 0, idx = 0; r < nRows; r++)
    for (int c = 0; c < nCols; c++, idx++) m(r, c) = data.operator()(idx).as<entry_t>();
}

template <typename Scalar>
inline std::vector<Scalar> yaml::toStdVector() const
{
  ASSERT_(isSequence());
  const auto& seq = asSequence();

  std::vector<Scalar> ret;
  ret.reserve(seq.size());

  for (const auto& n : seq) ret.push_back(n.as<Scalar>());
  return ret;
}

/** Sort operator required for std::map with node_t as key */
inline bool operator<(const yaml::node_t& lhs, const yaml::node_t& rhs)
{
  const auto lStr = lhs.internalAsStr();
  const auto rStr = rhs.internalAsStr();
  return lStr < rStr;
}

}  // namespace mrpt::containers

namespace mrpt::containers::internal
{
template <typename T>
T implAnyAsGetter(const mrpt::containers::yaml::scalar_t& s)
{
  const auto& expectedType = typeid(T);
  const auto& storedType = s.type();

  // 1) Exact match?
  if (storedType == expectedType) return std::any_cast<const T&>(s);

  // 2) bool:
  if constexpr (std::is_same_v<T, bool>)
  {
    if (storedType == typeid(std::string))
    {
      const auto str = implAnyAsGetter<std::string>(s);
      std::optional<int> intVal;

      char* retStr = nullptr;
      const long long ret = std::strtoll(str.c_str(), &retStr, 0 /*auto base*/);
      if (retStr != 0 && retStr != str.c_str()) intVal = ret;

      return str == "y" || str == "Y" || str == "yes" || str == "Yes" || str == "YES" ||
             str == "true" || str == "True" || str == "TRUE" || str == "on" || str == "ON" ||
             str == "On" || (intVal.has_value() && intVal.value() != 0);
    }
  }

  // 3) Recognize double/float:
  if constexpr (std::is_same_v<T, double> || std::is_same_v<T, float>)
  {
    if (storedType == typeid(double))
      return static_cast<T>(implAnyAsGetter<double>(s));
    else if (storedType == typeid(float))
      return static_cast<T>(implAnyAsGetter<float>(s));

    std::stringstream ss;
    yaml::internalPrintAsYAML(s, ss, {}, {});
    T ret;
    ss >> ret;
    if (!ss.fail()) return ret;
  }

  // 4) Integers. Recognize hex or octal prefixes with strtol()
  if constexpr (std::is_convertible_v<int, T>)
  {
    std::stringstream ss;
    yaml::internalPrintAsYAML(s, ss, {}, {});
    const std::string str = ss.str();

    char* retStr = nullptr;
    const long long ret = std::strtoll(str.c_str(), &retStr, 0 /*auto base*/);
    if (retStr != 0 && retStr != str.c_str())
    {
      const auto minVal = static_cast<long long>(std::numeric_limits<T>::min());
      auto maxVal = static_cast<long long>(std::numeric_limits<T>::max());
      // Handle the case of unsigned long long:
      if (maxVal < 0) maxVal = std::numeric_limits<long long>::max();

      if ((ret == 0 && errno == ERANGE) || ret < minVal || ret > maxVal)
      {
        std::stringstream sError;
        sError << "yaml: Out of range integer: '" << str << "' (Valid range [" << minVal << ","
               << maxVal << "], parsed=" << ret;
        if (errno == ERANGE) sError << " errno=ERANGE";
        sError << "')";
        THROW_EXCEPTION(sError.str());
      }
      return static_cast<T>(ret);
    }
  }

  // 5) Strings:
  if constexpr (std::is_convertible_v<std::string, T>)
  {
    if (expectedType == typeid(std::string))
    {
      std::stringstream ss;
      yaml::internalPrintAsYAML(s, ss, {}, {});
      return ss.str();
    }
  }

  // No known way to convert it:
  std::stringstream ss;
  yaml::internalPrintAsYAML(s, ss, {}, {});
  THROW_EXCEPTION_FMT(
      "Trying to access scalar (value='%s') of type `%s` as if it was `%s` "
      "and no obvious conversion found .",
      ss.str().c_str(), mrpt::demangle(storedType.name()).c_str(),
      mrpt::demangle(expectedType.name()).c_str());
}

template <typename T>
T implAsGetter(const yaml& p)
{
  MRPT_START
  ASSERTMSG_(
      p.isScalar(),
      mrpt::format(
          "Trying to read from a non-scalar. Actual node type: `%s`", p.node().typeName().c_str()));
  const yaml::scalar_t& s = p.asScalar();
  return implAnyAsGetter<T>(s);
  MRPT_END
}

}  // namespace mrpt::containers::internal
