#include "xml-vym.h"

#include <QColor>
#include <QMessageBox>
#include <QTextStream>

#include "attributeitem.h"
#include "branchitem.h"
#include "flag.h"
#include "mainwindow.h"
#include "misc.h"
#include "settings.h"
#include "slideitem.h"
#include "task.h"
#include "taskmodel.h"
#include "vymmodel.h"
#include "xlink.h"
#include "xlinkitem.h"
#include "xlinkobj.h"

extern Main *mainWindow;
extern Settings settings;
extern TaskModel *taskModel;
extern QString vymVersion;

VymReader::VymReader(VymModel* m)
    : BaseReader(m)
{
    //qDebug() << "Constr. VymReader";

    // When importing maps, content could be filtered,
    // e.g. without slides
    contentFilter = 0x0000;

    branchesTotal = 0;
    useProgress = false;

    lastBranch = nullptr;
    lastMI = nullptr;
}

bool VymReader::read(QIODevice *device)
{
    xml.setDevice(device);

    if (xml.readNextStartElement()) {
        if (xml.name() == QLatin1String("vymmap")) {
            readVymMap();
        } else {
            xml.raiseError("No vymmap or heading as next element.");
        }
    }
    return !xml.error();
}

void VymReader::readVymMap()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("vymmap"));

    // Check version
    if (!xml.attributes().hasAttribute("version")) {
        xml.raiseError("No version found for vymmap.");
        return;
    }

    if (!xml.attributes().value("version").isEmpty()) {
        version = xml.attributes().value("version").toString();
        if (!versionLowerOrEqualThanVym(version)) {
            QMessageBox::warning(
                0, QObject::tr("Warning: Version Problem"),
                QObject::tr(
                    "<h3>Map is newer than VYM</h3>"
                    "<p>The map you are just trying to load was "
                    "saved using vym %1. "
                    "The version of this vym is %2. "
                    "If you run into problems after pressing "
                    "the ok-button below, updating vym should help.</p>")
                    .arg(version, vymVersion));
        }
        model->setMapVersion(version);
    }

    branchesTotal = 0;
    branchesCounter = 0;

    if (loadMode == File::NewMap || loadMode == File::DefaultMap) {
        // Create mapCenter
        model->clear();
        lastBranch = model->getRootItem();

        readVymMapAttr();
    } else {
        // Imports need a dedicated branch
        lastBranch = insertBranch;

        if (loadMode == File::ImportReplace) {
            if (!lastBranch) {
                xml.raiseError("readVymMap - ImportReplace map, but nothing selected!");
                return;
            }

            insertPos = lastBranch->num();
            BranchItem *pb = lastBranch->parentBranch();
            if (!pb) {
                xml.raiseError("readVymMap - No parent branch for selection in ImportReplace!");
                return;
            }

            model->deleteSelection(lastBranch->getID());
            lastBranch = pb;
            loadMode = File::ImportAdd;
        } else {
            // ImportAdd
            if (insertPos < 0)
                insertPos = 0;
        }
    }

    if (!lastBranch)
        // Make sure, that mapcenters can be pasted on empty map e.g. for undo
        lastBranch = model->getRootItem();

    lastMI = lastBranch;


    while (xml.readNextStartElement()) {
        if (xml.name() == QLatin1String("mapdesign"))
            readMapDesign();
        else if (xml.name() == QLatin1String("mapcenter") ||
            xml.name() == QLatin1String("branch")) {
            readBranchOrMapCenter(loadMode, insertPos);
            insertPos++;
        } else if (xml.name() == QLatin1String("floatimage"))
            readImage();    // Used when pasting image
        else if (xml.name() == QLatin1String("setting"))
            readSetting();
        else if (xml.name() == QLatin1String("select"))
            readSelection();
        else if (xml.name() == QLatin1String("userflagdef"))
            readUserFlagDef();
        else if (xml.name() == QLatin1String("xlink"))
            readXLink();
        else if (xml.name() == QLatin1String("slide"))
            readSlide();
        else {
            raiseUnknownElementError();
            return;
        }
    }
}

void VymReader::readMapDesign()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("mapdesign"));

    while (xml.readNextStartElement()) {
        if (xml.name() == QLatin1String("md"))
            readMapDesignElement();
        else {
            raiseUnknownElementError();
            return;
        }
    }
}

void VymReader::readMapDesignElement()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("md"));

    readMapDesignCompatibleAttributes();

    QString k = xml.attributes().value("key").toString();
    QString v = xml.attributes().value("val").toString();
    QString d = xml.attributes().value("d").toString();
    if (!v.isEmpty()) {
        if (!model->mapDesign()->setElement(k, v, d)) {
            xml.raiseError(QString("MapDesign: Failed to set key %1 to %2").arg(k, v));
            return;
        }
    }

    if (xml.readNextStartElement())
        raiseUnknownElementError();
}

