/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Assistant of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qhelpgenerator_p.h"
#include "qhelpdatainterface_p.h"

#include <QtCore/QtMath>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#include <QtCore/QSet>
#include <QtCore/QVariant>
#include <QtCore/QDateTime>
#include <QtCore/QTextCodec>
#include <QtCore/QDataStream>
#include <QtSql/QSqlQuery>

QT_BEGIN_NAMESPACE

class QHelpGeneratorPrivate
{
public:
    QString error;
    QSqlQuery *query = nullptr;

    int namespaceId = -1;
    int virtualFolderId = -1;

    QMap<QString, int> fileMap;
    QMap<int, QSet<int> > fileFilterMap;

    double progress;
    double oldProgress;
    double contentStep;
    double fileStep;
    double indexStep;
};

/*!
    \internal
    \class QHelpGenerator
    \since 4.4
    \brief The QHelpGenerator class generates a new
    Qt compressed help file (.qch).

    The help generator takes a help data structure as
    input for generating a new Qt compressed help files. Since
    the generation may takes some time, the generator emits
    various signals to inform about its current state.
*/

/*!
    \fn void QHelpGenerator::statusChanged(const QString &msg)

    This signal is emitted when the generation status changes.
    The status is basically a specific task like inserting
    files or building up the keyword index. The parameter
    \a msg contains the detailed status description.
*/

/*!
    \fn void QHelpGenerator::progressChanged(double progress)

    This signal is emitted when the progress changes. The
    \a progress ranges from 0 to 100.
*/

/*!
    \fn void QHelpGenerator::warning(const QString &msg)

    This signal is emitted when a non critical error occurs,
    e.g. when a referenced file cannot be found. \a msg
    contains the exact warning message.
*/

/*!
    Constructs a new help generator with the give \a parent.
*/
QHelpGenerator::QHelpGenerator(QObject *parent)
    : QObject(parent)
{
    d = new QHelpGeneratorPrivate;
}

/*!
    Destructs the help generator.
*/
QHelpGenerator::~QHelpGenerator()
{
    delete d;
}

/*!
    Takes the \a helpData and generates a new documentation
    set from it. The Qt compressed help file is written to \a
    outputFileName. Returns true on success, otherwise false.
*/
bool QHelpGenerator::generate(QHelpDataInterface *helpData,
                              const QString &outputFileName)
{
    emit progressChanged(0);
    d->error.clear();
    if (!helpData || helpData->namespaceName().isEmpty()) {
        d->error = tr("Invalid help data.");
        return false;
    }

    QString outFileName = outputFileName;
    if (outFileName.isEmpty()) {
        d->error = tr("No output file name specified.");
        return false;
    }

    QFileInfo fi(outFileName);
    if (fi.exists()) {
        if (!fi.dir().remove(fi.fileName())) {
            d->error = tr("The file %1 cannot be overwritten.").arg(outFileName);
            return false;
        }
    }

    setupProgress(helpData);

    emit statusChanged(tr("Building up file structure..."));
    bool openingOk = true;
    {
        QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), QLatin1String("builder"));
        db.setDatabaseName(outFileName);
        openingOk = db.open();
        if (openingOk)
            d->query = new QSqlQuery(db);
    }

    if (!openingOk) {
        d->error = tr("Cannot open data base file %1.").arg(outFileName);
        cleanupDB();
        return false;
    }

    d->query->exec(QLatin1String("PRAGMA synchronous=OFF"));
    d->query->exec(QLatin1String("PRAGMA cache_size=3000"));

    addProgress(1.0);
    createTables();
    insertFileNotFoundFile();
    insertMetaData(helpData->metaData());

    if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) {
        d->error = tr("Cannot register namespace %1.").arg(helpData->namespaceName());
        cleanupDB();
        return false;
    }
    addProgress(1.0);

    emit statusChanged(tr("Insert custom filters..."));
    for (const QHelpDataCustomFilter &f : helpData->customFilters()) {
        if (!registerCustomFilter(f.name, f.filterAttributes, true)) {
            cleanupDB();
            return false;
        }
    }
    addProgress(1.0);

    int i = 1;
    for (const QHelpDataFilterSection &fs : helpData->filterSections()) {
        emit statusChanged(tr("Insert help data for filter section (%1 of %2)...")
            .arg(i++).arg(helpData->filterSections().count()));
        insertFilterAttributes(fs.filterAttributes());
        QByteArray ba;
        QDataStream s(&ba, QIODevice::WriteOnly);
        for (QHelpDataContentItem *itm : fs.contents())
            writeTree(s, itm, 0);
        if (!insertFiles(fs.files(), helpData->rootPath(), fs.filterAttributes())
            || !insertContents(ba, fs.filterAttributes())
            || !insertKeywords(fs.indices(), fs.filterAttributes())) {
            cleanupDB();
            return false;
        }
    }

    cleanupDB();
    emit progressChanged(100);
    emit statusChanged(tr("Documentation successfully generated."));
    return true;
}

