/*******************************************************************************
 * Copyright (c) 2026, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include "documentmanager.h"
#include "htmlimport.h"

#include <algorithm>
#include <iostream>

#include <htmlpp/html.h>
#include <htmlpp/exception.h>

using blogi::webedit::EditPlugin;
using blogi::webedit::EditPluginLoad;
using blogi::webedit::EditPluginDeleter;

DocumentManager::DocumentManager(const QString &pluginDir, QObject *parent)
    : QObject(parent)
{
    std::vector<std::string> dirs;
    dirs.push_back(pluginDir.toStdString());
    _pluginLoader = std::make_unique<EditPluginLoad>(dirs);
    newDocument();
}

DocumentManager::~DocumentManager() {
    clearDocument();
}

void DocumentManager::clearDocument() {
    _docRoot = nullptr;
    for (auto *el : _ownedElements)
        delete el;
    _ownedElements.clear();
    _doc.Clear();
    _rootEl = nullptr;
}

void DocumentManager::newDocument() {
    clearDocument();
    _rootEl = _doc.NewElement("WebEditor");
    _rootEl->SetAttribute("Version", "1.0");
    _rootEl->SetAttribute("editor", "blogi");
    _doc.InsertFirstChild(_rootEl);
}

bool DocumentManager::loadFromFile(const QString &path) {
    clearDocument();

    tinyxml2::XMLDocument loadDoc;
    if (loadDoc.LoadFile(path.toUtf8().constData()) != tinyxml2::XML_SUCCESS)
        return false;

    tinyxml2::XMLElement *root = loadDoc.FirstChildElement("WebEditor");
    if (!root) return false;

    /* Rebuild our internal doc */
    _rootEl = _doc.NewElement("WebEditor");
    const char *ver = root->Attribute("Version");
    if (ver) _rootEl->SetAttribute("Version", ver);
    _rootEl->SetAttribute("editor", "blogi");
    const char *gcss = root->Attribute("GlobalCss");
    if (gcss) _rootEl->SetAttribute("GlobalCss", gcss);
    _doc.InsertFirstChild(_rootEl);

    loadWidgetsFromXml(root, nullptr);
    return true;
}

bool DocumentManager::saveToFile(const QString &path) {
    /* Serialize live tree back into XML doc */
    _doc.Clear();
    _rootEl = _doc.NewElement("WebEditor");
    _rootEl->SetAttribute("Version", "1.0");
    _rootEl->SetAttribute("editor", "blogi");
    _doc.InsertFirstChild(_rootEl);

    EditPlugin *el = _docRoot;
    while (el) {
        tinyxml2::XMLElement *xmlEl = el->toXmlObject(&_doc);
        if (xmlEl) _rootEl->InsertEndChild(xmlEl);
        el = const_cast<EditPlugin*>(el->nextElement());
    }

    return _doc.SaveFile(path.toUtf8().constData()) == tinyxml2::XML_SUCCESS;
}

QString DocumentManager::exportXml() {
    tinyxml2::XMLDocument expDoc;
    tinyxml2::XMLElement *root = expDoc.NewElement("WebEditor");
    root->SetAttribute("Version", "1.0");
    root->SetAttribute("editor", "blogi");
    expDoc.InsertFirstChild(root);

    EditPlugin *el = _docRoot;
    while (el) {
        tinyxml2::XMLElement *xmlEl = el->toXmlObject(&expDoc);
        if (xmlEl) root->InsertEndChild(xmlEl);
        el = const_cast<EditPlugin*>(el->nextElement());
    }

    tinyxml2::XMLPrinter printer;
    expDoc.Print(&printer);
    return QString::fromUtf8(printer.CStr());
}

bool DocumentManager::importXml(const QString &xml) {
    clearDocument();

    tinyxml2::XMLDocument loadDoc;
    if (loadDoc.Parse(xml.toUtf8().constData()) != tinyxml2::XML_SUCCESS)
        return false;

    tinyxml2::XMLElement *root = loadDoc.FirstChildElement("WebEditor");
    if (!root) return false;

    _rootEl = _doc.NewElement("WebEditor");
    _rootEl->SetAttribute("Version", "1.0");
    _rootEl->SetAttribute("editor", "blogi");
    _doc.InsertFirstChild(_rootEl);

    loadWidgetsFromXml(root, nullptr);
    return true;
}

bool DocumentManager::importHtml(const QString &html) {
    std::string xmlStr, globalCss;
    if (!blogi::htmlimport::convertHtmlToWidgetXml(html.toStdString(), xmlStr, globalCss))
        return false;
    return importXml(QString::fromStdString(xmlStr));
}

