#include "xml-document.h"

#include <libxml/xinclude.h>
#include <libxml/xpathInternals.h>

#include <library/cpp/xml/init/init.h>

#include <util/generic/yexception.h>
#include <util/folder/dirut.h>

namespace {
    struct TInit {
        inline TInit() {
            NXml::InitEngine();
        }
    } initer;
}

namespace NXml {
    TDocument::TDocument(const TString& xml, Source type) {
        switch (type) {
            case File:
                ParseFile(xml);
                break;
            case String:
                ParseString(xml);
                break;
            case RootName: {
                TDocHolder doc(xmlNewDoc(XMLCHAR("1.0")));
                if (!doc)
                    THROW(XmlException, "Can't create xml document.");
                doc->encoding = xmlStrdup(XMLCHAR("utf-8"));

                TNodePtr node(xmlNewNode(nullptr, XMLCHAR(xml.c_str())));
                if (!node)
                    THROW(XmlException, "Can't create root node.");
                xmlDocSetRootElement(doc.Get(), node.Get());
                Y_UNUSED(node.Release());
                Doc = std::move(doc);
            } break;
            default:
                THROW(InvalidArgument, "Wrong source type");
        }
    }

    TDocument::TDocument(TDocument&& doc)
        : Doc(std::move(doc.Doc))
    {
    }

    TDocument& TDocument::operator=(TDocument&& doc) {
        if (this != &doc)
            doc.Swap(*this);

        return *this;
    }

    void TDocument::ParseFile(const TString& file) {
        if (!NFs::Exists(file))
            THROW(XmlException, "File " << file << " doesn't exist");

        TParserCtxtPtr pctx(xmlNewParserCtxt());
        if (!pctx)
            THROW(XmlException, "Can't create parser context");

        TDocHolder doc(xmlCtxtReadFile(pctx.Get(), file.c_str(), nullptr, XML_PARSE_NOCDATA));
        if (!doc)
            THROW(XmlException, "Can't parse file " << file);

        int res = xmlXIncludeProcessFlags(doc.Get(), XML_PARSE_XINCLUDE | XML_PARSE_NOCDATA | XML_PARSE_NOXINCNODE);

        if (res == -1)
            THROW(XmlException, "XIncludes processing failed");

        Doc = std::move(doc);
    }

    void TDocument::ParseString(TZtStringBuf xml) {
        TParserCtxtPtr pctx(xmlNewParserCtxt());
        if (pctx.Get() == nullptr)
            THROW(XmlException, "Can't create parser context");

        TDocHolder doc(xmlCtxtReadMemory(pctx.Get(), xml.c_str(), (int)xml.size(), nullptr, nullptr, XML_PARSE_NOCDATA));

        if (!doc)
            THROW(XmlException, "Can't parse string");

        Doc = std::move(doc);
    }

    TNode TDocument::Root() {
        xmlNode* r = xmlDocGetRootElement(Doc.Get());
        if (r == nullptr)
            THROW(XmlException, "TDocument hasn't root element");

        return TNode(Doc.Get(), r);
    }

    TConstNode TDocument::Root() const {
        xmlNode* r = xmlDocGetRootElement(Doc.Get());
        if (r == nullptr)
            THROW(XmlException, "TDocument hasn't root element");

        return TConstNode(TNode(Doc.Get(), r));
    }

    bool TNode::IsNull() const {
        return NodePointer == nullptr;
    }

    bool TNode::IsElementNode() const {
        return !IsNull() && (NodePointer->type == XML_ELEMENT_NODE);
    }

    TXPathContextPtr TNode::CreateXPathContext(const TNamespacesForXPath& nss) const {
        TXPathContextPtr ctx = xmlXPathNewContext(DocPointer);
        if (!ctx)
            THROW(XmlException, "Can't create empty xpath context");

        for (const auto& ns : nss) {
            const int r = xmlXPathRegisterNs(ctx.Get(), XMLCHAR(ns.Prefix.c_str()), XMLCHAR(ns.Url.c_str()));
            if (r != 0)
                THROW(XmlException, "Can't register namespace " << ns.Url << " with prefix " << ns.Prefix);
        }

        return ctx;
    }

    TConstNodes TNode::XPath(TZtStringBuf xpath, bool quiet, const TNamespacesForXPath& ns) const {
        TXPathContextPtr ctxt = CreateXPathContext(ns);
        return XPath(xpath, quiet, *ctxt);
    }

