/* Copyright (C) 2002 The gtkmm Development Team
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */

_DEFS(glibmm,glib)

#include <glibmm/error.h>
#include <sigc++/sigc++.h>
#include <array>
#include <map>

#ifndef DOXYGEN_SHOULD_SKIP_THIS
extern "C" { typedef struct _GMarkupParseContext GMarkupParseContext; }
#endif

namespace Glib
{

/** @defgroup Markup Simple XML Subset Parser
 *
 * The Glib::Markup parser is intended to parse a simple markup format that's a
 * subset of XML. This is a small, efficient, easy-to-use parser. It should not
 * be used if you expect to interoperate with other applications generating
 * full-scale XML. However, it's very useful for application data files, config
 * files, etc. where you know your application will be the only one writing the
 * file. Full-scale XML parsers should be able to parse the subset used by
 * Glib::Markup parser, so you can easily migrate to full-scale XML at a later
 * time if the need arises.
 *
 * Glib::Markup is not guaranteed to signal an error on all invalid XML;
 * the parser may accept documents that an XML parser would not. However,
 * invalid XML documents are not considered valid Glib::Markup documents.
 *
 * @par Simplifications to XML include:
 *
 * - Only UTF-8 encoding is allowed.
 * - No user-defined entities.
 * - Processing instructions, comments and the doctype declaration are "passed
 *   through" but are not interpreted in any way.
 * - No DTD or validation.
 *
 * @par The markup format does support:
 *
 * - Elements
 * - Attributes
 * - 5 standard entities: <tt>\&amp; \&lt; \&gt; \&quot; \&apos;</tt>
 * - Character references
 * - Sections marked as <tt>CDATA</tt>
 *
 * @{
 */

/** %Exception class for markup parsing errors.
 */
_WRAP_GERROR(MarkupError, GMarkupError, G_MARKUP_ERROR, NO_GTYPE, decl_prefix GLIBMM_API)

/** @} group Markup */

namespace Markup
{

class ParseContext;

/** @ingroup Markup */
using Error = Glib::MarkupError;


/** Escapes text so that the markup parser will parse it verbatim.
 * Less than, greater than, ampersand, etc. are replaced with the corresponding
 * entities.  This function would typically be used when writing out a file to
 * be parsed with the markup parser.
 * @ingroup Markup
 * @param text Some valid UTF-8 text.
 * @return Escaped text.
 */
GLIBMM_API
Glib::ustring escape_text(const Glib::ustring& text);

_WRAP_ENUM(ParseFlags, GMarkupParseFlags, NO_GTYPE, decl_prefix GLIBMM_API)

/** Binary predicate used by Markup::Parser::AttributeMap.
 * @ingroup Markup
 * Unlike <tt>operator<(const ustring& lhs, const ustring& rhs)</tt>
 * which would be used by the default <tt>std::less<></tt> predicate,
 * the AttributeKeyLess predicate is locale-independent.  This is both
 * more correct and much more efficient.
 */
class GLIBMM_API AttributeKeyLess
{
public:
  using first_argument_type = Glib::ustring;
  using second_argument_type = Glib::ustring;
  typedef bool          result_type;

  bool operator()(const Glib::ustring& lhs, const Glib::ustring& rhs) const;
};


#ifndef DOXYGEN_SHOULD_SKIP_THIS
class ParserCallbacks;
#endif

/** The abstract markup parser base class.
 * @ingroup Markup
 * To implement a parser for your markup format, derive from
 * Glib::Markup::Parser and implement the virtual methods.
 *
 * You don't have to override all of the virtual methods.  If a particular
 * method is not implement the data passed to it will be ignored.  Except for
 * the error method, any of these callbacks can throw an error exception; in
 * particular the MarkupError::UNKNOWN_ELEMENT,
 * MarkupError::UNKNOWN_ATTRIBUTE, and MarkupError::INVALID_CONTENT errors
 * are intended to be thrown from these overridden methods. If you throw an
 * error from a method, Glib::Markup::ParseContext::parse() will report that
 * error back to its caller.
 */
class GLIBMM_API Parser : public sigc::trackable
{
public:
  typedef std::map<Glib::ustring, Glib::ustring, Glib::Markup::AttributeKeyLess> AttributeMap;

  virtual ~Parser() = 0;

protected:
  /** Constructs a Parser object.
   * Note that Markup::Parser is an abstract class which can't be instantiated
   * directly.  To implement the parser for your markup format, derive from
   * Markup::Parser and implement the virtual methods.
   */
  Parser();

  Parser(const Parser&) = delete;
  Parser& operator=(const Parser&) = delete;

