/****************************************************************************
**
** 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 "qhelpdbreader_p.h"
#include "qhelp_global.h"

#include <QtCore/QVariant>
#include <QtCore/QFile>
#include <QtSql/QSqlError>
#include <QtSql/QSqlQuery>

QT_BEGIN_NAMESPACE

QHelpDBReader::QHelpDBReader(const QString &dbName)
    : QObject(0),
      m_dbName(dbName),
      m_uniqueId(QHelpGlobal::uniquifyConnectionName(QLatin1String("QHelpDBReader"),
                                                     this))
{
}

QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId,
                           QObject *parent)
    : QObject(parent),
      m_dbName(dbName),
      m_uniqueId(uniqueId)
{
}

QHelpDBReader::~QHelpDBReader()
{
    if (m_initDone) {
        delete m_query;
        QSqlDatabase::removeDatabase(m_uniqueId);
    }
}

bool QHelpDBReader::init()
{
    if (m_initDone)
        return true;

    if (!QFile::exists(m_dbName))
        return false;

    if (!initDB()) {
        QSqlDatabase::removeDatabase(m_uniqueId);
        return false;
    }

    m_initDone = true;
    m_query = new QSqlQuery(QSqlDatabase::database(m_uniqueId));

    return true;
}

bool QHelpDBReader::initDB()
{
    QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), m_uniqueId);
    db.setConnectOptions(QLatin1String("QSQLITE_OPEN_READONLY"));
    db.setDatabaseName(m_dbName);
    if (!db.open()) {
        /*: The placeholders are: %1 - The name of the database which cannot be opened
                                  %2 - The unique id for the connection
                                  %3 - The actual error string */
        m_error = tr("Cannot open database '%1' '%2': %3").arg(m_dbName, m_uniqueId, db.lastError().text());
        return false;
    }
    return true;
}

QString QHelpDBReader::databaseName() const
{
    return m_dbName;
}

QString QHelpDBReader::errorMessage() const
{
    return m_error;
}

QString QHelpDBReader::namespaceName() const
{
    if (!m_namespace.isEmpty())
        return m_namespace;
    if (m_query) {
        m_query->exec(QLatin1String("SELECT Name FROM NamespaceTable"));
        if (m_query->next())
            m_namespace = m_query->value(0).toString();
    }
    return m_namespace;
}

QString QHelpDBReader::virtualFolder() const
{
    if (m_query) {
        m_query->exec(QLatin1String("SELECT Name FROM FolderTable WHERE Id=1"));
        if (m_query->next())
            return m_query->value(0).toString();
    }
    return QString();
}

QList<QStringList> QHelpDBReader::filterAttributeSets() const
{
    QList<QStringList> result;
    if (m_query) {
        m_query->exec(QLatin1String("SELECT a.Id, b.Name FROM FileAttributeSetTable a, "
            "FilterAttributeTable b WHERE a.FilterAttributeId=b.Id ORDER BY a.Id"));
        int oldId = -1;
        while (m_query->next()) {
            const int id = m_query->value(0).toInt();
            if (id != oldId) {
                result.append(QStringList());
                oldId = id;
            }
            result.last().append(m_query->value(1).toString());
        }
    }
    return result;
}