    TConstNodes TNode::XPath(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const {
        if (xmlXPathSetContextNode(NodePointer, &ctxt) != 0)
            THROW(XmlException, "Can't set xpath context node, probably the context is associated with another document");

        TXPathObjectPtr obj = xmlXPathEvalExpression(XMLCHAR(xpath.c_str()), &ctxt);
        if (!obj)
            THROW(XmlException, "Can't evaluate xpath expression " << xpath);

        TConstNodes nodes(DocPointer, obj);

        if (nodes.Size() == 0 && !quiet)
            THROW(NodeNotFound, xpath);

        return nodes;
    }

    TConstNodes TNode::Nodes(TZtStringBuf xpath, bool quiet, const TNamespacesForXPath& ns) const {
        TXPathContextPtr ctxt = CreateXPathContext(ns);
        return Nodes(xpath, quiet, *ctxt);
    }

    TConstNodes TNode::Nodes(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const {
        TConstNodes nodes = XPath(xpath, quiet, ctxt);
        if (nodes.Size() != 0 && !nodes[0].IsElementNode())
            THROW(XmlException, "xpath points to non-element nodes: " << xpath);
        return nodes;
    }

    TNode TNode::Node(TZtStringBuf xpath, bool quiet, const TNamespacesForXPath& ns) {
        TXPathContextPtr ctxt = CreateXPathContext(ns);
        return Node(xpath, quiet, *ctxt);
    }

    TConstNode TNode::Node(TZtStringBuf xpath, bool quiet, const TNamespacesForXPath& ns) const {
        TXPathContextPtr ctxt = CreateXPathContext(ns);
        return Node(xpath, quiet, *ctxt);
    }

    TNode TNode::Node(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) {
        TConstNodes n = Nodes(xpath, quiet, ctxt);

        if (n.Size() == 0 && !quiet)
            THROW(NodeNotFound, xpath);

        if (n.Size() == 0)
            return TNode();
        else
            return n[0].ConstCast();
    }

    TConstNode TNode::Node(TZtStringBuf xpath, bool quiet, TXPathContext& ctxt) const {
        return const_cast<TNode*>(this)->Node(xpath, quiet, ctxt);
    }

    TNode TNode::FirstChild(TZtStringBuf name) {
        if (IsNull())
            THROW(XmlException, "Node is null");

        return Find(NodePointer->children, name);
    }

    TConstNode TNode::FirstChild(TZtStringBuf name) const {
        return const_cast<TNode*>(this)->FirstChild(name);
    }

    TNode TNode::FirstChild() {
        if (IsNull())
            THROW(XmlException, "Node is null");

        return TNode(DocPointer, NodePointer->children);
    }

    TConstNode TNode::FirstChild() const {
        return const_cast<TNode*>(this)->FirstChild();
    }

    TNode TNode::Parent() {
        if (nullptr == NodePointer->parent)
            THROW(XmlException, "Parent node not exists");

        return TNode(DocPointer, NodePointer->parent);
    }

    TConstNode TNode::Parent() const {
        return const_cast<TNode*>(this)->Parent();
    }

    TNode TNode::NextSibling(TZtStringBuf name) {
        if (IsNull())
            THROW(XmlException, "Node is null");

        return Find(NodePointer->next, name);
    }

    TConstNode TNode::NextSibling(TZtStringBuf name) const {
        return const_cast<TNode*>(this)->NextSibling(name);
    }

    TNode TNode::NextSibling() {
        if (IsNull())
            THROW(XmlException, "Node is null");

        return TNode(DocPointer, NodePointer->next);
    }

    TConstNode TNode::NextSibling() const {
        return const_cast<TNode*>(this)->NextSibling();
    }

    /* NOTE: by default child will inherit it's parent ns */

    TNode TNode::AddChild(TZtStringBuf name) {
        return AddChild(name, "");
    }

    /* NOTE: source node will be copied, as otherwise it will be double-freed from this and its own document */

    TNode TNode::AddChild(const TConstNode& node) {
        xmlNodePtr copy = xmlDocCopyNode(node.ConstCast().NodePointer, DocPointer, 1 /* recursive */);
        copy = xmlAddChild(NodePointer, copy);
        return TNode(DocPointer, copy);
    }

    void TNode::SetPrivate(void* priv) {
        NodePointer->_private = priv;
    }

    void* TNode::GetPrivate() const {
        return NodePointer->_private;
    }

    TNode TNode::Find(xmlNode* start, TZtStringBuf name) {
        for (; start; start = start->next)
            if (start->type == XML_ELEMENT_NODE && (name.empty() || !xmlStrcmp(start->name, XMLCHAR(name.c_str()))))
                return TNode(DocPointer, start);

        return TNode();
    }

    TString TNode::Name() const {
        if (IsNull())
            THROW(XmlException, "Node is null");

        return CAST2CHAR(NodePointer->name);
    }

    TString TNode::Path() const {
        TCharPtr path(xmlGetNodePath(NodePointer));
        if (!!path)
            return CAST2CHAR(path.Get());
        else
            return "";
    }

    xmlNode* TNode::GetPtr() {
        return NodePointer;
    }

    const xmlNode* TNode::GetPtr() const {
        return NodePointer;
    }

    bool TNode::IsText() const {
        if (IsNull())
            THROW(XmlException, "Node is null");

        return NodePointer->type == XML_TEXT_NODE;
    }

    void TNode::Remove() {
        xmlNode* nodePtr = GetPtr();
        xmlUnlinkNode(nodePtr);
        xmlFreeNode(nodePtr);
        NodePointer = nullptr;
    }

    static int XmlWriteToOstream(void* context, const char* buffer, int len) {
        // possibly use to save doc as well
        IOutputStream* out = (IOutputStream*)context;
        out->Write(buffer, len);
        return len;
    }

    void TNode::SaveInternal(IOutputStream& stream, TZtStringBuf enc, int options) const {
        const char* encoding = enc.size() ? enc.data() : "utf-8";
        TSaveCtxtPtr ctx(xmlSaveToIO(XmlWriteToOstream, /* close */ nullptr, &stream,
                                     encoding, options));
        if (xmlSaveTree(ctx.Get(), (xmlNode*)GetPtr()) < 0)
            THROW(XmlException, "Failed saving node to stream");
    }

    void TNode::Save(IOutputStream& stream, TZtStringBuf enc, bool shouldFormat) const {
        SaveInternal(stream, enc, shouldFormat ? XML_SAVE_FORMAT : 0);
    }

    void TNode::SaveAsHtml(IOutputStream& stream, TZtStringBuf enc, bool shouldFormat) const {
        int options = XML_SAVE_AS_HTML;
        options |= shouldFormat ? XML_SAVE_FORMAT : 0;
        SaveInternal(stream, enc, options);
    }

    TConstNodes::TConstNodes(const TConstNodes& nodes)
        : SizeValue(nodes.Size())
        , Doc(nodes.Doc)
        , Obj(nodes.Obj)
    {
    }

    TConstNodes& TConstNodes::operator=(const TConstNodes& nodes) {
        if (this != &nodes) {
            SizeValue = nodes.Size();
            Doc = nodes.Doc;
            Obj = nodes.Obj;
        }

        return *this;
    }

    TConstNodes::TConstNodes(TConstNodesRef ref)
        : SizeValue(ref.r_.Size())
        , Doc(ref.r_.Doc)
        , Obj(ref.r_.Obj)
    {
    }

    TConstNodes& TConstNodes::operator=(TConstNodesRef ref) {
        if (this != &ref.r_) {
            SizeValue = ref.r_.Size();
            Doc = ref.r_.Doc;
            Obj = ref.r_.Obj;
        }
        return *this;
    }

    TConstNodes::operator TConstNodesRef() {
        return TConstNodesRef(*this);
    }

    TConstNodes::TConstNodes(xmlDoc* doc, TXPathObjectPtr obj)
        : SizeValue(obj && obj->nodesetval ? obj->nodesetval->nodeNr : 0)
        , Doc(doc)
        , Obj(obj)
    {
    }

    TConstNode TConstNodes::operator[](size_t number) const {
        if (number + 1 > Size())
            THROW(XmlException, "index out of range " << number);

        if (!Obj || !Obj->nodesetval)
            THROW(XmlException, "Broken TConstNodes object, Obj is null");

        xmlNode* node = Obj->nodesetval->nodeTab[number];
        return TNode(Doc, node);
    }

    TConstNode TConstNodes::TNodeIter::operator*() const {
        return Nodes[Index];
    }

}