/* This file is part of the KDE project

   SPDX-FileCopyrightText: 2013 Inge Wallin <inge@lysator.liu.se>

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

// Own
#include "KoXmlStreamReader.h"

// Qt
#include <QSet>
#include <QString>
#include <QStringList>

#include "Odf2Debug.h"

// ================================================================
//             class KoXmlStreamReader and Private class

class Q_DECL_HIDDEN KoXmlStreamReader::Private
{
public:
    Private(KoXmlStreamReader *qq);
    ~Private();

    void clear();

    // Checks the soundness of the whole document.  Must be called
    // after the namespace declarations are read.
    void checkSoundness();

    // Builds a prefix for the current element. Only called if the
    // document is unsound.
    QStringView buildNewPrefix();
    // Builds a qualified name for the current element. Only called if
    // the document is unsound.
    QStringView buildQName();

    KoXmlStreamReader *q; // The owner object;

    bool isSound; // True if the document is sound (see the class doc for details)
    bool isChecked; // True if the soundness is checked

    // These are filled using the addEx{pected,tra}Namespace() functions.
    QHash<QString, QString> expectedNamespaces; // nsUri, prefix
    QHash<QString, QString> extraNamespaces; // nsUri, prefix

    // This is only used when a document is unsound, but is always created.
    QHash<QString, QString> prefixes; // nsUri, prefix

    // If the document is unsound, we need to build the prefix and
    // qualified names from a prefix that we get from "prefixes" and
    // the actual name.  But we return a QStringView and not QString so
    // we need to make sure that the stringref is valid until this
    // KoXmlStreamReader is destructed.  So we use this QSet as a
    // cache of all constructed qualified names that we ever generate.
    QSet<QString> prefixCache;
    QSet<QString> qualifiedNamesCache;
};

KoXmlStreamReader::Private::Private(KoXmlStreamReader *qq)
    : q(qq)
{
    clear();
}

KoXmlStreamReader::Private::~Private() = default;

void KoXmlStreamReader::Private::clear()
{
    // The initial state is Unsound and Unchecked.  The check may
    // switch it to Sound.  See the class documentation for an
    // explanation of those terms.
    isSound = false;
    isChecked = false;

    expectedNamespaces.clear();
    extraNamespaces.clear();

    prefixes.clear();
    prefixCache.clear();
    qualifiedNamesCache.clear();
}

void KoXmlStreamReader::Private::checkSoundness()
{
    isSound = true;

    // Temp values
    QStringList namespacesToFix; // List of namespaces we need to create a unique prefix for
    QSet<QString> usedPrefixes;

    // Initialize by setting all expected prefixes and all extra ones.
    prefixes.clear();
    foreach (const QString &nsUri, expectedNamespaces.keys()) {
        QString prefix = expectedNamespaces.value(nsUri);

        prefixes.insert(nsUri, prefix);
        usedPrefixes.insert(prefix);
    }
    foreach (const QString &nsUri, extraNamespaces.keys()) {
        QString prefix = extraNamespaces.value(nsUri);

        prefixes.insert(nsUri, prefix);
        usedPrefixes.insert(prefix);
    }

    // The document is "Sound" if for all the declared namespaces in
    // the document, the prefix is the same as the one in the expected
    // namespaces for the same namespace URI.
    //
    // If it is not Sound, then we need to rewrite the prefixes for
    // the qualified names when the caller wants to access them.
    // Hopefully this only happens once in a million documents (and it
    // would be interesting to know which application created such a
    // strange beast).
    //
    QXmlStreamNamespaceDeclarations nsDeclarations = q->QXmlStreamReader::namespaceDeclarations();
    foreach (const QXmlStreamNamespaceDeclaration &decl, nsDeclarations) {
        QString nsUri(decl.namespaceUri().toString());
        QString prefix(decl.prefix().toString());

        if (prefixes.contains(nsUri)) {
            if (prefix == prefixes.value(nsUri)) {
                // 1. nsUri = expected nsUri AND prefix = expected prefix:
                //
                // Soundness is not disturbed. Let's continue with the next declaration.
                continue;
            } else {
                // 2. nsUri = expected nsUri AND prefix != expected prefix:
                //
                // Document is not sound but we don't need to do
                // anything else; the expected prefix is already in
                // prefixes[] and the prefix will be rewritten to the
                // expected one when used later in the document.
                isSound = false;
                continue;
            }
        } else {
            // 3. nsUri is not among the expected nsUri's
            //
            // Let's check if the prefix is unique or if it already
            // exists among the expected ones.  If it is unique the
            // soundness is not affected, otherwise it is unsound.
            if (usedPrefixes.contains(prefix)) {
                // Yes, the prefix is used for another namespace among
                // the expected ones.  Let's store this namespace for
                // now and create a unique, non-"expected" prefix
                // later when all namespaces and prefixes are known.
                isSound = false;
                namespacesToFix.append(nsUri);
            } else {
                prefixes.insert(nsUri, prefix);
                usedPrefixes.insert(prefix);
            }
        }
    }

    // debugOdf2 << "namespaces to fix:" << namespacesToFix;

    // Finally, if necessary, create unique prefixes for namespaces
    // that are found to use one of the expected prefixes.  It doesn't
    // much matter what we come up with here since these will have to
    // be accessed through namespaceUri() anyway.
    int number = 1;
    foreach (const QString &ns, namespacesToFix) {
        bool ok = false;
        QString pfx;
        while (!ok) {
            pfx = QString("pfx%1").arg(number++);
            if (!usedPrefixes.contains(pfx)) {
                ok = true;
            }
        }
        prefixes.insert(ns, pfx);
    }

    // debugOdf2 << "Document soundness:" << isSound;
    // debugOdf2 << "prefixes:" << prefixes;

    isChecked = true;
}

QStringView KoXmlStreamReader::Private::buildNewPrefix()
{
    if (!isChecked) {
        checkSoundness(); // Sets isChecked and isSound;
    }

    if (isSound) {
        return q->prefix();
    }

    // FIXME: Handle undeclared prefixes.  (Is that even legal?)
    // QString nsUri = q->QXmlStreamReader::namespaceUri().toString();
    QString newPrefix = prefixes.value(q->QXmlStreamReader::namespaceUri().toString());

    // The following code is because prefix() returns a QStringView,
    // not a QString.  So we need to make sure that the QString that
    // it references stays valid until the end of the document is
    // parsed.  We do this by storing all prefix names that are
    // accessed in a QSet<QString> and return a QStringView that
    // references the copy in the set.  Ugly but effective.
#if 1
    if (!prefixCache.contains(newPrefix)) {
        // FIXME: Is there a way to do this at the same time as the
        // check without creating a double copy if it was already inserted?
        prefixCache.insert(newPrefix);
    }

    QSet<QString>::ConstIterator it = prefixCache.find(newPrefix);
#else
    // This should work too but it's unclear from the documentation
    // what is returned if it was already in the set.  It only
    // mentions "the inserted item"
    QSet<QString>::ConstIterator it = prefixCace.insert(newPrefix);
#endif

    // Will always succeed since we entered it if it didn't exist already.
    return QStringView{*it}.left(-1);
}

QStringView KoXmlStreamReader::Private::buildQName()
{
    if (!isChecked) {
        checkSoundness(); // Sets isChecked and isSound;
    }

    if (isSound) {
        return q->qualifiedName();
    }

    // FIXME: Handle undeclared prefixes.  (Is that even legal?)
    // QString nsUri = q->QXmlStreamReader::namespaceUri().toString();
    QString qualifiedName = prefixes.value(q->QXmlStreamReader::namespaceUri().toString()) + ':' + q->QXmlStreamReader::name().toString();

    // The following code is because qualifiedName() returns a
    // QStringView, not a QString.  So we need to make sure that the
    // QString that it references stays valid until the end of the
    // document is parsed.  We do this by storing all qualified names
    // that are accessed in a QSet<QString> and return a QStringView
    // that references the copy in the set.  Ugly but effective.
#if 1
    if (!qualifiedNamesCache.contains(qualifiedName)) {
        // FIXME: Is there a way to do this at the same time as the
        // check without creating a double copy if it was already inserted?
        qualifiedNamesCache.insert(qualifiedName);
    }

    QSet<QString>::ConstIterator it = qualifiedNamesCache.find(qualifiedName);
#else
    // This should work too but it's unclear from the documentation
    // what is returned if it was already in the set.  It only
    // mentions "the inserted item"
    QSet<QString>::ConstIterator it = qualifiedNamesCache.insert(qualifiedName);
#endif

    // Will always succeed since we entered it if it didn't exist already.
    return QStringView(*it).left(-1);
}

// ----------------------------------------------------------------
//                     class KoXmlStreamReader

KoXmlStreamReader::KoXmlStreamReader()
    : QXmlStreamReader()
    , d(new KoXmlStreamReader::Private(this))
{
}

KoXmlStreamReader::KoXmlStreamReader(QIODevice *device)
    : QXmlStreamReader(device)
    , d(new KoXmlStreamReader::Private(this))
{
}

KoXmlStreamReader::KoXmlStreamReader(const QByteArray &data)
    : QXmlStreamReader(data)
    , d(new KoXmlStreamReader::Private(this))
{
}

KoXmlStreamReader::KoXmlStreamReader(const QString &data)
    : QXmlStreamReader(data)
    , d(new KoXmlStreamReader::Private(this))
{
}

KoXmlStreamReader::KoXmlStreamReader(const char *data)
    : QXmlStreamReader(data)
    , d(new KoXmlStreamReader::Private(this))
{
}

KoXmlStreamReader::~KoXmlStreamReader()
{
    delete d;
}

void KoXmlStreamReader::clear()
{
    d->clear();

    QXmlStreamReader::clear();
}

void KoXmlStreamReader::addExpectedNamespace(const QString &prefix, const QString &namespaceUri)
{
    d->expectedNamespaces.insert(namespaceUri, prefix);

    d->isChecked = false;
    d->isSound = false;
}

void KoXmlStreamReader::addExtraNamespace(const QString &prefix, const QString &namespaceUri)
{
    d->extraNamespaces.insert(namespaceUri, prefix);

    d->isChecked = false;
    d->isSound = false;
}

// ----------------------------------------------------------------
//                 Reimplemented from QXmlStreamReader

// Should these be made inline?  that would make it very fast at the
// cost of a possibly unstable API.

QStringView KoXmlStreamReader::prefix() const
{
    return d->isSound ? QXmlStreamReader::prefix() : d->buildNewPrefix();
}

QStringView KoXmlStreamReader::qualifiedName() const
{
    return d->isSound ? QXmlStreamReader::qualifiedName() : d->buildQName();
}

void KoXmlStreamReader::setDevice(QIODevice *device)
{
    // Setting the device clears the checked status since we don't know
    // which namespace declarations that will be in the new document.
    d->isChecked = false;
    d->isSound = false;

    QXmlStreamReader::setDevice(device);
}

KoXmlStreamAttributes KoXmlStreamReader::attributes() const
{
    QXmlStreamAttributes qAttrs = QXmlStreamReader::attributes();
    KoXmlStreamAttributes retval = KoXmlStreamAttributes(this, qAttrs);

    return retval;
}

// ----------------------------------------------------------------
//                         private functions

bool KoXmlStreamReader::isSound() const
{
    return d->isSound;
}

// ================================================================
//             class KoXmlStreamAttribute and Private class

class Q_DECL_HIDDEN KoXmlStreamAttribute::Private
{
public:
    Private(const QXmlStreamAttribute *attr, const KoXmlStreamReader *r);
    Private(const KoXmlStreamAttribute::Private &other);
    ~Private();

    void generateQName();

    const QXmlStreamAttribute *qAttr;
    const KoXmlStreamReader *reader;

    // These two are only used when the document is unsound
    QString qName; // qualifiedName if it was ever asked for.
    int prefixLen; // the length of the prefix alone. -1 means uninitialized.
};

KoXmlStreamAttribute::Private::Private(const QXmlStreamAttribute *attr, const KoXmlStreamReader *r)
    : qAttr(attr)
    , reader(r)
    , qName()
    , prefixLen(-1)
{
}

KoXmlStreamAttribute::Private::Private(const KoXmlStreamAttribute::Private &other)

    = default;

KoXmlStreamAttribute::Private::~Private() = default;

void KoXmlStreamAttribute::Private::generateQName()
{
    qName = reader->d->prefixes.value(qAttr->namespaceUri().toString());
    prefixLen = qName.size();

    qName += QLatin1Char(':') + qAttr->name();
}

// ----------------------------------------------------------------

KoXmlStreamAttribute::KoXmlStreamAttribute()
    : d(new KoXmlStreamAttribute::Private(nullptr, nullptr))
{
    // debugOdf2 << "default constructor called";
}

KoXmlStreamAttribute::KoXmlStreamAttribute(const QXmlStreamAttribute *attr, const KoXmlStreamReader *reader)
    : d(new KoXmlStreamAttribute::Private(attr, reader))
{
    // debugOdf2 << "normal constructor called";
}

KoXmlStreamAttribute::KoXmlStreamAttribute(const KoXmlStreamAttribute &other)
    : d(new KoXmlStreamAttribute::Private(*other.d))
{
    // debugOdf2 << "copy constructor called";
}

KoXmlStreamAttribute::~KoXmlStreamAttribute()
{
    delete d;
}

bool KoXmlStreamAttribute::isDefault() const
{
    return d->qAttr->isDefault();
}

QStringView KoXmlStreamAttribute::name() const
{
    return d->qAttr->name();
}

QStringView KoXmlStreamAttribute::namespaceUri() const
{
    return d->qAttr->namespaceUri();
}

QStringView KoXmlStreamAttribute::prefix() const
{
    if (d->reader->isSound()) {
        return d->qAttr->prefix();
    }

    if (d->prefixLen == -1) {
        d->generateQName();
    }

    return QStringView{d->qName}.left(d->prefixLen);
}

QStringView KoXmlStreamAttribute::qualifiedName() const
{
    if (d->reader->isSound()) {
        return d->qAttr->qualifiedName();
    }

    if (d->prefixLen == -1) {
        d->generateQName();
    }

    return QStringView{d->qName}.left(-1);
}

QStringView KoXmlStreamAttribute::value() const
{
    return d->qAttr->value();
}

bool KoXmlStreamAttribute::operator==(const KoXmlStreamAttribute &other) const
{
    return d->qAttr == other.d->qAttr;
}

bool KoXmlStreamAttribute::operator!=(const KoXmlStreamAttribute &other) const
{
    return d->qAttr != other.d->qAttr;
}

KoXmlStreamAttribute &KoXmlStreamAttribute::operator=(const KoXmlStreamAttribute &other)
{
    d->qAttr = other.d->qAttr;
    d->reader = other.d->reader;
    d->qName = other.d->qName;
    d->prefixLen = other.d->prefixLen;

    return *this;
}

// ================================================================
//             class KoXmlStreamAttributes and Private class

class Q_DECL_HIDDEN KoXmlStreamAttributes::Private : public QSharedData
{
public:
    Private(const KoXmlStreamReader *r, const QXmlStreamAttributes &qa);
    ~Private();

    const KoXmlStreamReader *reader;
    QList<KoXmlStreamAttribute> koAttrs;
    const QXmlStreamAttributes qAttrs; // We need the Q attributes to live while this class lives.
};

KoXmlStreamAttributes::Private::Private(const KoXmlStreamReader *r, const QXmlStreamAttributes &qa)
    : reader(r)
    , koAttrs(qa.size())
    , qAttrs(qa)
{
}

KoXmlStreamAttributes::Private::~Private() = default;

// ----------------------------------------------------------------

KoXmlStreamAttributes::KoXmlStreamAttributes(const KoXmlStreamReader *r, const QXmlStreamAttributes &qAttrs)
    : d(new KoXmlStreamAttributes::Private(r, qAttrs))
{
    for (int i = 0; i < qAttrs.size(); ++i) {
        d->koAttrs[i] = KoXmlStreamAttribute(&qAttrs[i], d->reader);
    }
}

KoXmlStreamAttributes::KoXmlStreamAttributes(const KoXmlStreamAttributes &other)

    = default;

KoXmlStreamAttributes::~KoXmlStreamAttributes()
{
    // No delete because d is a QSharedDataPointer;
}

KoXmlStreamAttributes &KoXmlStreamAttributes::operator=(const KoXmlStreamAttributes &other) = default;

// Relevant parts of the QVector API
const KoXmlStreamAttribute &KoXmlStreamAttributes::at(int i) const
{
    return d->koAttrs[i];
}

int KoXmlStreamAttributes::size() const
{
    return d->koAttrs.size();
}

KoXmlStreamAttribute KoXmlStreamAttributes::value(int i) const
{
    return d->koAttrs.value(i);
}

const KoXmlStreamAttribute &KoXmlStreamAttributes::operator[](int i) const
{
    return d->koAttrs[i]; //.operator[](i);
}

KoXmlStreamAttributes::const_iterator KoXmlStreamAttributes::begin() const
{
    return const_iterator(d->koAttrs.begin());
}

KoXmlStreamAttributes::const_iterator KoXmlStreamAttributes::end() const
{
    return const_iterator(d->koAttrs.end());
}

// reimplemented from QXmlStreamAttributes
bool KoXmlStreamAttributes::hasAttribute(const QString &qualifiedName) const
{
    for (int i = 0; i < size(); ++i) {
        // This compares with the expected name always.
        if (qualifiedName == this->at(i).qualifiedName()) {
            return true;
        }
    }

    return false;
}

bool KoXmlStreamAttributes::hasAttribute(const QLatin1String &qualifiedName) const
{
#if 1
    const QString qName(qualifiedName);
    return hasAttribute(qName);
#else
    for (int i = 0; i < size(); ++i) {
        if (qualifiedName == this->at(i).qualifiedName()) {
            return true;
        }
    }

    return false;
#endif
}

QStringView KoXmlStreamAttributes::value(const QString &qualifiedName) const
{
    for (int i = 0; i < size(); ++i) {
        if (qualifiedName == this->at(i).qualifiedName()) {
            return this->at(i).value();
        }
    }

    return {};
}

QStringView KoXmlStreamAttributes::value(const QLatin1String &qualifiedName) const
{
    // FIXME: Find faster way.
    const QString qName(qualifiedName);
    return value(qName);
}

// ================================================================
//                         non-class functions

void prepareForOdf(KoXmlStreamReader &reader)
{
    // This list of namespaces is taken from KoXmlNs.cpp
    // Maybe not all of them are expected in an ODF document?
    reader.addExpectedNamespace("office", "urn:oasis:names:tc:opendocument:xmlns:office:1.0");
    reader.addExpectedNamespace("meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0");
    reader.addExpectedNamespace("config", "urn:oasis:names:tc:opendocument:xmlns:config:1.0");
    reader.addExpectedNamespace("text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0");
    reader.addExpectedNamespace("table", "urn:oasis:names:tc:opendocument:xmlns:table:1.0");
    reader.addExpectedNamespace("draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0");
    reader.addExpectedNamespace("presentation", "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0");
    reader.addExpectedNamespace("dr3d", "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0");
    reader.addExpectedNamespace("chart", "urn:oasis:names:tc:opendocument:xmlns:chart:1.0");
    reader.addExpectedNamespace("form", "urn:oasis:names:tc:opendocument:xmlns:form:1.0");
    reader.addExpectedNamespace("script", "urn:oasis:names:tc:opendocument:xmlns:script:1.0");
    reader.addExpectedNamespace("style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0");
    reader.addExpectedNamespace("number", "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0");
    reader.addExpectedNamespace("manifest", "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0");
    reader.addExpectedNamespace("anim", "urn:oasis:names:tc:opendocument:xmlns:animation:1.0");

    reader.addExpectedNamespace("math", "http://www.w3.org/1998/Math/MathML");
    reader.addExpectedNamespace("svg", "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0");
    reader.addExpectedNamespace("fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0");
    reader.addExpectedNamespace("dc", "http://purl.org/dc/elements/1.1/");
    reader.addExpectedNamespace("xlink", "http://www.w3.org/1999/xlink");
    reader.addExpectedNamespace("VL", "http://openoffice.org/2001/versions-list");
    reader.addExpectedNamespace("smil", "urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0");
    reader.addExpectedNamespace("xhtml", "http://www.w3.org/1999/xhtml");
    reader.addExpectedNamespace("xml", "http://www.w3.org/XML/1998/namespace");

    reader.addExpectedNamespace("calligra", "http://www.calligra.org/2005/");
    reader.addExpectedNamespace("officeooo", "http://openoffice.org/2009/office");
    reader.addExpectedNamespace("ooo", "http://openoffice.org/2004/office");

    reader.addExpectedNamespace("delta", "http://www.deltaxml.com/ns/track-changes/delta-namespace");
    reader.addExpectedNamespace("split", "http://www.deltaxml.com/ns/track-changes/split-namespace");
    reader.addExpectedNamespace("ac", "http://www.deltaxml.com/ns/track-changes/attribute-change-namespace");

    // This list of namespaces is taken from KoXmlReader::fixNamespace()
    // They were generated by old versions of OpenOffice.org.
    reader.addExtraNamespace("office", "http://openoffice.org/2000/office");
    reader.addExtraNamespace("text", "http://openoffice.org/2000/text");
    reader.addExtraNamespace("style", "http://openoffice.org/2000/style");
    reader.addExtraNamespace("fo", "http://www.w3.org/1999/XSL/Format");
    reader.addExtraNamespace("table", "http://openoffice.org/2000/table");
    reader.addExtraNamespace("drawing", "http://openoffice.org/2000/drawing");
    reader.addExtraNamespace("datastyle", "http://openoffice.org/2000/datastyle");
    reader.addExtraNamespace("svg", "http://www.w3.org/2000/svg");
    reader.addExtraNamespace("chart", "http://openoffice.org/2000/chart");
    reader.addExtraNamespace("dr3d", "http://openoffice.org/2000/dr3d");
    reader.addExtraNamespace("form", "http://openoffice.org/2000/form");
    reader.addExtraNamespace("script", "http://openoffice.org/2000/script");
    reader.addExtraNamespace("meta", "http://openoffice.org/2000/meta");
    reader.addExtraNamespace("config", "http://openoffice.org/2001/config");
    reader.addExtraNamespace("pres", "http://openoffice.org/2000/presentation");
    reader.addExtraNamespace("manifest", "http://openoffice.org/2001/manifest");
}