QString DocumentManager::renderToHtml() {
    if (!_docRoot) return {};

    libhtmlpp::HtmlElement wrapper("div");

    const char *gcss = _rootEl ? _rootEl->Attribute("GlobalCss") : nullptr;
    if (gcss && gcss[0]) {
        libhtmlpp::HtmlElement globalStyle("style");
        globalStyle.appendChild(libhtmlpp::TextElement(gcss));
        wrapper.appendChild(globalStyle);
    }

    const EditPlugin *el = _docRoot;
    while (el) {
        el->Render(wrapper);
        el = el->nextElement();
    }

    libhtmlpp::HtmlString htmlOut;
    libhtmlpp::print(wrapper, htmlOut, true);
    return QString::fromStdString(htmlOut.str());
}

/* ---- Plugin info ---- */

QString DocumentManager::getPluginTypes() const {
    json_object *arr = json_object_new_array();

    for (const EditPluginLoad::PluginData *p = _pluginLoader->getFirstPlugin();
         p; p = p->getNextPlg()) {
        json_object *obj = json_object_new_object();
        json_object_object_add(obj, "name",
            json_object_new_string(p->getInstace()->getName().c_str()));
        json_object_object_add(obj, "typeId",
            json_object_new_string(p->getInstace()->getTypeId().c_str()));
        json_object_array_add(arr, obj);
    }

    QString result = QString::fromUtf8(json_object_to_json_string(arr));
    json_object_put(arr);
    return result;
}

/* ---- Document tree ---- */

json_object *DocumentManager::buildTreeJson(const EditPlugin *el) const {
    if (!el) return nullptr;

    json_object *obj = json_object_new_object();
    json_object_object_add(obj, "uuid",
        json_object_new_string(el->getInstanceId().c_str()));
    json_object_object_add(obj, "name",
        json_object_new_string(el->getName().c_str()));
    json_object_object_add(obj, "typeId",
        json_object_new_string(el->getTypeId().c_str()));

    json_object *children = json_object_new_array();
    int count = 0;
    const EditPlugin *child = el->getChildElement();
    while (child) {
        json_object_array_add(children, buildTreeJson(child));
        ++count;
        child = child->nextElement();
    }
    json_object_object_add(obj, "children", children);
    json_object_object_add(obj, "children_count", json_object_new_int(count));
    return obj;
}

QString DocumentManager::getDocTree() const {
    json_object *root = json_object_new_object();
    json_object_object_add(root, "status", json_object_new_string("success"));

    json_object *tree = json_object_new_object();
    json_object_object_add(tree, "uuid", json_object_new_string("root"));
    json_object_object_add(tree, "name", json_object_new_string("Document"));

    json_object *children = json_object_new_array();
    int count = 0;
    const EditPlugin *el = _docRoot;
    while (el) {
        json_object_array_add(children, buildTreeJson(el));
        ++count;
        el = el->nextElement();
    }
    json_object_object_add(tree, "children", children);
    json_object_object_add(tree, "children_count", json_object_new_int(count));
    json_object_object_add(root, "doc_tree", tree);

    QString result = QString::fromUtf8(json_object_to_json_string(root));
    json_object_put(root);
    return result;
}

/* ---- Element operations ---- */

EditPlugin *DocumentManager::createPluginByName(const std::string &name) {
    std::string lowerName = name;
    std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(),
        [](unsigned char c) { return std::tolower(c); });

    for (const EditPluginLoad::PluginData *p = _pluginLoader->getFirstPlugin();
         p; p = p->getNextPlg()) {
        std::string plgName = p->getInstace()->getName();
        std::transform(plgName.begin(), plgName.end(), plgName.begin(),
            [](unsigned char c) { return std::tolower(c); });
        if (plgName == lowerName) {
            EditPlugin *inst = p->createNewInstance();
            if (inst) {
                _ownedElements.push_back(inst);
                return inst;
            }
        }
    }
    return nullptr;
}

EditPlugin *DocumentManager::findByUuid(EditPlugin *root, const std::string &uuid) {
    if (!root) return nullptr;
    EditPlugin *cur = root;
    while (cur) {
        if (cur->getInstanceId() == uuid) return cur;
        EditPlugin *child = const_cast<EditPlugin*>(cur->getChildElement());
        while (child) {
            EditPlugin *found = findByUuid(child, uuid);
            if (found) return found;
            child = const_cast<EditPlugin*>(child->nextElement());
        }
        cur = const_cast<EditPlugin*>(cur->nextElement());
    }
    return nullptr;
}