bool QHelpDBReader::fileExists(const QString &virtualFolder,
                               const QString &filePath,
                               const QStringList &filterAttributes) const
{
    if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
        return false;

//SELECT COUNT(a.Name) FROM FileNameTable a, FolderTable b, FileFilterTable c, FilterAttributeTable d WHERE a.FolderId=b.Id AND b.Name='qtdoc' AND a.Name='qstring.html' AND a.FileId=c.FileId AND c.FilterAttributeId=d.Id AND d.Name='qtrefdoc'

    QString query;
    namespaceName();
    if (filterAttributes.isEmpty()) {
        query = QString(QLatin1String("SELECT COUNT(a.Name) FROM FileNameTable a, FolderTable b "
            "WHERE a.FolderId=b.Id AND b.Name=\'%1\' AND a.Name=\'%2\'")).arg(quote(virtualFolder)).arg(quote(filePath));
    } else {
        query = QString(QLatin1String("SELECT COUNT(a.Name) FROM FileNameTable a, FolderTable b, "
            "FileFilterTable c, FilterAttributeTable d WHERE a.FolderId=b.Id "
            "AND b.Name=\'%1\' AND a.Name=\'%2\' AND a.FileId=c.FileId AND "
            "c.FilterAttributeId=d.Id AND d.Name=\'%3\'"))
            .arg(quote(virtualFolder)).arg(quote(filePath))
            .arg(quote(filterAttributes.first()));
        for (int i = 1; i < filterAttributes.count(); ++i) {
            query.append(QString(QLatin1String(" INTERSECT SELECT COUNT(a.Name) FROM FileNameTable a, "
                "FolderTable b, FileFilterTable c, FilterAttributeTable d WHERE a.FolderId=b.Id "
                "AND b.Name=\'%1\' AND a.Name=\'%2\' AND a.FileId=c.FileId AND "
                "c.FilterAttributeId=d.Id AND d.Name=\'%3\'"))
                .arg(quote(virtualFolder)).arg(quote(filePath))
                .arg(quote(filterAttributes.at(i))));
        }
    }
    m_query->exec(query);
    if (m_query->next() && m_query->isValid() && m_query->value(0).toInt())
        return true;
    return false;
}

QByteArray QHelpDBReader::fileData(const QString &virtualFolder,
                                   const QString &filePath) const
{
    QByteArray ba;
    if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
        return ba;

    namespaceName();
    m_query->prepare(QLatin1String("SELECT a.Data FROM FileDataTable a, FileNameTable b, FolderTable c, "
        "NamespaceTable d WHERE a.Id=b.FileId AND (b.Name=? OR b.Name=?) AND b.FolderId=c.Id "
        "AND c.Name=? AND c.NamespaceId=d.Id AND d.Name=?"));
    m_query->bindValue(0, filePath);
    m_query->bindValue(1, QString(QLatin1String("./") + filePath));
    m_query->bindValue(2, virtualFolder);
    m_query->bindValue(3, m_namespace);
    m_query->exec();
    if (m_query->next() && m_query->isValid())
        ba = qUncompress(m_query->value(0).toByteArray());
    return ba;
}

QStringList QHelpDBReader::customFilters() const
{
    QStringList lst;
    if (m_query) {
        m_query->exec(QLatin1String("SELECT Name FROM FilterNameTable"));
        while (m_query->next())
            lst.append(m_query->value(0).toString());
    }
    return lst;
}

QStringList QHelpDBReader::filterAttributes(const QString &filterName) const
{
    QStringList lst;
    if (m_query) {
        if (filterName.isEmpty()) {
            m_query->prepare(QLatin1String("SELECT Name FROM FilterAttributeTable"));
        } else {
            m_query->prepare(QLatin1String("SELECT a.Name FROM FilterAttributeTable a, "
                "FilterTable b, FilterNameTable c WHERE c.Name=? "
                "AND c.Id=b.NameId AND b.FilterAttributeId=a.Id"));
            m_query->bindValue(0, filterName);
        }
        m_query->exec();
        while (m_query->next())
            lst.append(m_query->value(0).toString());
    }
    return lst;
}

QStringList QHelpDBReader::indicesForFilter(const QStringList &filterAttributes) const
{
    QStringList indices;
    if (!m_query)
        return indices;

    //SELECT DISTINCT a.Name FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c WHERE a.Id=b.IndexId AND b.FilterAttributeId=c.Id AND c.Name in ('4.2.3', 'qt')

    QString query;
    if (filterAttributes.isEmpty()) {
        query = QLatin1String("SELECT DISTINCT Name FROM IndexTable");
    } else {
        query = QString(QLatin1String("SELECT DISTINCT a.Name FROM IndexTable a, "
        "IndexFilterTable b, FilterAttributeTable c WHERE a.Id=b.IndexId "
        "AND b.FilterAttributeId=c.Id AND c.Name='%1'")).arg(quote(filterAttributes.first()));
        for (int i = 1; i < filterAttributes.count(); ++i) {
            query.append(QString(QLatin1String(" INTERSECT SELECT DISTINCT a.Name FROM IndexTable a, "
                "IndexFilterTable b, FilterAttributeTable c WHERE a.Id=b.IndexId "
                "AND b.FilterAttributeId=c.Id AND c.Name='%1'"))
                .arg(quote(filterAttributes.at(i))));
        }
    }

    m_query->exec(query);
    while (m_query->next()) {
        if (!m_query->value(0).toString().isEmpty())
            indices.append(m_query->value(0).toString());
    }
    return indices;
}