void QHelpGenerator::setupProgress(QHelpDataInterface *helpData)
{
    d->progress = 0;
    d->oldProgress = 0;

    int numberOfFiles = 0;
    int numberOfIndices = 0;
    for (const QHelpDataFilterSection &fs : helpData->filterSections()) {
        numberOfFiles += fs.files().count();
        numberOfIndices += fs.indices().count();
    }
    // init      2%
    // filters   1%
    // contents 10%
    // files    60%
    // indices  27%
    d->contentStep = 10.0/(double)helpData->customFilters().count();
    d->fileStep = 60.0/(double)numberOfFiles;
    d->indexStep = 27.0/(double)numberOfIndices;
}

void QHelpGenerator::addProgress(double step)
{
    d->progress += step;
    if ((d->progress - d->oldProgress) >= 1.0 && d->progress <= 100.0) {
        d->oldProgress = d->progress;
        emit progressChanged(qCeil(d->progress));
    }
}

void QHelpGenerator::cleanupDB()
{
    if (d->query) {
        d->query->clear();
        delete d->query;
        d->query = 0;
    }
    QSqlDatabase::removeDatabase(QLatin1String("builder"));
}

void QHelpGenerator::writeTree(QDataStream &s, QHelpDataContentItem *item, int depth)
{
    s << depth;
    s << item->reference();
    s << item->title();
    for (QHelpDataContentItem *i : item->children())
        writeTree(s, i, depth + 1);
}

/*!
    Returns the last error message.
*/
QString QHelpGenerator::error() const
{
    return d->error;
}

bool QHelpGenerator::createTables()
{
    if (!d->query)
        return false;

    d->query->exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'"
        "AND Name=\'NamespaceTable\'"));
    d->query->next();
    if (d->query->value(0).toInt() > 0) {
        d->error = tr("Some tables already exist.");
        return false;
    }

    const QStringList tables = QStringList()
            << QLatin1String("CREATE TABLE NamespaceTable ("
                             "Id INTEGER PRIMARY KEY,"
                             "Name TEXT )")
            << QLatin1String("CREATE TABLE FilterAttributeTable ("
                             "Id INTEGER PRIMARY KEY, "
                             "Name TEXT )")
            << QLatin1String("CREATE TABLE FilterNameTable ("
                             "Id INTEGER PRIMARY KEY, "
                             "Name TEXT )")
            << QLatin1String("CREATE TABLE FilterTable ("
                             "NameId INTEGER, "
                             "FilterAttributeId INTEGER )")
            << QLatin1String("CREATE TABLE IndexTable ("
                             "Id INTEGER PRIMARY KEY, "
                             "Name TEXT, "
                             "Identifier TEXT, "
                             "NamespaceId INTEGER, "
                             "FileId INTEGER, "
                             "Anchor TEXT )")
            << QLatin1String("CREATE TABLE IndexItemTable ("
                             "Id INTEGER, "
                             "IndexId INTEGER )")
            << QLatin1String("CREATE TABLE IndexFilterTable ("
                             "FilterAttributeId INTEGER, "
                             "IndexId INTEGER )")
            << QLatin1String("CREATE TABLE ContentsTable ("
                             "Id INTEGER PRIMARY KEY, "
                             "NamespaceId INTEGER, "
                             "Data BLOB )")
            << QLatin1String("CREATE TABLE ContentsFilterTable ("
                             "FilterAttributeId INTEGER, "
                             "ContentsId INTEGER )")
            << QLatin1String("CREATE TABLE FileAttributeSetTable ("
                             "Id INTEGER, "
                             "FilterAttributeId INTEGER )")
            << QLatin1String("CREATE TABLE FileDataTable ("
                             "Id INTEGER PRIMARY KEY, "
                             "Data BLOB )")
            << QLatin1String("CREATE TABLE FileFilterTable ("
                             "FilterAttributeId INTEGER, "
                             "FileId INTEGER )")
            << QLatin1String("CREATE TABLE FileNameTable ("
                             "FolderId INTEGER, "
                             "Name TEXT, "
                             "FileId INTEGER, "
                             "Title TEXT )")
            << QLatin1String("CREATE TABLE FolderTable("
                             "Id INTEGER PRIMARY KEY, "
                             "Name Text, "
                             "NamespaceID INTEGER )")
            << QLatin1String("CREATE TABLE MetaDataTable("
                             "Name Text, "
                             "Value BLOB )");

    for (const QString &q : tables) {
        if (!d->query->exec(q)) {
            d->error = tr("Cannot create tables.");
            return false;
        }
    }

    d->query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')"));

    d->query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('CreationDate', '2012-12-20T12:00:00')"));

    return true;
}