void VymReader::readMapDesignCompatibleAttributes()
{
    // Reads attributes which before 2.9.13 used to be
    // in <vymmap> and now are in <mapdesign>

    Q_ASSERT(xml.isStartElement() &&
            ( xml.name() == QLatin1String("vymmap") ||
              xml.name() == QLatin1String("md")));

    QString a = "backgroundColor";
    QString s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        model->setBackgroundColor(QColor(s));
    }

    a = "backgroundImage";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        model->loadBackgroundImage(parseHREF(s));

    a = "backgroundImageName";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        model->setBackgroundImageName(s);
    }

    a = "font";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        QFont font;
        font.fromString(s);
        model->mapDesign()->setFont(font);
    }

    QColor col;

    // Only for backwards compatibility reading <vymmap>.
    // moved to <mapdesign> starting 2.9.513
    a = "selectionColor";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        col = QColor::fromString(s);
        model->setSelectionPenColor(col);
        model->setSelectionBrushColor(col);
    }

    // Only for backwards compatibility reading <vymmap>.
    // moved to <mapdesign> starting 2.9.513
    a = "selectionPenColor";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        col = QColor::fromString(s);
        model->setSelectionPenColor(col);
    }

    bool ok;
    // Only for backwards compatibility reading <vymmap>.
    // moved to <mapdesign> starting 2.9.513
    a = "selectionPenWidth"; 
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        float  w = s.toFloat(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute  " + a);
            return;
        }
        model->setSelectionPenWidth(w);
    }

    a = "selectionBrushColor";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        model->setSelectionBrushColor(QColor(s));

    a = "linkColorHint";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        if (s == "HeadingColor")
            model->setLinkColorHint(LinkObj::HeadingColor);
        else
            model->setLinkColorHint(LinkObj::DefaultColor);
    }

    a = "linkStyle";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        // Legacy Pre 2.9.518: Style defined in <vymmap> for all levels
        model->setLinkStyle(s);
    }

    a = "linkColor";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        model->setDefaultLinkColor(QColor(s));
    }

    QPen pen(model->mapDesign()->defXLinkPen());
    a = "defXLinkColor";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        if (!s.isEmpty()) {
            col = QColor::fromString(s);
            pen.setColor(col);
        }
    }

    a = "defXLinkWidth";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        int i = s.toInt(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute  " + a);
            return;
        }
        pen.setWidth(i);
    }

    a = "defXLinkPenStyle";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        bool ok;
        Qt::PenStyle ps = penStyle(s, ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute " + a);
            return;
        }
        pen.setStyle(ps);
    }
    model->setDefXLinkPen(pen);

    a = "defXLinkStyleBegin";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        model->setDefXLinkStyleBegin(s);

    a = "defXLinkStyleEnd";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        model->setDefXLinkStyleEnd(s);
}

void VymReader::readSelection()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("select"));

    QString s = xml.readElementText();
    model->select(s);
}

void VymReader::readSetting()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("setting"));

    QString k = xml.attributes().value("key").toString();
    if (!k.isEmpty()) {
        QString v = xml.attributes().value("value").toString();
        if (v.isEmpty()) {
            // Version >= 2.5.0 have value as element text
            v = xml.readElementText();
            if (!v.isEmpty()) {
                settings.setLocalValue( model->getDestPath(), k, v);
            }
        } else {
            // Version < 2.5.0 have value as element attribute
            settings.setLocalValue( model->getDestPath(), k, v);
        }
    }

    if (xml.tokenType() == QXmlStreamReader::EndElement) return;

    if (xml.readNextStartElement()) {
        raiseUnknownElementError();
        return;
    }
}

void VymReader::readAttribute() // FIXME-5 Checking types no longer needed. Check with firefox export/import
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("attribute"));

    QString key = xml.attributes().value("key").toString();
    QString type = xml.attributes().value("type").toString();   // May be empty!
    QVariant val;
    if (lastBranch && !key.isEmpty() && !type.isEmpty()) {
        if (type == "Integer" || type == "int")
            val = xml.attributes().value("value").toInt();
        else if (type == "qlonglong")
            val = xml.attributes().value("value").toLongLong();
        else if (type == "QString" || type == "String")
            val = xml.attributes().value("value").toString();
        else if (type == "QDateTime" || type == "DateTime")
            val = QDateTime::fromString(
                    xml.attributes().value("value").toString(),
                    Qt::ISODate);
        else if (type == "Undefined") {
            val = xml.attributes().value("value").toString();
            qWarning() << "Found attribute type 'Undefined': " << val;
        } else {
            xml.raiseError("readAttribute: Found unknown attribute type");
            return;
        }

        model->setAttribute(lastBranch, key, val);
    }

    if (xml.readNextStartElement()) {
        raiseUnknownElementError();
        return;
    }
}

