#pragma once

#include <library/cpp/string_utils/ztstrbuf/ztstrbuf.h>

#include <util/generic/string.h>
#include <util/generic/vector.h>
#include <util/stream/output.h>
#include <util/stream/str.h>
#include <algorithm>
#include "libxml-guards.h"

namespace NXml {
    class TNode;

    class TConstNodes;
    class TConstNode;

    using TXPathContext = xmlXPathContext;

    class TDocument {
    public:
        enum Source {
            File,
            String,
            RootName,
        };

    public:
        /**
        * create TDocument
        * @param source: filename, XML string, or name for the root element (depends on @src)
        * @param src: source type: File | String | RootName
        * throws if file not found or cannot be parsed
        */
        TDocument(const TString& source, Source type = File);

    public:
        TDocument(const TDocument& that) = delete;
        TDocument& operator=(const TDocument& that) = delete;

        TDocument(TDocument&& that);
        TDocument& operator=(TDocument&& that);

        /**
        * get root element
        */
        TNode Root();
        TConstNode Root() const;

        void Save(IOutputStream& stream, TZtStringBuf enc = "", bool shouldFormat = true) const {
            int bufferSize = 0;
            xmlChar* xmlBuff = nullptr;
            const char* encoding = enc.size() ? enc.data() : Doc->encoding ? nullptr : "UTF-8";
            xmlDocDumpFormatMemoryEnc(Doc.Get(), &xmlBuff, &bufferSize, encoding, shouldFormat);
            TCharPtr xmlCharBuffPtr(xmlBuff);
            stream.Write(xmlBuff, bufferSize);
        }

        TString ToString(TZtStringBuf enc = "", bool shouldFormat = true) const {
            TStringStream s;
            Save(s, enc, shouldFormat);
            return s.Str();
        }

        void Swap(TDocument& that) {
            std::swap(this->Doc, that.Doc);
        }

        xmlDocPtr GetImpl() {
            return Doc.Get();
        }

    private:
        void ParseFile(const TString& file);
        void ParseString(TZtStringBuf xml);

        TDocument(TDocHolder doc)
            : Doc(std::move(doc))
        {
        }

        TDocHolder Doc;
    };

    struct TNamespaceForXPath {
        TString Prefix;
        TString Url;
    };
    typedef TVector<TNamespaceForXPath> TNamespacesForXPath;

    class TConstNodes {
    private:
        struct TConstNodesRef {
            explicit TConstNodesRef(TConstNodes& n)
                : r_(n)
            {
            }
            TConstNodes& r_;
        };

    public:
        TConstNodes(const TConstNodes& nodes);
        TConstNodes& operator=(const TConstNodes& nodes);

        TConstNodes(TConstNodesRef ref);
        TConstNodes& operator=(TConstNodesRef ref);

        operator TConstNodesRef();

        /**
        * get node by id
        * @param number: node id
        */
        TConstNode operator[](size_t number) const;

        /**
        * get number of nodes
        */
        size_t Size() const {
            return SizeValue;
        }
        size_t size() const {
            return SizeValue;
        }

        struct TNodeIter {
            const TConstNodes& Nodes;
            size_t Index;
            TConstNode operator*() const;
            bool operator==(const TNodeIter& other) const {
                return Index == other.Index;
            }
            bool operator!=(const TNodeIter& other) const {
                return !(*this == other);
            }
            TNodeIter operator++() {
                Index++;
                return *this;
            }
        };
        TNodeIter begin() const {
            return TNodeIter{*this, 0};
        }
        TNodeIter end() const {
            return TNodeIter{*this, size()};
        }

    private:
        friend class TDocument;
        friend class TConstNode;
        friend class TNode;

        TConstNodes(xmlDoc* doc, TXPathObjectPtr obj);

        size_t SizeValue;
        xmlDoc* Doc;
        TXPathObjectPtr Obj;
    };

    class TNode {
    public:
        friend class TDocument;
        friend class TConstNode;
        friend class TTextReader;

        /**
        * check if node is null
        */
        bool IsNull() const;

        /**
        * check if node is element node
        */
        bool IsElementNode() const;