bool QHelpGenerator::insertFileNotFoundFile()
{
    if (!d->query)
        return false;

    d->query->exec(QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'"));
    if (d->query->next() && d->query->isValid())
        return true;

    d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)"));
    d->query->bindValue(0, QByteArray());
    if (!d->query->exec())
        return false;

    const int fileId = d->query->lastInsertId().toInt();
    d->query->prepare(QLatin1String("INSERT INTO FileNameTable (FolderId, Name, FileId, Title) "
        " VALUES (0, '', ?, '')"));
    d->query->bindValue(0, fileId);
    if (fileId > -1 && d->query->exec()) {
        d->fileMap.insert(QString(), fileId);
        return true;
    }
    return false;
}

bool QHelpGenerator::registerVirtualFolder(const QString &folderName, const QString &ns)
{
    if (!d->query || folderName.isEmpty() || ns.isEmpty())
        return false;

    d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
    d->query->bindValue(0, folderName);
    d->query->exec();
    d->query->next();
    if (d->query->isValid() && d->query->value(0).toInt() > 0)
        return true;

    d->namespaceId = -1;
    d->query->prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?"));
    d->query->bindValue(0, ns);
    d->query->exec();
    while (d->query->next()) {
        d->namespaceId = d->query->value(0).toInt();
        break;
    }

    if (d->namespaceId < 0) {
        d->query->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)"));
        d->query->bindValue(0, ns);
        if (d->query->exec())
            d->namespaceId = d->query->lastInsertId().toInt();
    }

    if (d->namespaceId > 0) {
        d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?"));
        d->query->bindValue(0, folderName);
        d->query->exec();
        while (d->query->next())
            d->virtualFolderId = d->query->value(0).toInt();

        if (d->virtualFolderId > 0)
            return true;

        d->query->prepare(QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) "
            "VALUES (?, ?)"));
        d->query->bindValue(0, d->namespaceId);
        d->query->bindValue(1, folderName);
        if (d->query->exec()) {
            d->virtualFolderId = d->query->lastInsertId().toInt();
            return d->virtualFolderId > 0;
        }
    }
    d->error = tr("Cannot register virtual folder.");
    return false;
}