void VymReader::readBranchOrMapCenter(File::LoadMode loadModeBranch, int insertPosBranch)
{
    Q_ASSERT(xml.isStartElement() &&
            (xml.name() == QLatin1String("branch") ||
             xml.name() == QLatin1String("mapcenter")));

    // Create branch or mapCenter
    if (loadModeBranch == File::NewMap || loadModeBranch == File::DefaultMap)
        lastBranch = model->createBranchWhileLoading(lastBranch);
    else {
        // For Imports create branch at insertPos
        // (Here we only use ImportInsert, replacements already have
        // been done before)
        if (loadModeBranch == File::ImportAdd)
            lastBranch = model->createBranchWhileLoading(lastBranch, insertPos);
    }
    // Prepare parsing heading later
    lastMI = lastBranch;

    readBranchAttr();

    // While going deeper, no longer "import" but just load as usual
    while (xml.readNextStartElement()) {
        if (xml.name() == QLatin1String("heading") ||
            xml.name() == QLatin1String("vymnote") ||
            xml.name() == QLatin1String("htmlnote") ||
            xml.name() == QLatin1String("note"))
            readHeadingOrVymNote();
        else if (xml.name() == QLatin1String("branch")) {
            if (lastBranch && lastBranch->depth() < 3 && false) {   // FIXME-2 Updates during load disabled for now. Too many selectionCHanges
                // Some graphical repainting during loading of map
                lastBranch->updateVisuals();
                model->select(lastBranch);
                model->reposition(true);
            }

            // Going deeper we regard incoming data as "new", no inserts/replacements
            readBranchOrMapCenter(File::NewMap, -1);

        } else if (xml.name() == QLatin1String("frame"))
            readFrame();
        else if (xml.name() == QLatin1String("standardFlag") ||
                 xml.name() == QLatin1String("standardflag"))
            readStandardFlag();
        else if (xml.name() == QLatin1String("userflag"))
            readUserFlag();
        else if (xml.name() == QLatin1String("task"))
            readTask();
        else if (xml.name() == QLatin1String("floatimage"))
            readImage();
        else if (xml.name() == QLatin1String("attribute"))
            readAttribute();
        else if (xml.name() == QLatin1String("xlink"))
            readLegacyXLink();
        else {
            raiseUnknownElementError();
            return;
        }
    }

    // Empty branches may not be scrolled
    // (happens if bookmarks are imported)
    if (lastBranch->isScrolled() && lastBranch->branchCount() == 0)
        lastBranch->unScroll();

    lastBranch->updateVisuals();

    lastBranch = lastBranch->parentBranch();
    lastBranch->setLastSelectedBranch(0);
}