  Parser(Parser&& other) noexcept;
  Parser& operator=(Parser&& other) noexcept;

  /** Called for open tags <tt>\<foo bar="baz"\></tt>.
   * This virtual method is invoked when the opening tag of an element is seen.
   * @param context The Markup::ParseContext object the parsed data belongs to.
   * @param element_name The name of the element.
   * @param attributes A map of attribute name/value pairs.
   * @throw Glib::MarkupError An exception <em>you</em> should throw if
   * something went wrong, for instance if an unknown attribute name was
   * encountered.  In particular the MarkupError::UNKNOWN_ELEMENT,
   * MarkupError::UNKNOWN_ATTRIBUTE, and MarkupError::INVALID_CONTENT
   * errors are intended to be thrown from user-implemented methods.
   */
  virtual void on_start_element(ParseContext&        context,
                                const Glib::ustring& element_name,
                                const AttributeMap&  attributes);

  /** Called for close tags <tt>\</foo\></tt>.
   * This virtual method is invoked when the closing tag of an element is seen.
   * @param context The Markup::ParseContext object the parsed data belongs to.
   * @param element_name The name of the element.
   * @throw Glib::MarkupError An exception <em>you</em> should throw if
   * something went wrong, for instance if an unknown attribute name was
   * encountered.  In particular the MarkupError::UNKNOWN_ELEMENT,
   * MarkupError::UNKNOWN_ATTRIBUTE, and MarkupError::INVALID_CONTENT
   * errors are intended to be thrown from user-implemented methods.
   */
  virtual void on_end_element(ParseContext& context, const Glib::ustring& element_name);

  /** Called for character data.
   * This virtual method is invoked when some text is seen (text is always
   * inside an element).
   * @param context The Markup::ParseContext object the parsed data belongs to.
   * @param text The parsed text in UTF-8 encoding.
   * @throw Glib::MarkupError An exception <em>you</em> should throw if
   * something went wrong, for instance if an unknown attribute name was
   * encountered.  In particular the MarkupError::UNKNOWN_ELEMENT,
   * MarkupError::UNKNOWN_ATTRIBUTE, and MarkupError::INVALID_CONTENT
   * errors are intended to be thrown from user-implemented methods.
   */
  virtual void on_text(ParseContext& context, const Glib::ustring& text);

  /** Called for strings that should be re-saved verbatim in this same
   * position, but are not otherwise interpretable.
   * This virtual method is invoked for comments, processing instructions and
   * doctype declarations; if you're re-writing the parsed document, write the
   * passthrough text back out in the same position.
   * @param context The Markup::ParseContext object the parsed data belongs to.
   * @param passthrough_text The text that should be passed through.
   * @throw Glib::MarkupError An exception <em>you</em> should throw if
   * something went wrong, for instance if an unknown attribute name was
   * encountered.  In particular the MarkupError::UNKNOWN_ELEMENT,
   * MarkupError::UNKNOWN_ATTRIBUTE, and MarkupError::INVALID_CONTENT
   * errors are intended to be thrown from user-implemented methods.
   */
  virtual void on_passthrough(ParseContext& context, const Glib::ustring& passthrough_text);

  /** Called on error, including one thrown by an overridden virtual method.
   * @param context The Markup::ParseContext object the parsed data belongs to.
   * @param error A MarkupError object with detailed information about the error.
   */
  virtual void on_error(ParseContext& context, const MarkupError& error);

private:

#ifndef DOXYGEN_SHOULD_SKIP_THIS
  friend class Glib::Markup::ParserCallbacks;
#endif
};


/** A parse context is used to parse marked-up documents.
 * @ingroup Markup
 * You can feed any number of documents into a context, as long as no errors
 * occur; once an error occurs, the parse context can't continue to parse text
 * (you have to destroy it and create a new parse context).
 */
class GLIBMM_API ParseContext : public sigc::trackable
{
public:
  /** Creates a new parse context.
   * @param parser A Markup::Parser instance.
   * @param flags Bitwise combination of Markup::ParseFlags.
   */
  explicit ParseContext(Parser& parser, ParseFlags flags = ParseFlags(0));

  ParseContext(const ParseContext&) = delete;
  ParseContext& operator=(const ParseContext&) = delete;

  ParseContext(ParseContext&& other) noexcept;
  ParseContext& operator=(ParseContext&& other) noexcept;

  virtual ~ParseContext();