bool QHelpGenerator::insertFiles(const QStringList &files, const QString &rootPath,
                                 const QStringList &filterAttributes)
{
    if (!d->query)
        return false;

    emit statusChanged(tr("Insert files..."));
    QList<int> filterAtts;
    for (const QString &filterAtt : filterAttributes) {
        d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable "
            "WHERE Name=?"));
        d->query->bindValue(0, filterAtt);
        d->query->exec();
        if (d->query->next())
            filterAtts.append(d->query->value(0).toInt());
    }

    int filterSetId = -1;
    d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable"));
    if (d->query->next())
        filterSetId = d->query->value(0).toInt();
    if (filterSetId < 0)
        return false;
    ++filterSetId;
    for (int attId : qAsConst(filterAtts)) {
        d->query->prepare(QLatin1String("INSERT INTO FileAttributeSetTable "
            "VALUES(?, ?)"));
        d->query->bindValue(0, filterSetId);
        d->query->bindValue(1, attId);
        d->query->exec();
    }

    int tableFileId = 1;
    d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
    if (d->query->next())
        tableFileId = d->query->value(0).toInt() + 1;

    QString title;
    QString charSet;
    FileNameTableData fileNameData;
    QList<QByteArray> fileDataList;
    QMap<int, QSet<int> > tmpFileFilterMap;
    QList<FileNameTableData> fileNameDataList;

    int i = 0;
    for (const QString &file : files) {
        const QString fileName = QDir::cleanPath(file);

        QFile fi(rootPath + QDir::separator() + fileName);
        if (!fi.exists()) {
            emit warning(tr("The file %1 does not exist! Skipping it.")
                .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
            continue;
        }

        if (!fi.open(QIODevice::ReadOnly)) {
            emit warning(tr("Cannot open file %1! Skipping it.")
                .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName)));
            continue;
        }

        QByteArray data = fi.readAll();
        if (fileName.endsWith(QLatin1String(".html"))
            || fileName.endsWith(QLatin1String(".htm"))) {
                charSet = QHelpGlobal::codecFromData(data);
                QTextStream stream(&data);
                stream.setCodec(QTextCodec::codecForName(charSet.toLatin1().constData()));
                title = QHelpGlobal::documentTitle(stream.readAll());
        } else {
            title = fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1);
        }

        int fileId = -1;
        const auto &it = d->fileMap.constFind(fileName);
        if (it == d->fileMap.cend()) {
            fileDataList.append(qCompress(data));

            fileNameData.name = fileName;
            fileNameData.fileId = tableFileId;
            fileNameData.title = title;
            fileNameDataList.append(fileNameData);

            d->fileMap.insert(fileName, tableFileId);
            d->fileFilterMap.insert(tableFileId, filterAtts.toSet());
            tmpFileFilterMap.insert(tableFileId, filterAtts.toSet());

            ++tableFileId;
        } else {
            fileId = it.value();
            QSet<int> &fileFilterSet = d->fileFilterMap[fileId];
            QSet<int> &tmpFileFilterSet = tmpFileFilterMap[fileId];
            for (int filter : qAsConst(filterAtts)) {
                if (!fileFilterSet.contains(filter)
                    && !tmpFileFilterSet.contains(filter)) {
                    fileFilterSet.insert(filter);
                    tmpFileFilterSet.insert(filter);
                }
            }
        }
    }

    if (!tmpFileFilterMap.isEmpty()) {
        d->query->exec(QLatin1String("BEGIN"));
        for (auto it = tmpFileFilterMap.cbegin(), end = tmpFileFilterMap.cend(); it != end; ++it) {
            QList<int> filterValues = it.value().toList();
            std::sort(filterValues.begin(), filterValues.end());
            for (int fv : qAsConst(filterValues)) {
                d->query->prepare(QLatin1String("INSERT INTO FileFilterTable "
                    "VALUES(?, ?)"));
                d->query->bindValue(0, fv);
                d->query->bindValue(1, it.key());
                d->query->exec();
            }
        }

        for (const QByteArray &fileData : qAsConst(fileDataList)) {
            d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES "
                "(Null, ?)"));
            d->query->bindValue(0, fileData);
            d->query->exec();
            if (++i % 20 == 0)
                addProgress(d->fileStep * 20.0);
        }

        for (const FileNameTableData &fnd : qAsConst(fileNameDataList)) {
            d->query->prepare(QLatin1String("INSERT INTO FileNameTable "
                "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)"));
            d->query->bindValue(0, 1);
            d->query->bindValue(1, fnd.name);
            d->query->bindValue(2, fnd.fileId);
            d->query->bindValue(3, fnd.title);
            d->query->exec();
        }
        d->query->exec(QLatin1String("COMMIT"));
    }

    d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable"));
    if (d->query->next()
            && d->query->value(0).toInt() == tableFileId - 1) {
        addProgress(d->fileStep*(i % 20));
        return true;
    }
    return false;
}