        /**
        * Create xpath context to be used later for fast xpath evaluation.
        * @param nss: explicitly specify XML namespaces to use and their prefixes
        *
        * For better performance, when you need to evaluate several xpath expressions,
        * it makes sense to create a context, load namespace prefixes once
        * and use the context several times in Node(), Nodes(), XPath() function calls for several nodes.
        * The context may be used with any node of the current document, but
        * cannot be shared between different XML documents.
        */
        TXPathContextPtr CreateXPathContext(const TNamespacesForXPath& nss = TNamespacesForXPath()) const;

        /**
        * get all element nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ns: explicitly specify XML namespaces to use and their prefixes
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        TConstNodes Nodes(TZtStringBuf xpath, bool quiet = false, const TNamespacesForXPath& ns = TNamespacesForXPath()) const;

        /**
        * get all element nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ctxt: reusable xpath context
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        TConstNodes Nodes(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const;

        /**
        * get all nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ns: explicitly specify XML namespaces to use and their prefixes
        */
        TConstNodes XPath(TZtStringBuf xpath, bool quiet = false, const TNamespacesForXPath& ns = TNamespacesForXPath()) const;

        /**
        * get all nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ctxt: reusable xpath context
        */
        TConstNodes XPath(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const;

        /**
        * get the first element node matching given xpath expression
        * @param xpath: path to node (from current node)
        * @param quiet: don't throw exception if node not found,
        *               return null node (@see IsNull())
        * @param ns: explicitly specify XML namespaces to use and their prefixes
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        /// @todo: quiet should be default, empty nodeset is not an error
        TNode Node(TZtStringBuf xpath, bool quiet = false, const TNamespacesForXPath& ns = TNamespacesForXPath());
        TConstNode Node(TZtStringBuf xpath, bool quiet = false, const TNamespacesForXPath& ns = TNamespacesForXPath()) const;

        /**
        * get the first element node matching given xpath expression
        * @param xpath: path to node (from current node)
        * @param quiet: don't throw exception if node not found,
        *               return null node (@see IsNull())
        * @param ctxt: reusable xpath context
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        TNode Node(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt);
        TConstNode Node(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const;

        /**
        * get node first child
        * @param name: child name
        * @note if name is empty, returns the first child node of type "element"
        * @note returns null node if no child found
        */
        TNode FirstChild(TZtStringBuf name);
        TConstNode FirstChild(TZtStringBuf name) const;

        TNode FirstChild();
        TConstNode FirstChild() const;

        /**
        * get parent node
        * throws exception if has no parent
        */
        TNode Parent();
        TConstNode Parent() const;

        /**
        * get node neighbour
        * @param name: neighbour name
        * @note if name is empty, returns the next sibling node of type "element"
        * @node returns null node if no neighbour found
        */
        TNode NextSibling(TZtStringBuf name);
        TConstNode NextSibling(TZtStringBuf name) const;

        TNode NextSibling();
        TConstNode NextSibling() const;

        /**
        * create child node
        * @param name: child name
        * returns new empty node
        */
        TNode AddChild(TZtStringBuf name);

        /**
        * create child node with given value
        * @param name: child name
        * @param value: node value
        */
        template <class T>
        typename std::enable_if<!std::is_convertible_v<T, TZtStringBuf>, TNode>::type
        AddChild(TZtStringBuf name, const T& value);

        TNode AddChild(TZtStringBuf name, TZtStringBuf value);

        /**
        * add child node, making recursive copy of original
        * @param node: node to copy from
        * returns added node
        */
        TNode AddChild(const TConstNode& node);

        /**
        * create text child node
        * @param name: child name
        * @param value: node value
        */
        template <class T>
        typename std::enable_if<!std::is_convertible_v<T, TStringBuf>, TNode>::type
        AddText(const T& value);

        TNode AddText(TStringBuf value);

        /**
        * get node attribute
        * @param name: attribute name
        * throws exception if attribute not found
        */
        template <class T>
        T Attr(TZtStringBuf name) const;

        /**
        * get node attribute
        * @param name: attribute name
        * returns default value if attribute not found
        */
        template <class T>
        T Attr(TZtStringBuf name, const T& defvalue) const;

        /**
        * get node attribute
        * @param name: attribute name
        * @param value: return-value
        * throws exception if attribute not found
        */
        template <class T>
        void Attr(TZtStringBuf name, T& value) const;

        /**
        * get node attribute
        * @param name: attribute name
        * @param defvalue: default value
        * @param value: return-value
        * returns default value if attribute not found, attr value otherwise
        */
        template <class T>
        void Attr(TZtStringBuf name, T& value, const T& defvalue) const;

        /**
        * get node value (text)
        * @throws exception if node is blank
        */
        template <class T>
        T Value() const;

        /**
        * get node value
        * @param defvalue: default value
        * returns default value if node is blank
        */
        template <class T>
        T Value(const T& defvalue) const;

        /**
        * set node value
        * @param value: new text value
        */
        template <class T>
        typename std::enable_if<!std::is_convertible_v<T, TStringBuf>, void>::type
        SetValue(const T& value);

        void SetValue(TStringBuf value);

        /**
        * set/reset node attribute value,
        * if attribute does not exist, it'll be created
        * @param name: attribute name
        * @param value: attribute value
        */
        template<class T>
        typename std::enable_if<!std::is_convertible_v<T, TZtStringBuf>, void>::type
        SetAttr(TZtStringBuf name, const T& value);

        void SetAttr(TZtStringBuf name, TZtStringBuf value);

        void SetAttr(TZtStringBuf name);

        /**
        * delete node attribute
        * @param name: attribute name
        */
        void DelAttr(TZtStringBuf name);

        /**
        * set node application data
        * @param priv: new application data pointer
        */
        void SetPrivate(void* priv);

        /**
        * @return application data pointer, passed by SetPrivate
        */
        void* GetPrivate() const;

        /**
        * get node name
        */
        TString Name() const;

        /**
        * get node xpath
        */
        TString Path() const;

        /**
        * get node xml representation
        */
        TString ToString(TZtStringBuf enc = "") const {
            TStringStream s;
            Save(s, enc);
            return s.Str();
        }
        void Save(IOutputStream& stream, TZtStringBuf enc = "", bool shouldFormat = false) const;
        void SaveAsHtml(IOutputStream& stream, TZtStringBuf enc = "", bool shouldFormat = false) const;

        /**
        * get pointer to internal node
        */
        xmlNode* GetPtr();
        const xmlNode* GetPtr() const;

        /**
        * check if node is text-only node
        */
        bool IsText() const;

        /**
        * unlink node from parent and free
        */
        void Remove();

        /**
        * constructs null node
        */
        TNode()
            : NodePointer(nullptr)
            , DocPointer(nullptr)
        {
        }

    private:
        friend class TConstNodes;

        TNode(xmlDoc* doc, xmlNode* node)
            : NodePointer(node)
            , DocPointer(doc)
        {
        }

        TNode Find(xmlNode* start, TZtStringBuf name);

        template <class T>
        void AttrInternal(TCharPtr& value, T& res, TStringBuf errContext) const;

        void SaveInternal(IOutputStream& stream, TZtStringBuf enc, int options) const;

        xmlNode* NodePointer;
        xmlDoc* DocPointer;
    };

