/*

Copyright 2014 S. Razi Alavizadeh
Copyright 2012-2015 Adam Reichold
Copyright 2012 Michał Trybus

This file is part of qpdfview.

qpdfview is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

qpdfview is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with qpdfview.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "database.h"

#include <QApplication>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDebug>
#include <QDir>

#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)

#include <QStandardPaths>

#else

#include <QDesktopServices>

#endif // QT_VERSION

#ifdef WITH_SQL

#include <QSqlError>
#include <QSqlQuery>

#endif // WITH_SQL

#include "settings.h"
#include "documentview.h"
#include "bookmarkmodel.h"

#ifdef WITH_SQL

namespace
{

using namespace qpdfview;

class Transaction
{
public:
    Transaction(QSqlDatabase& database) :
        m_database(database),
        m_committed(false)
    {
        if(s_current != 0)
        {
            return;
        }

        if(!m_database.transaction())
        {
            throw m_database.lastError();
        }

        s_current = this;
    }

    ~Transaction() throw()
    {
        if(s_current != this)
        {
            return;
        }

        if(!m_committed)
        {
            m_database.rollback();
        }

        s_current = 0;
    }

    void commit()
    {
        if(s_current != this)
        {
            return;
        }

        if(!m_database.commit())
        {
            throw m_database.lastError();
        }

        m_committed = true;
    }

private:
    Q_DISABLE_COPY(Transaction)

    QSqlDatabase& m_database;
    bool m_committed;

    static Transaction* s_current;

};

Transaction* Transaction::s_current = 0;

class Query
{
private:
    template< typename T >
    class BindValue
    {
    private:
        friend class Query;

        explicit BindValue(const T& value) : m_value(value) {}

    public:
        inline operator QVariant() const
        {
            return Conversion< T >::convert(m_value);
        }

    private:
        const T& m_value;

        template< typename S, bool Defined = QMetaTypeId2< S >::Defined >
        struct Conversion
        {
            static inline QVariant convert(const S& value)
            {
                return QVariant(value);
            }
        };

        template< typename S >
        struct Conversion< S, false >
        {
            static inline QVariant convert(const S& value)
            {
                return QVariant(static_cast< uint >(value));
            }
        };

    };

public:
    class Value
    {
    private:
        friend class Query;

        explicit Value(const QVariant& value) : m_value(value) {}

    public:
        template< typename T >
        inline operator T() const
        {
            return Conversion< T >::convert(m_value);
        }

    private:
        const QVariant m_value;

        template< typename T, bool Defined = QMetaTypeId2< T >::Defined >
        struct Conversion
        {
            static inline T convert(const QVariant& value)
            {
                return value.value< T >();
            }
        };

        template< typename T >
        struct Conversion< T, false >
        {
            static inline T convert(const QVariant& value)
            {
                return static_cast< T >(value.value< uint >());
            }
        };

    };

public:
    Query(QSqlDatabase& database) :
        m_query(database),
        m_bindValueIndex(0),
        m_valueIndex(0)
    {
    }

    void prepare(const QString& query)
    {
        if(!m_query.prepare(query))
        {
            throw m_query.lastError();
        }

        m_bindValueIndex = 0;
    }

    void exec()
    {
        if(!m_query.exec())
        {
            throw m_query.lastError();
        }

        m_bindValueIndex = 0;
    }

    void exec(const QString& query)
    {
        if(!m_query.exec(query))
        {
            throw m_query.lastError();
        }

        m_bindValueIndex = 0;
    }

    Query& operator <<(const QVariant& value)
    {
        m_query.bindValue(m_bindValueIndex++, value);

        return *this;
    }

    template< typename T >
    Query& operator <<(const T& value)
    {
        m_query.bindValue(m_bindValueIndex++, BindValue< T >(value));

        return *this;
    }

    bool nextRecord()
    {
        if(!m_query.isActive())
        {
            throw m_query.lastError();
        }

        m_valueIndex = 0;

        return m_query.next();
    }

    Value nextValue()
    {
        return Value(m_query.value(m_valueIndex++));
    }

private:
    Q_DISABLE_COPY(Query)

    QSqlQuery m_query;
    int m_bindValueIndex;
    int m_valueIndex;

};

inline QByteArray hashFilePath(const QString& filePath)
{
    return QCryptographicHash::hash(filePath.toUtf8(), QCryptographicHash::Sha1).toBase64();
}

} // anonymous

#endif // WITH_SQL

namespace qpdfview
{

Database* Database::s_instance = 0;

Database* Database::instance()
{
    if(s_instance == 0)
    {
        s_instance = new Database(qApp);
    }

    return s_instance;
}

Database::~Database()
{
    s_instance = 0;
}

QStringList Database::loadInstanceNames()
{
    QStringList instanceNames;

#ifdef WITH_SQL

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.exec("SELECT DISTINCT(instanceName) FROM tabs_v2");

        while(query.nextRecord())
        {
            const QString instanceName = query.nextValue();

            if(!instanceName.isEmpty())
            {
                instanceNames.append(instanceName);
            }
        }

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#endif // WITH_SQL

    return instanceNames;
}

void Database::restoreTabs(const RestoreTab& restoreTab)
{
#ifdef WITH_SQL

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.prepare("SELECT filePath,currentPage,continuousMode,layoutMode,rightToLeftMode,scaleMode,scaleFactor,rotation,renderFlags,firstPage"
                      " FROM tabs_v4 WHERE instanceName==?");

        query << instanceName();

        query.exec();

        while(query.nextRecord())
        {
            if(DocumentView* newTab = restoreTab(query.nextValue()))
            {
                const int page = query.nextValue();

                newTab->setContinuousMode(query.nextValue());
                newTab->setLayoutMode(query.nextValue());
                newTab->setRightToLeftMode(query.nextValue());

                newTab->setScaleMode(query.nextValue());
                newTab->setScaleFactor(query.nextValue());

                newTab->setRotation(query.nextValue());
                newTab->setRenderFlags(query.nextValue());

                newTab->setFirstPage(query.nextValue());

                newTab->jumpToPage(page, false);
            }
        }

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#else

    Q_UNUSED(restoreTab);

#endif // WITH_SQL
}

void Database::saveTabs(const QList< DocumentView* >& tabs)
{
#ifdef WITH_SQL

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.prepare("DELETE FROM tabs_v4 WHERE instanceName==?");

        query << instanceName();

        query.exec();

        query.prepare("INSERT INTO tabs_v4"
                      " (filePath,instanceName,currentPage,continuousMode,layoutMode,rightToLeftMode,scaleMode,scaleFactor,rotation,renderFlags,firstPage)"
                      " VALUES (?,?,?,?,?,?,?,?,?,?,?)");

        foreach(const DocumentView* tab, tabs)
        {
            query << tab->fileInfo().absoluteFilePath()
                  << instanceName()
                  << tab->currentPage()

                  << tab->continuousMode()
                  << tab->layoutMode()
                  << tab->rightToLeftMode()

                  << tab->scaleMode()
                  << tab->scaleFactor()

                  << tab->rotation()
                  << tab->renderFlags()

                  << tab->firstPage();

            query.exec();
        }

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#else

    Q_UNUSED(tabs);

#endif // WITH_SQL
}

void Database::clearTabs()
{
#ifdef WITH_SQL

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.exec("DELETE FROM tabs_v4");

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#endif // WITH_SQL
}

void Database::restoreBookmarks()
{
#ifdef WITH_SQL

    try
    {
        Transaction transaction(m_database);
        Query outerQuery(m_database);
        Query innerQuery(m_database);

        outerQuery.exec("SELECT DISTINCT(filePath) FROM bookmarks_v3");

        innerQuery.prepare("SELECT page,label,comment,modified"
                           " FROM bookmarks_v3 WHERE filePath==?");

        while(outerQuery.nextRecord())
        {
            const QString filePath = outerQuery.nextValue();

            innerQuery << filePath;

            innerQuery.exec();

            BookmarkModel* model = BookmarkModel::fromPath(filePath, true);

            while(innerQuery.nextRecord())
            {
                const int page = innerQuery.nextValue();
                const QString label = innerQuery.nextValue();
                const QString comment = innerQuery.nextValue();
                const QDateTime modified = innerQuery.nextValue();

                model->addBookmark(BookmarkItem(page, label, comment, modified));
            }
        }

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#endif // WITH_SQL
}

void Database::saveBookmarks()
{
#ifdef WITH_SQL

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.exec("DELETE FROM bookmarks_v3");

        if(Settings::instance()->mainWindow().restoreBookmarks())
        {
            query.prepare("INSERT INTO bookmarks_v3"
                          " (filePath,page,label,comment,modified)"
                          " VALUES (?,?,?,?,?)");

            foreach(const QString& filePath, BookmarkModel::knownPaths())
            {
                const BookmarkModel* model = BookmarkModel::fromPath(filePath);

                for(int row = 0, rowCount = model->rowCount(); row < rowCount; ++row)
                {
                    const QModelIndex index = model->index(row);

                    query << filePath

                          << index.data(BookmarkModel::PageRole)
                          << index.data(BookmarkModel::LabelRole)
                          << index.data(BookmarkModel::CommentRole)
                          << index.data(BookmarkModel::ModifiedRole);

                    query.exec();
                }
            }
        }

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#endif // WITH_SQL
}

void Database::clearBookmarks()
{
#ifdef WITH_SQL

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.exec("DELETE FROM bookmarks_v3");

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#endif // WITH_SQL
}

void Database::restorePerFileSettings(DocumentView* tab)
{
#ifdef WITH_SQL

    if(!Settings::instance()->mainWindow().restorePerFileSettings())
    {
        return;
    }

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.prepare("SELECT currentPage,continuousMode,layoutMode,rightToLeftMode,scaleMode,scaleFactor,rotation,renderFlags,firstPage"
                      " FROM perfilesettings_v4 WHERE filePath==?");

        query << hashFilePath(tab->fileInfo().absoluteFilePath());

        query.exec();

        if(query.nextRecord())
        {
            const int page = query.nextValue();

            tab->setContinuousMode(query.nextValue());
            tab->setLayoutMode(query.nextValue());
            tab->setRightToLeftMode(query.nextValue());

            tab->setScaleMode(query.nextValue());
            tab->setScaleFactor(query.nextValue());

            tab->setRotation(query.nextValue());

            tab->setRenderFlags(query.nextValue());

            tab->setFirstPage(query.nextValue());

            tab->jumpToPage(page, false);
        }

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#else

    Q_UNUSED(tab);

#endif // WITH_SQL
}

void Database::savePerFileSettings(const DocumentView* tab)
{
#ifdef WITH_SQL

    if(!Settings::instance()->mainWindow().restorePerFileSettings())
    {
        return;
    }

    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.prepare("INSERT OR REPLACE INTO perfilesettings_v4"
                      " (lastUsed,filePath,currentPage,continuousMode,layoutMode,rightToLeftMode,scaleMode,scaleFactor,rotation,renderFlags,firstPage)"
                      " VALUES (strftime('%s','now'),?,?,?,?,?,?,?,?,?,?)");

        query << hashFilePath(tab->fileInfo().absoluteFilePath())
              << tab->currentPage()

              << tab->continuousMode()
              << tab->layoutMode()
              << tab->rightToLeftMode()

              << tab->scaleMode()
              << tab->scaleFactor()

              << tab->rotation()
              << tab->renderFlags()

              << tab->firstPage();

        query.exec();

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

#else

    Q_UNUSED(tab);

#endif // WITH_SQL
}

Database::Database(QObject* parent) : QObject(parent)
{
#ifdef WITH_SQL

#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)

    const QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation);

#else

    const QString path = QDesktopServices::storageLocation(QDesktopServices::DataLocation);

#endif // QT_VERSION

    QDir().mkpath(path);

    m_database = QSqlDatabase::addDatabase("QSQLITE");
    m_database.setDatabaseName(QDir(path).filePath("database"));

    if(!m_database.open())
    {
        qDebug() << m_database.lastError();
        return;
    }

    try
    {
        Query query(m_database);

        query.exec("PRAGMA synchronous = OFF");
        query.exec("PRAGMA journal_mode = MEMORY");
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }

    const QStringList tables = m_database.tables();

    // tabs

    if(!tables.contains("tabs_v4"))
    {
        if(prepareTabs_v4())
        {
            if(tables.contains("tabs_v3"))
            {
                migrateTabs_v3_v4();
            }
            else if(tables.contains("tabs_v2"))
            {
                migrateTabs_v2_v4();
            }
            else if(tables.contains("tabs_v1"))
            {
                migrateTabs_v1_v4();
            }
        }
    }

    // bookmarks

    if(!tables.contains("bookmarks_v3"))
    {
        if(prepareBookmarks_v3())
        {
            if(tables.contains("bookmarks_v2"))
            {
                migrateBookmarks_v2_v3();
            }
            else if(tables.contains("bookmarks_v1"))
            {
                migrateBookmarks_v1_v3();
            }
        }
    }

    // per-file settings

    if(!tables.contains("perfilesettings_v4"))
    {
        if(preparePerFileSettings_v4())
        {
            if(tables.contains("perfilesettings_v3"))
            {
                migratePerFileSettings_v3_v4();
            }
            else if(tables.contains("perfilesettings_v2"))
            {
                migratePerFileSettings_v2_v4();
            }
            else if(tables.contains("perfilesettings_v1"))
            {
                migratePerFileSettings_v1_v4();
            }
        }
    }

    limitPerFileSettings();

#endif // WITH_SQL
}

QString Database::instanceName()
{
    return !qApp->objectName().isNull() ? qApp->objectName() : QString("");
}

#ifdef WITH_SQL

bool Database::prepareTabs_v4()
{
    return prepareTable("CREATE TABLE tabs_v4 ("
                        " filePath TEXT"
                        " ,instanceName TEXT"
                        " ,currentPage INTEGER"
                        " ,continuousMode INTEGER"
                        " ,layoutMode INTEGER"
                        " ,rightToLeftMode INTEGER"
                        " ,scaleMode INTEGER"
                        " ,scaleFactor REAL"
                        " ,rotation INTEGER"
                        " ,renderFlags INTEGER"
                        " ,firstPage INTEGER"
                        " )");
}

bool Database::prepareBookmarks_v3()
{
    return prepareTable("CREATE TABLE bookmarks_v3 ("
                        " filePath TEXT"
                        " ,page INTEGER"
                        " ,label TEXT"
                        " ,comment TEXT"
                        " ,modified DATETIME"
                        " )");
}

bool Database::preparePerFileSettings_v4()
{
    return prepareTable("CREATE TABLE perfilesettings_v4 ("
                        " lastUsed INTEGER"
                        " ,filePath TEXT PRIMARY KEY"
                        " ,currentPage INTEGER"
                        " ,continuousMode INTEGER"
                        " ,layoutMode INTEGER"
                        " ,rightToLeftMode INTEGER"
                        " ,scaleMode INTEGER"
                        " ,scaleFactor REAL"
                        " ,rotation INTEGER"
                        " ,renderFlags INTEGER"
                        " ,firstPage INTEGER"
                        " )");
}

void Database::migrateTabs_v3_v4()
{
    migrateTable("INSERT INTO tabs_v4"
                 " SELECT filePath,instanceName,currentPage,continuousMode,layoutMode,rightToLeftMode,scaleMode,scaleFactor,rotation,0,-1"
                 " FROM tabs_v3",

                 "DROP TABLE tabs_v3",

                 "Migrated tabs from v3 to v4, dropping v3.");
}

void Database::migrateTabs_v2_v4()
{
    migrateTable("INSERT INTO tabs_v4"
                 " SELECT filePath,instanceName,currentPage,continuousMode,layoutMode,0,scaleMode,scaleFactor,rotation,0,-1"
                 " FROM tabs_v2",

                 "DROP TABLE tabs_v2"  ,

                 "Migrated tabs from v2 to v4, dropping v2.");
}

void Database::migrateTabs_v1_v4()
{
    migrateTable("INSERT INTO tabs_v4"
                 " SELECT filePath,?,currentPage,continuousMode,layoutMode,0,scaleMode,scaleFactor,rotation,0,-1"
                 " FROM tabs_v1",

                 "DROP TABLE tabs_v1",

                 "Migrated tabs from v1 to v4, dropping v1.");
}

void Database::migrateBookmarks_v2_v3()
{
    migrateTable("INSERT INTO bookmarks_v3"
                 " SELECT filePath,page,label,'',datetime('now')"
                 " FROM bookmarks_v2",

                 "DROP TABLE bookmarks_v2",

                 "Migrated bookmarks from v2 to v3, dropping v2.");
}

void Database::migrateBookmarks_v1_v3()
{
    try
    {
        Transaction transaction(m_database);
        Query outerQuery(m_database);
        Query innerQuery(m_database);

        outerQuery.exec("SELECT filePath,pages FROM bookmarks_v1");

        innerQuery.prepare("INSERT INTO bookmarks_v3"
                           " (filePath,page,label,comment,modified)"
                           " VALUES (?,?,?,'',datetime('now'))");

        while(outerQuery.nextRecord())
        {
            const QString filePath = outerQuery.nextValue();
            const QString pages = outerQuery.nextValue();

            foreach(const QString& page, pages.split(",", QString::SkipEmptyParts))
            {
                innerQuery << filePath
                           << page
                           << tr("Jump to page %1").arg(page);

                innerQuery.exec();
            }
        }

        qWarning() << "Migrated bookmarks from v1 to v3, dropping v1.";

        outerQuery.exec("DROP TABLE bookmarks_v1");

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }
}

void Database::migratePerFileSettings_v3_v4()
{
    migrateTable("INSERT INTO perfilesettings_v4"
                 " SELECT lastUsed,filePath,currentPage,continuousMode,layoutMode,rightToLeftMode,scaleMode,scaleFactor,rotation,0,firstPage"
                 " FROM perfilesettings_v3",

                 "DROP TABLE perfilesettings_v3",

                 "Migrated per-file settings from v3 to v4, dropping v3.");
}

void Database::migratePerFileSettings_v2_v4()
{
    migrateTable("INSERT INTO perfilesettings_v4"
                 " SELECT lastUsed,filePath,currentPage,continuousMode,layoutMode,rightToLeftMode,scaleMode,scaleFactor,rotation,0,-1"
                 " FROM perfilesettings_v2",

                 "DROP TABLE perfilesettings_v2",

                 "Migrated per-file settings from v2 to v4, dropping v2.");
}

void Database::migratePerFileSettings_v1_v4()
{
    migrateTable("INSERT INTO perfilesettings_v4"
                 " SELECT lastUsed,filePath,currentPage,continuousMode,layoutMode,0,scaleMode,scaleFactor,rotation,0,-1"
                 " FROM perfilesettings_v1",

                 "DROP TABLE perfilesettings_v1",

                 "Migrated per-file settings from v1 to v4, dropping v1.");
}

bool Database::prepareTable(const QString& prepare)
{
    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.exec(prepare);

        transaction.commit();
        return true;
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
        return false;
    }
}

void Database::migrateTable(const QString& migrate, const QString& prune, const QString& warning)
{
    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        query.exec(migrate);

        qWarning() << warning;

        query.exec(prune);

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }
}

void Database::limitPerFileSettings()
{
    try
    {
        Transaction transaction(m_database);
        Query query(m_database);

        if(Settings::instance()->mainWindow().restorePerFileSettings())
        {
            query.prepare("DELETE FROM perfilesettings_v4"
                          " WHERE filePath NOT IN ("
                          "  SELECT filePath FROM perfilesettings_v4"
                          "  ORDER BY lastUsed DESC LIMIT ?"
                          " )");

            query << Settings::instance()->mainWindow().perFileSettingsLimit();

            query.exec();
        }
        else
        {
            query.exec("DELETE FROM perfilesettings_v4");
        }

        transaction.commit();
    }
    catch(QSqlError& error)
    {
        qDebug() << error;
    }
}

#endif // WITH_SQL

} // qpdfview