DocumentManager::NodeContext DocumentManager::findNodeContext(const std::string &uuid) {
    NodeContext ctx;

    EditPlugin *prev = nullptr;
    EditPlugin *cur = _docRoot;
    while (cur) {
        if (cur->getInstanceId() == uuid) {
            ctx.node = cur;
            ctx.prev = prev;
            ctx.parent = nullptr;
            return ctx;
        }
        /* Search children with stack */
        struct Frame { EditPlugin *parentNode; EditPlugin *firstChild; };
        std::vector<Frame> stack;
        EditPlugin *child = const_cast<EditPlugin*>(cur->getChildElement());
        if (child) stack.push_back({cur, child});

        while (!stack.empty()) {
            Frame f = stack.back();
            stack.pop_back();
            EditPlugin *cprev = nullptr;
            EditPlugin *c = f.firstChild;
            while (c) {
                if (c->getInstanceId() == uuid) {
                    ctx.node = c;
                    ctx.prev = cprev;
                    ctx.parent = f.parentNode;
                    return ctx;
                }
                EditPlugin *gc = const_cast<EditPlugin*>(c->getChildElement());
                if (gc) stack.push_back({c, gc});
                cprev = c;
                c = const_cast<EditPlugin*>(c->nextElement());
            }
        }
        prev = cur;
        cur = const_cast<EditPlugin*>(cur->nextElement());
    }
    return ctx;
}

QString DocumentManager::addElement(const QString &parentUuid, const QString &pluginName, int position) {
    json_object *resp = json_object_new_object();

    EditPlugin *inst = createPluginByName(pluginName.toStdString());
    if (!inst) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Unknown plugin type"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    if (parentUuid.isEmpty() || parentUuid == "root") {
        /* Add to top level */
        if (!_docRoot) {
            _docRoot = inst;
        } else {
            EditPlugin *tail = _docRoot;
            while (tail->nextElement())
                tail = const_cast<EditPlugin*>(tail->nextElement());
            tail->setNextEl(inst);
        }
    } else {
        EditPlugin *parent = findByUuid(_docRoot, parentUuid.toStdString());
        if (!parent || !parent->canHaveChildren()) {
            json_object_object_add(resp, "status", json_object_new_string("error"));
            json_object_object_add(resp, "message", json_object_new_string("Invalid parent"));
            QString r = QString::fromUtf8(json_object_to_json_string(resp));
            json_object_put(resp);
            return r;
        }
        if (!parent->getChildElement()) {
            parent->setChildElement(*inst);
        } else {
            EditPlugin *tail = const_cast<EditPlugin*>(parent->getChildElement());
            while (tail->nextElement())
                tail = const_cast<EditPlugin*>(tail->nextElement());
            tail->setNextEl(inst);
        }
    }

    json_object_object_add(resp, "status", json_object_new_string("success"));
    json_object_object_add(resp, "uuid",
        json_object_new_string(inst->getInstanceId().c_str()));
    json_object_object_add(resp, "name",
        json_object_new_string(inst->getName().c_str()));

    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