bool QHelpGenerator::registerCustomFilter(const QString &filterName,
    const QStringList &filterAttribs, bool forceUpdate)
{
    if (!d->query)
        return false;

    d->query->exec(QLatin1String("SELECT Id, Name FROM FilterAttributeTable"));
    QStringList idsToInsert = filterAttribs;
    QMap<QString, int> attributeMap;
    while (d->query->next()) {
        attributeMap.insert(d->query->value(1).toString(),
            d->query->value(0).toInt());
        idsToInsert.removeAll(d->query->value(1).toString());
    }

    for (const QString &id : qAsConst(idsToInsert)) {
        d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
        d->query->bindValue(0, id);
        d->query->exec();
        attributeMap.insert(id, d->query->lastInsertId().toInt());
    }

    int nameId = -1;
    d->query->prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?"));
    d->query->bindValue(0, filterName);
    d->query->exec();
    while (d->query->next()) {
        nameId = d->query->value(0).toInt();
        break;
    }

    if (nameId < 0) {
        d->query->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)"));
        d->query->bindValue(0, filterName);
        if (d->query->exec())
            nameId = d->query->lastInsertId().toInt();
    } else if (!forceUpdate) {
        d->error = tr("The filter %1 is already registered.").arg(filterName);
        return false;
    }

    if (nameId < 0) {
        d->error = tr("Cannot register filter %1.").arg(filterName);
        return false;
    }

    d->query->prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?"));
    d->query->bindValue(0, nameId);
    d->query->exec();

    for (const QString &att : filterAttribs) {
        d->query->prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)"));
        d->query->bindValue(0, nameId);
        d->query->bindValue(1, attributeMap[att]);
        if (!d->query->exec())
            return false;
    }
    return true;
}

bool QHelpGenerator::insertKeywords(const QList<QHelpDataIndexItem> &keywords,
                                    const QStringList &filterAttributes)
{
    if (!d->query)
        return false;

    emit statusChanged(tr("Insert indices..."));
    int indexId = 1;
    d->query->exec(QLatin1String("SELECT MAX(Id) FROM IndexTable"));
    if (d->query->next())
        indexId = d->query->value(0).toInt() + 1;

    QList<int> filterAtts;
    for (const QString &filterAtt : filterAttributes) {
        d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable WHERE Name=?"));
        d->query->bindValue(0, filterAtt);
        d->query->exec();
        if (d->query->next())
            filterAtts.append(d->query->value(0).toInt());
    }

    QList<int> indexFilterTable;

    int i = 0;
    d->query->exec(QLatin1String("BEGIN"));
    QSet<QString> indices;
    for (const QHelpDataIndexItem &itm : keywords) {
         // Identical ids make no sense and just confuse the Assistant user,
         // so we ignore all repetitions.
        if (indices.contains(itm.identifier))
            continue;

        // Still empty ids should be ignored, as otherwise we will include only
        // the first keyword with an empty id.
        if (!itm.identifier.isEmpty())
            indices.insert(itm.identifier);

        const int pos = itm.reference.indexOf(QLatin1Char('#'));
        const QString &fileName = itm.reference.left(pos);
        const QString anchor = pos < 0 ? QString() : itm.reference.mid(pos + 1);

        const QString &fName = QDir::cleanPath(fileName);

        const auto &it = d->fileMap.constFind(fName);
        const int fileId = it == d->fileMap.cend() ? 1 : it.value();

        d->query->prepare(QLatin1String("INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) "
            "VALUES(?, ?, ?, ?, ?)"));
        d->query->bindValue(0, itm.name);
        d->query->bindValue(1, itm.identifier);
        d->query->bindValue(2, d->namespaceId);
        d->query->bindValue(3, fileId);
        d->query->bindValue(4, anchor);
        d->query->exec();

        indexFilterTable.append(indexId++);
        if (++i % 100 == 0)
            addProgress(d->indexStep * 100.0);
    }
    d->query->exec(QLatin1String("COMMIT"));

    d->query->exec(QLatin1String("BEGIN"));
    for (int idx : qAsConst(indexFilterTable)) {
        for (int a : qAsConst(filterAtts)) {
            d->query->prepare(QLatin1String("INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) "
                "VALUES(?, ?)"));
            d->query->bindValue(0, a);
            d->query->bindValue(1, idx);
            d->query->exec();
        }
    }
    d->query->exec(QLatin1String("COMMIT"));

    d->query->exec(QLatin1String("SELECT COUNT(Id) FROM IndexTable"));
    if (d->query->next() && d->query->value(0).toInt() >= indices.count())
        return true;
    return false;
}