void VymReader::readHeadingOrVymNote()
{
    Q_ASSERT(xml.isStartElement() &&
            (xml.name() == QLatin1String("heading") ||
             xml.name() == QLatin1String("vymnote") ||
             xml.name() == QLatin1String("htmlnote") ||
             xml.name() == QLatin1String("note") ));

    if (!lastMI) {
        xml.raiseError("No lastMI available to set <heading>, <vymnote>, or <htmlnote>.");
        return;
    }

    // Save type for later (after reading html)
    QString textType = xml.name().toString();
    htmldata.clear();
    vymtext.clear();

    QString a = "fonthint";
    QString s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        vymtext.setFontHint(s);

    a = "textMode";
    s = xml.attributes().value(a).toString();
    if (s == "richText")
        vymtext.setRichText(true);
    else
        vymtext.setRichText(false);

    QString href = xml.attributes().value("href").toString();
    a = "text";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        vymtext.setText(unquoteQuotes(s));
    } else if (!href.isEmpty()) {
        // <note> element using an external file with href="..."
        // only for backward compatibility (<1.4.6).
        // Later htmlnote was used and meanwhile vymnote.
        QString fn = parseHREF(href);
        QFile file(fn);

        if (!file.open(QIODevice::ReadOnly)) {
            xml.raiseError("parseVYMHandler::readLegacyNote:  Couldn't load " + fn);
            return;
        }
        QTextStream stream(&file);
        QString lines;
        while (!stream.atEnd()) {
            lines += stream.readLine() + "\n";
        }
        file.close();

        if (lines.contains("<html"))
            vymtext.setRichText(lines);
        else
            vymtext.setPlainText(lines);

        xml.readNext();
        if (xml.tokenType() == QXmlStreamReader::Characters) {
            htmldata += xml.text().toString();
            qWarning() << "Found characters AND href in legacy <note> element. Ignoring characters...";
            // Read to end element. There should be no <html> coming up...
            xml.readNext();
            if (xml.tokenType() != QXmlStreamReader::EndElement) {
                xml.raiseError(QString("Found unexpected element <%1>").arg(xml.name()));
                return;
            }
        }
    } else {
        // Legacy versions did not use the "text" attribute, 
        // but had the content as characters or inline <html>

        bool finished = false;
        while (!finished) {
            xml.readNext();
            if (xml.tokenType() == 1) {
                xml.raiseError(QString("Invalid token  found: " +  xml.errorString()));
                return;
            }
            switch(xml.tokenType())
            {
                case QXmlStreamReader::StartElement:
                    if (xml.name() == QLatin1String("html")) {
                        vymtext.setRichText(true);
                        readHtml();
                    } else {
                        raiseUnknownElementError();
                        return;
                    }
                    break;
                case QXmlStreamReader::EndElement:
                    if (xml.name().toString() != textType) {
                        xml.raiseError("Expected end token: " + textType + " but found " + xml.name().toString());
                        return;
                    }
                    finished = true;
                    break;
                case QXmlStreamReader::Characters:
                    htmldata += xml.text().toString();
                    break;
                default:
                    break;
            }
        }
    } // Legacy text as characters instead of text attribute

    //qDebug() << "xml.name()=" <<xml.name() << " " << xml.tokenString()<<" htmldata: " << htmldata << " vT=" <<vymtext.getText();

    if (versionLowerOrEqual(version, "2.4.99") &&
        htmldata.contains("<html>"))
        // versions before 2.5.0 didn't use CDATA to save richtext
        vymtext.setAutoText(htmldata);
    else {
        // Versions 2.5.0 to 2.7.562  had HTML data encoded as CDATA
        // Later versions use the <heading text="...">  attribute,
        // If both htmldata and vymtext are already available, use the
        // vymtext
        if (vymtext.isEmpty())
            vymtext.setText(htmldata);
    }

    if (textType == "heading") {
        a = "textColor";
        s = xml.attributes().value(a).toString();
        if (!s.isEmpty()) {
            QColor col(s);
            vymtext.setColor(col);

            // For compatibility with <= 2.4.0 set both branch and
            // heading color
            lastMI->setHeadingColor(col);
        }

        lastMI->setHeading(vymtext);
    } else {
        if (lastMI->hasTypeBranch()) {
            if (textType == "vymnote" || textType == "note" || textType == "htmlnote")
                lastMI->setNote(vymtext);
            else {
                qDebug() << "texttype=" << textType << " in line " << xml.lineNumber() << lastMI->headingText();
                xml.raiseError("Trying to set note for lastMI which is not a branch");
                return;
            }
        }
    }


    if (xml.tokenType() == QXmlStreamReader::EndElement) return;

    if (xml.readNextStartElement())
        raiseUnknownElementError();
}

void VymReader::readFrame()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("frame"));

    readFrameAttr();

    if (xml.readNextStartElement()) {
        raiseUnknownElementError();
        return;
    }
}

void VymReader::readLegacyXLink()
{ // only for backward compatibility
  // Before 1.13.2 xlinks used to be part of <branch>
    Q_ASSERT(xml.isStartElement() &&
            xml.name() == QLatin1String("xlink"));

    QString a = "beginID";
    QString s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        TreeItem *beginBI = model->findBySelectString(s);
        a = "endID";
        s = xml.attributes().value(a).toString();
        if (!s.isEmpty()) {
            TreeItem *endBI = model->findBySelectString(s);
            if (beginBI && endBI && beginBI->hasTypeBranch() && endBI->hasTypeBranch()) {
                XLink *xl = new XLink(model);
                xl->setBeginBranch((BranchItem *)beginBI);
                xl->setEndBranch((BranchItem *)endBI);
                model->createXLink(xl);

                QPen pen = xl->getPen();

                a = "color";
                s = xml.attributes().value(a).toString();
                if (!s.isEmpty()) {
                    QColor col;
                    col = QColor::fromString(s);
                    pen.setColor(col);
                }

                a = "width";
                s = xml.attributes().value(a).toString();
                if (!s.isEmpty()) {
                    bool okx;
                    pen.setWidth(s.toInt(&okx, 10));
                }
                xl->setPen(pen);
            }
        }
    }

    if (xml.readNextStartElement()) {
        raiseUnknownElementError();
        return;
    }
}