  /** Feed some data to the ParseContext.
   * The data need not be valid UTF-8; an error will be signalled if it's
   * invalid. The data need not be an entire document; you can feed a document
   * into the parser incrementally, via multiple calls to this function.
   * Typically, as you receive data from a network connection or file, you feed
   * each received chunk of data into this function, aborting the process if an
   * error occurs. Once an error is reported, no further data may be fed to the
   * ParseContext; all errors are fatal.
   * @param text Chunk of text to parse.
   * @throw Glib::MarkupError
   */
  void parse(const Glib::ustring& text);

  /** Feed some data to the ParseContext.
   * The data need not be valid UTF-8; an error will be signalled if it's
   * invalid. The data need not be an entire document; you can feed a document
   * into the parser incrementally, via multiple calls to this function.
   * Typically, as you receive data from a network connection or file, you feed
   * each received chunk of data into this function, aborting the process if an
   * error occurs. Once an error is reported, no further data may be fed to the
   * ParseContext; all errors are fatal.
   * @param text_begin Begin of chunk of text to parse.
   * @param text_end End of chunk of text to parse.
   * @throw Glib::MarkupError
   */
  void parse(const char* text_begin, const char* text_end);

  /** Signals to the ParseContext that all data has been fed into the parse
   * context with parse(). This method reports an error if the document isn't
   * complete, for example if elements are still open.
   * @throw Glib::MarkupError
   */
  void end_parse();

  /** Retrieves the name of the currently open element.
   * @return The name of the currently open element, or <tt>""</tt>.
   */
  Glib::ustring get_element() const;

  /** Retrieves the current line number.
   * Intended for use in error messages; there are no strict semantics for what
   * constitutes the "current" line number other than "the best number we could
   * come up with for error messages."
   */
  int get_line_number() const;

  /** Retrieves the number of the current character on the current line.
   * Intended for use in error messages; there are no strict semantics for what
   * constitutes the "current" character number other than "the best number we
   * could come up with for error messages."
   */
  int get_char_number() const;

  /** Retrieves the current offset from the beginning of the document, in bytes.
   *
   * The information is meant to accompany the values returned by
   * get_line_number() and get_char_number(), and comes with the
   * same accuracy guarantees.
   *
   * @newin{2,88}
   */
  gsize get_offset() const;

  /** Retrieves the start position of the current start or end tag.
   *
   * This function can be used in the `start_element` or `end_element`
   * callbacks to obtain location information for error reporting.
   *
   * Note that @a line_number and @a char_number are intended for human
   * readable error messages and are therefore 1-based and in %Unicode
   * characters. @a offset on the other hand is meant for programmatic
   * use, and thus is 0-based and in bytes.
   *
   * The information is meant to accompany the values returned by
   * get_line_number() and get_char_number(), and comes with the
   * same accuracy guarantees.
   *
   * @param[out] line_number The line number.
   * @param[out] char_number The character number.
   * @param[out] offset Offset from the beginning of the document.
   *
   * @newin{2,88}
   */
  void get_tag_start(gsize& line_number, gsize& char_number, gsize& offset) const;

  /** Retrieves the start and end positions of an attribute assignment
   * in a start tag.
   *
   * This function can be used in the `start_element` callback to
   * obtain location information for error reporting.
   *
   * Calling it outside of the `start_element` callback
   * has undefined results.
   *
   * Note that @a line_number and @a char_number are intended for human
   * readable error messages and are therefore 1-based and in %Unicode
   * characters. @a offset on the other hand is meant for programmatic
   * use, and thus is 0-based and in bytes.
   *
   * The information is meant to accompany the values returned by
   * get_line_number() and get_char_number(), and comes with the
   * same accuracy guarantees.
   *
   * @param attr The index of the attribute to query.
   * @return A 6-element array.
   *         - Element 0: start_line: The line number of the attribute assignment start.
   *         - Element 1: start_char: The character number of the attribute assignment start.
   *         - Element 2: start_offset: Offset of the attribute assignment start.
   *         - Element 3: end_line: The line number of the attribute assignment end.
   *         - Element 4: end_char: The character number of the attribute assignment end.
   *         - Element 5: end_offset: Offset of the attribute assignment end.
   * @newin{2,90}
   */
  std::array<size_t,6> get_attribute_position(unsigned int attr) const;

  Parser*       get_parser()       { return parser_; }
  const Parser* get_parser() const { return parser_; }

#ifndef DOXYGEN_SHOULD_SKIP_THIS
  GMarkupParseContext*       gobj()       { return gobject_; }
  const GMarkupParseContext* gobj() const { return gobject_; }
#endif

private:
  Markup::Parser*       parser_;
  GMarkupParseContext*  gobject_;

  static void destroy_notify_callback(void* data);
};

} // namespace Markup

} // namespace Glib