bool QHelpGenerator::insertContents(const QByteArray &ba,
                                    const QStringList &filterAttributes)
{
    if (!d->query)
        return false;

    emit statusChanged(tr("Insert contents..."));
    d->query->prepare(QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) "
        "VALUES(?, ?)"));
    d->query->bindValue(0, d->namespaceId);
    d->query->bindValue(1, ba);
    d->query->exec();
    int contentId = d->query->lastInsertId().toInt();
    if (contentId < 1) {
        d->error = tr("Cannot insert contents.");
        return false;
    }

    // associate the filter attributes
    for (const QString &filterAtt : filterAttributes) {
        d->query->prepare(QLatin1String("INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) "
            "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?"));
        d->query->bindValue(0, contentId);
        d->query->bindValue(1, filterAtt);
        d->query->exec();
        if (!d->query->isActive()) {
            d->error = tr("Cannot register contents.");
            return false;
        }
    }
    addProgress(d->contentStep);
    return true;
}

bool QHelpGenerator::insertFilterAttributes(const QStringList &attributes)
{
    if (!d->query)
        return false;

    d->query->exec(QLatin1String("SELECT Name FROM FilterAttributeTable"));
    QSet<QString> atts;
    while (d->query->next())
        atts.insert(d->query->value(0).toString());

    for (const QString &s : attributes) {
        if (!atts.contains(s)) {
            d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)"));
            d->query->bindValue(0, s);
            d->query->exec();
        }
    }
    return true;
}

bool QHelpGenerator::insertMetaData(const QMap<QString, QVariant> &metaData)
{
    if (!d->query)
        return false;

    for (auto it = metaData.cbegin(), end = metaData.cend(); it != end; ++it) {
        d->query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES(?, ?)"));
        d->query->bindValue(0, it.key());
        d->query->bindValue(1, it.value());
        d->query->exec();
    }
    return true;
}

bool QHelpGenerator::checkLinks(const QHelpDataInterface &helpData)
{
    /*
     * Step 1: Gather the canoncal file paths of all files in the project.
     *         We use a set, because there will be a lot of look-ups.
     */
    QSet<QString> files;
    for (const QHelpDataFilterSection &filterSection : helpData.filterSections()) {
        for (const QString &file : filterSection.files()) {
            const QFileInfo fileInfo(helpData.rootPath() + QDir::separator() + file);
            const QString &canonicalFileName = fileInfo.canonicalFilePath();
            if (!fileInfo.exists())
                emit warning(tr("File '%1' does not exist.").arg(file));
            else
                files.insert(canonicalFileName);
        }
    }

    /*
     * Step 2: Check the hypertext and image references of all HTML files.
     *         Note that we don't parse the files, but simply grep for the
     *         respective HTML elements. Therefore. contents that are e.g.
     *         commented out can cause false warning.
     */
    bool allLinksOk = true;
    for (const QString &fileName : qAsConst(files)) {
        if (!fileName.endsWith(QLatin1String("html"))
            && !fileName.endsWith(QLatin1String("htm")))
            continue;
        QFile htmlFile(fileName);
        if (!htmlFile.open(QIODevice::ReadOnly)) {
            emit warning(tr("File '%1' cannot be opened.").arg(fileName));
            continue;
        }
        const QRegExp linkPattern(QLatin1String("<(?:a href|img src)=\"?([^#\">]+)[#\">]"));
        QTextStream stream(&htmlFile);
        const QString codec = QHelpGlobal::codecFromData(htmlFile.read(1000));
        stream.setCodec(QTextCodec::codecForName(codec.toLatin1().constData()));
        const QString &content = stream.readAll();
        QStringList invalidLinks;
        for (int pos = linkPattern.indexIn(content); pos != -1;
             pos = linkPattern.indexIn(content, pos + 1)) {
            const QString &linkedFileName = linkPattern.cap(1);
            if (linkedFileName.contains(QLatin1String("://")))
                continue;
            const QString &curDir = QFileInfo(fileName).dir().path();
            const QString &canonicalLinkedFileName =
                QFileInfo(curDir + QDir::separator() + linkedFileName).canonicalFilePath();
            if (!files.contains(canonicalLinkedFileName)
                && !invalidLinks.contains(canonicalLinkedFileName)) {
                emit warning(tr("File '%1' contains an invalid link to file '%2'").
                         arg(fileName).arg(linkedFileName));
                allLinksOk = false;
                invalidLinks.append(canonicalLinkedFileName);
            }
        }
    }

    if (!allLinksOk)
        d->error = tr("Invalid links in HTML files.");
    return allLinksOk;
}

QT_END_NAMESPACE