void VymReader::readStandardFlag()
{
    Q_ASSERT(xml.isStartElement() &&
            (xml.name() == QLatin1String("standardFlag") ||
             xml.name() == QLatin1String("standardflag")));

    QString s = xml.readElementText();
    lastBranch->activateStandardFlagByName(s);
}

void VymReader::readUserFlagDef()
{
    Q_ASSERT(xml.isStartElement() &&
             xml.name() == QLatin1String("userflagdef"));

    QString name;
    QString path;
    QString tooltip;
    QUuid uid;

    QString a = "name";
    QString s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        name = s;

    a = "tooltip";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        tooltip = s;

    a = "uuid";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        uid = QUuid(s);

    Flag *flag;

    a = "href";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        // Setup flag with image
        flag = mainWindow->setupFlag(parseHREF(s), Flag::UserFlag,
                                     name, tooltip, uid);
        if (!flag) {
            xml.raiseError("Couldn't read userflag from: " + s);
            return;
        }
    } else {
        xml.raiseError("readUserFlagDefAttr:  Couldn't read href of flag " + name);
        return;
    }

    a = "group";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        flag->setGroup(s);

    if (xml.readNextStartElement()) {
        raiseUnknownElementError();
        return;
    }
}

void VymReader::readUserFlag()
{
    Q_ASSERT(xml.isStartElement() &&
             xml.name() == QLatin1String("userflag"));

    QString a = "uuid";
    QString s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        lastBranch->toggleFlagByUid(QUuid(s));

    if (xml.readNextStartElement()) {
        raiseUnknownElementError();
        return;
    }
}

void VymReader::readImage()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("floatimage"));

    lastImage = model->createImage(lastBranch);
    lastMI = lastImage;

    QString s;

    s = attributeToString("href");
    if (!s.isEmpty()) {
        // Load Image
        if (!lastImage->load(parseHREF(s))) {
            QMessageBox::warning(0, "Warning: ",
                                 "Couldn't load image\n" +
                                     parseHREF(s));
            lastImage = nullptr;
            return;
        }
    }

    // Scale image
    // scaleX and scaleY are no longer used since 2.7.509 and replaced by
    // scaleFactor
    float x = 1;
    float y = 1;
    bool okx, oky;
    s = attributeToString("scaleX");
    if (!s.isEmpty()) {
        x = s.toFloat(&okx);
        if (!okx) {
            xml.raiseError("Couldn't read scaleX of image");
            return;
        }
    }

    s = attributeToString("scaleY");
    if (!s.isEmpty()) {
        y = s.toFloat(&oky);
        if (!oky) {
            xml.raiseError("Couldn't read scaleY of image");
            return;
        }
    }

    s = attributeToString("scale");
    if (!s.isEmpty()) {
        x = s.toFloat(&okx);
        if (!okx) {
            xml.raiseError("Couldn't read scale of image");
            return;
        }
    }

    s = attributeToString("scaleFactor"); // Legacy: Used in version < 2.9.518
    if (!s.isEmpty()) {
        x = s.toFloat(&okx);
        if (!okx) {
            xml.raiseError("Couldn't read scaleFactor of image");
            return;
        }
    }

    if (x != 1)
        lastImage->setScale(x);

    readOrnamentsAttr();

    s = attributeToString("originalName");
    if (!s.isEmpty())
        lastImage->setOriginalFilename(s);

    while (xml.readNextStartElement()) {
        if (xml.name() == QLatin1String("heading"))
            readHeadingOrVymNote();
        else {
            raiseUnknownElementError();
            return;
        }
    }
}