QString DocumentManager::removeElement(const QString &uuid) {
    json_object *resp = json_object_new_object();
    NodeContext ctx = findNodeContext(uuid.toStdString());

    if (!ctx.node) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Element not found"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    EditPlugin *next = const_cast<EditPlugin*>(ctx.node->nextElement());

    if (ctx.parent) {
        if (!ctx.prev) {
            ctx.parent->setChildEl(next);
        } else {
            ctx.prev->setNextEl(next);
        }
    } else {
        if (!ctx.prev) {
            _docRoot = next;
        } else {
            ctx.prev->setNextEl(next);
        }
    }
    ctx.node->setNextEl(nullptr);

    json_object_object_add(resp, "status", json_object_new_string("success"));
    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

QString DocumentManager::moveElementUp(const QString &uuid) {
    json_object *resp = json_object_new_object();
    NodeContext ctx = findNodeContext(uuid.toStdString());

    if (!ctx.node || !ctx.prev) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Cannot move up"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    /* Find prev-prev to relink */
    EditPlugin *next = const_cast<EditPlugin*>(ctx.node->nextElement());

    /* Unlink node from chain */
    ctx.prev->setNextEl(next);

    /* Find what points to prev */
    NodeContext prevCtx = findNodeContext(std::string(ctx.prev->getInstanceId().c_str()));

    if (prevCtx.prev) {
        prevCtx.prev->setNextEl(ctx.node);
    } else if (prevCtx.parent) {
        prevCtx.parent->setChildEl(ctx.node);
    } else {
        _docRoot = ctx.node;
    }
    ctx.node->setNextEl(ctx.prev);

    json_object_object_add(resp, "status", json_object_new_string("success"));
    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

QString DocumentManager::moveElementDown(const QString &uuid) {
    json_object *resp = json_object_new_object();
    NodeContext ctx = findNodeContext(uuid.toStdString());

    if (!ctx.node || !ctx.node->nextElement()) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Cannot move down"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    EditPlugin *nextNode = const_cast<EditPlugin*>(ctx.node->nextElement());
    EditPlugin *afterNext = const_cast<EditPlugin*>(nextNode->nextElement());

    /* Unlink node */
    if (ctx.prev) {
        ctx.prev->setNextEl(nextNode);
    } else if (ctx.parent) {
        ctx.parent->setChildEl(nextNode);
    } else {
        _docRoot = nextNode;
    }

    nextNode->setNextEl(ctx.node);
    ctx.node->setNextEl(afterNext);

    json_object_object_add(resp, "status", json_object_new_string("success"));
    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

QString DocumentManager::indentElement(const QString &uuid) {
    json_object *resp = json_object_new_object();
    NodeContext ctx = findNodeContext(uuid.toStdString());

    if (!ctx.node || !ctx.prev || !ctx.prev->canHaveChildren()) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Cannot indent"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    EditPlugin *next = const_cast<EditPlugin*>(ctx.node->nextElement());
    ctx.prev->setNextEl(next);
    ctx.node->setNextEl(nullptr);

    /* Append as last child of prev */
    EditPlugin *child = const_cast<EditPlugin*>(ctx.prev->getChildElement());
    if (!child) {
        ctx.prev->setChildElement(*ctx.node);
    } else {
        while (child->nextElement())
            child = const_cast<EditPlugin*>(child->nextElement());
        child->setNextEl(ctx.node);
    }

    json_object_object_add(resp, "status", json_object_new_string("success"));
    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

QString DocumentManager::outdentElement(const QString &uuid) {
    json_object *resp = json_object_new_object();
    NodeContext ctx = findNodeContext(uuid.toStdString());

    if (!ctx.node || !ctx.parent) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Cannot outdent"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    EditPlugin *next = const_cast<EditPlugin*>(ctx.node->nextElement());

    /* Unlink from parent's children */
    if (!ctx.prev) {
        ctx.parent->setChildEl(next);
    } else {
        ctx.prev->setNextEl(next);
    }

    /* Insert after parent in parent's sibling chain */
    EditPlugin *parentNext = const_cast<EditPlugin*>(ctx.parent->nextElement());
    ctx.parent->setNextEl(ctx.node);
    ctx.node->setNextEl(parentNext);

    json_object_object_add(resp, "status", json_object_new_string("success"));
    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

QString DocumentManager::copyElement(const QString &uuid) {
    json_object *resp = json_object_new_object();
    EditPlugin *el = findByUuid(_docRoot, uuid.toStdString());
    if (!el) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Element not found"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    _clipDoc.Clear();
    _clipEl = el->toXmlObject(&_clipDoc);
    if (_clipEl) _clipDoc.InsertFirstChild(_clipEl);

    json_object_object_add(resp, "status", json_object_new_string("success"));
    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

QString DocumentManager::pasteElement(const QString &targetUuid) {
    json_object *resp = json_object_new_object();
    if (!_clipEl) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Clipboard empty"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    const char *elName = _clipEl->Name();
    EditPlugin *inst = createPluginByName(elName ? elName : "");
    if (!inst) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Cannot create element"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    inst->regenerateId();
    inst->fromXmlObject(_clipEl);

    if (targetUuid.isEmpty() || targetUuid == "root") {
        if (!_docRoot) {
            _docRoot = inst;
        } else {
            EditPlugin *tail = _docRoot;
            while (tail->nextElement())
                tail = const_cast<EditPlugin*>(tail->nextElement());
            tail->setNextEl(inst);
        }
    } else {
        EditPlugin *parent = findByUuid(_docRoot, targetUuid.toStdString());
        if (parent && parent->canHaveChildren()) {
            EditPlugin *child = const_cast<EditPlugin*>(parent->getChildElement());
            if (!child) {
                parent->setChildElement(*inst);
            } else {
                while (child->nextElement())
                    child = const_cast<EditPlugin*>(child->nextElement());
                child->setNextEl(inst);
            }
        } else {
            if (!_docRoot) {
                _docRoot = inst;
            } else {
                EditPlugin *tail = _docRoot;
                while (tail->nextElement())
                    tail = const_cast<EditPlugin*>(tail->nextElement());
                tail->setNextEl(inst);
            }
        }
    }

    json_object_object_add(resp, "status", json_object_new_string("success"));
    json_object_object_add(resp, "uuid",
        json_object_new_string(inst->getInstanceId().c_str()));
    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(resp);
    return r;
}

/* ---- Property access ---- */

QString DocumentManager::getElementProperties(const QString &uuid) {
    json_object *resp = json_object_new_object();
    EditPlugin *el = findByUuid(_docRoot, uuid.toStdString());
    if (!el) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Element not found"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    json_object *req = json_object_new_object();
    json_object_object_add(req, "action", json_object_new_string("read"));
    json_object *inner_resp = json_object_new_object();
    el->JsonApi(req, inner_resp);

    /* Also get schema */
    json_object *schema_req = json_object_new_object();
    json_object_object_add(schema_req, "action", json_object_new_string("get_schema"));
    json_object *schema_resp = json_object_new_object();
    el->JsonApi(schema_req, schema_resp);

    json_object_object_add(resp, "status", json_object_new_string("success"));
    json_object_object_add(resp, "widget_name",
        json_object_new_string(el->getName().c_str()));
    json_object_object_add(resp, "widget_uuid",
        json_object_new_string(el->getInstanceId().c_str()));

    /* Merge properties from inner_resp */
    json_object *data_obj = nullptr;
    if (json_object_object_get_ex(inner_resp, "data", &data_obj)) {
        json_object *props_str = nullptr;
        if (json_object_object_get_ex(data_obj, "properties", &props_str)) {
            const char *ps = json_object_get_string(props_str);
            if (ps) {
                json_object *parsed = json_tokener_parse(ps);
                if (parsed) {
                    json_object_object_add(resp, "properties", parsed);
                }
            }
        }
    }
    json_object *schema = nullptr;
    if (json_object_object_get_ex(schema_resp, "schema", &schema)) {
        json_object_object_add(resp, "schema", json_object_get(schema));
    }

    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(req);
    json_object_put(inner_resp);
    json_object_put(schema_req);
    json_object_put(schema_resp);
    json_object_put(resp);
    return r;
}

QString DocumentManager::setElementProperties(const QString &uuid, const QString &propsJson) {
    json_object *resp = json_object_new_object();
    EditPlugin *el = findByUuid(_docRoot, uuid.toStdString());
    if (!el) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Element not found"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    json_object *req = json_tokener_parse(propsJson.toUtf8().constData());
    if (!req) {
        json_object_object_add(resp, "status", json_object_new_string("error"));
        json_object_object_add(resp, "message", json_object_new_string("Invalid JSON"));
        QString r = QString::fromUtf8(json_object_to_json_string(resp));
        json_object_put(resp);
        return r;
    }

    json_object_object_add(req, "action", json_object_new_string("modify_bulk"));
    json_object *inner_resp = json_object_new_object();
    el->JsonApi(req, inner_resp);

    json_object_object_add(resp, "status", json_object_new_string("success"));

    QString r = QString::fromUtf8(json_object_to_json_string(resp));
    json_object_put(req);
    json_object_put(inner_resp);
    json_object_put(resp);
    return r;
}

/* ---- XML loading ---- */

void DocumentManager::loadWidgetsFromXml(tinyxml2::XMLElement *xmlEl, EditPlugin *parent) {
    if (!xmlEl) return;

    EditPlugin *lastSibling = nullptr;

    tinyxml2::XMLElement *childXml = xmlEl->FirstChildElement();
    while (childXml) {
        const char *elementName = childXml->Name();
        if (!elementName) {
            childXml = childXml->NextSiblingElement();
            continue;
        }

        EditPlugin *newWidget = createPluginByName(elementName);
        if (newWidget) {
            newWidget->fromXmlObject(childXml);

            if (parent) {
                if (!lastSibling) {
                    parent->setChildElement(*newWidget);
                } else {
                    lastSibling->setNextEl(newWidget);
                }
            } else {
                if (!_docRoot) {
                    _docRoot = newWidget;
                } else if (!lastSibling) {
                    EditPlugin *tail = _docRoot;
                    while (tail->nextElement())
                        tail = const_cast<EditPlugin*>(tail->nextElement());
                    tail->setNextEl(newWidget);
                } else {
                    lastSibling->setNextEl(newWidget);
                }
            }
            lastSibling = newWidget;

            if (newWidget->canHaveChildren()) {
                loadWidgetsFromXml(childXml, newWidget);
            }
        }
        childXml = childXml->NextSiblingElement();
    }
}