void QHelpDBReader::linksForKeyword(const QString &keyword,
                                    const QStringList &filterAttributes,
                                    QMap<QString, QUrl> *linkMap) const
{
    if (!m_query)
        return;

    QString query;
    if (filterAttributes.isEmpty()) {
        query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor "
            "FROM IndexTable a, FileNameTable d, "
            "FolderTable e, NamespaceTable f WHERE "
            "a.FileId=d.FileId AND d.FolderId=e.Id AND a.NamespaceId=f.Id "
            "AND a.Name='%1'")).arg(quote(keyword));
    } else if (m_useAttributesCache) {
        query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor, a.Id "
            "FROM IndexTable a, "
            "FileNameTable d, FolderTable e, NamespaceTable f WHERE "
            "a.FileId=d.FileId AND d.FolderId=e.Id "
            "AND a.NamespaceId=f.Id AND a.Name='%1'"))
            .arg(quote(keyword));
        m_query->exec(query);
        while (m_query->next()) {
            if (m_indicesCache.contains(m_query->value(5).toInt())) {
                linkMap->insertMulti(m_query->value(0).toString(), buildQUrl(m_query->value(1).toString(),
                    m_query->value(2).toString(), m_query->value(3).toString(),
                    m_query->value(4).toString()));
            }
        }
        return;
    } else {
        query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor "
            "FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c, "
            "FileNameTable d, FolderTable e, NamespaceTable f "
            "WHERE a.FileId=d.FileId AND d.FolderId=e.Id "
            "AND a.NamespaceId=f.Id AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id "
            "AND a.Name='%1' AND c.Name='%2'")).arg(quote(keyword))
            .arg(quote(filterAttributes.first()));
        for (int i = 1; i < filterAttributes.count(); ++i) {
            query.append(QString(QLatin1String(" INTERSECT SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor "
                "FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c, "
                "FileNameTable d, FolderTable e, NamespaceTable f "
                "WHERE a.FileId=d.FileId AND d.FolderId=e.Id "
                "AND a.NamespaceId=f.Id AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id "
                "AND a.Name='%1' AND c.Name='%2'")).arg(quote(keyword))
                .arg(quote(filterAttributes.at(i))));
        }
    }

    QString title;
    m_query->exec(query);
    while (m_query->next()) {
        title = m_query->value(0).toString();
        if (title.isEmpty()) // generate a title + corresponding path
            title = keyword + QLatin1String(" : ") + m_query->value(3).toString();
        linkMap->insertMulti(title, buildQUrl(m_query->value(1).toString(),
            m_query->value(2).toString(), m_query->value(3).toString(),
            m_query->value(4).toString()));
    }
}