void VymReader::readXLink()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("xlink"));

    QString s;

    QString beginID = xml.attributes().value("beginID").toString();
    QString endID = xml.attributes().value("endID").toString();
    QString xlinkID = xml.attributes().value("uuid").toString();
    TreeItem *beginBI;
    TreeItem *endBI;

    if (beginID.contains(":")) {
        // Legacy versions <= 2.9.533
        beginBI = model->findBySelectString(beginID);
        endBI = model->findBySelectString(endID);
    } else {
        // Version >= 2.9.534
        beginBI = model->findUuid(QUuid(beginID));
        endBI = model->findUuid(QUuid(endID));
    }

    if (beginBI && endBI && beginBI->hasTypeBranch() && endBI->hasTypeBranch()) {
        XLink *xl = new XLink(model);
        xl->setBeginBranch((BranchItem *)beginBI);
        xl->setEndBranch((BranchItem *)endBI);

        model->createXLink(xl);

        QPen pen = xl->getPen();
        bool ok;
        s = attributeToString("color");
        if (!s.isEmpty())
            pen.setColor(QColor(s));

        s = attributeToString("type");
        if (!s.isEmpty())
            xl->setLinkType(s);

        s = attributeToString("width");
        if (!s.isEmpty())
            pen.setWidth(s.toInt(&ok, 10));

        s = attributeToString("penstyle");
        if (!s.isEmpty())
            pen.setStyle(penStyle(s, ok));

        xl->setPen(pen);

        s = attributeToString("styleBegin");
        if (!s.isEmpty())
            xl->setStyleBegin(s);

        s = attributeToString("styleEnd");
        if (!s.isEmpty())
            xl->setStyleEnd(s);

        /* FIXME-4 better set control points via VymModel for saveState
         * (no longer include XLO then...)
        */

        XLinkObj *xlo = xl->getXLinkObj();
        s = attributeToString("c0");
        if (xlo && !s.isEmpty()) {
            QPointF p = point(s, ok);
            if (ok)
                xlo->setC0(p);
        }
        s = attributeToString("c1");
        if (xlo && !s.isEmpty()) {
            QPointF p = point(s, ok);
            if (ok)
                xlo->setC1(p);
        }
        s = attributeToString("uuid");
        if (!s.isEmpty())
            xl->setUuid(s);

    }

    if (xml.readNextStartElement())
        raiseUnknownElementError();
}

void VymReader::readSlide()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("slide"));

    QStringList scriptlines; // FIXME-5 needed for switching to inScript
                             // Most attributes are obsolete with inScript

    QString s;
    bool ok;
    qreal r;
    int i;

    if (!(contentFilter & SlideContent)) {
        lastSlide = model->addSlide();

        s = attributeToString("name");
        if (!s.isEmpty())
            lastSlide->setName(s);

        s = attributeToString("zoom");
        if (!s.isEmpty()) {
            r = s.toDouble(&ok);
            if (ok) scriptlines.append(QString("setZoom(%1)").arg(r));
        }

        s = attributeToString("rotation");
        if (!s.isEmpty()) {
            r = s.toDouble(&ok);
            if (ok) scriptlines.append(QString("setRotation(%1)").arg(r));
        }

        s = attributeToString("duration");
        if (!s.isEmpty()) {
            i = s.toInt(&ok);
            if (ok) scriptlines.append(QString("setAnimCurve(%1)").arg(i));
        }

        s = attributeToString("curve");
        if (!s.isEmpty()) {
            i = s.toInt(&ok);
            if (ok) scriptlines.append(QString("setAnimDuration(%1)").arg(i));
        }

        s = attributeToString("mapitem");
        if (!s.isEmpty()) {
            TreeItem *ti = model->findUuid(QUuid(s));
            if (ti) scriptlines.append(
                QString("centerOnID(\"%1\")").arg(ti->getUuid().toString()));
        }

        // Up to 2.9.0 at least only inScript seems to be used
        s = attributeToString("inScript");
        if (!s.isEmpty())
            lastSlide->setInScript(unquoteMeta(s));
        else
            lastSlide->setInScript(unquoteMeta(scriptlines.join(";\n"))); // FIXME-5 unquote needed? Not used currently anyway

        s = attributeToString("outScript");
        if (!s.isEmpty())
            lastSlide->setOutScript(unquoteMeta(s));
    }

    if (xml.readNextStartElement())
        raiseUnknownElementError();
}

void VymReader::readTask()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("task"));

    if (lastBranch) {
        lastTask = taskModel->createTask(lastBranch);

        QString s = attributeToString("status");
        if (!s.isEmpty())
            lastTask->setStatus(s);

        s = attributeToString("awake");
        if (!s.isEmpty())
            lastTask->setAwake(s);

        s = attributeToString("date_creation");
        if (!s.isEmpty())
            lastTask->setDateCreation(s);

        s = attributeToString("date_modification");
        if (!s.isEmpty())
            lastTask->setDateModification(s);

        s = attributeToString("date_sleep");
        if (!s.isEmpty()) {
            if (!lastTask->setDateSleep(s)) {
                xml.raiseError("Could not set sleep time for task: " + s);
                return;
            }
        }
        s = attributeToString("prio_delta");
        if (!s.isEmpty()) {
            bool ok;
            int d = s.toInt(&ok);
            if (ok)
                lastTask->setPriorityDelta(d);
        }
    }

    if (xml.readNextStartElement())
        raiseUnknownElementError();
}