    class TConstNode {
    public:
        friend class TDocument;
        friend class TConstNodes;
        friend class TNode;
        /**
        * check if node is null
        */
        bool IsNull() const {
            return ActualNode.IsNull();
        }

        bool IsElementNode() const {
            return ActualNode.IsElementNode();
        }

        TConstNode Parent() const {
            return ActualNode.Parent();
        }

        /**
        * Create xpath context to be used later for fast xpath evaluation.
        * @param nss: explicitly specify XML namespaces to use and their prefixes
        */
        TXPathContextPtr CreateXPathContext(const TNamespacesForXPath& nss = TNamespacesForXPath()) const {
            return ActualNode.CreateXPathContext(nss);
        }

        /**
        * get all element nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ns: explicitly specify XML namespaces to use and their prefixes
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        TConstNodes Nodes(TZtStringBuf xpath, bool quiet = false, const TNamespacesForXPath& ns = TNamespacesForXPath()) const {
            return ActualNode.Nodes(xpath, quiet, ns);
        }

        /**
        * get all element nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ctxt: reusable xpath context
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        TConstNodes Nodes(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const {
            return ActualNode.Nodes(xpath, quiet, ctxt);
        }

        /**
        * get all nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ns: explicitly specify XML namespaces to use and their prefixes
        */
        TConstNodes XPath(TZtStringBuf xpath, bool quiet = false, const TNamespacesForXPath& ns = TNamespacesForXPath()) const {
            return ActualNode.XPath(xpath, quiet, ns);
        }

