/* This file is part of the KDE project
   SPDX-FileCopyrightText: 2004 David Faure <faure@kde.org>
   SPDX-FileCopyrightText: 2007 Thomas Zander <zander@kde.org>
   SPDX-FileCopyrightText: 2011 Lukáš Tvrdý <lukas.tvrdy@ixonos.com>

   SPDX-License-Identifier: LGPL-2.0-or-later
*/

#ifndef XMLWRITER_H
#define XMLWRITER_H

#include <QMap>
#include <QIODevice>
#include "kostore_export.h"

#include <QDebug>

/**
 * A class for writing out XML (to any QIODevice), with a special attention on performance.
 * The XML is being written out along the way, which avoids requiring the entire
 * document in memory (like QDom does), and avoids using QTextStream at all
 * (which in Qt3 has major performance issues when converting to utf8).
 */
class KOSTORE_EXPORT KoXmlWriter
{
public:
    /**
     * Create a KoXmlWriter instance to write out an XML document into
     * the given QIODevice.
     */
    explicit KoXmlWriter(QIODevice* dev, int indentLevel = 0);

    /// Destructor
    ~KoXmlWriter();

    QIODevice *device() const;

    /**
     * Start the XML document.
     * This writes out the \<?xml?\> tag with utf8 encoding, and the DOCTYPE.
     * @param rootElemName the name of the root element, used in the DOCTYPE tag.
     * @param publicId the public identifier, e.g. "-//OpenOffice.org//DTD OfficeDocument 1.0//EN"
     * @param systemId the system identifier, e.g. "office.dtd" or a full URL to it.
     */
    void startDocument(const char* rootElemName, const char* publicId = nullptr, const char* systemId = nullptr);

    /// Call this to terminate an XML document.
    void endDocument();

    /**
     * Start a new element, as a child of the current element.
     * @param tagName the name of the tag. Warning: this string must
     * remain alive until endElement, no copy is internally made.
     * Usually tagName is a string constant so this is no problem anyway.
     * @param indentInside if set to false, there will be no indentation inside
     * this tag. This is useful for elements where whitespace matters.
     */
    void startElement(const char* tagName, bool indentInside = true);

    /**
     * Overloaded version of addAttribute(const char*, const char*),
     * which is a bit slower because it needs to convert @p value to utf8 first.
     */
    inline void addAttribute(const char* attrName, const QString& value) {
        addAttribute(attrName, value.toUtf8());
    }
    /**
     * Add an attribute whose value is an integer
     */
    inline void addAttribute(const char* attrName, int value) {
        addAttribute(attrName, QByteArray::number(value));
    }
    /**
     * Add an attribute whose value is an unsigned integer
     */
    inline void addAttribute(const char* attrName, uint value) {
        addAttribute(attrName, QByteArray::number(value));
    }
    /**
     * Add an attribute whose value is an bool
     * It is written as "true" or "false" based on value
     */
    inline void addAttribute(const char* attrName, bool value) {
        addAttribute(attrName, value ? "true" : "false");
    }
    /**
     * Add an attribute whose value is a floating point number
     * The number is written out with the highest possible precision
     * (unlike QString::number and setNum, which default to 6 digits)
     */
    void addAttribute(const char* attrName, double value);
    /**
     * Add an attribute whose value is a floating point number
     * The number is written out with the highest possible precision
     * (unlike QString::number and setNum, which default to 6 digits)
     */
    void addAttribute(const char* attrName, float value);
    /**
     * Add an attribute which represents a distance, measured in pt
     * The number is written out with the highest possible precision
     * (unlike QString::number and setNum, which default to 6 digits),
     * and the unit name ("pt") is appended to it.
     */
    void addAttributePt(const char* attrName, double value);
    /**
     * Add an attribute which represents a distance, measured in pt
     * The number is written out with the highest possible precision
     * (unlike QString::number and setNum, which default to 6 digits),
     * and the unit name ("pt") is appended to it.
     */
    void addAttributePt(const char* attrName, float value);

    /// Overloaded version of the one taking a const char* argument, for convenience
    void addAttribute(const char* attrName, const QByteArray& value);

    /**
     * Add an attribute to the current element.
     */
    void addAttribute(const char* attrName, const char* value);
    /**
     * Terminate the current element. After this you should start a new one (sibling),
     * add a sibling text node, or close another one (end of siblings).
     */
    void endElement();
    /**
     * Overloaded version of addTextNode(const char*),
     * which is a bit slower because it needs to convert @p str to utf8 first.
     */
    inline void addTextNode(const QString& str) {
        addTextNode(str.toUtf8());
    }
    /// Overloaded version of the one taking a const char* argument
    void addTextNode(const QByteArray& cstr);
    /**
     * @brief Adds a text node as a child of the current element.
     *
     * This is appends the literal content of @p str to the contents of the element.
     * E.g. addTextNode("foo") inside a \<p\> element gives \<p\>foo\</p\>,
     * and startElement("b"); endElement("b"); addTextNode("foo") gives \<p\>\<b/\>foo\</p\>
     */
    void addTextNode(const char* cstr);