void QHelpDBReader::linksForIdentifier(const QString &id,
                                       const QStringList &filterAttributes,
                                       QMap<QString, QUrl> *linkMap) const
{
    if (!m_query)
        return;

    QString query;
    if (filterAttributes.isEmpty()) {
        query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor "
        "FROM IndexTable a, FileNameTable d, FolderTable e, "
        "NamespaceTable f WHERE a.FileId=d.FileId AND "
        "d.FolderId=e.Id AND a.NamespaceId=f.Id AND a.Identifier='%1'"))
        .arg(quote(id));
    } else if (m_useAttributesCache) {
        query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor, a.Id "
            "FROM IndexTable a,"
            "FileNameTable d, FolderTable e, NamespaceTable f WHERE "
            "a.FileId=d.FileId AND d.FolderId=e.Id "
            "AND a.NamespaceId=f.Id AND a.Identifier='%1'"))
            .arg(quote(id));
        m_query->exec(query);
        while (m_query->next()) {
            if (m_indicesCache.contains(m_query->value(5).toInt())) {
                linkMap->insertMulti(m_query->value(0).toString(), buildQUrl(m_query->value(1).toString(),
                    m_query->value(2).toString(), m_query->value(3).toString(),
                    m_query->value(4).toString()));
            }
        }
        return;
    } else {
        query = QString(QLatin1String("SELECT d.Title, f.Name, e.Name, d.Name, a.Anchor "
            "FROM IndexTable a, IndexFilterTable b, FilterAttributeTable c, "
            "FileNameTable d, FolderTable e, NamespaceTable f "
            "WHERE a.FileId=d.FileId AND d.FolderId=e.Id "
            "AND a.NamespaceId=f.Id AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id "
            "AND a.Identifier='%1' AND c.Name='%2'")).arg(quote(id))
            .arg(quote(filterAttributes.first()));
        for (int i = 0; i < filterAttributes.count(); ++i) {
            query.append(QString(QLatin1String(" INTERSECT SELECT d.Title, f.Name, e.Name, "
                "d.Name, a.Anchor FROM IndexTable a, IndexFilterTable b, "
                "FilterAttributeTable c, FileNameTable d, "
                "FolderTable e, NamespaceTable f WHERE "
                "a.FileId=d.FileId AND d.FolderId=e.Id AND a.NamespaceId=f.Id "
                "AND b.IndexId=a.Id AND b.FilterAttributeId=c.Id AND "
                "a.Identifier='%1' AND c.Name='%2'")).arg(quote(id))
                .arg(quote(filterAttributes.at(i))));
        }
    }

    m_query->exec(query);
    while (m_query->next()) {
        linkMap->insertMulti(m_query->value(0).toString(), buildQUrl(m_query->value(1).toString(),
            m_query->value(2).toString(), m_query->value(3).toString(),
            m_query->value(4).toString()));
    }
}

QUrl QHelpDBReader::buildQUrl(const QString &ns, const QString &folder,
                              const QString &relFileName, const QString &anchor) const
{
    QUrl url;
    url.setScheme(QLatin1String("qthelp"));
    url.setAuthority(ns);
    url.setPath(QLatin1Char('/') + folder + QLatin1Char('/') + relFileName);
    url.setFragment(anchor);
    return url;
}

QList<QByteArray> QHelpDBReader::contentsForFilter(const QStringList &filterAttributes) const
{
    QList<QByteArray> contents;
    if (!m_query)
        return contents;

    //SELECT DISTINCT a.Data FROM ContentsTable a, ContentsFilterTable b, FilterAttributeTable c WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id AND c.Name='qt' INTERSECT SELECT DISTINCT a.Data FROM ContentsTable a, ContentsFilterTable b, FilterAttributeTable c WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id AND c.Name='3.3.8';

    QString query;
    if (filterAttributes.isEmpty()) {
        query = QLatin1String("SELECT Data from ContentsTable");
    } else {
        query = QString(QLatin1String("SELECT a.Data FROM ContentsTable a, "
            "ContentsFilterTable b, FilterAttributeTable c "
            "WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id "
            "AND c.Name='%1'")).arg(quote(filterAttributes.first()));
        for (int i = 1; i < filterAttributes.count(); ++i) {
            query.append(QString(QLatin1String(" INTERSECT SELECT a.Data FROM ContentsTable a, "
            "ContentsFilterTable b, FilterAttributeTable c "
            "WHERE a.Id=b.ContentsId AND b.FilterAttributeId=c.Id "
            "AND c.Name='%1'")).arg(quote(filterAttributes.at(i))));
        }
    }

    m_query->exec(query);
    while (m_query->next())
        contents.append(m_query->value(0).toByteArray());
    return contents;
}

QUrl QHelpDBReader::urlOfPath(const QString &relativePath) const
{
    if (!m_query)
        return QUrl();

    m_query->exec(QLatin1String("SELECT a.Name, b.Name FROM NamespaceTable a, "
        "FolderTable b WHERE a.id=b.NamespaceId and a.Id=1"));
    if (!m_query->next())
        return QUrl();

    const int idx = relativePath.indexOf(QLatin1Char('#'));
    const QString &rp = idx < 0 ? relativePath : relativePath.left(idx);
    const QString anchor = idx < 0 ? QString() : relativePath.mid(idx + 1);
    return buildQUrl(m_query->value(0).toString(),
                     m_query->value(1).toString(), rp, anchor);
}