void VymReader::readVymMapAttr()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("vymmap"));

    QString a = "author";
    QString s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        model->setMapAuthor(s);

    a = "title";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        model->setMapTitle(s);

    a = "comment";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        model->setMapComment(unquoteMeta(s));

    a = "branchCount";
    s = xml.attributes().value(a).toString();
    int i;
    bool ok;
    if (!s.isEmpty()) {
        i = s.toInt(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute " + a);
            return;
        }
        branchesTotal = i;
    }
    if (branchesTotal > 10) {
        useProgress = true;
        mainWindow->setProgressMaximum(branchesTotal);
    }

    qreal r;
    a = "mapZoomFactor";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        r = s.toDouble(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute" + a);
            return;
        }
        model->setMapZoomFactor(r);
    }

    a = "mapRotation";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        r = s.toDouble(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute " + a);
            return;
        }
        model->setMapRotation(r);
    }

    readMapDesignCompatibleAttributes();
}

void VymReader::readBranchAttr()
{
    Q_ASSERT(xml.isStartElement() && (
            xml.name() == QLatin1String("branch") ||
            xml.name() == QLatin1String("mapcenter")));

    branchesCounter++;
    if (useProgress)
        mainWindow->addProgressValue((float)branchesCounter / branchesTotal);

    lastMI = lastBranch;
    BranchContainer *lastBC = lastBranch->getBranchContainer();

    readOrnamentsAttr();

    QString a = "scrolled";
    QString s = xml.attributes().value(a).toString();
    if (!s.isEmpty())
        lastBranch->toggleScroll();

    a = "incImgV";
    s = xml.attributes().value(a).toString();
    if (s == "true")      // pre 2.9 feature
        lastBranch->setImagesLayout("FloatingBounded");

    a = "incImgH";
    s = xml.attributes().value(a).toString();
    if (s == "true")      // pre 2.9 feature
        lastBranch->setImagesLayout("FloatingBounded");

    a = "childrenFreePos";
    s = xml.attributes().value(a).toString();
    if (s == "true")      // pre 2.9 feature
        lastBranch->setBranchesLayout("FloatingBounded");

    // Container layouts
    a = "branchesLayout";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        lastBranch->setBranchesLayout(s);
        lastBC->branchesContainerAutoLayout = false;
    }

    a = "imagesLayout";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        lastBC->imagesContainerAutoLayout = false;
        lastBranch->setImagesLayout(s);
    }

    bool ok;
    qreal r;
    a = "rotHeading";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        r = s.toDouble(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute " + a);
            return;
        }
        lastBC->setRotationsAutoDesign(false, false);
        lastBC->setRotationHeading(r);
    }

    a = "rotSubtree";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        r = s.toDouble(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute " + a);
            return;
        }
        lastBC->setRotationsAutoDesign(false, false);
        lastBC->setRotationSubtree(r);
    }

    a = "scaleHeading";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        r = s.toDouble(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute " + a);
            return;
        }
        lastBC->setScaleAutoDesign(false, false);
        lastBC->setScaleHeading(r);
    }

    a = "scaleSubtree";
    s = xml.attributes().value(a).toString();
    if (!s.isEmpty()) {
        r = s.toDouble(&ok);
        if (!ok) {
            xml.raiseError("Could not parse attribute " + a);
            return;
        }
        lastBC->setScaleAutoDesign(false, false);
        lastBC->setScaleSubtree(r);
    }
}