        /**
        * get all nodes matching given xpath expression
        * @param xpath: xpath expression
        * @param quiet: don't throw exception if zero nodes found
        * @param ctxt: reusable xpath context
        */
        TConstNodes XPath(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const {
            return ActualNode.XPath(xpath, quiet, ctxt);
        }

        /**
        * get the first element node matching given xpath expression
        * @param xpath: path to node (from current node)
        * @param quiet: don't throw exception if node not found,
        *               return null node (@see IsNull())
        * @param ns: explicitly specify XML namespaces to use and their prefixes
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        TConstNode Node(TZtStringBuf xpath, bool quiet = false, const TNamespacesForXPath& ns = TNamespacesForXPath()) const {
            return ActualNode.Node(xpath, quiet, ns);
        }

        /**
        * get the first element node matching given xpath expression
        * @param xpath: path to node (from current node)
        * @param quiet: don't throw exception if node not found,
        *               return null node (@see IsNull())
        * @param ctxt: reusable xpath context
        *
        * For historical reasons, this only works for *element* nodes.
        * Use the XPath function if you need other kinds of nodes.
        */
        TConstNode Node(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const {
            return ActualNode.Node(xpath, quiet, ctxt);
        }

        TConstNode FirstChild(TZtStringBuf name) const {
            return ActualNode.FirstChild(name);
        }

        TConstNode FirstChild() const {
            return ActualNode.FirstChild();
        }

        /**
        * get node neighbour
        * @param name: neighbour name
        * throws exception if no neighbour found
        */
        TConstNode NextSibling(TZtStringBuf name) const {
            return ActualNode.NextSibling(name);
        }

        TConstNode NextSibling() const {
            return ActualNode.NextSibling();
        }

        /**
        * get node attribute
        * @param name: attribute name
        * throws exception if attribute not found
        */
        template <class T>
        T Attr(TZtStringBuf name) const {
            return ActualNode.Attr<T>(name);
        }

        /**
        * get node attribute
        * @param name: attribute name
        * returns default value if attribute not found
        */
        template <class T>
        T Attr(TZtStringBuf name, const T& defvalue) const {
            return ActualNode.Attr(name, defvalue);
        }

        /**
        * get node attribute
        * @param name: attribute name
        * @param value: return-value
        * throws exception if attribute not found
        */
        template <class T>
        void Attr(TZtStringBuf name, T& value) const {
            return ActualNode.Attr(name, value);
        }

        /**
        * get node attribute
        * @param name: attribute name
        * @param defvalue: default value
        * @param value: return-value
        * returns default value if attribute not found, attr value otherwise
        */
        template <class T>
        void Attr(TZtStringBuf name, T& value, const T& defvalue) const {
            return ActualNode.Attr(name, value, defvalue);
        }

        /**
        * get node value (text)
        * @throws exception if node is blank
        */
        template <class T>
        T Value() const {
            return ActualNode.Value<T>();
        }

        /**
        * get node value
        * @param defvalue: default value
        * returns default value if node is blank
        */
        template <class T>
        T Value(const T& defvalue) const {
            return ActualNode.Value(defvalue);
        }

        /**
        * get node name
        */
        TString Name() const {
            return ActualNode.Name();
        }

        /**
        * @return application data pointer, passed by SetPrivate
        */
        void* GetPrivate() const {
            return ActualNode.GetPrivate();
        }

        /**
        * get pointer to internal node
        */
        const xmlNode* GetPtr() const {
            return ActualNode.GetPtr();
        }

        /**
        * check if node is text-only node
        */
        bool IsText() const {
            return ActualNode.IsText();
        }

        /**
        * get node xpath
        */
        TString Path() const {
            return ActualNode.Path();
        }

        /**
        * get node xml representation
        */
        TString ToString(TZtStringBuf enc = "") const {
            return ActualNode.ToString(enc);
        }

        TConstNode() = default;
        TConstNode(TNode node)
            : ActualNode(node)
        {
        }

        TNode ConstCast() const {
            return ActualNode;
        }

    private:
        TNode ActualNode;
    };

}