QStringList QHelpDBReader::files(const QStringList &filterAttributes,
                                 const QString &extensionFilter) const
{
    QStringList lst;
    if (!m_query)
        return lst;

    QString query;
    QString extension;
    if (!extensionFilter.isEmpty())
        extension = QString(QLatin1String("AND b.Name like \'%.%1\'")).arg(extensionFilter);

    if (filterAttributes.isEmpty()) {
        query = QString(QLatin1String("SELECT a.Name, b.Name FROM FolderTable a, "
            "FileNameTable b WHERE b.FolderId=a.Id %1"))
            .arg(extension);
    } else {
        query = QString(QLatin1String("SELECT a.Name, b.Name FROM FolderTable a, "
            "FileNameTable b, FileFilterTable c, FilterAttributeTable d "
            "WHERE b.FolderId=a.Id AND b.FileId=c.FileId "
            "AND c.FilterAttributeId=d.Id AND d.Name=\'%1\' %2"))
            .arg(quote(filterAttributes.first())).arg(extension);
        for (int i = 1; i < filterAttributes.count(); ++i) {
            query.append(QString(QLatin1String(" INTERSECT SELECT a.Name, b.Name FROM "
                "FolderTable a, FileNameTable b, FileFilterTable c, "
                "FilterAttributeTable d WHERE b.FolderId=a.Id AND "
                "b.FileId=c.FileId AND c.FilterAttributeId=d.Id AND "
                "d.Name=\'%1\' %2")).arg(quote(filterAttributes.at(i)))
                .arg(extension));
        }
    }
    m_query->exec(query);
    while (m_query->next()) {
        lst.append(m_query->value(0).toString() + QLatin1Char('/')
            + m_query->value(1).toString());
    }

    return lst;
}

QVariant QHelpDBReader::metaData(const QString &name) const
{
    QVariant v;
    if (!m_query)
        return v;

    m_query->prepare(QLatin1String("SELECT COUNT(Value), Value FROM MetaDataTable "
        "WHERE Name=?"));
    m_query->bindValue(0, name);
    if (m_query->exec() && m_query->next()
        && m_query->value(0).toInt() == 1)
        v = m_query->value(1);
    return v;
}

QString QHelpDBReader::mergeList(const QStringList &list) const
{
    QString str;
    for (const QString &s : list)
        str.append(QLatin1Char('\'') + quote(s) + QLatin1String("\', "));
    if (str.endsWith(QLatin1String(", ")))
        str.chop(2);
    return str;
}

QString QHelpDBReader::quote(const QString &string) const
{
    QString s = string;
    s.replace(QLatin1Char('\''), QLatin1String("\'\'"));
    return s;
}

QSet<int> QHelpDBReader::indexIds(const QStringList &attributes) const
{
    QSet<int> ids;

    if (attributes.isEmpty())
        return ids;

    QString query = QString(QLatin1String("SELECT a.IndexId FROM IndexFilterTable a, "
        "FilterAttributeTable b WHERE a.FilterAttributeId=b.Id "
        "AND b.Name='%1'")).arg(attributes.first());
    for (const QString &attribute : attributes) {
        query.append(QString(QLatin1String(" INTERSECT SELECT a.IndexId FROM "
            "IndexFilterTable a, FilterAttributeTable b WHERE "
            "a.FilterAttributeId=b.Id AND b.Name='%1'"))
            .arg(attribute));
    }

    if (!m_query->exec(query))
            return ids;

    while (m_query->next())
        ids.insert(m_query->value(0).toInt());

    return ids;
}

bool QHelpDBReader::createAttributesCache(const QStringList &attributes,
                                          const QSet<int> &indexIds)
{
    m_useAttributesCache = false;

    if (attributes.count() < 2) {
        m_viewAttributes.clear();
        return true;
    }

    const bool needUpdate = !m_viewAttributes.count();

    for (const QString &s : attributes)
        m_viewAttributes.remove(s);

    if (m_viewAttributes.count() || needUpdate) {
        m_viewAttributes.clear();
        m_indicesCache = indexIds;
    }
    for (const QString &s : attributes)
        m_viewAttributes.insert(s);
    m_useAttributesCache = true;
    return true;
}

QT_END_NAMESPACE