void VymReader::readOrnamentsAttr()
{
    Q_ASSERT(xml.isStartElement() && (
            xml.name() == QLatin1String("branch") ||
            xml.name() == QLatin1String("mapcenter") ||
            xml.name() == QLatin1String("floatimage")));

    float x, y;
    bool okx, oky;

    QString s = attributeToString("posX");
    QString t = attributeToString("posY");
    if (!s.isEmpty() || !t.isEmpty()) {
        x = s.toFloat(&okx);
        y = t.toFloat(&oky);
        if (okx && oky)
            lastMI->setPos(QPointF(x, y));
        else {
            xml.raiseError("Couldn't read position of item");
            return;
        }
    }

    // Only left for compatibility with versions < 2.9.500
    s = attributeToString("relPosX");
    t = attributeToString("relPosY");
    if (!s.isEmpty() || !t.isEmpty()) {
        x = s.toFloat(&okx);
        y = t.toFloat(&oky);
        if (okx && oky)
            lastMI->setPos(QPointF(x, y));
        else {
            xml.raiseError("Couldn't read relative position of item");
            return;
        }
    }

    // Only left for compatibility with versions < 2.9.500
    s = attributeToString("absPosX");
    t = attributeToString("absPosY");
    if (!s.isEmpty() || !t.isEmpty()) {
        x = s.toFloat(&okx);
        y = t.toFloat(&oky);
        if (okx && oky)
            lastMI->setPos(QPointF(x, y));
        else {
            xml.raiseError("Couldn't read absolute position of item");
            return;
        }
    }

    s = attributeToString("url");
    if (!s.isEmpty())
        lastMI->setUrl(s);
    s = attributeToString("vymLink");
    if (!s.isEmpty())
        lastMI->setVymLink(s);
    s = attributeToString("hideInExport");
    if (!s.isEmpty())
        if (s == "true")
            lastMI->setHideTemporary(true);

    s = attributeToString("hideLink");
    if (!s.isEmpty()) {
        if (s == "true")
            lastMI->setHideLinkUnselected(true);
        else
            lastMI->setHideLinkUnselected(false);
    }

    s = attributeToString("localTarget");
    if (!s.isEmpty())
        if (s == "true")
            lastMI->toggleTarget();

    s = attributeToString("uuid");
    if (!s.isEmpty()) {
        // While pasting, check for existing UUID
        if (loadMode == File::ImportAdd || loadMode == File::ImportReplace) {
            bool x = model->findUuid(QUuid(s));
            if (!x)
                // Only set Uuid if not adding replacing in map - then duplicate Uuids might cause problems
                // In testing one map will import itself - no new Uuids then.
                lastMI->setUuid(s);
        } else
            lastMI->setUuid(s);
    }

    s = attributeToString("colWidth");
    if (!s.isEmpty()) {
        int i = s.toInt(&okx);
        if (okx) {
            lastBranch->getBranchContainer()->setColumnWidthAutoDesign(false);
            lastBranch->getBranchContainer()->setColumnWidth(i);
        } else {
            xml.raiseError("Couldn't read colWidth of branch");
            return;
        }
    }
}

void VymReader::readFrameAttr()
{
    Q_ASSERT(xml.isStartElement() && xml.name() == QLatin1String("frame"));

    if (lastBranch) {
        BranchContainer *bc = lastBranch->getBranchContainer();

        bool useInnerFrame = true;
        // useInnerFrame was introduced in 2.9.506
        // It replaces the previous "includeChildren" attribute
        bool ok = false;
        QString a = "includeChildren";
        QString s = attributeToString(a);
        if (!s.isEmpty()) {
            ok = true;
            if (s == "true")
                useInnerFrame = false;
        }

        a = "frameUsage";
        s = attributeToString(a);
        if (s == "innerFrame") {
            useInnerFrame = true;
            ok = true;
        } else if (s == "outerFrame") {
            useInnerFrame = false;
            ok =true;
        }

        if (!ok) {
            // In pre 3.0.0 versions "includeChildren" was optional for frame.
            // Without "includeCHildren" and without the later "frameUsage, assume
            // a frame is an innerFrame:
            useInnerFrame = true;
        }


        // We will override AutoDesign
        // bc->setFrameAutoDesign(useInnerFrame, false);

        a = "autoDesign";
        s = attributeToString(a);
        if (s == "true")
            // bc->setFrameAutoDesign(useInnerFrame, true);
            // Set all frame parameters via model
            model->setFrameAutoDesign(useInnerFrame, true, lastBranch);
        else {
            a = "frameType";
            s = attributeToString(a);
            if (s.isEmpty())
                s = "Rectangle";

            // Start with setting/creating frame. 
            // assuming that there is no "NoFrame" frame in the xml
            bc->setFrameType(useInnerFrame, s);

            bc->setFrameAutoDesign(useInnerFrame, false);

            a = "penColor";
            s = attributeToString(a);
            if (!s.isEmpty())
                bc->setFramePenColor(useInnerFrame, s);

            a = "brushColor";
            s = attributeToString(a);
            if (!s.isEmpty())
                bc->setFrameBrushColor(useInnerFrame, s);

            int i;
            a = "padding";
            s = attributeToString(a);
            i = s.toInt(&ok);
            if (ok)
                bc->setFramePadding(useInnerFrame, i);

            a = "borderWidth";
            s = attributeToString(a);
            i = s.toInt(&ok);
            if (ok)
                bc->setFramePenWidth(useInnerFrame, i);

            a = "penWidth";
            s = attributeToString(a);
            i = s.toInt(&ok);
            if (ok)
                bc->setFramePenWidth(useInnerFrame, i);
        }
    }   // lastBranch available
}