    /**
     * @brief Adds a processing instruction
     *
     * This writes a processing instruction, like <?foo bar blah?>, where foo
     * is the target, and the rest is the data.
     *
     * Processing instructions are used in XML to keep processor-specific
     * information in the text of the document.
     */
    void addProcessingInstruction(const char* cstr);

    /**
     * This is quite a special-purpose method, not for everyday use.
     * It adds a complete element (with its attributes and child elements)
     * as a child of the current element. The string is supposed to be escaped
     * for XML already, so it will usually come from another KoXmlWriter.
     */
    void addCompleteElement(const char* cstr);

    /**
     * This is quite a special-purpose method, not for everyday use.
     * It adds a complete element (with its attributes and child elements)
     * as a child of the current element. The iodevice is supposed to be escaped
     * for XML already, so it will usually come from another KoXmlWriter.
     * This is usually used with KTempFile.
     */
    void addCompleteElement(QIODevice* dev);

    // #### Maybe we want to subclass KoXmlWriter for manifest files.
    /**
     * Special helper for writing "manifest" files
     * This is equivalent to startElement/2*addAttribute/endElement
     * This API will probably have to change (or not be used anymore)
     * when we add support for encrypting/signing.
     * @note OASIS-specific
     */
    void addManifestEntry(const QString& fullPath, const QString& mediaType);
    void addManifestEntry(const QString& fullPath, const QByteArray &mediaType);
    void addManifestEntry(const char *fullPath, const char *mediaType);

    /**
     * Special helper for writing config item into settings.xml
     * @note OASIS-specific
     */
    void addConfigItem(const QString & configName, const QString& value);
    /// @note OASIS-specific
    void addConfigItem(const QString & configName, bool value);
    /// @note OASIS-specific
    void addConfigItem(const QString & configName, int value);
    /// @note OASIS-specific
    void addConfigItem(const QString & configName, double value);
    /// @note OASIS-specific
    void addConfigItem(const QString & configName, float value);
    /// @note OASIS-specific
    void addConfigItem(const QString & configName, long value);
    /// @note OASIS-specific
    void addConfigItem(const QString & configName, short value);

    // TODO addConfigItem for datetime and base64Binary

    /**
     * @brief Adds a text span as nodes of the current element.
     *
     * Unlike KoXmlWriter::addTextNode it handles tabulations, linebreaks,
     * and multiple spaces by using the appropriate OASIS tags.
     *
     * @param text the text to write
     *
     * @note OASIS-specific
     */
    void addTextSpan(const QString& text);
    /**
     * Overloaded version of addTextSpan which takes an additional tabCache map.
     * @param text the text to write
     * @param tabCache optional map allowing to find a tab for a given character index
     * @note OASIS-specific
     */
    void addTextSpan(const QString& text, const QMap<int, int>& tabCache);

    /**
     * @return the current indentation level.
     * Useful when creating a sub-KoXmlWriter (see addCompleteElement)
     */
    int indentLevel() const;

    /**
     * Return all the open tags at this time, root element first.
     */
    QList<const char*> tagHierarchy() const;

    /**
     * Return the so far written XML as string for debugging purposes.
     */
    QString toString() const;

private:
    struct Tag {
        Tag(const char* t = nullptr, bool ind = true)
                : tagName(t), hasChildren(false), lastChildIsText(false),
                openingTagClosed(false), indentInside(ind) {}
        Tag(const Tag &original)
        {
            tagName = original.tagName;
            hasChildren = original.hasChildren;
            lastChildIsText = original.lastChildIsText;
            openingTagClosed = original.openingTagClosed;
            indentInside = original.indentInside;
        }
        const char* tagName;
        bool hasChildren : 1; ///< element or text children
        bool lastChildIsText : 1; ///< last child is a text node
        bool openingTagClosed : 1; ///< true once the '\>' in \<tag a="b"\> is written out
        bool indentInside : 1; ///< whether to indent the contents of this tag
    };

    /// Write out \n followed by the number of spaces required.
    void writeIndent();

    // writeCString is much faster than writeString.
    // Try to use it as much as possible, especially with constants.
    void writeString(const QString& str);

    // TODO check return value!!!
    inline void writeCString(const char* cstr) {
        device()->write(cstr, qstrlen(cstr));
    }
    inline void writeChar(char c) {
        device()->putChar(c);
    }
    inline void closeStartElement(Tag& tag) {
        if (!tag.openingTagClosed) {
            tag.openingTagClosed = true;
            writeChar('>');
        }
    }
    char* escapeForXML(const char* source, int length) const;
    bool prepareForChild();
    void prepareForTextNode();
    void init();

    class Private;
    Private * const d;

    KoXmlWriter(const KoXmlWriter &);   // forbidden
    KoXmlWriter& operator=(const KoXmlWriter &);   // forbidden
};

#endif /* XMLWRITER_H */

